Skip to content

Commit 7c3f338

Browse files
authored
Merge pull request #5906 from EdgeApp/jon/fix/ios-camera-permission
iOS Camera Permission
2 parents bdf285e + aa30c76 commit 7c3f338

File tree

3 files changed

+43
-33
lines changed

3 files changed

+43
-33
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- fixed: ramps: Various Infinite UI/UX issues
1717
- fixed: Search keyboard not dismissing when submitting search
1818
- fixed: Auto-correct not disabled for search input
19+
- fixed: Inaccurate camera permissions detection
1920
- fixed: In-app review for iOS 18+
2021

2122
## 4.41.1 (2025-12-29)

src/components/modals/ScanModal.tsx

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { sprintf } from 'sprintf-js'
1717
import { useLayout } from '../../hooks/useLayout'
1818
import { lstrings } from '../../locales/strings'
1919
import { config } from '../../theme/appConfig'
20-
import { useSelector } from '../../types/reactRedux'
20+
import { useDispatch, useSelector } from '../../types/reactRedux'
2121
import { triggerHaptic } from '../../util/haptic'
2222
import { logActivity } from '../../util/logger'
2323
import { ModalButtons } from '../buttons/ModalButtons'
@@ -35,7 +35,7 @@ import { checkAndRequestPermission } from '../services/PermissionsManager'
3535
import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext'
3636
import { EdgeText, Paragraph } from '../themed/EdgeText'
3737
import { ModalFooter } from '../themed/ModalParts'
38-
import { SceneHeader } from '../themed/SceneHeader'
38+
import { SceneHeaderUi4 } from '../themed/SceneHeaderUi4'
3939
import { EdgeModal } from './EdgeModal'
4040

