skip to content

In an Expo receipt-scanning app, why might expo-haptics feedback on capture work on Android but do nothing on iOS?

level: seniorimportance: nice to knowfreq 18%

answer

  1. the Taptic Engine can stay silent
  2. camera active, Low Power Mode
  3. the promise still resolves
  4. Android simulates with Vibrator
  5. performAndroidHapticsAsync on Android

basics

~20 s

On iOS the Taptic Engine does nothing while the camera is active, in Low Power Mode, during dictation, or when the user disabled it, and the call still resolves. Android simulates impacts with the Vibrator service, which has no such documented rule.

solid answer

~40 s

`expo-haptics` maps `impactAsync`, `notificationAsync` and `selectionAsync` to the Taptic Engine's feedback generators on iOS. The Expo docs list conditions under which the Taptic Engine does nothing: Low Power Mode, haptics disabled in settings, the camera active, and dictation active. A capture button on a live `CameraView` hits the camera case, and the promise resolves anyway, so nothing reports the silence. On Android the same calls are simulated with the `Vibrator` service, whose `VIBRATE` permission the package adds automatically, and no camera rule is documented there, so a buzz is typically felt during capture. The package recommends `performAndroidHapticsAsync` with `AndroidHaptics` types on Android instead, since it uses the system haptics and needs no `VIBRATE` permission. So I fire the success haptic once the camera is closed, and never make haptics the only confirmation.

code

tsx · 11 lines
tsx
import * as Haptics from 'expo-haptics';
import { Platform } from 'react-native';

// Call when the receipt review screen appears, after CameraView has unmounted.
export async function confirmReceiptSaved() {
  if (Platform.OS === 'android') {
    await Haptics.performAndroidHapticsAsync(Haptics.AndroidHaptics.Confirm);
  } else {
    await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
  }
}

go deeper

for a junior

Recall the three calls, impactAsync, notificationAsync and selectionAsync, and that iOS can silently skip haptics in some conditions.

for a middle

Explain the platform mapping: Taptic Engine generators on iOS, Vibrator simulation on Android, and when performAndroidHapticsAsync is the better Android call.

for a senior

Diagnose silent haptics in a camera flow, move feedback outside the camera session, and never make haptics the sole confirmation.

for a principal

Set feedback guidelines for the app: where haptics add meaning, how they pair with visual and accessible cues, and how they are verified on devices.

## What the calls map to `expo-haptics` exposes three cross-platform calls and one Android-only call: | call | iOS | Android | |---|---|---| | `impactAsync(style)` | impact feedback generator, styles `Light`, `Medium` (default), `Heavy`, `Soft`, `Rigid` | simulated with the `Vibrator` service | | `notificationAsync(type)` | notification feedback, `Success` (default), `Warning`, `Error` | simulated with the `Vibrator` service | | `selectionAsync()` | selection feedback | simulated with the `Vibrator` service | | `performAndroidHapticsAsync(type)` | does nothing | the system haptic feedback for an `AndroidHaptics` type such as `Confirm` or `Reject` | On the web the package falls back to the Web Vibration API where the browser supports it. ## Why iOS can stay silent The Expo docs list the conditions in which the **Taptic Engine does nothing** on a user's device: - **Low Power Mode** is enabled; - the user **disabled** system haptics in settings; - the **camera is active**, to avoid destabilising it; - **dictation** is active, to avoid disturbing the microphone. A receipt scanner that fires `notificationAsync(Success)` from the capture button while `CameraView` is running meets the third condition on every capture. Crucially, the returned **promise still resolves**: it fulfils once the native call is triggered, not once the user has felt anything. No error reaches JavaScript, so the silence goes unnoticed in testing until someone checks by hand. ## Why Android behaves differently On Android, `impactAsync`, `notificationAsync` and `selectionAsync` are **simulated with the `Vibrator` service**, playing short waveforms. The `VIBRATE` permission they need is **added automatically**, so no config plugin option is involved. The camera-active suppression is documented for the iOS Taptic Engine only; Android's vibration path has no such documented rule, so the same code is typically felt there during capture. The package's own documentation discourages `Vibrator`-based haptics on Android and points to **`performAndroidHapticsAsync`**, which uses the platform's haptic feedback constants. It is closer to iOS haptics in feel and **does not require the `VIBRATE` permission**. It resolves immediately on other platforms, so it can be called unconditionally, though branching by platform keeps intent clear. ## Designing the feedback 1. **Move the success haptic out of the camera session**: fire it when the review screen appears after capture, once `CameraView` is unmounted. 2. **Use platform-appropriate calls**: `notificationAsync(Success)` on iOS, `performAndroidHapticsAsync(AndroidHaptics.Confirm)` on Android. 3. **Never rely on haptics alone**: pair them with a visible state change and an accessible announcement, because Low Power Mode or user settings can silence them at any time. 4. **Keep them sparse**: a haptic on every scroll or keystroke dilutes the signal and drains the battery. ## Choosing the feedback type Match the call to the meaning of the moment: - **`notificationAsync(Success)`** or **`AndroidHaptics.Confirm`**: a receipt was saved or uploaded. - **`notificationAsync(Error)`** or **`AndroidHaptics.Reject`**: the upload failed or the image was unreadable. - **`selectionAsync()`**: the user moved between discrete choices, such as expense categories. - **`impactAsync(Light)`**: a small physical-feeling moment, such as a crop handle snapping into place. Using a success pattern for a failure, or a heavy impact for a minor tap, teaches users to ignore the signal. ## Configuration summary for the capture flow Of the packages in a receipt scanner, `expo-haptics` is the one that needs **no config plugin options**: iOS haptics need no permission, and the Android permission is added automatically. `expo-camera` and `expo-image-picker` do carry plugin options, for prompt texts and optional permissions, and `expo-image` needs none. ## Testing it Haptics are invisible to simulators and screenshots, so verify on physical devices, including one in Low Power Mode, and record the camera-active limitation in the feature's test notes rather than filing it as a bug each release.

  • Does a resolved notificationAsync promise mean the user felt the haptic?
    No. It fulfils once the native haptic call is triggered. If iOS suppresses the Taptic Engine, for Low Power Mode, disabled haptics, an active camera or dictation, the promise still resolves, so code cannot detect the silence from the result.
  • Why prefer performAndroidHapticsAsync over impactAsync on Android?
    `impactAsync` on Android is simulated with the `Vibrator` service, which the package itself says is not recommended for haptic feedback. `performAndroidHapticsAsync` uses the system's haptic feedback types, feels closer to iOS haptics and does not need the `VIBRATE` permission.

saying these in an interview costs you the question

  • If notificationAsync resolves, the user must have felt the haptic.
  • expo-haptics needs a config plugin entry to work on Android.
  • iOS haptics always fire regardless of Low Power Mode or camera use.
  • impactAsync uses the same native mechanism on Android as on iOS.
  • Haptics alone are enough confirmation that a receipt was saved.