SecureBit is now in offline mode. Some features are limited, but your data stays safe.
${feature(GREEN_BG, GREEN_BD, '#3ecf8e', '2.3', '', 'Your session and keys are preserved')}
${feature(GREEN_BG, GREEN_BD, '#3ecf8e', '1.9', '', 'No data is stored on servers')}
${feature(ORANGE_BG, ORANGE_BD, '#f0892a', '1.9', '', 'Messages & files sync when you reconnect')}
`;
const detailsHTML = `
When you reconnect
A dropped connection costs you nothing. SecureBit queues everything locally and resumes the encrypted session the instant you're back online.
${card(GREEN_BG, GREEN_BD, '#3ecf8e', '', 'Your messages get delivered', 'Everything you wrote while offline is sent to your contact automatically.')}
${card(GREEN_BG, GREEN_BD, '#3ecf8e', '', 'Files finish transferring', 'Uploads resume from where they stopped โ no need to resend.')}
${card(GREEN_BG, GREEN_BD, '#3ecf8e', '', 'Their messages & files arrive', 'Whatever your contact sent during the outage is delivered to you in order.')}
${card(ORANGE_BG, ORANGE_BD, '#f0892a', '', 'Nothing is lost', "After reconnect there's no gap โ the conversation continues exactly where it paused.")}
`;
const cardWrap = document.createElement('div');
cardWrap.style.cssText = "position:relative; z-index:2; width:470px; max-width:calc(100vw - 48px); border-radius:22px; background:#121214; border:1px solid rgba(255,255,255,0.08); padding:34px 30px 26px; box-shadow:0 30px 70px rgba(0,0,0,0.6); animation:omPop .32s cubic-bezier(.2,.7,.3,1);";
guidance.appendChild(cardWrap);
const hoverLift = (btn) => {
btn.addEventListener('mouseenter', () => { btn.style.background = '#ff9637'; btn.style.transform = 'translateY(-2px)'; });
btn.addEventListener('mouseleave', () => { btn.style.background = '#f0892a'; btn.style.transform = 'none'; });
};
const close = () => guidance.remove();
const renderMain = () => {
cardWrap.innerHTML = mainHTML;
const cont = cardWrap.querySelector('.om-continue');
hoverLift(cont);
cont.addEventListener('click', close);
const disc = cardWrap.querySelector('.om-disconnect');
disc.addEventListener('mouseenter', () => { disc.style.background = 'rgba(229,114,122,0.14)'; disc.style.borderColor = 'rgba(229,114,122,0.5)'; });
disc.addEventListener('mouseleave', () => { disc.style.background = 'rgba(229,114,122,0.08)'; disc.style.borderColor = 'rgba(229,114,122,0.3)'; });
disc.addEventListener('click', () => {
try {
if (window.webrtcManager && typeof window.webrtcManager.disconnect === 'function') {
window.webrtcManager.disconnect();
}
} catch (e) { console.warn('Offline modal disconnect failed:', e); }
close();
});
const learn = cardWrap.querySelector('.om-learn');
learn.addEventListener('mouseenter', () => { learn.style.color = '#f0892a'; });
learn.addEventListener('mouseleave', () => { learn.style.color = '#9a9aa2'; });
learn.addEventListener('click', renderDetails);
};
const renderDetails = () => {
cardWrap.innerHTML = detailsHTML;
const back = cardWrap.querySelector('.om-back');
back.addEventListener('mouseenter', () => { back.style.color = '#f0892a'; back.style.borderColor = 'rgba(240,137,42,0.45)'; });
back.addEventListener('mouseleave', () => { back.style.color = '#cfcfd4'; back.style.borderColor = 'rgba(255,255,255,0.1)'; });
back.addEventListener('click', renderMain);
const gotit = cardWrap.querySelector('.om-gotit');
hoverLift(gotit);
gotit.addEventListener('click', renderMain);
};
renderMain();
// Click on the backdrop (outside the card) dismisses.
guidance.addEventListener('click', (e) => { if (e.target === guidance) close(); });
document.body.appendChild(guidance);
// Save that we showed the guidance
localStorage.setItem('offline_guidance_shown', Date.now().toString());
}
startReconnectionAttempts() {
if (this.reconnectInterval) {
clearInterval(this.reconnectInterval);
}
this.reconnectInterval = setInterval(() => {
if (this.isOnline) {
clearInterval(this.reconnectInterval);
this.reconnectInterval = null;
return;
}
this.reconnectAttempts++;
// Try to detect if we're actually back online
this.checkOnlineStatus();
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
clearInterval(this.reconnectInterval);
this.reconnectInterval = null;
}
}, 10000); // Try every 10 seconds
}
async checkOnlineStatus() {
try {
// Try to fetch a small resource to check connectivity
const response = await fetch('/favicon.ico', {
method: 'HEAD',
cache: 'no-cache',
signal: AbortSignal.timeout(5000)
});
if (response.ok && !this.isOnline) {
this.isOnline = true;
this.handleConnectionRestored();
}
} catch (error) {
// Still offline
console.log('๐ด Still offline');
}
}
async queueOfflineAction(action) {
const queueItem = {
...action,
id: Date.now() + Math.random(),
timestamp: Date.now(),
priority: action.priority || 1,
retryCount: 0,
maxRetries: action.maxRetries || 3
};
// Always add to memory queue as fallback
this.offlineQueue.push(queueItem);
if (!this.offlineDB) {
console.warn('โ ๏ธ Offline database not available, using memory queue only');
return;
}
try {
await this.ensureDatabaseOpen();
const transaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite');
const store = transaction.objectStore('offlineQueue');
await this.promisifyRequest(store.add(queueItem));
// Try to register background sync
if (this.registration) {
await this.registration.sync.register('offline-sync');
}
} catch (error) {
if (error.name === 'InvalidStateError' || error.message.includes('closing')) {
console.warn('โ ๏ธ Database was closing, item added to memory queue only');
} else {
console.error('โ Failed to queue offline action:', error);
}
// Item already in memory queue, so no action needed
}
}
async processOfflineQueue() {
if (this.syncInProgress || !this.isOnline) {
return;
}
this.syncInProgress = true;
let processedCount = 0;
let errorCount = 0;
try {
// Process database queue
if (this.offlineDB) {
// Ensure database is open before processing
await this.ensureDatabaseOpen();
// Check if database is still open
if (!this.offlineDB || this.offlineDB.objectStoreNames.length === 0) {
console.warn('โ ๏ธ Database not available, skipping queue processing');
return;
}
// Get all items first in a single transaction
let allItems = [];
try {
const readTransaction = this.offlineDB.transaction(['offlineQueue'], 'readonly');
const readStore = readTransaction.objectStore('offlineQueue');
allItems = await this.promisifyRequest(readStore.getAll());
} catch (error) {
if (error.name === 'InvalidStateError' || error.message.includes('closing')) {
console.warn('โ ๏ธ Database was closing during read, retrying...');
await this.ensureDatabaseOpen();
const retryTransaction = this.offlineDB.transaction(['offlineQueue'], 'readonly');
const retryStore = retryTransaction.objectStore('offlineQueue');
allItems = await this.promisifyRequest(retryStore.getAll());
} else {
throw error;
}
}
// Sort by priority and timestamp
allItems.sort((a, b) => {
if (a.priority !== b.priority) {
return b.priority - a.priority; // Higher priority first
}
return a.timestamp - b.timestamp; // Older first
});
// Process each item with its own transaction to avoid "database closing" errors
for (const item of allItems) {
try {
await this.processQueueItem(item);
// Create a new transaction for each delete operation
await this.ensureDatabaseOpen();
const deleteTransaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite');
const deleteStore = deleteTransaction.objectStore('offlineQueue');
await this.promisifyRequest(deleteStore.delete(item.id));
processedCount++;
} catch (error) {
console.error('โ Failed to process offline action:', error);
errorCount++;
// Increment retry count
item.retryCount = (item.retryCount || 0) + 1;
if (item.retryCount >= item.maxRetries) {
// Max retries reached, remove from queue
try {
await this.ensureDatabaseOpen();
const removeTransaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite');
const removeStore = removeTransaction.objectStore('offlineQueue');
await this.promisifyRequest(removeStore.delete(item.id));
console.log('โ Max retries reached for action:', item.type);
} catch (removeError) {
console.error('โ Failed to remove item after max retries:', removeError);
}
} else {
// Update retry count in database
try {
await this.ensureDatabaseOpen();
const updateTransaction = this.offlineDB.transaction(['offlineQueue'], 'readwrite');
const updateStore = updateTransaction.objectStore('offlineQueue');
await this.promisifyRequest(updateStore.put(item));
} catch (updateError) {
console.error('โ Failed to update retry count:', updateError);
}
}
}
}
}
// Process in-memory queue as fallback
const memoryQueue = [...this.offlineQueue];
this.offlineQueue = [];
for (const item of memoryQueue) {
try {
await this.processQueueItem(item);
processedCount++;
} catch (error) {
console.error('โ Failed to process memory queue item:', error);
errorCount++;
item.retryCount = (item.retryCount || 0) + 1;
if (item.retryCount < item.maxRetries) {
this.offlineQueue.push(item); // Re-queue for retry
}
}
}
this.lastSyncTime = Date.now();
if (processedCount > 0 || errorCount > 0) {
this.showSyncNotification(processedCount, errorCount);
}
} catch (error) {
console.error('โ Error processing offline queue:', error);
} finally {
this.syncInProgress = false;
}
}
async processQueueItem(item) {
switch (item.type) {
case 'message':
return this.retryMessage(item.data);
case 'connection':
return this.retryConnection(item.data);
case 'payment_check':
return this.retryPaymentCheck(item.data);
case 'session_verification':
return this.retrySessionVerification(item.data);
case 'key_exchange':
return this.retryKeyExchange(item.data);
default:
console.warn('Unknown queue item type:', item.type);
throw new Error(`Unknown queue item type: ${item.type}`);
}
}
async retryMessage(messageData) {
// Retry sending message when back online
if (window.webrtcManager && window.webrtcManager.isConnected()) {
return window.webrtcManager.sendMessage(messageData.content);
}
throw new Error('WebRTC not connected');
}
async retryConnection(connectionData) {
// Retry connection establishment
if (window.webrtcManager) {
return window.webrtcManager.retryConnection();
}
throw new Error('WebRTC manager not available');
}
async retryPaymentCheck(paymentData) {
// Retry payment verification
if (window.sessionManager) {
return window.sessionManager.checkPaymentStatus(paymentData.checkingId);
}
throw new Error('Session manager not available');
}
async retrySessionVerification(sessionData) {
// Retry session verification
if (window.sessionManager) {
return window.sessionManager.verifyPayment(sessionData.preimage, sessionData.paymentHash);
}
throw new Error('Session manager not available');
}
async retryKeyExchange(keyData) {
// Retry key exchange
if (window.webrtcManager) {
return window.webrtcManager.handleKeyExchange(keyData);
}
throw new Error('WebRTC manager not available');
}
showSyncNotification(successCount, errorCount) {
const notification = document.createElement('div');
notification.className = 'fixed bottom-4 right-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm transform translate-x-full transition-transform duration-300';
let message = '';
if (successCount > 0 && errorCount === 0) {
message = `โ Synced ${successCount} offline action${successCount > 1 ? 's' : ''}`;
} else if (successCount > 0 && errorCount > 0) {
message = `โ ๏ธ Synced ${successCount}, ${errorCount} failed`;
} else if (errorCount > 0) {
message = `โ ${errorCount} sync error${errorCount > 1 ? 's' : ''}`;
}
notification.innerHTML = `
Sync Complete
${message}
`;
document.body.appendChild(notification);
// Animate in
setTimeout(() => {
notification.classList.remove('translate-x-full');
}, 100);
// Auto-remove after 4 seconds
setTimeout(() => {
notification.classList.add('translate-x-full');
setTimeout(() => notification.remove(), 300);
}, 4000);
}
async attemptWebRTCReconnection() {
if (!window.webrtcManager) return;
try {
// Check if we had an active connection before going offline
const savedConnectionState = await this.getStoredData('sessionData', 'connection_state');
if (savedConnectionState && savedConnectionState.wasConnected) {
// Show reconnection indicator
this.showReconnectionIndicator();
// Attempt to restore connection
// This would depend on your specific WebRTC implementation
if (window.webrtcManager.attemptReconnection) {
await window.webrtcManager.attemptReconnection(savedConnectionState.data);
}
}
} catch (error) {
console.error('โ WebRTC reconnection failed:', error);
}
}
showReconnectionIndicator() {
const indicator = document.createElement('div');
indicator.className = 'fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-blue-500/90 text-white px-6 py-4 rounded-lg backdrop-blur-sm z-50';
indicator.innerHTML = `
Reconnecting...
Restoring secure connection
`;
document.body.appendChild(indicator);
// Remove after 5 seconds or when connection is restored
setTimeout(() => {
if (indicator.parentElement) {
indicator.remove();
}
}, 5000);
}
showReconnectionSuccess() {
const notification = document.createElement('div');
notification.className = 'fixed top-4 right-4 bg-green-500 text-white p-4 rounded-lg shadow-lg z-50 max-w-sm';
notification.innerHTML = `