I'm facing critical differences between the development version and the build version of my React Native app on Android using Expo. The main issues I'm encountering are:
Close Button Disappears: The close button visible in the development version is missing in the build version.
Double Tap Gesture Not Working: In the Expo Go app, I can double-tap the screen to take a picture, but this feature doesn't work in the build version. There are no errors in the build logs.
Here are the 2 screens:
Development Version (Expo Go)
Build version
Here is the relevant code that handles the double-tap gesture and the UI rendering:
import React, { useState, useEffect, useRef, useCallback } from 'react';import { StyleSheet, Text, View, Dimensions, Image, TouchableWithoutFeedback, TouchableOpacity } from 'react-native';// librariesimport { CameraView, useCameraPermissions } from 'expo-camera';import { Button, Divider, IconButton } from 'react-native-paper';import Icon from 'react-native-vector-icons/MaterialIcons';import * as ImageManipulator from 'expo-image-manipulator';import { useTranslation } from 'react-i18next';// componentsimport CustomDashedLine from '../components/CustomDashline';// ----------------------------------------------------------------------const { height: DEVICE_HEIGHT, width: DEVICE_WIDTH } = Dimensions.get('window');// ----------------------------------------------------------------------const CameraScreen = ({ navigation, route }) => { const { updatedImages } = route.params; const { t, i18n } = useTranslation(); const textDirection = i18n.dir(); const [permission, requestPermission] = useCameraPermissions(); const [isTakingPicture, setIsTakingPicture] = useState(false); const [step, setStep] = useState(STEPS.CLOSE); const [images, setImages] = useState(updatedImages || initialImagesState); const [enableTorch, setEnableTorch] = useState(false); const [showPrompt, setShowPrompt] = useState(true); const tapRef = useRef(null); const handleDoubleTap = useCallback(() => { if (tapRef.current !== null) { clearTimeout(tapRef.current); setShowPrompt(false); tapRef.current = null; } else { tapRef.current = setTimeout(() => { tapRef.current = null; }, 200); } }, []); useEffect(() => { setShowPrompt(true); }, [step]); const takePicture = async () => { // take the pic and send with API }; return (<View style={styles.container}><TouchableOpacity style={{ flex: 1, zIndex: 1 }} onPress={!showPrompt ? takePicture : null} disabled={isTakingPicture} activeOpacity={1}><CameraView style={styles.camera} type='back' flashMode='auto' enableTorch={enableTorch} ref={(ref) => { camera = ref; }} onCameraReady={() => { setEnableTorch(true); }} shutterSound={false} mute={true} cameraRatio="16:9"><View style={styles.buttonContainer}><IconButton icon={() => <Icon name="close" size={30} color="white" />} size={40} onPress={() => { navigation.goBack(); setEnableTorch(false) }} style={styles.iconButton} /></View> {showPrompt && (<TouchableWithoutFeedback onPress={handleDoubleTap}><View style={styles.fullScreenTouchable}><View style={styles.promptContainer}><Text style={styles.promptText}>A Text here</Text><Image source={imageUrl} style={{alignSelf: 'center'}} resizeMode="contain" /></View></View></TouchableWithoutFeedback> )} {!showPrompt && (<View style={styles.overlay}><CustomDashedLine style={[styles.cropLine, { top: (DEVICE_HEIGHT - DEVICE_WIDTH * 4 / 3) / 2 }]} /><CustomDashedLine style={[styles.cropLine, { bottom: (DEVICE_HEIGHT - DEVICE_WIDTH * 4 / 3) / 2 }]} /></View> )}</CameraView></TouchableOpacity></View> );};const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', }, camera: { flex: 1, width: '100%', height: '100%', }, buttonContainer: { position: 'absolute', right: 20, bottom: 20, alignSelf: 'center', alignItems: 'center', zIndex: 10, backgroundColor: 'transparent', }, iconButton: { backgroundColor: 'rgba(0, 0, 0, 0.5)', borderRadius: 50, }, text: { fontSize: 24, fontWeight: 'bold', color: 'white', }, divider: { backgroundColor: 'white', width: 2, height: 200, marginVertical: 25, opacity: 0.5, }, fullScreenTouchable: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center', zIndex: 1, }, promptContainer: { backgroundColor: 'rgba(0, 0, 0, 0.7)', padding: 20, borderRadius: 10, transform: [{ rotate: '90deg' }], }, promptText: { color: 'white', fontSize: 18, textAlign: 'center', transform: [{ scaleX: -1 }] }, overlay: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, }, cropLine: { position: 'absolute', left: '20%', width: '70%', },});export default CameraScreen;
Here is my app.json:
{"expo": {"name": "my-app","slug": "my-app","version": "1.0.0","orientation": "portrait","icon": "./assets/ic_launcher.png","userInterfaceStyle": "light","splash": {"image": "./assets/splash.png","resizeMode": "contain","backgroundColor": "#ffffff" },"android": {"adaptiveIcon": {"foregroundImage": "./assets/adaptive-icon.png","backgroundColor": "#ffffff" },"permissions": ["android.permission.RECORD_AUDIO","android.permission.CAMERA","android.permission.READ_EXTERNAL_STORAGE","android.permission.WRITE_EXTERNAL_STORAGE","CAMERA","RECORD_AUDIO","READ_EXTERNAL_STORAGE","WRITE_EXTERNAL_STORAGE" ],"package": "com.my.app" },"ios": {"supportsTablet": true,"bundleIdentifier": "com.my.app" },"web": {"favicon": "./assets/favicon.png" },"plugins": [ ["expo-image-picker", {"photosPermission": "The app accesses your photos to let you share them with your friends." } ] ],"extra": {"eas": {"projectId": "my_id" } } }}
And my eas.json
{"cli": {"version": ">= 10.0.2" },"build": {"development": {"developmentClient": true,"distribution": "internal" },"previewApk": {"distribution": "internal","android": {"buildType": "apk" } },"previewBundle": {"distribution": "internal","android": {"buildType": "app-bundle" } },"preview2": {"android": {"gradleCommand": ":app:assembleRelease" } },"production": {"android": {"buildType": "app-bundle" } } },"submit": {"production": {} }}