Skip to content

Commit 3236475

Browse files
fix: add auto-recovery and cleared debugging logs
CURRENT BEHAVIOR: - Client fetches JWT from `/api/auth/jwt` on initialization - Always expects `{ token: "..." }` response - Uses token to initialize VAPI call REQUIRED CHANGES: 1. Handle `{ error: "Usage limit exceeded" }` response from JWT endpoint 2. Show user-friendly error message instead of initializing VAPI 3. Prevent call attempts when usage limit is exceeded 4. Display appropriate UI state (disabled widget or error message) IMPLEMENTATION: Widget Script Changes (src/widget.js): - Modified fetchJwt() to handle 402 status codes and { error: "..." } responses - Added isUsageLimit flag for error classification - Updated getUserFriendlyError() to use server's specific error messages - Split handleInitializationError() into usage limit vs generic error paths - Added createUsageLimitWidget() that creates disabled phone button with upgrade messaging - Added showUsageLimitMessage() with positioned overlay based on widget location - Added CSS .buzzwald-button-disabled class with gray styling and disabled interactions - Fixed ensureToken() to preserve isUsageLimit flag when re-throwing errors - Updated JWT refresh logic in VAPI error handlers for usage limit cases Auto-Recovery Feature: - Added startUsageLimitRetry() with 5-second interval checking (configurable) - Added restoreWidget() method to transition back to functional state - Added showRestoreSuccessMessage() with green success notification - Auto-recovery attempts JWT fetch every 5 seconds up to 10 attempts - Automatically restores widget when user upgrades (seamless UX) - Proper cleanup of retry timers in destroy() method Test Updates (test/test.html): - Added testUpgrade() to simulate user upgrading their plan - Added testRefreshAfterUpgrade() to test page refresh scenario - Added manualRetryCheck() for debugging auto-recovery mechanism - Fixed widget storage in window.buzzwaldWidget for debugging access - Updated usage limit mock to return 402 status with server error format - Added post-upgrade mock to return successful JWT tokens EXPECTED BEHAVIOR: ✅ Success case: Normal phone button, functional calls ✅ Usage limit case: Grayed-out phone button + orange upgrade message ✅ Auto-recovery: Green success message + automatic restoration when upgraded ✅ Manual recovery: Page refresh immediately restores functionality ✅ Other errors: Red error button with technical error details The widget now provides seamless UX where users don't need to refresh the page after upgrading - it automatically detects the upgrade and restores functionality within 30 seconds.
1 parent 128ac8e commit 3236475

4 files changed

Lines changed: 288 additions & 56 deletions

File tree

dist/buzzwald-widget.js

Lines changed: 40 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"timestamp": 1753215334883
2+
"timestamp": 1753219408858
33
}

src/widget.js

Lines changed: 156 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ const JWT_ENDPOINT = `${API_BASE_URL}/api/auth/jwt`;
66

