@blinkdotnew/sdk 0.14.8 → 0.14.10

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.
Files changed (2) hide show
  1. package/README.md +138 -11
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -187,9 +187,13 @@ await blink.auth.updateMe({ displayName: 'New Name' })
187
187
  blink.auth.setToken(jwt, persist?)
188
188
  const isAuth = blink.auth.isAuthenticated()
189
189
 
190
- // Auth state listener
190
+ // Auth state listener (REQUIRED for React apps!)
191
191
  const unsubscribe = blink.auth.onAuthStateChanged((state) => {
192
192
  console.log('Auth state:', state)
193
+ // state.user - current user or null
194
+ // state.isLoading - true while auth is initializing
195
+ // state.isAuthenticated - true if user is logged in
196
+ // state.tokens - current auth tokens
193
197
  })
194
198
  ```
195
199
 
@@ -1275,6 +1279,8 @@ All `{{secret_name}}` placeholders are replaced with encrypted values from your
1275
1279
  The most common mistake is using async functions in useEffect that lose the cleanup function:
1276
1280
 
1277
1281
  ```typescript
1282
+ import type { RealtimeChannel } from '@blinkdotnew/sdk'
1283
+
1278
1284
  // ❌ WRONG - Async function loses cleanup (causes "Subscription cancelled" errors)
