UNPKG

mediasfu-reactnative-expo

Version:
561 lines (556 loc) 21.8 kB
import React, { useState, useEffect } from 'react'; import { View, Text, TextInput, Pressable, StyleSheet, Image, ScrollView, Linking, KeyboardAvoidingView, Platform, Button, } from 'react-native'; import { CameraView, Camera } from 'expo-camera'; import { FontAwesome5 } from '@expo/vector-icons'; import Orientation from 'react-native-orientation-locker'; import AsyncStorage from '@react-native-async-storage/async-storage'; const MAX_ATTEMPTS = 10; // Maximum number of unsuccessful attempts before rate limiting const RATE_LIMIT_DURATION = 3 * 60 * 60 * 1000; // 3 hours in milliseconds /** * WelcomePage component allows users to enter event details manually or by scanning a QR code. * It validates the input and attempts to connect to a socket with the provided credentials. * * @component * @param {WelcomePageOptions} props - The properties for the WelcomePage component. * @returns {JSX.Element} The rendered WelcomePage component. * * @example * ```tsx * import React from 'react'; * import { WelcomePage } from 'mediasfu-reactnative-expo'; * * function App() { * const parameters = { * imgSrc: 'https://example.com/logo.png', * showAlert: alertFunction, * updateIsLoadingModalVisible: toggleLoadingModal, * connectSocket: connectToSocket, * updateSocket: setSocket, * updateValidated: setValidated, * updateApiUserName: setApiUserName, * updateApiToken: setApiToken, * updateLink: setEventLink, * updateRoomName: setRoomName, * updateMember: setUserName, * }; * * return ( * <WelcomePage parameters={parameters} /> * ); * } * * export default App; * ``` */ const WelcomePage = ({ parameters }) => { // State variables for input fields const [name, setName] = useState(''); const [secret, setSecret] = useState(''); const [eventID, setEventID] = useState(''); const [link, setLink] = useState(''); // State variables for QR Code Scanner const [isScannerVisible, setScannerVisible] = useState(false); const [scannedData, setScannedData] = useState(null); const [hasPermission, setHasPermission] = useState(null); const [scanned, setScanned] = useState(false); // Destructure parameters const { showAlert, updateIsLoadingModalVisible, connectSocket, updateSocket, updateValidated, updateApiUserName, updateApiToken, updateLink, updateRoomName, updateMember, } = parameters; /** * Requests camera permissions for QR Code Scanner. */ const getCameraPermissions = async () => { const { status } = await Camera.requestCameraPermissionsAsync(); setHasPermission(status === 'granted'); if (status !== 'granted') { showAlert?.({ message: 'Please allow camera access to scan QR code.', type: 'danger', duration: 3000, }); } }; /** * Checks rate limits and makes a socket connection request. */ const checkLimitsAndMakeRequest = async ({ apiUserName, apiToken, link, apiKey = '', userName, }) => { const TIMEOUT_DURATION = 10000; // 10 seconds try { // Retrieve unsuccessful attempts and last request timestamp from AsyncStorage let unsuccessfulAttempts = parseInt((await AsyncStorage.getItem('unsuccessfulAttempts')) || '0'); const lastRequestTimestamp = parseInt((await AsyncStorage.getItem('lastRequestTimestamp')) || '0'); // Check if user has exceeded maximum attempts if (unsuccessfulAttempts >= MAX_ATTEMPTS) { if (Date.now() - lastRequestTimestamp < RATE_LIMIT_DURATION) { showAlert?.({ message: 'Too many unsuccessful attempts. Please try again later.', type: 'danger', duration: 3000, }); await AsyncStorage.setItem('lastRequestTimestamp', Date.now().toString()); return; } // Reset unsuccessful attempts after rate limit duration unsuccessfulAttempts = 0; await AsyncStorage.setItem('unsuccessfulAttempts', unsuccessfulAttempts.toString()); await AsyncStorage.setItem('lastRequestTimestamp', Date.now().toString()); } // Show loading modal updateIsLoadingModalVisible(true); // Attempt to connect to socket with a timeout const socketPromise = connectSocket({ apiUserName, apiKey, apiToken, link, }); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timed out')), TIMEOUT_DURATION)); const socket = await Promise.race([socketPromise, timeoutPromise]); if (socket && socket.id) { // Successful connection unsuccessfulAttempts = 0; await AsyncStorage.setItem('unsuccessfulAttempts', unsuccessfulAttempts.toString()); await AsyncStorage.setItem('lastRequestTimestamp', Date.now().toString()); // Update parent state with socket and user details updateSocket(socket); updateApiUserName(apiUserName); updateApiToken(apiToken); updateLink(link); updateRoomName(apiUserName); updateMember(userName); updateValidated(true); } else { // Unsuccessful connection unsuccessfulAttempts += 1; await AsyncStorage.setItem('unsuccessfulAttempts', unsuccessfulAttempts.toString()); await AsyncStorage.setItem('lastRequestTimestamp', Date.now().toString()); updateIsLoadingModalVisible(false); if (unsuccessfulAttempts >= MAX_ATTEMPTS) { showAlert?.({ message: 'Too many unsuccessful attempts. Please try again later.', type: 'danger', duration: 3000, }); } else { showAlert?.({ message: 'Invalid credentials.', type: 'danger', duration: 3000, }); } } } catch (error) { // Handle errors during connection console.error('Error connecting to socket:', error); showAlert?.({ message: 'Unable to connect. Check your credentials and try again.', type: 'danger', duration: 3000, }); // Increment unsuccessful attempts let unsuccessfulAttempts = parseInt((await AsyncStorage.getItem('unsuccessfulAttempts')) || '0'); unsuccessfulAttempts += 1; await AsyncStorage.setItem('unsuccessfulAttempts', unsuccessfulAttempts.toString()); await AsyncStorage.setItem('lastRequestTimestamp', Date.now().toString()); updateIsLoadingModalVisible(false); } }; /** * Validates if a string contains only alphanumeric characters. * @param {string} str - The string to validate. * @returns {boolean} True if valid, else false. */ const validateAlphanumeric = (str) => { if (str.length === 0) return true; // Allow empty string (for secret) const alphanumericRegex = /^[a-zA-Z0-9]+$/; return alphanumericRegex.test(str); }; /** * Handles changes in the event display name input field. * @param {string} text - The new text input value. */ const handleNameChange = (text) => { if (text.length <= 12 && validateAlphanumeric(text)) { setName(text); } }; /** * Handles changes in the event token (secret) input field. * @param {string} text - The new text input value. */ const handleSecretChange = (text) => { if (text.length <= 64 && validateAlphanumeric(text)) { setSecret(text); } }; /** * Handles changes in the event ID input field. * @param {string} text - The new text input value. */ const handleEventIDChange = (text) => { if (text.length <= 32 && validateAlphanumeric(text)) { setEventID(text); } }; /** * Handles confirmation action when the user presses the Confirm button. */ const handleConfirm = async () => { updateIsLoadingModalVisible(true); // Check if all fields are filled if (name.length === 0 || secret.length === 0 || eventID.length === 0 || link.length === 0) { showAlert?.({ message: 'Please fill all the fields.', type: 'danger', duration: 3000, }); updateIsLoadingModalVisible(false); return; } // Validate inputs if (!validateAlphanumeric(name) || !validateAlphanumeric(secret) || !validateAlphanumeric(eventID) || !link.includes('mediasfu.com') || eventID.toLowerCase().startsWith('d')) { showAlert?.({ message: 'Please enter valid details.', type: 'danger', duration: 3000, }); updateIsLoadingModalVisible(false); return; } if (secret.length !== 64 || name.length > 12 || name.length < 2 || eventID.length > 32 || eventID.length < 8 || link.length < 12) { showAlert?.({ message: 'Please enter valid details.', type: 'danger', duration: 3000, }); updateIsLoadingModalVisible(false); return; } // Make the request to connect to the socket await checkLimitsAndMakeRequest({ apiUserName: eventID, apiToken: secret, link, userName: name, }); updateIsLoadingModalVisible(false); }; /** * Handles the QR Code scanning result. * @param {object} param0 - The scanned data object. */ const handleBarCodeScanned = ({ data, }) => { setScannedData(data); setScanned(true); }; /** * Handles changes in scanned data. */ useEffect(() => { const processScannedData = async () => { if (scannedData) { try { const data = scannedData.trim(); const parts = data.split(';'); if (parts.length === 5) { const [userName, link_, userSecret, passWord, meetingID] = parts; // Validate scanned data if (userName.length === 0 || link_.length === 0 || userSecret.length === 0 || passWord.length === 0 || meetingID.length === 0) { showAlert?.({ message: 'Invalid scanned data.', type: 'danger', duration: 3000, }); return; } if (!validateAlphanumeric(userName) || !validateAlphanumeric(userSecret) || !validateAlphanumeric(passWord) || !validateAlphanumeric(meetingID)) { showAlert?.({ message: 'Invalid scanned data.', type: 'danger', duration: 3000, }); return; } if (userSecret.length !== 64 || userName.length > 12 || userName.length < 2 || meetingID.length > 32 || meetingID.length < 8 || !link_.includes('mediasfu.com') || meetingID.toLowerCase().startsWith('d')) { showAlert?.({ message: 'Invalid scanned data.', type: 'danger', duration: 3000, }); return; } // Set the name and link from scanned data setName(userName); setLink(link_); // Hide the scanner setScannerVisible(false); setScannedData(null); setScanned(false); // Make the request with scanned data await checkLimitsAndMakeRequest({ apiUserName: meetingID, apiToken: userSecret, link: link_, userName, }); } else { // Handle unexpected data format showAlert?.({ message: 'Invalid scanned data format.', type: 'danger', duration: 3000, }); } } catch (error) { console.error('Error processing scanned data:', error); showAlert?.({ message: 'An error occurred while processing scanned data.', type: 'danger', duration: 3000, }); } } }; processScannedData(); }, [scannedData]); /** * Locks the orientation to portrait when the component mounts and unlocks on unmount. */ useEffect(() => { Orientation.lockToPortrait(); return () => { Orientation.unlockAllOrientations(); }; }, []); /** * Toggles the visibility of the QR Code Scanner. */ const handleScannerToggle = () => { if (!isScannerVisible && hasPermission === null) { getCameraPermissions(); } setScannerVisible(!isScannerVisible); }; return (<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={styles.keyboardAvoidingContainer}> <ScrollView contentContainerStyle={styles.scrollContainer}> <View style={[styles.container, Platform.OS === 'web' && { maxWidth: 600, alignSelf: 'center' }]}> {/* Brand Logo */} <View style={styles.logoContainer}> <Image source={{ uri: parameters.imgSrc || 'https://mediasfu.com/images/logo192.png', }} style={styles.logoImage}/> </View> {/* Input Fields */} <View style={styles.inputContainer}> <TextInput style={styles.inputField} placeholder="Event Display Name" value={name} onChangeText={handleNameChange} autoCapitalize="none" autoCorrect={false} accessibilityLabel="Event Display Name" placeholderTextColor="gray"/> <TextInput style={styles.inputField} placeholder="Event Token (Secret)" value={secret} onChangeText={handleSecretChange} autoCapitalize="none" autoCorrect={false} secureTextEntry accessibilityLabel="Event Token (Secret)" placeholderTextColor="gray"/> <TextInput style={styles.inputField} placeholder="Event ID" value={eventID} onChangeText={handleEventIDChange} autoCapitalize="none" autoCorrect={false} accessibilityLabel="Event ID" placeholderTextColor="gray"/> <TextInput style={styles.inputField} placeholder="Event Link" value={link} onChangeText={setLink} autoCapitalize="none" autoCorrect={false} accessibilityLabel="Event Link" placeholderTextColor="gray"/> </View> {/* Confirm Button */} <Pressable style={styles.confirmButton} onPress={handleConfirm}> <Text style={styles.confirmButtonText}>Confirm</Text> </Pressable> {/* Horizontal Line with "OR" */} <View style={styles.horizontalLineContainer}> <View style={styles.horizontalLine}/> <Text style={styles.orText}>OR</Text> <View style={styles.horizontalLine}/> </View> {/* QR Code Scanner Section */} <View style={styles.scannerContainer}> {isScannerVisible && hasPermission ? (<View style={styles.scanner}> <CameraView onBarcodeScanned={scanned ? undefined : handleBarCodeScanned} barcodeScannerSettings={{ barcodeTypes: ['qr', 'pdf417'], }} style={StyleSheet.absoluteFillObject}/> <Pressable style={styles.closeScannerButton} onPress={handleScannerToggle} accessibilityRole="button" accessibilityLabel="Close QR Scanner"> <FontAwesome5 name="times-circle" size={24} color="red"/> </Pressable> {scanned && (<Button title="Tap to Scan Again" onPress={() => setScanned(false)}/>)} </View>) : (<Pressable style={styles.scanButton} onPress={handleScannerToggle} accessibilityRole="button" accessibilityLabel="Scan QR Code"> <FontAwesome5 name="qrcode" size={20} color="white" style={styles.scanIcon}/> <Text style={styles.scanButtonText}>Scan QR Code</Text> </Pressable>)} </View> {/* Additional Options */} <View style={styles.additionalOptionsContainer}> <Text style={styles.additionalOptionsText}> Provide the event details either by typing manually or scanning the QR-code to autofill. </Text> <Text style={styles.additionalOptionsText}>Do not have a secret?</Text> <Pressable onPress={() => { Linking.openURL('https://meeting.mediasfu.com/meeting/start/'); }} accessibilityRole="link" accessibilityLabel="Get one from mediasfu.com"> <Text style={styles.getOneLinkText}>Get one from mediasfu.com</Text> </Pressable> </View> </View> </ScrollView> </KeyboardAvoidingView>); }; export default WelcomePage; /** * Stylesheet for the WelcomePage component. */ const styles = StyleSheet.create({ keyboardAvoidingContainer: { flex: 1, }, scrollContainer: { flexGrow: 1, justifyContent: 'center', backgroundColor: '#53C6E0', }, container: { flex: 1, paddingHorizontal: '10%', justifyContent: 'center', }, logoContainer: { marginBottom: 20, alignItems: 'center', }, logoImage: { width: 100, height: 100, borderRadius: 50, }, inputContainer: { width: '100%', marginBottom: 20, }, inputField: { height: 40, borderColor: 'gray', borderWidth: 1, marginBottom: 10, paddingHorizontal: 10, borderRadius: 5, backgroundColor: '#ffffff', fontSize: 16, }, confirmButton: { backgroundColor: 'black', paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginBottom: 15, }, confirmButtonText: { color: 'white', fontWeight: 'bold', fontSize: 16, }, horizontalLineContainer: { flexDirection: 'row', alignItems: 'center', marginBottom: 20, marginTop: 10, }, horizontalLine: { flex: 1, height: 1, backgroundColor: 'black', }, orText: { color: 'black', marginHorizontal: 10, fontWeight: 'bold', fontSize: 14, }, scannerContainer: { width: 240, height: 240, marginBottom: 20, justifyContent: 'center', alignItems: 'center', backgroundColor: '#e0e0e0', borderRadius: 10, overflow: 'hidden', alignSelf: 'center', maxWidth: 350, maxHeight: 350, }, scanner: { width: 240, height: 240, position: 'relative', }, closeScannerButton: { position: 'absolute', top: 10, right: 10, backgroundColor: 'transparent', }, scanButton: { flexDirection: 'row', backgroundColor: 'black', paddingVertical: 12, paddingHorizontal: 20, borderRadius: 8, alignItems: 'center', justifyContent: 'center', }, scanButtonText: { color: 'white', fontSize: 16, fontWeight: 'bold', }, scanIcon: { marginRight: 10, }, additionalOptionsContainer: { alignItems: 'center', paddingHorizontal: 20, }, additionalOptionsText: { color: 'black', textAlign: 'center', fontWeight: 'bold', marginBottom: 10, fontSize: 14, }, getOneLinkText: { color: 'white', backgroundColor: 'black', paddingVertical: 12, paddingHorizontal: 25, borderRadius: 8, fontWeight: 'bold', textAlign: 'center', fontSize: 16, }, }); //# sourceMappingURL=WelcomePage.js.map