Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased (develop)

- fixed: Keep the QR scanner scam warning on screen until the user dismisses it, instead of letting the camera permission prompt replace it.

## 4.51.0 (staging)

- added: Push info-server attestation tokens into edge-core-js via `setAttestationToken` so the login server can skip CAPTCHA for attested devices, and allow `LOGIN_SERVER` / `INFO_SERVER` env overrides for local E2E stacks.
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export default [
'src/components/modals/RadioListModal.tsx',
'src/components/modals/RawTextModal.tsx',
'src/components/modals/ScamWarningModal.tsx',
'src/components/modals/ScanModal.tsx',

'src/components/modals/StateProvinceListModal.tsx',

'src/components/modals/TransferModal.tsx',
Expand Down
58 changes: 49 additions & 9 deletions src/components/modals/ScanModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ import { ModalFooter } from '../themed/ModalParts'
import { SceneHeaderUi4 } from '../themed/SceneHeaderUi4'
import { EdgeModal } from './EdgeModal'

/**
* How long the scam warning is on screen before the system camera permission
* prompt is requested on top of it.
*/
const PERMISSION_PROMPT_DELAY_MS = 750

interface Props {
bridge: AirshipBridge<string | undefined>

Expand Down Expand Up @@ -79,6 +85,13 @@ export const ScanModal: React.FC<Props> = props => {
const cameraPermission = useSelector(state => state.permissions.camera)
const [torchEnabled, setTorchEnabled] = React.useState(false)
const [scanEnabled, setScanEnabled] = React.useState(false)
const [isPermissionResolved, setIsPermissionResolved] = React.useState(false)
const [isWarningAcknowledged, setIsWarningAcknowledged] =
React.useState(false)

const isCameraAllowed =
cameraPermission === RNPermissions.RESULTS.GRANTED ||
cameraPermission === RNPermissions.RESULTS.LIMITED

const handleFlash = (): void => {
triggerHaptic('impactLight')
Expand All @@ -88,10 +101,22 @@ export const ScanModal: React.FC<Props> = props => {
// Mount effects
React.useEffect(() => {
setScanEnabled(true)
dispatch(checkAndRequestPermission('camera')).catch((error: unknown) => {
showError(error)
})

// Show the scam warning first and give it a beat to land, so the system
// permission prompt covers a modal the user has already seen and the
// warning is still there once the prompt is dismissed.
const timeoutId = setTimeout(() => {
dispatch(checkAndRequestPermission('camera'))
.catch((error: unknown) => {
showError(error)
})
.finally(() => {
setIsPermissionResolved(true)
})
}, PERMISSION_PROMPT_DELAY_MS)

return () => {
clearTimeout(timeoutId)
setScanEnabled(false)
}
}, [dispatch])
Expand All @@ -107,6 +132,11 @@ export const ScanModal: React.FC<Props> = props => {
await Linking.openSettings()
}

const handleAcknowledgeWarning = (): void => {
triggerHaptic('impactLight')
setIsWarningAcknowledged(true)
}

const handleTextInput = async (): Promise<void> => {
triggerHaptic('impactLight')
const uri = await Airship.show<string | undefined>(bridge => (
Expand Down Expand Up @@ -276,8 +306,18 @@ export const ScanModal: React.FC<Props> = props => {
)
}

return cameraPermission === RNPermissions.RESULTS.GRANTED ||
cameraPermission === RNPermissions.RESULTS.LIMITED ? (
// The scam warning gates the scanner: it stays up until the user dismisses
// it, whichever way the camera permission prompt was answered.
const primaryButton =
isPermissionResolved && !isCameraAllowed
? { onPress: handleSettings, label: lstrings.open_settings }
: {
onPress: handleAcknowledgeWarning,
label: lstrings.string_got_it,
disabled: !isCameraAllowed
}

return isCameraAllowed && isWarningAcknowledged ? (
<AirshipModal
bridge={bridge}
margin={[airshipMarginTop, 0, 0]}
Expand All @@ -293,7 +333,9 @@ export const ScanModal: React.FC<Props> = props => {
</AirshipModal>
) : (
<EdgeModal bridge={bridge} onCancel={handleClose}>
<Paragraph>{lstrings.scan_camera_permission_denied}</Paragraph>
{isPermissionResolved && !isCameraAllowed ? (
<Paragraph>{lstrings.scan_camera_permission_denied}</Paragraph>
) : null}
<AlertCardUi4
title={lstrings.warning_scam_title}
type="warning"
Expand All @@ -307,9 +349,7 @@ export const ScanModal: React.FC<Props> = props => {
]}
footer={sprintf(lstrings.warning_scam_footer_s, config.supportEmail)}
/>
<ModalButtons
primary={{ onPress: handleSettings, label: lstrings.open_settings }}
/>
<ModalButtons primary={primaryButton} />
</EdgeModal>
)
}
Expand Down
Loading