Fluxo por Permissão e Particularidades de Plataforma
Última atualização: 16 de julho de 2026
Negação permanente e o atalho pras Configurações
Para localização, câmera, galeria e notificações, o pedido de permissão passa direto pela API nativa correspondente (expo-location, expo-camera, expo-media-library, expo-notifications), que devolve tanto se foi concedida quanto se ainda é possível perguntar de novo. Quando o sistema já bloqueou permanentemente aquela permissão, o mesmo bloco de tratamento (duplicado entre permission.modal.tsx e o permission.tsx órfão, ver Uso Real e Armadilhas Conhecidas) oferece um atalho direto pras Configurações do sistema:
// permission.modal.tsx
const res = await requestPermission(activeStep.id);
if (!res.granted && res.canAskAgain === false) {
Alert.alert(
"Permissão bloqueada",
"Parece que você bloqueou essa permissão. Você pode habilitar em Ajustes.",
[
{ text: "Cancelar", style: "cancel" },
{ text: "Abrir Ajustes", onPress: () => Linking.openSettings() },
],
);
}
Esse tratamento não existe no service, ele vive na UI. checkPermission/requestPermission (services/permissions.service.ts) só repassam o resultado bruto da API nativa:
// permissions.service.ts
case "location": {
const { granted, canAskAgain } = await Location.requestForegroundPermissionsAsync();
return { granted, canAskAgain };
}
A permissão de segundo plano não segue esse padrão, e diverge entre Android e iOS
batteryOptimization (Android) e backgroundRefresh (iOS) usam o mesmo texto de descrição, mas título e ícone diferem, e o mecanismo por trás de cada uma é bem diferente:
Android (batteryOptimization) | iOS (backgroundRefresh) | |
|---|---|---|
| Título | "Execução em segundo plano" | "Atualização em segundo plano" |
| Ícone | battery-alert | refresh |
| Fonte de verdade do "concedido" | Flag local em MMKV | Consulta real ao SO (expo-background-fetch) |
No Android, o check nem consulta o sistema, ele só lê uma flag local que significa "o usuário já viu o intent de bateria uma vez":
// permissions.service.ts
case "batteryOptimization":
if (Platform.OS !== "android") return { granted: true };
return { granted: getBatteryOptAsked() };
E o pedido sempre retorna sucesso, independente do que o usuário de fato escolher no diálogo nativo que se abre:
case "batteryOptimization": {
if (Platform.OS !== "android") return { granted: true };
setBatteryOptAsked();
try {
await IntentLauncher.startActivityAsync(
"android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
{ data: "package:com.vidyacode.force" },
);
} catch {
// intent não suportado em alguns devices — ignorar silenciosamente
}
return { granted: true };
}
Isso não é um bug de UI, é uma limitação real de plataforma: o conjunto de bibliotecas Expo usado no projeto não expõe uma forma direta de consultar se o app está na lista de exceções de otimização de bateria do Android (seria necessário um módulo nativo dedicado, que o projeto não tem). Na prática, isso significa que essa etapa do onboarding fica marcada como "resolvida" assim que o vendedor toca em "Permitir" uma única vez, mesmo que ele cancele o diálogo do sistema em seguida, e não existe forma de resetar esse estado pela interface.
No iOS, ao contrário, o check é real, consulta de fato o status do sistema:
case "backgroundRefresh": {
if (Platform.OS !== "ios") return { granted: true };
const status = await BackgroundFetch.getStatusAsync();
const granted = status === BackgroundFetch.BackgroundFetchStatus.Available;
return { granted, canAskAgain: !granted };
}
E o pedido simplesmente abre as Configurações do app (o iOS não permite deep link direto pra tela específica de "Atualização em Segundo Plano") e reconsulta o status ao voltar. Diferente do Android, essa etapa pode permanecer pendente indefinidamente se o vendedor não ativar a opção manualmente nas Configurações.
Por que só a permissão de bateria precisa de storage próprio
// storage/background.storage.ts
const KEYS = { BATTERY_OPT_ASKED: "background:batteryOptAsked" };
export function getBatteryOptAsked(): boolean {
return getStorage().getBoolean(KEYS.BATTERY_OPT_ASKED) ?? false;
}
export function setBatteryOptAsked(): void {
getStorage().set(KEYS.BATTERY_OPT_ASKED, true);
}
As outras quatro permissões (localização, câmera, galeria, notificações) e a variante iOS de segundo plano têm uma API nativa que devolve o status atual direto do sistema operacional, então não precisam guardar nada localmente, o SO já é a fonte de verdade. Só o Android, no conjunto de bibliotecas usado aqui, não tem uma forma de consultar se a otimização de bateria já foi desativada para o app, então o projeto simula esse estado com uma única flag booleana em MMKV que significa apenas "já mostrei o intent uma vez", não "o usuário de fato desativou a otimização".
Por que essa permissão de segundo plano existe
backgroundKeepAliveService.start() é chamado no mount do layout raiz e sempre que o app volta a ficar active (app/_layout.tsx), junto de saveSaleDraftService.processPendingQueue(). Sem a isenção de otimização de bateria no Android, ou sem "Atualização em Segundo Plano" habilitado no iOS, o sistema operacional pode encerrar essa tarefa em segundo plano, e a sincronização incremental (motor já documentado em Sincronização Offline) para de rodar enquanto o app estiver minimizado.