UNPKG

mediasfu-reactnative

Version:
606 lines 23.5 kB
import React, { useState, useEffect } from 'react'; import { View, Text, TextInput, Pressable, StyleSheet, Image, ScrollView, Linking, KeyboardAvoidingView, Platform, // Button, } from 'react-native'; // import {RNCamera} from 'react-native-camera'; // Updated import for camera // import Icon from 'react-native-vector-icons/FontAwesome5'; // Updated import for icons import Orientation from 'react-native-orientation-locker'; import AsyncStorage from '@react-native-async-storage/async-storage'; // import {request, PERMISSIONS, RESULTS} from 'react-native-permissions'; 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'; * * 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<boolean>(false); const [scannedData, setScannedData] = useState(null); // const [hasPermission, setHasPermission] = useState<boolean | null>(null); // const [scanned, setScanned] = useState<boolean>(false); // Reference to the camera component // const cameraRef = React.useRef<RNCamera | null>(null); // 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 permission = // Platform.OS === 'ios' // ? PERMISSIONS.IOS.CAMERA // : PERMISSIONS.ANDROID.CAMERA; // const status = await request(permission); // setHasPermission(status === RESULTS.GRANTED); // if (status !== RESULTS.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', 10); const lastRequestTimestamp = parseInt((await AsyncStorage.getItem('lastRequestTimestamp')) || '0', 10); // Check if user has exceeded maximum attempts if (unsuccessfulAttempts >= MAX_ATTEMPTS) { if (Date.now() - lastRequestTimestamp < RATE_LIMIT_DURATION) { showAlert === null || showAlert === void 0 ? void 0 : 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 === null || showAlert === void 0 ? void 0 : showAlert({ message: 'Too many unsuccessful attempts. Please try again later.', type: 'danger', duration: 3000, }); } else { showAlert === null || showAlert === void 0 ? void 0 : showAlert({ message: 'Invalid credentials.', type: 'danger', duration: 3000, }); } } } catch (error) { // Handle errors during connection console.error('Error connecting to socket:', error); showAlert === null || showAlert === void 0 ? void 0 : 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', 10); 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 === null || showAlert === void 0 ? void 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 === null || showAlert === void 0 ? void 0 : 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 === null || showAlert === void 0 ? void 0 : 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}: {data: string}) => { // 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 === null || showAlert === void 0 ? void 0 : showAlert({ message: 'Invalid scanned data.', type: 'danger', duration: 3000, }); return; } if (!validateAlphanumeric(userName) || !validateAlphanumeric(userSecret) || !validateAlphanumeric(passWord) || !validateAlphanumeric(meetingID)) { showAlert === null || showAlert === void 0 ? void 0 : 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 === null || showAlert === void 0 ? void 0 : 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 === null || showAlert === void 0 ? void 0 : showAlert({ message: 'Invalid scanned data format.', type: 'danger', duration: 3000, }); } } catch (error) { console.error('Error processing scanned data:', error); showAlert === null || showAlert === void 0 ? void 0 : 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}> <RNCamera ref={cameraRef} style={styles.camera} type={RNCamera.Constants.Type.back} flashMode={RNCamera.Constants.FlashMode.off} onBarCodeRead={scanned ? undefined : handleBarCodeScanned} captureAudio={false} /> <Pressable style={styles.closeScannerButton} onPress={handleScannerToggle} accessibilityRole="button" accessibilityLabel="Close QR Scanner"> <Icon 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"> <Icon 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, }, camera: { width: '100%', height: '100%', }, }); //# sourceMappingURL=WelcomePage.js.map