Pular para o conteúdo principal

Estado, Modal e Recuperação de Falha

Última atualização: 15 de julho de 2026

Visão Geral

Uma sincronização pode ser interrompida a qualquer momento, o app pode ser fechado, o sistema pode matar o processo, a bateria pode acabar. O SyncProvider trata isso como um caso esperado, não como uma exceção: o progresso é persistido a cada mudança, e ao reabrir o app, uma sincronização que ficou presa em "em andamento" é reclassificada como erro recuperável, preservando o que já tinha sido concluído.

Arquivos-chave

ArquivoResponsabilidade
providers/sync/sync.provider.tsxEstado, persistência, detecção de interrupção, retomada
storage/sync/sync.storage.tsgetSyncState/setSyncState, com pub-sub reativo
features/sync/sync.tsx + sync.modal.tsxModal de progresso, lista de erros, ação de minimizar
services/sync-progress-notification/sync-progress-notification.service.tsEspelha o progresso numa notificação Android

O estado é persistido a cada mudança

Todo estado de sincronização, com a lista de passos e o status de cada um, é gravado em MMKV a cada atualização, exceto quando está idle ou success (nesses casos o estado guardado é simplesmente removido):

// sync.provider.tsx
useEffect(() => {
if (state.status === "idle" || state.status === "success") {
clearSyncState();
} else {
setSyncState(state as unknown as Record<string, unknown>);
}
}, [state]);

Detectando uma sincronização interrompida

Ao montar o provider, restoreOrBuildInitialState lê o que sobrou de uma sessão anterior. Se o status salvo ainda for "running", é sinal de que o app foi fechado no meio da sincronização, sem chance de marcar nada como erro. O provider então reclassifica esse estado como erro, e qualquer passo que estivesse "active" no momento da interrupção recebe uma mensagem específica:

// sync.provider.tsx
if (status === "running") {
return {
status: "error",
startedAt: raw.startedAt,
finishedAt: nowIso(),
runOptions,
steps: (steps ?? buildInitialState().steps).map((s) => ({
...s,
status: s.status === "active" ? "error" : s.status,
errors: s.status === "active" ? ["Sincronização interrompida"] : s.errors,
})),
};
}

Como o modal de sincronização abre automaticamente sempre que o status é "running" ou "error", isso garante que o vendedor veja essa situação e possa decidir retomar assim que abrir o app de novo, em vez de continuar operando com dados possivelmente incompletos sem saber.

Retomando de onde parou

Nem todo reinício depois de uma falha refaz tudo. Se pelo menos um passo já tinha terminado ("done") quando o erro aconteceu, o próximo startSyncAll entra em modo de retomada:

function shouldResumeSync(state: SyncState): boolean {
if (state.status !== "error") return false;
return state.steps.some((step) => step.status === "done");
}

Passos concluídos são preservados como estão (só reafirmando progresso 100% e limpando erros antigos), enquanto os demais voltam para "pending", prontos para rodar de novo:

function clearRetryableStepState(step: SyncStep): SyncStep {
if (step.status === "done") {
return { ...step, errors: [], progress: 100 };
}
return { ...step, status: "pending", errors: [], progress: undefined, startedAt: undefined, finishedAt: undefined };
}

Dentro de startSyncAll, cada passo já concluído é pulado de fato, não só visualmente: runStepIfNeeded verifica um conjunto de ids já concluídos antes de decidir se chama runStep de novo.

O modal de progresso

features/sync/sync.tsx (o conteúdo) e sync.modal.tsx (o wrapper de Modal do React Native) mostram uma barra de progresso geral, um card por etapa com seu status e tempo decorrido, e uma lista de erros que pode ser expandida ou escondida:

<Text style={styles.secondaryActionText}>
{showErrors ? "Ocultar erros" : "Exibir erros"}
</Text>
<Text style={styles.retryActionText}>Tentar novamente</Text>

Minimizar a sincronização (fechar o modal sem cancelar o processo) pede confirmação enquanto ela ainda está rodando, avisando que os dados podem aparecer incompletos até o fim:

Alert.alert(
"Minimizar sincronização?",
"Os dados ainda não estão totalmente disponíveis e algumas informações do app podem aparecer incompletas até o fim da sincronização.",
[
{ text: "Continuar sincronizando", style: "cancel" },
{ text: "Minimizar", onPress: onClose },
],
);

O mesmo progresso também é espelhado numa notificação Android, através de syncProgressNotificationServiceInstance.syncState(state), chamado a cada mudança de estado, para que o vendedor acompanhe a sincronização mesmo com o app em segundo plano.

Armadilhas conhecidas

O provider tem dois efeitos separados, ambos comentados como "Control modal visibility based on status", reagindo à mesma transição de state.status para "success". Um só chama setIsModalVisible(false) depois de um setTimeout, o outro chama closeModal() (que já faz o mesmo setIsModalVisible(false), além de limpar o estado persistido) depois de um setTimeout equivalente. Não é um bug que quebra o fluxo, os dois convergem para o mesmo resultado, mas é uma duplicação real. Ao tocar nessa lógica de fechamento automático do modal, edite os dois efeitos, ou melhor, aproveite para unificá-los num só.