4141
interface Props {
@@ -52,7 +52,7 @@ interface Props {
5252
textModalTitle?: string
5353
}
5454

55-
export const ScanModal = (props: Props) => {
55+
export const ScanModal: React.FC<Props> = props => {
5656
const {
5757
bridge,
5858
textModalAutoFocus,
@@ -62,6 +62,7 @@ export const ScanModal = (props: Props) => {
6262
scanModalTitle
6363
} = props
6464

65+
const dispatch = useDispatch()
6566
const theme = useTheme()
6667
const styles = getStyles(theme)
6768

@@ -79,34 +80,34 @@ export const ScanModal = (props: Props) => {
7980
const [torchEnabled, setTorchEnabled] = React.useState(false)
8081
const [scanEnabled, setScanEnabled] = React.useState(false)
8182

82-
const handleFlash = () => {
83+
const handleFlash = (): void => {
8384
triggerHaptic('impactLight')
8485
setTorchEnabled(!torchEnabled)
8586
}
8687

8788
// Mount effects
8889
React.useEffect(() => {
8990
setScanEnabled(true)
90-
checkAndRequestPermission('camera').catch(err => {
91-
showError(err)
91+
dispatch(checkAndRequestPermission('camera')).catch((error: unknown) => {
92+
showError(error)
9293
})
9394
return () => {
9495
setScanEnabled(false)
9596
}
96-
}, [])
97+
}, [dispatch])
9798

98-
const handleBarCodeRead = (codes: Code[]) => {
99+
const handleBarCodeRead = (codes: Code[]): void => {
99100
setScanEnabled(false)
100101
triggerHaptic('impactLight')
101102
bridge.resolve(codes[0].value)
102103
}
103104

104-
const handleSettings = async () => {
105+
const handleSettings = async (): Promise<void> => {
105106
triggerHaptic('impactLight')
106107
await Linking.openSettings()
107108
}
108109

109-
const handleTextInput = async () => {
110+
const handleTextInput = async (): Promise<void> => {
110111
triggerHaptic('impactLight')
111112
const uri = await Airship.show<string | undefined>(bridge => (
112113
<TextInputModal
@@ -124,16 +125,16 @@ export const ScanModal = (props: Props) => {
124125
}
125126
}
126127

127-
const handleAlbum = () => {
128+
const handleAlbum = (): void => {
128129
triggerHaptic('impactLight')
129130
launchImageLibrary(
130131
{
131132
mediaType: 'photo'
132133
},
133134
result => {
134-
if (result.didCancel) return
135+
if (result.didCancel === true) return
135136

136-
if (result.errorMessage) {
137+
if (result.errorMessage != null && result.errorMessage !== '') {
137138
showDevError(result.errorMessage)
138139
return
139140
}
@@ -157,19 +158,18 @@ export const ScanModal = (props: Props) => {
157158
logActivity(`QR code read from photo library.`)
158159
bridge.resolve(response.values[0])
159160
})
160-
.catch(error => {
161+
.catch((error: unknown) => {
161162
showDevError(error)
162163
})
163164
}
164-
).catch(err => {
165-
showError(err)
165+
).catch((error: unknown) => {
166+
showError(error)
166167
})
167168
}
168169

169-
const handleClose = () => {
170+
const handleClose = (): void => {
170171
triggerHaptic('impactLight')
171-
// @ts-expect-error
172-
bridge.resolve()
172+
bridge.resolve(undefined)
173173
}
174174

175175
const airshipMarginTop = theme.rem(3)
@@ -186,7 +186,7 @@ export const ScanModal = (props: Props) => {
186186
headerContainerLayout.height +
187187
(peepholeSpaceLayout.height - holeSize) / 2
188188

189-
const renderModalContent = () => {
189+
const renderModalContent = (): React.ReactElement | null => {
190190
if (!scanEnabled) {
191191
return null
192192
}
@@ -223,7 +223,9 @@ export const ScanModal = (props: Props) => {
223223
style={styles.headerContainer}
224224
onLayout={handleLayoutHeaderContainer}
225225
>
226-
<SceneHeader title={scanModalTitle} underline withTopMargin />
226+
{/* This isn't technically a scene, so just using SceneHeaderUi4 directly for simplicity. */}
227+
{/* eslint-disable-next-line @typescript-eslint/no-deprecated */}
228+
<SceneHeaderUi4 title={scanModalTitle} />
227229
</View>
228230
<View
229231
style={[
@@ -340,8 +342,8 @@ const getStyles = cacheStyles((theme: Theme) => ({
340342
},
341343
headerContainer: {
342344
justifyContent: 'flex-end',
343-
marginBottom: theme.rem(0.5),
344-
marginTop: theme.rem(1)
345+
marginTop: theme.rem(2),
346+
marginLeft: theme.rem(0.5)
345347
},
346348
peepholeSpace: {
347349
flex: 2

src/components/services/PermissionsManager.tsx

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,24 @@ export async function requestContactsPermission(
8787
}
8888

8989
/**
90-
* Checks permission and attempts to request permissions (only if checked
91-
* permission was 'denied')
90+
* Checks permission and requests when denied, then syncs Redux with the
91+
* resulting status.
9292
*/
93-
export async function checkAndRequestPermission(
94-
data: Permission
95-
): Promise<PermissionStatus> {
96-
const status: PermissionStatus = await check(permissionNames[data])
93+
export const checkAndRequestPermission =
94+
(permission: Permission): ThunkAction<Promise<PermissionStatus>> =>
95+
async dispatch => {
96+
let status: PermissionStatus = await check(permissionNames[permission])
9797

98-
if (status === 'denied') return await request(permissionNames[data])
99-
else return status
100-
}
98+
if (status === 'denied') {
99+
status = await request(permissionNames[permission])
100+
}
101+
102+
dispatch({
103+
type: 'PERMISSIONS/UPDATE',
104+
data: { [permission]: status }
105+
})
106+
return status
107+
}
101108

102109
export const checkIfDenied = (status: PermissionStatus) =>
103110
status === 'blocked' || status === 'denied' || status === 'unavailable'
@@ -126,7 +133,7 @@ export async function requestPermissionOnSettings(
126133

127134
// User first time check. If mandatory, it needs to be checked if denied or accepted
128135
if (status === 'denied') {
129-
const result = await checkAndRequestPermission(data)
136+
const result = await request(permissionNames[data])
130137
return mandatory && checkIfDenied(result)
131138
}
132139

0 commit comments

Comments
 (0)