Pular para o conteúdo principal

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"
Íconebattery-alertrefresh
Fonte de verdade do "concedido"Flag local em MMKVConsulta 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.