spaps-mcp 0.1.0
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/CHANGELOG.md +7 -0
- package/README.md +76 -0
- package/dist/chunk-GPBTYWDD.js +496 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +9 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +26 -0
- package/dist/resources/SPAPS_SURFACE_CONTRACT.md +125 -0
- package/dist/resources/glossary.md +36 -0
- package/dist/resources/llms.txt +15 -0
- package/dist/wizard/step-01-setup.md +149 -0
- package/dist/wizard/step-02-environment.md +291 -0
- package/dist/wizard/step-03-sdk-init.md +351 -0
- package/dist/wizard/step-04-email-auth.md +311 -0
- package/dist/wizard/step-05-wallet-auth.md +368 -0
- package/dist/wizard/step-06-magic-link.md +560 -0
- package/dist/wizard/step-07-payments.md +529 -0
- package/dist/wizard/step-08-whitelist.md +338 -0
- package/dist/wizard/step-09-admin.md +579 -0
- package/dist/wizard/step-10-errors.md +525 -0
- package/dist/wizard/step-11-ui-polish.md +640 -0
- package/dist/wizard/step-12-testing.md +588 -0
- package/dist/wizard/wizard.lock +67 -0
- package/package.json +66 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
# SPAPS Integration Step 10/12: Error Handling & Token Management
|
|
2
|
+
|
|
3
|
+
## Prerequisites Check
|
|
4
|
+
|
|
5
|
+
Before starting, confirm you have completed:
|
|
6
|
+
- ✅ Step 1-9: Full implementation with auth, payments, admin
|
|
7
|
+
- ✅ TokenManager being used for storage
|
|
8
|
+
- ✅ Basic error handling in place
|
|
9
|
+
|
|
10
|
+
## Required TodoWrite List
|
|
11
|
+
|
|
12
|
+
Create a TodoWrite with EXACTLY these items:
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
TodoWrite({
|
|
16
|
+
todos: [
|
|
17
|
+
{ content: "Import SweetPotatoAPIError from spaps-sdk", status: "pending", activeForm: "Importing error class" },
|
|
18
|
+
{ content: "Create centralized error handler", status: "pending", activeForm: "Creating error handler" },
|
|
19
|
+
{ content: "Add specific error code handling", status: "pending", activeForm: "Adding error code handling" },
|
|
20
|
+
{ content: "Implement TokenManager.isTokenExpired checks", status: "pending", activeForm: "Implementing token expiry checks" },
|
|
21
|
+
{ content: "Setup auto-refresh properly", status: "pending", activeForm: "Setting up auto-refresh" },
|
|
22
|
+
{ content: "Add toast notifications for all errors", status: "pending", activeForm: "Adding toast notifications" },
|
|
23
|
+
{ content: "Create error boundary component", status: "pending", activeForm: "Creating error boundary" },
|
|
24
|
+
{ content: "Request step 11 of SPAPS integration wizard", status: "pending", activeForm: "Requesting next step" }
|
|
25
|
+
]
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Comprehensive Error Handling
|
|
30
|
+
|
|
31
|
+
### 1. Centralized Error Handler
|
|
32
|
+
|
|
33
|
+
Create a unified error handling utility:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// lib/error-handler.ts
|
|
37
|
+
import { SweetPotatoAPIError } from 'spaps-sdk'
|
|
38
|
+
import { toast } from 'sonner'
|
|
39
|
+
|
|
40
|
+
export interface ErrorContext {
|
|
41
|
+
operation?: string
|
|
42
|
+
userId?: string
|
|
43
|
+
details?: any
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class ErrorHandler {
|
|
47
|
+
private static errorCounts: Map<string, number> = new Map()
|
|
48
|
+
private static readonly MAX_ERRORS_BEFORE_RELOAD = 5
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Handle any error with appropriate user feedback
|
|
52
|
+
*/
|
|
53
|
+
static handle(error: any, context?: ErrorContext): void {
|
|
54
|
+
console.error(`Error in ${context?.operation || 'unknown operation'}:`, error)
|
|
55
|
+
|
|
56
|
+
if (error instanceof SweetPotatoAPIError) {
|
|
57
|
+
this.handleAPIError(error, context)
|
|
58
|
+
} else if (error?.code) {
|
|
59
|
+
this.handleCodedError(error, context)
|
|
60
|
+
} else if (error?.message) {
|
|
61
|
+
this.handleGenericError(error, context)
|
|
62
|
+
} else {
|
|
63
|
+
this.handleUnknownError(error, context)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Track error frequency
|
|
67
|
+
this.trackError(error)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Handle SweetPotatoAPIError specifically
|
|
72
|
+
*/
|
|
73
|
+
private static handleAPIError(error: SweetPotatoAPIError, context?: ErrorContext): void {
|
|
74
|
+
const errorMessages: Record<string, string> = {
|
|
75
|
+
// Authentication Errors
|
|
76
|
+
'INVALID_CREDENTIALS': 'Invalid email or password',
|
|
77
|
+
'USER_NOT_FOUND': 'No account found with these credentials',
|
|
78
|
+
'EMAIL_NOT_VERIFIED': 'Please verify your email before signing in',
|
|
79
|
+
'ACCOUNT_SUSPENDED': 'Your account has been suspended',
|
|
80
|
+
'ACCOUNT_DELETED': 'This account no longer exists',
|
|
81
|
+
|
|
82
|
+
// Authorization Errors
|
|
83
|
+
'UNAUTHORIZED': 'You must be logged in to do that',
|
|
84
|
+
'FORBIDDEN': 'You don\'t have permission to do that',
|
|
85
|
+
'INSUFFICIENT_PERMISSIONS': 'Additional permissions required',
|
|
86
|
+
'ADMIN_ONLY': 'Admin access required',
|
|
87
|
+
|
|
88
|
+
// Token Errors
|
|
89
|
+
'TOKEN_EXPIRED': 'Your session has expired. Please sign in again.',
|
|
90
|
+
'TOKEN_INVALID': 'Invalid authentication token',
|
|
91
|
+
'TOKEN_REVOKED': 'Your session has been revoked',
|
|
92
|
+
'REFRESH_TOKEN_EXPIRED': 'Please sign in again to continue',
|
|
93
|
+
|
|
94
|
+
// Rate Limiting
|
|
95
|
+
'RATE_LIMITED': 'Too many requests. Please slow down.',
|
|
96
|
+
'QUOTA_EXCEEDED': 'You\'ve exceeded your usage quota',
|
|
97
|
+
|
|
98
|
+
// Payment Errors
|
|
99
|
+
'PAYMENT_REQUIRED': 'Payment required to continue',
|
|
100
|
+
'CARD_DECLINED': 'Your card was declined',
|
|
101
|
+
'INSUFFICIENT_FUNDS': 'Insufficient funds for this transaction',
|
|
102
|
+
'SUBSCRIPTION_EXPIRED': 'Your subscription has expired',
|
|
103
|
+
'PAYMENT_FAILED': 'Payment processing failed',
|
|
104
|
+
|
|
105
|
+
// Wallet Errors
|
|
106
|
+
'INVALID_SIGNATURE': 'Invalid wallet signature',
|
|
107
|
+
'WALLET_NOT_FOUND': 'Wallet not registered',
|
|
108
|
+
'WALLET_ALREADY_LINKED': 'This wallet is already linked to another account',
|
|
109
|
+
'CHAIN_NOT_SUPPORTED': 'This blockchain is not supported',
|
|
110
|
+
|
|
111
|
+
// Validation Errors
|
|
112
|
+
'INVALID_EMAIL': 'Please enter a valid email address',
|
|
113
|
+
'INVALID_PASSWORD': 'Password must be at least 8 characters',
|
|
114
|
+
'INVALID_INPUT': 'Please check your input and try again',
|
|
115
|
+
'MISSING_REQUIRED_FIELD': 'Please fill in all required fields',
|
|
116
|
+
|
|
117
|
+
// Server Errors
|
|
118
|
+
'INTERNAL_ERROR': 'Something went wrong. Please try again.',
|
|
119
|
+
'SERVICE_UNAVAILABLE': 'Service temporarily unavailable',
|
|
120
|
+
'NETWORK_ERROR': 'Network connection failed',
|
|
121
|
+
'DATABASE_ERROR': 'Database operation failed',
|
|
122
|
+
|
|
123
|
+
// Business Logic Errors
|
|
124
|
+
'DUPLICATE_ENTRY': 'This already exists',
|
|
125
|
+
'NOT_FOUND': 'The requested item was not found',
|
|
126
|
+
'CONFLICT': 'This conflicts with existing data',
|
|
127
|
+
'PRECONDITION_FAILED': 'Preconditions not met'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const message = errorMessages[error.code || ''] || error.message || 'An error occurred'
|
|
131
|
+
|
|
132
|
+
// Show toast based on error severity
|
|
133
|
+
if (error.status && error.status >= 500) {
|
|
134
|
+
toast.error(message, {
|
|
135
|
+
description: 'Please try again later',
|
|
136
|
+
duration: 5000
|
|
137
|
+
})
|
|
138
|
+
} else if (error.code === 'TOKEN_EXPIRED') {
|
|
139
|
+
toast.error(message, {
|
|
140
|
+
action: {
|
|
141
|
+
label: 'Sign In',
|
|
142
|
+
onClick: () => window.location.href = '/auth'
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
} else {
|
|
146
|
+
toast.error(message)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Handle errors with codes (like MetaMask errors)
|
|
152
|
+
*/
|
|
153
|
+
private static handleCodedError(error: any, context?: ErrorContext): void {
|
|
154
|
+
const walletErrors: Record<number, string> = {
|
|
155
|
+
4001: 'Transaction rejected by user',
|
|
156
|
+
4100: 'The requested account and/or method has not been authorized',
|
|
157
|
+
4200: 'The requested method is not supported',
|
|
158
|
+
4900: 'Provider is disconnected',
|
|
159
|
+
4901: 'Provider is disconnected from all chains',
|
|
160
|
+
'-32002': 'Request already pending in wallet',
|
|
161
|
+
'-32003': 'Transaction rejected',
|
|
162
|
+
'-32603': 'Internal wallet error'
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const message = walletErrors[error.code] || error.message || 'Operation failed'
|
|
166
|
+
toast.error(message)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Handle generic errors with messages
|
|
171
|
+
*/
|
|
172
|
+
private static handleGenericError(error: any, context?: ErrorContext): void {
|
|
173
|
+
toast.error(error.message || 'An unexpected error occurred')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Handle completely unknown errors
|
|
178
|
+
*/
|
|
179
|
+
private static handleUnknownError(error: any, context?: ErrorContext): void {
|
|
180
|
+
toast.error('An unexpected error occurred. Please try again.')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Track error frequency and reload if too many
|
|
185
|
+
*/
|
|
186
|
+
private static trackError(error: any): void {
|
|
187
|
+
const key = error?.code || error?.message || 'unknown'
|
|
188
|
+
const count = (this.errorCounts.get(key) || 0) + 1
|
|
189
|
+
this.errorCounts.set(key, count)
|
|
190
|
+
|
|
191
|
+
if (count >= this.MAX_ERRORS_BEFORE_RELOAD) {
|
|
192
|
+
toast.error('Multiple errors detected. Reloading page...', {
|
|
193
|
+
duration: 3000,
|
|
194
|
+
onAutoClose: () => window.location.reload()
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Clear error tracking
|
|
201
|
+
*/
|
|
202
|
+
static clearErrors(): void {
|
|
203
|
+
this.errorCounts.clear()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### 2. Advanced Token Management
|
|
209
|
+
|
|
210
|
+
Enhance token management with expiry checks:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// lib/token-manager-enhanced.ts
|
|
214
|
+
import { TokenManager, sdk } from '@/lib/spaps'
|
|
215
|
+
|
|
216
|
+
export class TokenManagerEnhanced {
|
|
217
|
+
private static refreshPromise: Promise<boolean> | null = null
|
|
218
|
+
private static refreshInterval: NodeJS.Timeout | null = null
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Initialize auto-refresh with proper cleanup
|
|
222
|
+
*/
|
|
223
|
+
static startAutoRefresh(): void {
|
|
224
|
+
// Clear any existing interval
|
|
225
|
+
this.stopAutoRefresh()
|
|
226
|
+
|
|
227
|
+
// Check token every 5 minutes
|
|
228
|
+
this.refreshInterval = setInterval(() => {
|
|
229
|
+
this.checkAndRefreshToken()
|
|
230
|
+
}, 5 * 60 * 1000)
|
|
231
|
+
|
|
232
|
+
// Do initial check
|
|
233
|
+
this.checkAndRefreshToken()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Stop auto-refresh
|
|
238
|
+
*/
|
|
239
|
+
static stopAutoRefresh(): void {
|
|
240
|
+
if (this.refreshInterval) {
|
|
241
|
+
clearInterval(this.refreshInterval)
|
|
242
|
+
this.refreshInterval = null
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check token expiry and refresh if needed
|
|
248
|
+
*/
|
|
249
|
+
static async checkAndRefreshToken(): Promise<boolean> {
|
|
250
|
+
// Prevent multiple simultaneous refreshes
|
|
251
|
+
if (this.refreshPromise) {
|
|
252
|
+
return this.refreshPromise
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const accessToken = TokenManager.getAccessToken()
|
|
256
|
+
if (!accessToken) {
|
|
257
|
+
return false
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Check if token will expire in next 10 minutes
|
|
261
|
+
const tokenExpired = TokenManager.isTokenExpired(accessToken)
|
|
262
|
+
const tokenExpiringInfo = this.isTokenExpiringSoon(accessToken, 10 * 60)
|
|
263
|
+
|
|
264
|
+
if (tokenExpired || tokenExpiringInfo.expiringSoon) {
|
|
265
|
+
console.log(`Token ${tokenExpired ? 'expired' : 'expiring soon'}, refreshing...`)
|
|
266
|
+
|
|
267
|
+
this.refreshPromise = this.refreshToken()
|
|
268
|
+
const result = await this.refreshPromise
|
|
269
|
+
this.refreshPromise = null
|
|
270
|
+
|
|
271
|
+
return result
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return true
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Refresh the token
|
|
279
|
+
*/
|
|
280
|
+
private static async refreshToken(): Promise<boolean> {
|
|
281
|
+
const refreshToken = TokenManager.getRefreshToken()
|
|
282
|
+
if (!refreshToken) {
|
|
283
|
+
console.error('No refresh token available')
|
|
284
|
+
this.handleTokenRefreshFailure()
|
|
285
|
+
return false
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const newTokens = await sdk.auth.refreshToken(refreshToken)
|
|
290
|
+
TokenManager.storeTokens(newTokens)
|
|
291
|
+
console.log('Token refreshed successfully')
|
|
292
|
+
return true
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error('Token refresh failed:', error)
|
|
295
|
+
this.handleTokenRefreshFailure()
|
|
296
|
+
return false
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Check if token is expiring soon
|
|
302
|
+
*/
|
|
303
|
+
private static isTokenExpiringSoon(
|
|
304
|
+
token: string,
|
|
305
|
+
thresholdSeconds: number = 600
|
|
306
|
+
): { expiringSoon: boolean; remainingSeconds?: number } {
|
|
307
|
+
try {
|
|
308
|
+
const parts = token.split('.')
|
|
309
|
+
if (parts.length !== 3) {
|
|
310
|
+
return { expiringSoon: true }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const payload = JSON.parse(atob(parts[1]))
|
|
314
|
+
const expirationTime = payload.exp * 1000
|
|
315
|
+
const currentTime = Date.now()
|
|
316
|
+
const remainingTime = expirationTime - currentTime
|
|
317
|
+
const remainingSeconds = Math.floor(remainingTime / 1000)
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
expiringSoon: remainingSeconds < thresholdSeconds,
|
|
321
|
+
remainingSeconds
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
return { expiringSoon: true }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Handle token refresh failure
|
|
330
|
+
*/
|
|
331
|
+
private static handleTokenRefreshFailure(): void {
|
|
332
|
+
TokenManager.clearTokens()
|
|
333
|
+
this.stopAutoRefresh()
|
|
334
|
+
|
|
335
|
+
// Redirect to login
|
|
336
|
+
if (typeof window !== 'undefined') {
|
|
337
|
+
window.location.href = '/auth?error=session_expired'
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### 3. React Error Boundary
|
|
344
|
+
|
|
345
|
+
Create an error boundary for React components:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// components/error-boundary.tsx
|
|
349
|
+
'use client'
|
|
350
|
+
|
|
351
|
+
import React, { Component, ReactNode } from 'react'
|
|
352
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
353
|
+
import { Button } from "@/components/ui/button"
|
|
354
|
+
import { AlertTriangle } from 'lucide-react'
|
|
355
|
+
|
|
356
|
+
interface Props {
|
|
357
|
+
children: ReactNode
|
|
358
|
+
fallback?: ReactNode
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
interface State {
|
|
362
|
+
hasError: boolean
|
|
363
|
+
error: Error | null
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
367
|
+
constructor(props: Props) {
|
|
368
|
+
super(props)
|
|
369
|
+
this.state = { hasError: false, error: null }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
static getDerivedStateFromError(error: Error): State {
|
|
373
|
+
return { hasError: true, error }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
componentDidCatch(error: Error, errorInfo: any) {
|
|
377
|
+
console.error('Error boundary caught:', error, errorInfo)
|
|
378
|
+
|
|
379
|
+
// Log to error reporting service
|
|
380
|
+
if (typeof window !== 'undefined') {
|
|
381
|
+
// Example: Send to Sentry, LogRocket, etc.
|
|
382
|
+
// window.Sentry?.captureException(error)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
handleReset = () => {
|
|
387
|
+
this.setState({ hasError: false, error: null })
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
render() {
|
|
391
|
+
if (this.state.hasError) {
|
|
392
|
+
if (this.props.fallback) {
|
|
393
|
+
return this.props.fallback
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
<div className="min-h-screen flex items-center justify-center p-4">
|
|
398
|
+
<Card className="w-full max-w-md">
|
|
399
|
+
<CardHeader>
|
|
400
|
+
<CardTitle className="flex items-center gap-2 text-red-600">
|
|
401
|
+
<AlertTriangle className="h-5 w-5" />
|
|
402
|
+
Something went wrong
|
|
403
|
+
</CardTitle>
|
|
404
|
+
<CardDescription>
|
|
405
|
+
An unexpected error occurred. Please try again.
|
|
406
|
+
</CardDescription>
|
|
407
|
+
</CardHeader>
|
|
408
|
+
<CardContent className="space-y-4">
|
|
409
|
+
{process.env.NODE_ENV === 'development' && this.state.error && (
|
|
410
|
+
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
|
411
|
+
<p className="text-xs font-mono text-red-800 dark:text-red-200">
|
|
412
|
+
{this.state.error.message}
|
|
413
|
+
</p>
|
|
414
|
+
</div>
|
|
415
|
+
)}
|
|
416
|
+
<div className="flex gap-2">
|
|
417
|
+
<Button onClick={this.handleReset} variant="outline">
|
|
418
|
+
Try Again
|
|
419
|
+
</Button>
|
|
420
|
+
<Button onClick={() => window.location.href = '/'}>
|
|
421
|
+
Go Home
|
|
422
|
+
</Button>
|
|
423
|
+
</div>
|
|
424
|
+
</CardContent>
|
|
425
|
+
</Card>
|
|
426
|
+
</div>
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return this.props.children
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### 4. Usage Examples
|
|
436
|
+
|
|
437
|
+
Using the error handler in components:
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
// In your components
|
|
441
|
+
import { ErrorHandler } from '@/lib/error-handler'
|
|
442
|
+
|
|
443
|
+
// Example usage
|
|
444
|
+
try {
|
|
445
|
+
const result = await sdk.auth.signInWithPassword(credentials)
|
|
446
|
+
// Success handling
|
|
447
|
+
} catch (error) {
|
|
448
|
+
ErrorHandler.handle(error, {
|
|
449
|
+
operation: 'signInWithPassword',
|
|
450
|
+
details: { email: credentials.email }
|
|
451
|
+
})
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Using enhanced token manager
|
|
455
|
+
import { TokenManagerEnhanced } from '@/lib/token-manager-enhanced'
|
|
456
|
+
|
|
457
|
+
// Start auto-refresh when user logs in
|
|
458
|
+
TokenManagerEnhanced.startAutoRefresh()
|
|
459
|
+
|
|
460
|
+
// Stop when user logs out
|
|
461
|
+
TokenManagerEnhanced.stopAutoRefresh()
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Validation Checklist
|
|
465
|
+
|
|
466
|
+
✅ **Error Handling**:
|
|
467
|
+
- SweetPotatoAPIError imported and used
|
|
468
|
+
- Specific error codes handled
|
|
469
|
+
- User-friendly messages shown
|
|
470
|
+
- Toast notifications for all errors
|
|
471
|
+
|
|
472
|
+
✅ **Token Management**:
|
|
473
|
+
- TokenManager.isTokenExpired used
|
|
474
|
+
- Auto-refresh implemented
|
|
475
|
+
- Refresh failures handled
|
|
476
|
+
- Cleanup on logout
|
|
477
|
+
|
|
478
|
+
✅ **Error Boundary**:
|
|
479
|
+
- Error boundary wraps app
|
|
480
|
+
- Fallback UI provided
|
|
481
|
+
- Development errors shown
|
|
482
|
+
- Reset functionality works
|
|
483
|
+
|
|
484
|
+
## Common Mistakes to Avoid
|
|
485
|
+
|
|
486
|
+
❌ **Generic error messages**:
|
|
487
|
+
```typescript
|
|
488
|
+
// WRONG - Not helpful
|
|
489
|
+
catch (error) {
|
|
490
|
+
toast.error('Error occurred')
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
❌ **Not checking token expiry**:
|
|
495
|
+
```typescript
|
|
496
|
+
// WRONG - Token might be expired
|
|
497
|
+
const token = TokenManager.getAccessToken()
|
|
498
|
+
// Use without checking expiry
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
❌ **Missing error boundaries**:
|
|
502
|
+
```typescript
|
|
503
|
+
// WRONG - No error boundary
|
|
504
|
+
<App /> // Uncaught errors crash app
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
## Next Step
|
|
508
|
+
|
|
509
|
+
When ALL todos are ✅ complete:
|
|
510
|
+
|
|
511
|
+
```javascript
|
|
512
|
+
mcp__product-manager__get_agent_instructions({
|
|
513
|
+
category: "spaps-integration",
|
|
514
|
+
project: "spaps-demo",
|
|
515
|
+
step: 11
|
|
516
|
+
})
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
## Summary
|
|
520
|
+
|
|
521
|
+
Comprehensive error handling provides:
|
|
522
|
+
- **Better UX** - Clear, actionable error messages
|
|
523
|
+
- **Resilience** - App doesn't crash on errors
|
|
524
|
+
- **Security** - Tokens managed properly
|
|
525
|
+
- **Debugging** - Detailed error tracking
|