When you ask Rork to "build me a nearby-shop finder" or "build me a running tracker," the AI happily wires up expo-location or react-native-geolocation and the code looks reasonable. Then you run it on a real device and nothing happens. No coordinates. No permission dialog. Sometimes a silent timeout, sometimes a flat-out crash. I've spent more than half a day chasing this on a single project, and the punchline is almost always something other than the JavaScript itself.
Location is one of those features where the code can be perfect but a missing line in app.json, a simulator setting, or an OS-level toggle stops everything. This article walks through the five checks I now run, in order, whenever a Rork-generated app refuses to deliver coordinates.
1. Figure out whether the dialog is even showing
Before anything else, separate two very different problems: "the dialog appeared and the user denied it" versus "the dialog never appeared at all." The fixes are completely different.
On iOS, open Settings → your app → Location. If the row is missing entirely, your app has never asked for location permission. On Android, go to Settings → Apps → your app → Permissions and check the same way.
// Log the current permission state so you can tell which case you're in
import * as Location from 'expo-location';
async function checkLocationPermission() {
const { status, canAskAgain, granted } = await Location.getForegroundPermissionsAsync();
console.log('current state:', { status, canAskAgain, granted });
// Expected outputs:
// { status: 'undetermined', canAskAgain: true, granted: false } -> never requested
// { status: 'denied', canAskAgain: false, granted: false } -> denied, can't reprompt
// { status: 'granted', canAskAgain: true, granted: true } -> good to go
}If status is undetermined and you've never called requestForegroundPermissionsAsync(), jump to point 3. If it's denied with canAskAgain: false, you can't reprompt from code — your only path is a button that opens system settings.
2. The missing app.json plugin block (by far the most common cause)
In my experience, this is the single biggest reason Rork-generated apps silently fail at location. Just installing expo-location doesn't add the iOS usage descriptions, so the build succeeds but the dialog never appears on a real device.
Add the plugin block to app.json:
{
"expo": {
"plugins": [
[
"expo-location",
{
"locationAlwaysAndWhenInUsePermission": "We use your location to track runs in the background and on screen.",
"locationWhenInUsePermission": "We use your location to show nearby shops.",
"isIosBackgroundLocationEnabled": false,
"isAndroidBackgroundLocationEnabled": false
}
]
]
}
}The wording matters. Apple's review team will reject vague strings like "we use your location" — you need a sentence that makes the user's brain say "okay, that's reasonable." Tie the explanation to a concrete feature: tracking a run, showing nearby shops, calculating delivery distance.
After updating app.json, Expo Go will not pick up the change. You need to run npx expo prebuild --clean to regenerate the native projects and then build a fresh dev or production binary with EAS Build. Forgetting this step is what makes people swear they "fixed the JS but it still doesn't work."
3. Asking for permission too early
Rork tends to put the permission request inside the top-level useEffect, firing it the second the app launches. It works, but it's a bad UX choice — and on iOS, dialogs that appear before the user has done anything tend to get reflexively denied.
// Bad: hits the user with a dialog the moment the app opens
useEffect(() => {
(async () => {
await Location.requestForegroundPermissionsAsync();
})();
}, []);
// Good: ask only when the user taps "use my current location"
const handleUseCurrentLocation = async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
Alert.alert(
'Location needed',
'To search near you, please allow location access in Settings.',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Open Settings', onPress: () => Linking.openSettings() },
]
);
return;
}
const location = await Location.getCurrentPositionAsync({});
setLocation(location);
};Tying the request to a clear user action — a "find nearby" button, a map screen, a "start run" tap — typically doubles or triples the grant rate compared to asking on launch.
4. Works on the simulator, fails on real devices
If your code works in the simulator but breaks on a phone (or vice versa), one of these is almost always the culprit:
- iOS simulator location is set to None. In the simulator menu, Features → Location must be set to something other than "None" for the simulator to return coordinates. "Apple" or "City Run" gives you a moving fake location.
- Real device with weak signal indoors. GPS cold-start can take 10+ seconds indoors. A 3-second timeout will fail nearly every time. Set
getCurrentPositionAsync({ timeout: 15000 })or fall back to a network-based lookup. - Android device-wide location services off. On Android, app permission can be "granted" while the device's overall location switch is off. You need
Location.hasServicesEnabledAsync()as a pre-check.
async function getLocationSafely() {
// Android: is the device-wide location switch on?
const enabled = await Location.hasServicesEnabledAsync();
if (!enabled) {
Alert.alert('Location services off', 'Please turn on location services in your device settings.');
return null;
}
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') return null;
try {
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Balanced, // good enough for most map use cases
timeout: 15000,
});
return location;
} catch (error) {
console.error('location failed:', error);
return null;
}
}I'd avoid defaulting to Location.Accuracy.Highest. It drains battery and is much more likely to time out indoors. For a "show me on a map" feature, Balanced or High is plenty.
5. The background-location trap
For features like "keep tracking a run when the screen is off" or "follow a delivery driver in the background," foreground permission isn't enough. This is where things get noticeably harder, and Rork's initial scaffold rarely covers it.
On iOS you need isIosBackgroundLocationEnabled: true in your plugin config and UIBackgroundModes including location in the Info.plist. The user also has to choose "Always" — and Apple's review process scrutinizes background location more than almost any other API. If the user has only granted "While Using," background-location calls will silently return nothing.
// Always request foreground first, then background
async function requestBackgroundPermission() {
const fg = await Location.requestForegroundPermissionsAsync();
if (fg.status !== 'granted') return false;
// On iOS, a brief delay between the two prompts improves grant rates
await new Promise((r) => setTimeout(r, 500));
const bg = await Location.requestBackgroundPermissionsAsync();
return bg.status === 'granted';
}For App Store review, the reason for background location must be obvious from the feature itself. If you don't actually need "Always," strip it out — your reviewer will be happier and your users will grant foreground permission far more readily.
A quick checklist for next time
When location stops working, run through these in order:
- Log
getForegroundPermissionsAsync()and read the status - Open Settings on the device and check whether the app's Location row exists
- Verify the
expo-locationplugin block inapp.jsonis correct - Confirm you've run
npx expo prebuild --cleanand rebuilt with EAS - Check the simulator's Location setting, or the device's location-services toggle
- Bump the timeout to ~15 seconds and lower accuracy to
Balanced - Reconsider whether you actually need background location
Location bugs feel mysterious because the failure mode is so often silent. Working through the list above turns a half-day debugging session into a 15-minute checklist.
If you want to think more broadly about permissions in Rork apps, our Rork Permissions Troubleshooting Guide covers the same patterns for camera, notifications, and photo library — the ideas transfer directly.
The fastest move you can make today is to drop a single console.log of getForegroundPermissionsAsync() into your app and run it on a real device. That one log usually points straight at which of the five issues above is biting you.