@basictech/react 0.7.0-beta.0 → 0.7.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,18 @@
1
- // @ts-nocheck
2
-
3
1
  import React, { createContext, useContext, useEffect, useState, useRef } from 'react'
4
2
  import { jwtDecode } from 'jwt-decode'
5
3
 
6
4
  import { BasicSync } from './sync'
7
- import { get, add, update, deleteRecord } from './db'
8
- import { validateSchema, compareSchemas } from '@basictech/schema'
9
5
 
10
6
  import { log } from './config'
11
- import {version as currentVersion} from '../package.json'
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
+
12
16
 
13
17
  type BasicSyncType = {
14
18
  basic_schema: any;
@@ -20,7 +24,7 @@ type BasicSyncType = {
20
24
  count: () => Promise<number>;
21
25
  };
22
26
  };
23
- [key: string]: any; // For other potential methods and properties
27
+ [key: string]: any;
24
28
  };
25
29
 
26
30
 
@@ -46,7 +50,7 @@ type Token = {
46
50
  access_token: string,
47
51
  token_type: string,
48
52
  expires_in: number,
49
- refresh: string,
53
+ refresh_token: string,
50
54
  }
51
55
 
52
56
  export const BasicContext = createContext<{
@@ -54,10 +58,11 @@ export const BasicContext = createContext<{
54
58
  isAuthReady: boolean,
55
59
  isSignedIn: boolean,
56
60
  user: User | null,
57
- signout: () => void,
58
- signin: () => void,
61
+ signout: () => Promise<void>,
62
+ signin: () => Promise<void>,
63
+ signinWithCode: (code: string, state?: string) => Promise<{ success: boolean, error?: string }>,
59
64
  getToken: () => Promise<string>,
60
- getSignInLink: () => string,
65
+ getSignInLink: (redirectUri?: string) => Promise<string>,
61
66
  db: any,
62
67
  dbStatus: DBStatus
63
68
  }>({
@@ -65,165 +70,34 @@ export const BasicContext = createContext<{
65
70
  isAuthReady: false,
66
71
  isSignedIn: false,
67
72
  user: null,
68
- signout: () => { },
69
- signin: () => { },
73
+ signout: () => Promise.resolve(),
74
+ signin: () => Promise.resolve(),
75
+ signinWithCode: () => new Promise(() => { }),
70
76
  getToken: () => new Promise(() => { }),
71
- getSignInLink: () => "",
77
+ getSignInLink: () => Promise.resolve(""),
72
78
  db: {},
73
79
  dbStatus: DBStatus.LOADING
74
80
  });
75
81
 
76
- const EmptyDB: BasicSyncType = {
77
- isOpen: false,
78
- collection: () => {
79
- return {
80
- ref: {
81
- toArray: () => [],
82
- count: () => 0
83
- }
84
- }
85
- }
86
- }
87
-
88
- async function getSchemaStatus(schema: any) {
89
- const projectId = schema.project_id
90
- let status = ''
91
- const valid = validateSchema(schema)
92
-
93
- if (!valid.valid) {
94
- console.warn('BasicDB Error: your local schema is invalid. Please fix errors and try again - sync is disabled')
95
- return {
96
- valid: false,
97
- status: 'invalid',
98
- latest: null
99
- }
100
- }
101
-
102
- const latestSchema = await fetch(`https://api.basic.tech/project/${projectId}/schema`)
103
- .then(res => res.json())
104
- .then(data => data.data[0].schema)
105
- .catch(err => {
106
- return {
107
- valid: false,
108
- status: 'error',
109
- latest: null
110
- }
111
- })
112
-
113
- console.log('latestSchema', latestSchema)
114
-
115
- if (!latestSchema.version) {
116
- return {
117
- valid: false,
118
- status: 'error',
119
- latest: null
120
- }
121
- }
122
-
123
- if (latestSchema.version > schema.version) {
124
- // error_code: schema_behind
125
- console.warn('BasicDB Error: your local schema version is behind the latest. Found version:', schema.version, 'but expected', latestSchema.version, " - sync is disabled")
126
- return {
127
- valid: false,
128
- status: 'behind',
129
- latest: latestSchema
130
- }
131
- } else if (latestSchema.version < schema.version) {
132
- // error_code: schema_ahead
133
- console.warn('BasicDB Error: your local schema version is ahead of the latest. Found version:', schema.version, 'but expected', latestSchema.version, " - sync is disabled")
134
- return {
135
- valid: false,
136
- status: 'ahead',
137
- latest: latestSchema
138
- }
139
- } else if (latestSchema.version === schema.version) {
140
- const changes = compareSchemas(schema, latestSchema)
141
- if (changes.valid) {
142
- return {
143
- valid: true,
144
- status: 'current',
145
- latest: latestSchema
146
- }
147
- } else {
148
- // error_code: schema_conflict
149
- 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")
150
- return {
151
- valid: false,
152
- status: 'conflict',
153
- latest: latestSchema
154
- }
155
- }
156
- } else {
157
- return {
158
- valid: false,
159
- status: 'error',
160
- latest: null
161
- }
162
- }
163
- }
164
-
165
-
166
- function getSyncStatus(statusCode: number): string {
167
- switch (statusCode) {
168
- case -1:
169
- return "ERROR";
170
- case 0:
171
- return "OFFLINE";
172
- case 1:
173
- return "CONNECTING";
174
- case 2:
175
- return "ONLINE";
176
- case 3:
177
- return "SYNCING";
178
- case 4:
179
- return "ERROR_WILL_RETRY";
180
- default:
181
- return "UNKNOWN";
182
- }
183
- }
184
-
185
82
  type ErrorObject = {
186
83
  code: string;
187
84
  title: string;
188
85
  message: string;
189
86
  }
190
87
 
191
- async function checkForNewVersion(): Promise<{ hasNewVersion: boolean, latestVersion: string | null, currentVersion: string | null }> {
192
- try {
193
-
194
- const isBeta = currentVersion.includes('beta')
195
-
196
- const response = await fetch(`https://registry.npmjs.org/@basictech/react/${isBeta ? 'beta' : 'latest'}`);
197
- if (!response.ok) {
198
- throw new Error('Failed to fetch version from npm');
199
- }
200
-
201
- const data = await response.json();
202
- const latestVersion = data.version;
203
-
204
- if (latestVersion !== currentVersion) {
205
- console.warn('[basic] New version available:', latestVersion, `\nrun "npm install @basictech/react@${latestVersion}" to update`);
206
- }
207
- if (isBeta) {
208
- log('thank you for being on basictech/react beta :)')
209
- }
210
-
211
- return {
212
- hasNewVersion: currentVersion !== latestVersion,
213
- latestVersion,
214
- currentVersion
215
- };
216
- } catch (error) {
217
- log('Error checking for new version:', error);
218
- return {
219
- hasNewVersion: false,
220
- latestVersion: null,
221
- currentVersion: null
222
- };
223
- }
224
- }
225
-
226
- export function BasicProvider({ children, project_id, schema, debug = false }: { children: React.ReactNode, project_id?: string, schema?: any, debug?: boolean }) {
88
+ export function BasicProvider({
89
+ children,
90
+ project_id,
91
+ schema,
92
+ debug = false,
93
+ storage
94
+ }: {
95
+ children: React.ReactNode,
96
+ project_id?: string,
97
+ schema?: any,
98
+ debug?: boolean,
99
+ storage?: BasicStorage
100
+ }) {
227
101
  const [isAuthReady, setIsAuthReady] = useState(false)
228
102
  const [isSignedIn, setIsSignedIn] = useState<boolean>(false)
229
103
  const [token, setToken] = useState<Token | null>(null)
@@ -233,50 +107,82 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
233
107
 
234
108
  const [dbStatus, setDbStatus] = useState<DBStatus>(DBStatus.OFFLINE)
235
109
  const [error, setError] = useState<ErrorObject | null>(null)
110
+ const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine)
111
+ const [pendingRefresh, setPendingRefresh] = useState<boolean>(false)
236
112
 
237
113
  const syncRef = useRef<BasicSync | null>(null);
114
+ const storageAdapter = storage || new LocalStorageAdapter();
115
+
116
+ const isDevMode = () => isDevelopment(debug)
117
+
118
+ const cleanOAuthParams = () => cleanOAuthParamsFromUrl()
119
+
120
+ useEffect(() => {
121
+ const handleOnline = () => {
122
+ log('Network came back online')
123
+ setIsOnline(true)
124
+ if (pendingRefresh) {
125
+ log('Retrying pending token refresh')
126
+ setPendingRefresh(false)
127
+ if (token) {
128
+ const refreshToken = token.refresh_token || localStorage.getItem('basic_refresh_token')
129
+ if (refreshToken) {
130
+ fetchToken(refreshToken, true).catch(error => {
131
+ log('Retry refresh failed:', error)
132
+ })
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ const handleOffline = () => {
139
+ log('Network went offline')
140
+ setIsOnline(false)
141
+ }
142
+
143
+ window.addEventListener('online', handleOnline)
144
+ window.addEventListener('offline', handleOffline)
145
+
146
+ return () => {
147
+ window.removeEventListener('online', handleOnline)
148
+ window.removeEventListener('offline', handleOffline)
149
+ }
150
+ }, [pendingRefresh, token])
238
151
 
239
152
  useEffect(() => {
240
153
  function initDb(options: { shouldConnect: boolean }) {
241
154
  if (!syncRef.current) {
242
155
  log('Initializing Basic DB')
243
156
  syncRef.current = new BasicSync('basicdb', { schema: schema });
244
-
157
+
245
158
  syncRef.current.syncable.on('statusChanged', (status: number, url: string) => {
246
- setDbStatus(getSyncStatus(status))
247
- })
248
-
249
- syncRef.current.syncable.getStatus().then((status) => {
250
- setDbStatus(getSyncStatus(status))
159
+ setDbStatus(getSyncStatus(status) as DBStatus)
251
160
  })
252
161
 
253
- if (options.shouldConnect) {
162
+ // syncRef.current.syncable.getStatus().then((status: number) => {
163
+ // setDbStatus(getSyncStatus(status) as DBStatus)
164
+ // })
165
+
166
+ if (options.shouldConnect) {
254
167
  setShouldConnect(true)
255
- } else {
168
+ } else {
256
169
  log('Sync is disabled')
257
170
  }
258
171
 
259
172
  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
173
  }
267
174
  }
268
175
 
269
176
  async function checkSchema() {
270
- const valid = validateSchema(schema)
271
- if (!valid.valid) {
272
- log('Basic Schema is invalid!', valid.errors)
273
- console.group('Schema Errors')
177
+ const result = await validateAndCheckSchema(schema)
178
+
179
+ if (!result.isValid) {
274
180
  let errorMessage = ''
275
- valid.errors.forEach((error, index) => {
276
- log(`${index + 1}:`, error.message, ` - at ${error.instancePath}`)
277
- errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}\n`
278
- })
279
- console.groupEnd('Schema Errors')
181
+ if (result.errors) {
182
+ result.errors.forEach((error, index) => {
183
+ errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}\n`
184
+ })
185
+ }
280
186
  setError({
281
187
  code: 'schema_invalid',
282
188
  title: 'Basic Schema is invalid!',
@@ -286,22 +192,13 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
286
192
  return null
287
193
  }
288
194
 
289
-
290
- let schemaStatus = { valid: false }
291
- if (schema.version !== 0) {
292
- schemaStatus = await getSchemaStatus(schema)
293
- log('schemaStatus', schemaStatus)
294
- }else {
295
- log("schema not published - at version 0")
296
- }
297
-
298
- if (schemaStatus.valid) {
195
+ if (result.schemaStatus.valid) {
299
196
  initDb({ shouldConnect: true })
300
197
  } else {
301
- log('Schema is invalid!', schemaStatus)
198
+ log('Schema is invalid!', result.schemaStatus)
302
199
  initDb({ shouldConnect: false })
303
200
  }
304
-
201
+
305
202
  checkForNewVersion()
306
203
  }
307
204
 
@@ -312,46 +209,104 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
312
209
  }
313
210
  }, []);
314
211
 
315
-
316
212
  useEffect(() => {
317
- if (token && syncRef.current && isSignedIn && shouldConnect) {
318
- connectToDb()
319
- }
320
- }, [isSignedIn, shouldConnect])
213
+ async function connectToDb() {
214
+ if (token && syncRef.current && isSignedIn && shouldConnect) {
215
+ const tok = await getToken()
216
+ if (!tok) {
217
+ log('no token found')
218
+ return
219
+ }
321
220
 
322
- useEffect(() => {
323
- localStorage.setItem('basic_debug', debug ? 'true' : 'false')
221
+ log('connecting to db...')
324
222
 
325
- try {
326
- if (window.location.search.includes('code')) {
327
- let code = window.location?.search?.split('code=')[1].split('&')[0]
223
+ syncRef.current?.connect({ access_token: tok })
224
+ .catch((e) => {
225
+ log('error connecting to db', e)
226
+ })
227
+ }
228
+ }
229
+ connectToDb()
328
230
 
329
- const state = localStorage.getItem('basic_auth_state')
330
- if (!state || state !== window.location.search.split('state=')[1].split('&')[0]) {
331
- log('error: auth state does not match')
332
- setIsAuthReady(true)
231
+ }, [isSignedIn, shouldConnect])
333
232
 
334
- localStorage.removeItem('basic_auth_state')
335
- window.history.pushState({}, document.title, "/");
336
- return
337
- }
233
+ useEffect(() => {
234
+ const initializeAuth = async () => {
235
+ await storageAdapter.set(STORAGE_KEYS.DEBUG, debug ? 'true' : 'false')
338
236
 
339
- localStorage.removeItem('basic_auth_state')
237
+ try {
238
+ const versionUpdater = createVersionUpdater(storageAdapter, currentVersion, getMigrations())
239
+ const updateResult = await versionUpdater.checkAndUpdate()
340
240
 
341
- fetchToken(code)
342
- } else {
343
- let cookie_token = getCookie('basic_token')
344
- if (cookie_token !== '') {
345
- setToken(JSON.parse(cookie_token))
346
- } else {
347
- setIsAuthReady(true)
241
+ if (updateResult.updated) {
242
+ log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`)
243
+ } else {
244
+ log(`App version ${updateResult.toVersion} is current`)
348
245
  }
246
+ } catch (error) {
247
+ log('Version update failed:', error)
349
248
  }
350
249
 
250
+ try {
251
+ if (window.location.search.includes('code')) {
252
+ let code = window.location?.search?.split('code=')[1]?.split('&')[0]
253
+ if (!code) return
254
+
255
+ const state = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE)
256
+ const urlState = window.location.search.split('state=')[1]?.split('&')[0]
257
+ if (!state || state !== urlState) {
258
+ log('error: auth state does not match')
259
+ setIsAuthReady(true)
260
+
261
+ await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
262
+ cleanOAuthParams()
263
+ return
264
+ }
265
+
266
+ await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
267
+ cleanOAuthParams()
268
+
269
+ fetchToken(code, false).catch((error) => {
270
+ log('Error fetching token:', error)
271
+ })
272
+ } else {
273
+ const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
274
+ if (refreshToken) {
275
+ log('Found refresh token in storage, attempting to refresh access token')
276
+ fetchToken(refreshToken, true).catch((error) => {
277
+ log('Error fetching refresh token:', error)
278
+ })
279
+ } else {
280
+ let cookie_token = getCookie('basic_token')
281
+ if (cookie_token !== '') {
282
+ const tokenData = JSON.parse(cookie_token)
283
+ setToken(tokenData)
284
+ if (tokenData.refresh_token) {
285
+ await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, tokenData.refresh_token)
286
+ }
287
+ } else {
288
+ const cachedUserInfo = await storageAdapter.get(STORAGE_KEYS.USER_INFO)
289
+ if (cachedUserInfo) {
290
+ try {
291
+ const userData = JSON.parse(cachedUserInfo)
292
+ setUser(userData)
293
+ setIsSignedIn(true)
294
+ log('Loaded cached user info for offline mode')
295
+ } catch (error) {
296
+ log('Error parsing cached user info:', error)
297
+ }
298
+ }
299
+ setIsAuthReady(true)
300
+ }
301
+ }
302
+ }
351
303
 
352
- } catch (e) {
353
- log('error getting cookie', e)
304
+ } catch (e) {
305
+ log('error getting token', e)
306
+ }
354
307
  }
308
+
309
+ initializeAuth()
355
310
  }, [])
356
311
 
357
312
  useEffect(() => {
@@ -368,16 +323,18 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
368
323
 
369
324
  if (user.error) {
370
325
  log('error fetching user', user.error)
371
- // refreshToken()
372
326
  return
373
327
  } 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, "/");
328
+ if (token?.refresh_token) {
329
+ await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token)
379
330
  }
380
-
331
+
332
+ await storageAdapter.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user))
333
+ log('Cached user info in storage')
334
+
335
+ setCookie('basic_access_token', token?.access_token || '', { httpOnly: false });
336
+ setCookie('basic_token', JSON.stringify(token));
337
+
381
338
  setUser(user)
382
339
  setIsSignedIn(true)
383
340
 
@@ -398,77 +355,148 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
398
355
 
399
356
  if (isExpired) {
400
357
  log('token is expired - refreshing ...')
401
- const newToken = await fetchToken(token?.refresh)
402
- fetchUser(newToken.access_token)
358
+ try {
359
+ const newToken = await fetchToken(token?.refresh_token || '', true)
360
+ fetchUser(newToken?.access_token || '')
361
+ } catch (error) {
362
+ log('Failed to refresh token in checkToken:', error)
363
+
364
+ if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
365
+ log('Network issue - continuing with expired token until online')
366
+ fetchUser(token?.access_token || '')
367
+ } else {
368
+ setIsAuthReady(true)
369
+ }
370
+ }
403
371
  } else {
404
- fetchUser(token.access_token)
372
+ fetchUser(token?.access_token || '')
405
373
  }
406
374
  }
407
375
 
408
376
  if (token) {
409
377
  checkToken()
410
- }
378
+ }
411
379
  }, [token])
412
380
 
413
- const connectToDb = async () => {
414
- const tok = await getToken()
415
- if (!tok) {
416
- log('no token found')
417
- return
418
- }
381
+ const getSignInLink = async (redirectUri?: string) => {
382
+ try {
383
+ log('getting sign in link...')
419
384
 
420
- log('connecting to db...')
385
+ if (!project_id) {
386
+ throw new Error('Project ID is required to generate sign-in link')
387
+ }
421
388
 
422
- // TODO: handle if signed out after connect() is already called
389
+ const randomState = Math.random().toString(36).substring(6);
390
+ await storageAdapter.set(STORAGE_KEYS.AUTH_STATE, randomState)
423
391
 
424
- syncRef.current.connect({ access_token: tok })
425
- .catch((e) => {
426
- log('error connecting to db', e)
427
- })
392
+ const redirectUrl = redirectUri || window.location.href
393
+
394
+ if (!redirectUrl || (!redirectUrl.startsWith('http://') && !redirectUrl.startsWith('https://'))) {
395
+ throw new Error('Invalid redirect URI provided')
396
+ }
397
+
398
+ let baseUrl = "https://api.basic.tech/auth/authorize"
399
+ baseUrl += `?client_id=${project_id}`
400
+ baseUrl += `&redirect_uri=${encodeURIComponent(redirectUrl)}`
401
+ baseUrl += `&response_type=code`
402
+ baseUrl += `&scope=profile`
403
+ baseUrl += `&state=${randomState}`
404
+
405
+ log('Generated sign-in link successfully')
406
+ return baseUrl;
407
+
408
+ } catch (error) {
409
+ log('Error generating sign-in link:', error)
410
+ throw error
411
+ }
428
412
  }
429
413
 
430
- const getSignInLink = () => {
431
- log('getting sign in link...')
414
+ const signin = async () => {
415
+ try {
416
+ log('signing in...')
417
+
418
+ if (!project_id) {
419
+ log('Error: project_id is required for sign-in')
420
+ throw new Error('Project ID is required for authentication')
421
+ }
422
+
423
+ const signInLink = await getSignInLink()
424
+ log('Generated sign-in link:', signInLink)
425
+
426
+ if (!signInLink || !signInLink.startsWith('https://')) {
427
+ log('Error: Invalid sign-in link generated')
428
+ throw new Error('Failed to generate valid sign-in URL')
429
+ }
432
430
 
433
- const randomState = Math.random().toString(36).substring(6);
434
- localStorage.setItem('basic_auth_state', randomState)
431
+ window.location.href = signInLink
435
432
 
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}`
433
+ } catch (error) {
434
+ log('Error during sign-in:', error)
442
435
 
443
- return baseUrl;
436
+ if (isDevMode()) {
437
+ setError({
438
+ code: 'signin_error',
439
+ title: 'Sign-in Failed',
440
+ message: (error as Error).message || 'An error occurred during sign-in. Please try again.'
441
+ })
442
+ }
443
+
444
+ throw error
445
+ }
444
446
  }
445
447
 
446
- const signin = () => {
447
- log('signing in: ', getSignInLink())
448
- const signInLink = getSignInLink()
449
- //todo: change to the other thing?
450
- window.location.href = signInLink;
448
+ const signinWithCode = async (code: string, state?: string): Promise<{ success: boolean, error?: string }> => {
449
+ try {
450
+ log('signinWithCode called with code:', code)
451
+
452
+ if (!code || typeof code !== 'string') {
453
+ return { success: false, error: 'Invalid authorization code' }
454
+ }
455
+
456
+ if (state) {
457
+ const storedState = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE)
458
+ if (storedState && storedState !== state) {
459
+ log('State parameter mismatch:', { provided: state, stored: storedState })
460
+ return { success: false, error: 'State parameter mismatch' }
461
+ }
462
+ }
463
+
464
+ await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
465
+ cleanOAuthParams()
466
+
467
+ const token = await fetchToken(code, false)
468
+
469
+ if (token) {
470
+ log('signinWithCode successful')
471
+ return { success: true }
472
+ } else {
473
+ return { success: false, error: 'Failed to exchange code for token' }
474
+ }
475
+ } catch (error) {
476
+ log('signinWithCode error:', error)
477
+ return {
478
+ success: false,
479
+ error: (error as Error).message || 'Authentication failed'
480
+ }
481
+ }
451
482
  }
452
483
 
453
- const signout = () => {
484
+ const signout = async () => {
454
485
  log('signing out!')
455
486
  setUser({})
456
487
  setIsSignedIn(false)
457
488
  setToken(null)
458
- 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
489
 
465
-
466
- // }
490
+ clearCookie('basic_token');
491
+ clearCookie('basic_access_token');
492
+ await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE)
493
+ await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
494
+ await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
467
495
  if (syncRef.current) {
468
496
  (async () => {
469
497
  try {
470
- await syncRef.current.close()
471
- await syncRef.current.delete({disableAutoOpen: false})
498
+ await syncRef.current?.close()
499
+ await syncRef.current?.delete({ disableAutoOpen: false })
472
500
  syncRef.current = null
473
501
  window?.location?.reload()
474
502
  } catch (error) {
@@ -482,6 +510,30 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
482
510
  log('getting token...')
483
511
 
484
512
  if (!token) {
513
+ // Try to recover from storage refresh token
514
+ const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
515
+ if (refreshToken) {
516
+ log('No token in memory, attempting to refresh from storage')
517
+ try {
518
+ const newToken = await fetchToken(refreshToken, true)
519
+ if (newToken?.access_token) {
520
+ return newToken.access_token
521
+ }
522
+ } catch (error) {
523
+ log('Failed to refresh token from storage:', error)
524
+
525
+ if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
526
+ log('Network issue - continuing with potentially expired token')
527
+ const lastToken = localStorage.getItem('basic_access_token')
528
+ if (lastToken) {
529
+ return lastToken
530
+ }
531
+ throw new Error('Network offline - authentication will be retried when online')
532
+ }
533
+
534
+ throw new Error('Authentication expired. Please sign in again.')
535
+ }
536
+ }
485
537
  log('no token found')
486
538
  throw new Error('no token found')
487
539
  }
@@ -491,84 +543,116 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
491
543
 
492
544
  if (isExpired) {
493
545
  log('token is expired - refreshing ...')
494
- const newToken = await fetchToken(token?.refresh)
495
- return newToken?.access_token || ''
496
- }
546
+ const refreshToken = token?.refresh_token || await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN)
547
+ if (refreshToken) {
548
+ try {
549
+ const newToken = await fetchToken(refreshToken, true)
550
+ return newToken?.access_token || ''
551
+ } catch (error) {
552
+ log('Failed to refresh expired token:', error)
497
553
 
498
- return token?.access_token || ''
499
- }
554
+ if ((error as Error).message.includes('offline') || (error as Error).message.includes('Network')) {
555
+ log('Network issue - using expired token until network is restored')
556
+ return token.access_token
557
+ }
500
558
 
501
- function getCookie(name: string) {
502
- let cookieValue = '';
503
- if (document.cookie && document.cookie !== '') {
504
- const cookies = document.cookie.split(';');
505
- for (let i = 0; i < cookies.length; i++) {
506
- const cookie = cookies[i].trim();
507
- if (cookie.substring(0, name.length + 1) === (name + '=')) {
508
- cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
509
- break;
559
+ throw new Error('Authentication expired. Please sign in again.')
510
560
  }
561
+ } else {
562
+ throw new Error('no refresh token available')
511
563
  }
512
564
  }
513
- return cookieValue;
514
- }
515
565
 
516
- 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))
526
-
527
- if (token.error) {
528
- log('error fetching token', token.error)
529
- return
530
- } else {
531
- // log('token', token)
532
- setToken(token)
533
- }
534
- return token
566
+ return token?.access_token || ''
535
567
  }
536
568
 
537
-
538
- const db_ = (tableName: string) => {
539
- const checkSignIn = () => {
540
- if (!isSignedIn) {
541
- throw new Error('cannot use db. user not logged in.')
569
+ const fetchToken = async (codeOrRefreshToken: string, isRefreshToken: boolean = false) => {
570
+ try {
571
+ if (!isOnline) {
572
+ log('Network is offline, marking refresh as pending')
573
+ setPendingRefresh(true)
574
+ throw new Error('Network offline - refresh will be retried when online')
542
575
  }
543
- }
544
576
 
545
- return {
546
- get: async () => {
547
- checkSignIn()
548
- const tok = await getToken()
549
- return get({ projectId: project_id, accountId: user.id, tableName: tableName, token: tok })
550
- },
551
- add: async (value: any) => {
552
- checkSignIn()
553
- const tok = await getToken()
554
- return add({ projectId: project_id, accountId: user.id, tableName: tableName, value: value, token: tok })
555
- },
556
- update: async (id: string, value: any) => {
557
- checkSignIn()
558
- const tok = await getToken()
559
- return update({ projectId: project_id, accountId: user.id, tableName: tableName, id: id, value: value, token: tok })
560
- },
561
- delete: async (id: string) => {
562
- checkSignIn()
563
- const tok = await getToken()
564
- return deleteRecord({ projectId: project_id, accountId: user.id, tableName: tableName, id: id, token: tok })
577
+ const requestBody = isRefreshToken
578
+ ? {
579
+ grant_type: 'refresh_token',
580
+ refresh_token: codeOrRefreshToken
581
+ }
582
+ : {
583
+ grant_type: 'authorization_code',
584
+ code: codeOrRefreshToken
585
+ }
586
+
587
+ const token = await fetch('https://api.basic.tech/auth/token', {
588
+ method: 'POST',
589
+ headers: {
590
+ 'Content-Type': 'application/json'
591
+ },
592
+ body: JSON.stringify(requestBody)
593
+ })
594
+ .then(response => response.json())
595
+ .catch(error => {
596
+ log('Network error fetching token:', error)
597
+ if (!isOnline) {
598
+ setPendingRefresh(true)
599
+ throw new Error('Network offline - refresh will be retried when online')
600
+ }
601
+ throw new Error('Network error during token refresh')
602
+ })
603
+
604
+ if (token.error) {
605
+ log('error fetching token', token.error)
606
+
607
+ if (token.error.includes('network') || token.error.includes('timeout')) {
608
+ setPendingRefresh(true)
609
+ throw new Error('Network issue - refresh will be retried when online')
610
+ }
611
+
612
+ await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
613
+ await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
614
+ clearCookie('basic_token');
615
+ clearCookie('basic_access_token');
616
+
617
+ setUser({})
618
+ setIsSignedIn(false)
619
+ setToken(null)
620
+ setIsAuthReady(true)
621
+
622
+ throw new Error(`Token refresh failed: ${token.error}`)
623
+ } else {
624
+ setToken(token)
625
+ setPendingRefresh(false)
626
+
627
+ if (token.refresh_token) {
628
+ await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token)
629
+ log('Updated refresh token in storage')
630
+ }
631
+
632
+ setCookie('basic_access_token', token.access_token, { httpOnly: false });
633
+ log('Updated access token in cookie')
634
+ }
635
+ return token
636
+ } catch (error) {
637
+ log('Token refresh error:', error)
638
+
639
+ if (!(error as Error).message.includes('offline') && !(error as Error).message.includes('Network')) {
640
+ await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN)
641
+ await storageAdapter.remove(STORAGE_KEYS.USER_INFO)
642
+ clearCookie('basic_token');
643
+ clearCookie('basic_access_token');
644
+
645
+ setUser({})
646
+ setIsSignedIn(false)
647
+ setToken(null)
648
+ setIsAuthReady(true)
565
649
  }
566
650
 
651
+ throw error
567
652
  }
568
-
569
653
  }
570
654
 
571
- const noDb = ({
655
+ const noDb = ({
572
656
  collection: () => {
573
657
  throw new Error('no basicdb found - initialization failed. double check your schema.')
574
658
  }
@@ -582,22 +666,23 @@ export function BasicProvider({ children, project_id, schema, debug = false }: {
582
666
  user,
583
667
  signout,
584
668
  signin,
669
+ signinWithCode,
585
670
  getToken,
586
671
  getSignInLink,
587
672
  db: syncRef.current ? syncRef.current : noDb,
588
673
  dbStatus
589
674
  }}>
590
-
591
- {error && <ErrorDisplay error={error} />}
675
+
676
+ {error && isDevMode() && <ErrorDisplay error={error} />}
592
677
  {isReady && children}
593
678
  </BasicContext.Provider>
594
679
  )
595
680
  }
596
681
 
597
682
  function ErrorDisplay({ error }: { error: ErrorObject }) {
598
- return <div style={{
683
+ return <div style={{
599
684
  position: 'absolute',
600
- top: 20,
685
+ top: 20,
601
686
  left: 20,
602
687
  color: 'black',
603
688
  backgroundColor: '#f8d7da',
@@ -607,19 +692,14 @@ function ErrorDisplay({ error }: { error: ErrorObject }) {
607
692
  maxWidth: '400px',
608
693
  margin: '20px auto',
609
694
  boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
610
- fontFamily: 'monospace',
611
- }}>
612
- <h3 style={{fontSize: '0.8rem', opacity: 0.8}}>code: {error.code}</h3>
613
- <h1 style={{fontSize: '1.2rem', lineHeight: '1.5'}}>{error.title}</h1>
695
+ fontFamily: 'monospace',
696
+ }}>
697
+ <h3 style={{ fontSize: '0.8rem', opacity: 0.8 }}>code: {error.code}</h3>
698
+ <h1 style={{ fontSize: '1.2rem', lineHeight: '1.5' }}>{error.title}</h1>
614
699
  <p>{error.message}</p>
615
700
  </div>
616
701
  }
617
702
 
618
- /*
619
- possible errors:
620
- - projectid missing / invalid
621
- - schema missing / invalid
622
- */
623
703
 
624
704
  export function useBasic() {
625
705
  return useContext(BasicContext);