WellNuo/components/errors/BrowserNotSupported.tsx
Sergei c2064a76eb Add Web Bluetooth support for browser-based sensor setup
- Add webBluetooth.ts with browser detection and compatibility checks
- Add WebBLEManager implementing IBLEManager for Web Bluetooth API
- Add BrowserNotSupported component showing clear error for Safari/Firefox
- Update services/ble/index.ts to use WebBLEManager on web platform
- Add comprehensive tests for browser detection and WebBLEManager

Works in Chrome/Edge/Opera, shows user-friendly error in unsupported browsers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-01 10:48:01 -08:00

298 lines
7.1 KiB
TypeScript

/**
* BrowserNotSupported - Error screen for unsupported browsers
*
* Displayed when Web Bluetooth is not available:
* - Safari (no Web Bluetooth support)
* - Firefox (no Web Bluetooth support)
* - Insecure context (HTTP instead of HTTPS)
*/
import React from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
Linking,
Platform,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import {
WebBluetoothSupport,
getUnsupportedBrowserMessage,
BROWSER_HELP_URLS,
} from '@/services/ble/webBluetooth';
interface BrowserNotSupportedProps {
support: WebBluetoothSupport;
onDismiss?: () => void;
}
/**
* Get browser-specific icon
*/
function getBrowserIcon(browserName: string): keyof typeof Ionicons.glyphMap {
switch (browserName.toLowerCase()) {
case 'safari':
return 'logo-apple';
case 'firefox':
return 'logo-firefox';
case 'chrome':
return 'logo-chrome';
case 'edge':
return 'logo-edge';
default:
return 'globe-outline';
}
}
/**
* Get suggested browser based on current browser
*/
function getSuggestedBrowser(currentBrowser: string): { name: string; url: string } {
// On desktop, suggest Chrome
// On mobile, suggest appropriate browser
if (Platform.OS === 'web') {
return {
name: 'Chrome',
url: BROWSER_HELP_URLS.Chrome,
};
}
return {
name: 'Chrome',
url: BROWSER_HELP_URLS.Chrome,
};
}
export function BrowserNotSupported({ support, onDismiss }: BrowserNotSupportedProps) {
const errorInfo = getUnsupportedBrowserMessage(support);
const browserIcon = getBrowserIcon(support.browserName);
const suggestedBrowser = getSuggestedBrowser(support.browserName);
const handleOpenChrome = async () => {
try {
await Linking.openURL(suggestedBrowser.url);
} catch {
// Ignore errors opening URL
}
};
return (
<View style={styles.container}>
<View style={styles.content}>
{/* Browser Icon with X overlay */}
<View style={styles.iconContainer}>
<View style={styles.iconWrapper}>
<Ionicons name={browserIcon} size={48} color="#6B7280" />
<View style={styles.xOverlay}>
<Ionicons name="close-circle" size={24} color="#EF4444" />
</View>
</View>
</View>
{/* Title */}
<Text style={styles.title}>{errorInfo.title}</Text>
{/* Browser info */}
<Text style={styles.browserInfo}>
{support.browserName} {support.browserVersion}
</Text>
{/* Message */}
<Text style={styles.message}>{errorInfo.message}</Text>
{/* Suggestion */}
<View style={styles.suggestionBox}>
<Ionicons name="information-circle" size={20} color="#3B82F6" />
<Text style={styles.suggestionText}>{errorInfo.suggestion}</Text>
</View>
{/* Supported browsers list */}
<View style={styles.supportedBrowsers}>
<Text style={styles.supportedTitle}>Supported browsers:</Text>
<View style={styles.browserList}>
<View style={styles.browserItem}>
<Ionicons name="logo-chrome" size={24} color="#4285F4" />
<Text style={styles.browserName}>Chrome</Text>
</View>
<View style={styles.browserItem}>
<Ionicons name="logo-edge" size={24} color="#0078D4" />
<Text style={styles.browserName}>Edge</Text>
</View>
<View style={styles.browserItem}>
<Ionicons name="globe-outline" size={24} color="#FF1B2D" />
<Text style={styles.browserName}>Opera</Text>
</View>
</View>
</View>
{/* Action buttons */}
<View style={styles.buttons}>
<TouchableOpacity
style={styles.downloadButton}
onPress={handleOpenChrome}
activeOpacity={0.8}
>
<Ionicons name="logo-chrome" size={20} color="#fff" style={styles.buttonIcon} />
<Text style={styles.downloadButtonText}>Get Chrome</Text>
</TouchableOpacity>
{onDismiss && (
<TouchableOpacity
style={styles.dismissButton}
onPress={onDismiss}
activeOpacity={0.8}
>
<Text style={styles.dismissButtonText}>Continue anyway</Text>
</TouchableOpacity>
)}
</View>
{/* Note for secure context */}
{support.reason === 'insecure_context' && (
<View style={styles.secureNote}>
<Ionicons name="lock-closed" size={16} color="#9CA3AF" />
<Text style={styles.secureNoteText}>
This page must be served over HTTPS for Bluetooth to work.
</Text>
</View>
)}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
padding: 24,
},
content: {
alignItems: 'center',
maxWidth: 360,
},
iconContainer: {
marginBottom: 24,
},
iconWrapper: {
width: 96,
height: 96,
borderRadius: 48,
backgroundColor: '#F3F4F6',
justifyContent: 'center',
alignItems: 'center',
position: 'relative',
},
xOverlay: {
position: 'absolute',
bottom: -4,
right: -4,
backgroundColor: '#fff',
borderRadius: 12,
},
title: {
fontSize: 22,
fontWeight: '700',
color: '#1F2937',
textAlign: 'center',
marginBottom: 4,
},
browserInfo: {
fontSize: 14,
color: '#9CA3AF',
marginBottom: 12,
},
message: {
fontSize: 15,
color: '#6B7280',
textAlign: 'center',
lineHeight: 22,
marginBottom: 16,
},
suggestionBox: {
flexDirection: 'row',
alignItems: 'flex-start',
backgroundColor: '#EFF6FF',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 12,
marginBottom: 24,
gap: 10,
},
suggestionText: {
flex: 1,
fontSize: 14,
color: '#1E40AF',
lineHeight: 20,
},
supportedBrowsers: {
width: '100%',
marginBottom: 24,
},
supportedTitle: {
fontSize: 13,
fontWeight: '600',
color: '#6B7280',
marginBottom: 12,
textAlign: 'center',
},
browserList: {
flexDirection: 'row',
justifyContent: 'center',
gap: 24,
},
browserItem: {
alignItems: 'center',
gap: 4,
},
browserName: {
fontSize: 12,
color: '#6B7280',
},
buttons: {
width: '100%',
gap: 12,
},
downloadButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#3B82F6',
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 12,
},
buttonIcon: {
marginRight: 8,
},
downloadButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
dismissButton: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
},
dismissButtonText: {
color: '#6B7280',
fontSize: 15,
},
secureNote: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 24,
gap: 6,
},
secureNoteText: {
fontSize: 12,
color: '#9CA3AF',
},
});
export default BrowserNotSupported;