- 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>
298 lines
7.1 KiB
TypeScript
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;
|