diff --git a/CHANGELOG.md b/CHANGELOG.md index 49416566d4e..80d992df10a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - fixed: ramps: Various Infinite UI/UX issues - fixed: Search keyboard not dismissing when submitting search - fixed: Auto-correct not disabled for search input +- fixed: Inaccurate camera permissions detection - fixed: In-app review for iOS 18+ ## 4.41.1 (2025-12-29) diff --git a/src/components/modals/ScanModal.tsx b/src/components/modals/ScanModal.tsx index 60dd347a7aa..5d5d2dc3578 100644 --- a/src/components/modals/ScanModal.tsx +++ b/src/components/modals/ScanModal.tsx @@ -17,7 +17,7 @@ import { sprintf } from 'sprintf-js' import { useLayout } from '../../hooks/useLayout' import { lstrings } from '../../locales/strings' import { config } from '../../theme/appConfig' -import { useSelector } from '../../types/reactRedux' +import { useDispatch, useSelector } from '../../types/reactRedux' import { triggerHaptic } from '../../util/haptic' import { logActivity } from '../../util/logger' import { ModalButtons } from '../buttons/ModalButtons' @@ -35,7 +35,7 @@ import { checkAndRequestPermission } from '../services/PermissionsManager' import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext' import { EdgeText, Paragraph } from '../themed/EdgeText' import { ModalFooter } from '../themed/ModalParts' -import { SceneHeader } from '../themed/SceneHeader' +import { SceneHeaderUi4 } from '../themed/SceneHeaderUi4' import { EdgeModal } from './EdgeModal' interface Props { @@ -52,7 +52,7 @@ interface Props { textModalTitle?: string } -export const ScanModal = (props: Props) => { +export const ScanModal: React.FC = props => { const { bridge, textModalAutoFocus, @@ -62,6 +62,7 @@ export const ScanModal = (props: Props) => { scanModalTitle } = props + const dispatch = useDispatch() const theme = useTheme() const styles = getStyles(theme) @@ -79,7 +80,7 @@ export const ScanModal = (props: Props) => { const [torchEnabled, setTorchEnabled] = React.useState(false) const [scanEnabled, setScanEnabled] = React.useState(false) - const handleFlash = () => { + const handleFlash = (): void => { triggerHaptic('impactLight') setTorchEnabled(!torchEnabled) } @@ -87,26 +88,26 @@ export const ScanModal = (props: Props) => { // Mount effects React.useEffect(() => { setScanEnabled(true) - checkAndRequestPermission('camera').catch(err => { - showError(err) + dispatch(checkAndRequestPermission('camera')).catch((error: unknown) => { + showError(error) }) return () => { setScanEnabled(false) } - }, []) + }, [dispatch]) - const handleBarCodeRead = (codes: Code[]) => { + const handleBarCodeRead = (codes: Code[]): void => { setScanEnabled(false) triggerHaptic('impactLight') bridge.resolve(codes[0].value) } - const handleSettings = async () => { + const handleSettings = async (): Promise => { triggerHaptic('impactLight') await Linking.openSettings() } - const handleTextInput = async () => { + const handleTextInput = async (): Promise => { triggerHaptic('impactLight') const uri = await Airship.show(bridge => ( { } } - const handleAlbum = () => { + const handleAlbum = (): void => { triggerHaptic('impactLight') launchImageLibrary( { mediaType: 'photo' }, result => { - if (result.didCancel) return + if (result.didCancel === true) return - if (result.errorMessage) { + if (result.errorMessage != null && result.errorMessage !== '') { showDevError(result.errorMessage) return } @@ -157,19 +158,18 @@ export const ScanModal = (props: Props) => { logActivity(`QR code read from photo library.`) bridge.resolve(response.values[0]) }) - .catch(error => { + .catch((error: unknown) => { showDevError(error) }) } - ).catch(err => { - showError(err) + ).catch((error: unknown) => { + showError(error) }) } - const handleClose = () => { + const handleClose = (): void => { triggerHaptic('impactLight') - // @ts-expect-error - bridge.resolve() + bridge.resolve(undefined) } const airshipMarginTop = theme.rem(3) @@ -186,7 +186,7 @@ export const ScanModal = (props: Props) => { headerContainerLayout.height + (peepholeSpaceLayout.height - holeSize) / 2 - const renderModalContent = () => { + const renderModalContent = (): React.ReactElement | null => { if (!scanEnabled) { return null } @@ -223,7 +223,9 @@ export const ScanModal = (props: Props) => { style={styles.headerContainer} onLayout={handleLayoutHeaderContainer} > - + {/* This isn't technically a scene, so just using SceneHeaderUi4 directly for simplicity. */} + {/* eslint-disable-next-line @typescript-eslint/no-deprecated */} + ({ }, headerContainer: { justifyContent: 'flex-end', - marginBottom: theme.rem(0.5), - marginTop: theme.rem(1) + marginTop: theme.rem(2), + marginLeft: theme.rem(0.5) }, peepholeSpace: { flex: 2 diff --git a/src/components/services/PermissionsManager.tsx b/src/components/services/PermissionsManager.tsx index a2247c4f50e..9d480992805 100644 --- a/src/components/services/PermissionsManager.tsx +++ b/src/components/services/PermissionsManager.tsx @@ -87,17 +87,24 @@ export async function requestContactsPermission( } /** - * Checks permission and attempts to request permissions (only if checked - * permission was 'denied') + * Checks permission and requests when denied, then syncs Redux with the + * resulting status. */ -export async function checkAndRequestPermission( - data: Permission -): Promise { - const status: PermissionStatus = await check(permissionNames[data]) +export const checkAndRequestPermission = + (permission: Permission): ThunkAction> => + async dispatch => { + let status: PermissionStatus = await check(permissionNames[permission]) - if (status === 'denied') return await request(permissionNames[data]) - else return status -} + if (status === 'denied') { + status = await request(permissionNames[permission]) + } + + dispatch({ + type: 'PERMISSIONS/UPDATE', + data: { [permission]: status } + }) + return status + } export const checkIfDenied = (status: PermissionStatus) => status === 'blocked' || status === 'denied' || status === 'unavailable' @@ -126,7 +133,7 @@ export async function requestPermissionOnSettings( // User first time check. If mandatory, it needs to be checked if denied or accepted if (status === 'denied') { - const result = await checkAndRequestPermission(data) + const result = await request(permissionNames[data]) return mandatory && checkIfDenied(result) }