Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
48 changes: 25 additions & 23 deletions src/components/modals/ScanModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 {
Expand All @@ -52,7 +52,7 @@ interface Props {
textModalTitle?: string
}

export const ScanModal = (props: Props) => {
export const ScanModal: React.FC<Props> = props => {
const {
bridge,
textModalAutoFocus,
Expand All @@ -62,6 +62,7 @@ export const ScanModal = (props: Props) => {
scanModalTitle
} = props

const dispatch = useDispatch()
const theme = useTheme()
const styles = getStyles(theme)

Expand All @@ -79,34 +80,34 @@ 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)
}

// 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<void> => {
triggerHaptic('impactLight')
await Linking.openSettings()
}

const handleTextInput = async () => {
const handleTextInput = async (): Promise<void> => {
triggerHaptic('impactLight')
const uri = await Airship.show<string | undefined>(bridge => (
<TextInputModal
Expand All @@ -124,16 +125,16 @@ export const ScanModal = (props: Props) => {
}
}

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
}
Expand All @@ -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)
Expand All @@ -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
}
Expand Down Expand Up @@ -223,7 +223,9 @@ export const ScanModal = (props: Props) => {
style={styles.headerContainer}
onLayout={handleLayoutHeaderContainer}
>
<SceneHeader title={scanModalTitle} underline withTopMargin />
{/* This isn't technically a scene, so just using SceneHeaderUi4 directly for simplicity. */}
{/* eslint-disable-next-line @typescript-eslint/no-deprecated */}
<SceneHeaderUi4 title={scanModalTitle} />
</View>
<View
style={[
Expand Down Expand Up @@ -340,8 +342,8 @@ const getStyles = cacheStyles((theme: Theme) => ({
},
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
Expand Down
27 changes: 17 additions & 10 deletions src/components/services/PermissionsManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<PermissionStatus> {
const status: PermissionStatus = await check(permissionNames[data])
export const checkAndRequestPermission =
(permission: Permission): ThunkAction<Promise<PermissionStatus>> =>
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'
Expand Down Expand Up @@ -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)
}

Expand Down
Loading