@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.
- package/README.md +138 -11
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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. **
|
|
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
|
-
|
|
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:
|
|
1473
|
-
metadata: { displayName:
|
|
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
|
|
1643
|
+
}, [user?.id]) // ✅ Optional chaining in dependency
|
|
1517
1644
|
|
|
1518
1645
|
const sendMessage = async () => {
|
|
1519
1646
|
if (!newMessage.trim()) return
|
package/package.json
CHANGED