1279
1285
  useEffect(() => {
1280
1286
  const initApp = async () => {
@@ -1298,7 +1304,9 @@ useEffect(() => {
1298
1304
  ```typescript
1299
1305
  // ✅ CORRECT - Proper async cleanup handling
1300
1306
  useEffect(() => {
1301
- let channel = null
1307
+ if (!user?.id) return
1308
+
1309
+ let channel: RealtimeChannel | null = null
1302
1310
 
1303
1311
  const initApp = async () => {
1304
1312
  channel = blink.realtime.channel('room')
@@ -1311,35 +1319,119 @@ useEffect(() => {
1311
1319
  return () => {
1312
1320
  channel?.unsubscribe()
1313
1321
  }
1314
- }, [user.id]) // ✅ Only user.id dependency
1322
+ }, [user?.id]) // ✅ Optional chaining in dependency too
1315
1323
  ```
1316
1324
 
1317
1325
  ```typescript
1318
1326
  // ✅ ALTERNATIVE - Using state for cleanup
1319
- const [channel, setChannel] = useState(null)
1327
+ const [channel, setChannel] = useState<RealtimeChannel | null>(null)
1320
1328
 
1321
1329
  useEffect(() => {
1330
+ if (!user?.id) return
1331
+
1322
1332
  const initApp = async () => {
1323
1333
  const ch = blink.realtime.channel('room')
1324
1334
  await ch.subscribe({ userId: user.id })
1325
1335
  setChannel(ch)
1326
1336
  }
1327
1337
  initApp().catch(console.error)
1328
- }, [user.id])
1338
+ }, [user?.id])
1329
1339
 
1330
1340
  useEffect(() => {
1331
1341
  return () => channel?.unsubscribe()
1332
1342
  }, [channel])
1333
1343
  ```
1334
1344
 
1345
+ ```typescript
1346
+ // ✅ COMPLETE EXAMPLE - With proper loading states
1347
+ function MyRealtimeComponent() {
1348
+ const [user, setUser] = useState(null)
1349
+ const [messages, setMessages] = useState([])
1350
+
1351
+ // Auth state management
1352
+ useEffect(() => {
1353
+ const unsubscribe = blink.auth.onAuthStateChanged((state) => {
1354
+ setUser(state.user)
1355
+ })
1356
+ return unsubscribe
1357
+ }, [])
1358
+
1359
+ // Guard clause - prevent rendering if user not loaded
1360
+ if (!user) return <div>Loading...</div>
1361
+
1362
+ // Now safe to use user.id everywhere
1363
+ useEffect(() => {
1364
+ if (!user?.id) return
1365
+
1366
+ let channel: RealtimeChannel | null = null
1367
+
1368
+ const initApp = async () => {
1369
+ channel = blink.realtime.channel('room')
1370
+ await channel.subscribe({ userId: user.id })
1371
+
1372
+ channel.onMessage((message) => {
1373
+ setMessages(prev => [...prev, message])
1374
+ })
1375
+ }
1376
+
1377
+ initApp().catch(console.error)
1378
+
1379
+ return () => {
1380
+ channel?.unsubscribe()
1381
+ }
1382
+ }, [user?.id])
1383
+
1384
+ return <div>Welcome {user.email}! Messages: {messages.length}</div>
1385
+ }
1386
+ ```
1387
+
1335
1388
  **Rules:**
1336
1389
  1. **Never return cleanup from async functions** - useEffect cleanup must be synchronous
1337
- 2. **useEffect dependency**: `[user.id]` not `[user]` to avoid reconnections
1390
+ 2. **useEffect dependency**: `[user?.id]` not `[user]` to avoid reconnections
1338
1391
  3. **Store channel reference** outside async function for cleanup access
1339
- 4. **Zero connection management**: SDK handles all connection states automatically
1392
+ 4. **Add component-level guards** - Check `if (!user) return <Loading />` before rendering
1393
+ 5. **Zero connection management**: SDK handles all connection states automatically
1340
1394
 
1341
1395
  ### React
1342
1396
 
1397
+ **⚠️ Critical: Always Use Auth State Listener, Never One-Time Checks**
1398
+
1399
+ The most common authentication mistake is checking auth status once instead of listening to changes:
1400
+
1401
+ ```typescript
1402
+ // ❌ WRONG - One-time check misses auth completion
1403
+ useEffect(() => {
1404
+ const checkAuth = async () => {
1405
+ try {
1406
+ const userData = await blink.auth.me()
1407
+ setUser(userData)
1408
+ } catch (error) {
1409
+ console.error('Auth check failed:', error)
1410
+ } finally {
1411
+ setLoading(false)
1412
+ }
1413
+ }
1414
+ checkAuth() // Only runs once - misses when auth completes later!
1415
+ }, [])
1416
+
1417
+ // ✅ CORRECT - Listen to auth state changes
1418
+ useEffect(() => {
1419
+ const unsubscribe = blink.auth.onAuthStateChanged((state) => {
1420
+ setUser(state.user)
1421
+ setLoading(state.isLoading)
1422
+ })
1423
+ return unsubscribe
1424
+ }, [])
1425
+ ```
1426
+
1427
+ **Why the one-time check fails:**
1428
+ 1. App loads → `blink.auth.me()` called immediately
1429
+ 2. Auth still initializing → Call fails, user set to `null`
1430
+ 3. Auth completes later → App never knows because it only checked once
1431
+ 4. User stuck on "Please sign in" screen forever
1432
+
1433
+ **Always use `onAuthStateChanged()` for React apps!**
1434
+
1343
1435
  ```typescript
1344
1436
  import { createClient } from '@blinkdotnew/sdk'
1345
1437
  import { useState, useEffect } from 'react'
@@ -1348,19 +1440,46 @@ const blink = createClient({ projectId: 'your-project', authRequired: true })
1348
1440
 
1349
1441
  function App() {
1350
1442
  const [user, setUser] = useState(null)
1443
+ const [loading, setLoading] = useState(true)
1351
1444
 
1352
1445
  useEffect(() => {
1353
1446
  const unsubscribe = blink.auth.onAuthStateChanged((state) => {
1354
1447
  setUser(state.user)
1448
+ setLoading(state.isLoading)
1355
1449
  })
1356
1450
  return unsubscribe
1357
1451
  }, [])
1358
1452
 
1453
+ if (loading) return <div>Loading...</div>
1359
1454
  if (!user) return <div>Please log in</div>
1360
1455
 
1361
1456
  return <div>Welcome, {user.email}!</div>
1362
1457
  }
1363
1458
 
1459
+ // ❌ WRONG - One-time auth check (misses auth state changes)
1460
+ function BadApp() {
1461
+ const [user, setUser] = useState(null)
1462
+ const [loading, setLoading] = useState(true)
1463
+
1464
+ useEffect(() => {
1465
+ const checkAuth = async () => {
1466
+ try {
1467
+ const userData = await blink.auth.me()
1468
+ setUser(userData)
1469
+ } catch (error) {
1470
+ console.error('Auth check failed:', error)
1471
+ } finally {
1472
+ setLoading(false)
1473
+ }
1474
+ }
1475
+
1476
+ checkAuth() // ❌ Only runs once - misses when auth completes later!
1477
+ }, [])
1478
+
1479
+ // This pattern causes users to get stuck on "Please sign in"
1480
+ // even after authentication completes
1481
+ }
1482
+
1364
1483
  // React example with search functionality
1365
1484
  function SearchResults() {
1366
1485
  const [query, setQuery] = useState('')
@@ -1462,15 +1581,23 @@ function RealtimeChat() {
1462
1581
  const [onlineUsers, setOnlineUsers] = useState([])
1463
1582
  const [user] = useState({ id: 'user123', name: 'John Doe' }) // From auth
1464
1583
 
1584
+ // Guard clause - prevent rendering if user not loaded
1585
+ if (!user) return <div>Loading...</div>
1586
+
1587
+ const userRef = useRef(user)
1588
+ useEffect(() => { userRef.current = user }, [user])
1589
+
1465
1590
  useEffect(() => {
1466
- let channel = null
1591
+ if (!user?.id) return
1592
+
1593
+ let channel: RealtimeChannel | null = null
1467
1594
 
1468
1595
  // Subscribe and listen for messages
1469
1596
  const setupRealtime = async () => {
1470
1597
  channel = blink.realtime.channel('chat-room')
1471
1598
  await channel.subscribe({
1472
- userId: user.id,
1473
- metadata: { displayName: user.name, avatar: '/avatar.png' }
1599
+ userId: userRef.current.id,
1600
+ metadata: { displayName: userRef.current.name, avatar: '/avatar.png' }
1474
1601
  })
1475
1602
 
1476
1603
  // Listen for new messages
@@ -1513,7 +1640,7 @@ function RealtimeChat() {
1513
1640
  return () => {
1514
1641
  channel?.unsubscribe()
1515
1642
  }
1516
- }, [user.id, user.name])
1643
+ }, [user?.id]) // ✅ Optional chaining in dependency
1517
1644
 
1518
1645
  const sendMessage = async () => {
1519
1646
  if (!newMessage.trim()) return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blinkdotnew/sdk",
3
- "version": "0.14.8",
3
+ "version": "0.14.10",
4
4
  "description": "Blink TypeScript SDK for client-side applications - Zero-boilerplate CRUD + auth + AI + analytics + notifications for modern SaaS/AI apps",
5
5
  "keywords": [
6
6
  "blink",