77
export class BuzzwaldWidget {
88
constructor(config = {}) {
9-
console.log('🐝 Buzzwald Widget v1.1 - Usage Limit Support');
109
this.config = {
1110
id: '',
1211
token: '',
@@ -24,6 +23,9 @@ export class BuzzwaldWidget {
2423
this.currentCallState = 'idle';
2524
this.retryCount = 0;
2625
this.maxRetries = 3;
26+
this.usageLimitRetryTimer = null;
27+
this.usageLimitRetryAttempts = 0;
28+
this.maxUsageLimitRetries = 10;
2729

2830
this.init();
2931
}
@@ -56,13 +58,17 @@ export class BuzzwaldWidget {
5658
try {
5759
this.config.token = await this.fetchJwt(this.config.id);
5860
} catch (err) {
59-
throw new Error('Failed to fetch authentication token: ' + (err.message || err));
61+
// Preserve usage limit error properties when re-throwing
62+
if (err.isUsageLimit) {
63+
throw err; // Pass through usage limit errors without wrapping
64+
} else {
65+
throw new Error('Failed to fetch authentication token: ' + (err.message || err));
66+
}
6067
}
6168
}
6269
}
6370

6471
async fetchJwt(assistant_id) {
65-
console.log('🔑 Fetching JWT for assistant:', assistant_id, 'from:', JWT_ENDPOINT);
6672
try {
6773
const response = await fetch(JWT_ENDPOINT, {
6874
method: 'POST',
@@ -72,23 +78,23 @@ export class BuzzwaldWidget {
7278
body: JSON.stringify({ assistant_id }),
7379
credentials: 'include', // if you need cookies for auth, else remove
7480
});
75-
console.log('📡 JWT Response status:', response.ok, response.status);
7681

77-
if (!response.ok) {
78-
const error = await response.json().catch(() => ({}));
79-
throw new Error(error.message || `Failed to fetch JWT: ${response.status}`);
80-
}
81-
const data = await response.json();
82-
console.log('📄 JWT Response data:', data);
82+
// Handle both success and error responses
83+
const data = await response.json().catch(() => ({}));
8384

84-
// Check if response contains an error (usage limit exceeded)
85-
if (data.error) {
86-
console.log('🛑 Usage limit error detected:', data.error);
87-
const usageError = new Error(data.error);
85+
// Check for usage limit error (402 Payment Required or error field)
86+
if (response.status === 402 || data.error) {
87+
const usageError = new Error(data.error || `Usage limit exceeded (${response.status})`);
8888
usageError.isUsageLimit = true;
8989
throw usageError;
9090
}
9191

92+
// Handle other non-OK responses
93+
if (!response.ok) {
94+
throw new Error(data.message || `Failed to fetch JWT: ${response.status}`);
95+
}
96+
97+
// Success case
9298
return data.token;
9399
} catch (err) {
94100
console.error('JWT fetch error:', err);
@@ -154,21 +160,16 @@ export class BuzzwaldWidget {
154160
}
155161

156162
handleInitializationError(error) {
157-
console.log('🚨 Initialization error:', error.message, 'isUsageLimit:', error.isUsageLimit);
158163
// Check if this is a usage limit error
159164
if (error.isUsageLimit || (error.message && error.message.includes('Usage limit exceeded'))) {
160-
console.log('📞 Creating usage limit widget');
161165
this.createUsageLimitWidget(error);
162166
} else {
163-
console.log('⚠️ Creating generic error widget');
164167
this.createGenericErrorWidget(error);
165168
}
166169
}
167170

168171
createUsageLimitWidget(error) {
169172
try {
170-
console.log('Creating usage limit widget...');
171-
172173
// Inject styles first
173174
this.injectStyles();
174175

@@ -185,23 +186,142 @@ export class BuzzwaldWidget {
185186
// Add click handler to show usage limit message
186187
this.buttonElement.addEventListener('click', (e) => {
187188
e.preventDefault();
188-
this.showUsageLimitMessage();
189+
this.showUsageLimitMessage(error);
189190
});
190191

191192
this.widgetElement.appendChild(this.buttonElement);
192193
document.body.appendChild(this.widgetElement);
193194

194-
console.log('Usage limit widget created and added to DOM');
195-
196195
// Show initial usage limit message
197-
setTimeout(() => this.showUsageLimitMessage(), 500);
196+
setTimeout(() => this.showUsageLimitMessage(error), 500);
197+
198+
// Start auto-recovery mechanism
199+
this.startUsageLimitRetry();
198200
} catch (err) {
199201
console.error('Error creating usage limit widget:', err);
200202
// Fallback to generic error widget if usage limit widget fails
201203
this.createGenericErrorWidget(error);
202204
}
203205
}
204206

207+
startUsageLimitRetry() {
208+
// Clear any existing retry timer
209+
if (this.usageLimitRetryTimer) {
210+
clearInterval(this.usageLimitRetryTimer);
211+
}
212+
213+
this.usageLimitRetryTimer = setInterval(async () => {
214+
this.usageLimitRetryAttempts++;
215+
216+
try {
217+
// Try to fetch a new JWT token
218+
const newToken = await this.fetchJwt(this.config.id);
219+
220+
// Stop retrying
221+
clearInterval(this.usageLimitRetryTimer);
222+
this.usageLimitRetryTimer = null;
223+
224+
// Update config with new token
225+
this.config.token = newToken;
226+
227+
// Restore the widget to functional state
228+
this.restoreWidget();
229+
230+
} catch (err) {
231+
if (err.isUsageLimit) {
232+
// Stop retrying if max attempts reached
233+
if (this.usageLimitRetryAttempts >= this.maxUsageLimitRetries) {
234+
clearInterval(this.usageLimitRetryTimer);
235+
this.usageLimitRetryTimer = null;
236+
}
237+
} else {
238+
console.error('Auto-recovery failed with different error:', err);
239+
clearInterval(this.usageLimitRetryTimer);
240+
this.usageLimitRetryTimer = null;
241+
}
242+
}
243+
}, 30000); // Check every 30 seconds
244+
}
245+
246+
restoreWidget() {
247+
try {
248+
// Remove existing disabled widget
249+
if (this.widgetElement) {
250+
this.widgetElement.remove();
251+
this.widgetElement = null;
252+
}
253+
254+
// Complete the normal initialization process
255+
this.validateConfig();
256+
this.checkBrowserSupport();
257+
this.injectStyles();
258+
this.createWidget();
259+
this.initializeVapi();
260+
this.isInitialized = true;
261+
262+
// Show success message
263+
this.showRestoreSuccessMessage();
264+
} catch (err) {
265+
console.error('Failed to restore widget:', err);
266+
// Fall back to generic error widget if restore fails
267+
this.createGenericErrorWidget(err);
268+
}
269+
}
270+
271+
showRestoreSuccessMessage() {
272+
// Create success message overlay
273+
const messageOverlay = document.createElement('div');
274+
messageOverlay.className = 'buzzwald-restore-success-message';
275+
276+
// Position the message based on widget position
277+
let positionStyles = '';
278+
switch (this.config.position) {
279+
case 'bottom-right':
280+
positionStyles = 'bottom: 90px; right: 20px;';
281+
break;
282+
case 'bottom-left':
283+
positionStyles = 'bottom: 90px; left: 20px;';
284+
break;
285+
case 'top-right':
286+
positionStyles = 'top: 90px; right: 20px;';
287+
break;
288+
case 'top-left':
289+
positionStyles = 'top: 90px; left: 20px;';
290+
break;
291+
default:
292+
positionStyles = 'bottom: 90px; right: 20px;';
293+
}
294+
295+
messageOverlay.style.cssText = `
296+
position: fixed;
297+
${positionStyles}
298+
background: #28a745;
299+
color: white;
300+
padding: 12px 16px;
301+
border-radius: 8px;
302+
font-size: 14px;
303+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
304+
z-index: 2147483647;
305+
max-width: 250px;
306+
word-wrap: break-word;
307+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
308+
`;
309+
310+
messageOverlay.innerHTML = `
311+
<div style="font-weight: bold; margin-bottom: 4px;">🎉 Voice Calls Restored!</div>
312+
<div>You can now use voice calls again.</div>
313+
`;
314+
315+
document.body.appendChild(messageOverlay);
316+
317+
// Auto-remove after 4 seconds
318+
setTimeout(() => {
319+
if (messageOverlay.parentNode) {
320+
messageOverlay.parentNode.removeChild(messageOverlay);
321+
}
322+
}, 4000);
323+
}
324+
205325
createGenericErrorWidget(error) {
206326
// Create a simple error widget
207327
const errorWidget = document.createElement('div');
@@ -237,7 +357,7 @@ export class BuzzwaldWidget {
237357
this.errorWidget = errorWidget;
238358
}
239359

240-
showUsageLimitMessage() {
360+
showUsageLimitMessage(error) {
241361
// Create usage limit message overlay
242362
const messageOverlay = document.createElement('div');
243363
messageOverlay.className = 'buzzwald-usage-limit-message';
@@ -276,9 +396,12 @@ export class BuzzwaldWidget {
276396
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
277397
cursor: pointer;
278398
`;
399+
// Use the server's specific error message, or fallback to generic
400+
const errorMessage = error?.message || 'Please upgrade your plan to continue using voice calls.';
401+
279402
messageOverlay.innerHTML = `
280403
<div style="font-weight: bold; margin-bottom: 4px;">Voice Call Limit Reached</div>
281-
<div>Please upgrade your plan to continue using voice calls.</div>
404+
<div>${errorMessage}</div>
282405
<div style="font-size: 12px; margin-top: 8px; opacity: 0.9;">Click to dismiss</div>
283406
`;
284407

@@ -568,7 +691,8 @@ export class BuzzwaldWidget {
568691
const message = error.message || error.toString();
569692

570693
if (error.isUsageLimit || message.includes('Usage limit exceeded')) {
571-
return 'Voice call limit reached. Please upgrade your plan to continue.';
694+
// Use the server's specific message for usage limits
695+
return message;
572696
} else if (message.includes('permission') || message.includes('denied')) {
573697
return 'Please allow microphone access';
574698
} else if (message.includes('network') || message.includes('connection')) {
@@ -753,6 +877,12 @@ export class BuzzwaldWidget {
753877
this.retryTimeout = null;
754878
}
755879

880+
// Clear usage limit retry timer
881+
if (this.usageLimitRetryTimer) {
882+
clearInterval(this.usageLimitRetryTimer);
883+
this.usageLimitRetryTimer = null;
884+
}
885+
756886
// Clean up mutation observer
757887
if (this.vapiButtonObserver) {
758888
this.vapiButtonObserver.disconnect();

0 commit comments

Comments
 (0)