startPixPollingWithBackoff method
Implementation
@action
void startPixPollingWithBackoff(
String paymentId, {
required VoidCallback onSuccess,
}) {
cancelPixPolling(); // Garante que não existam dois loops concorrentes rodando
final token = Object();
_pixPollingToken = token;
// 1. SE NÃO HOUVER DATA DE EXPIRAÇÃO, COLOCA UM TIMEOUT PADRÃO DE SEGURANÇA
int calculatedMaxAttempts = 50;
if (pixExpirationDate != null) {
try {
final expiryTime = DateTime.parse(pixExpirationDate!);
final now = DateTime.now();
final totalDurationSeconds = expiryTime.difference(now).inSeconds;
if (totalDurationSeconds <= 0) {
// O PIX já nasceu morto ou o relógio está dessincronizado
setError('O tempo limite para o pagamento deste PIX expirou.');
_checkoutService.notifyPixExpired(paymentId).catchError((error) {
debugPrint('Erro ao notificar expiração do PIX: $error');
return ValueResult<String>.failure(error.toString());
});
return;
}
// 2. CALCULA MATEMATICAMENTE QUANTAS TENTATIVAS CABEM NO TEMPO RESTANTE
int remainingSeconds = totalDurationSeconds;
int virtualAttempt = 0;
while (remainingSeconds > 0) {
virtualAttempt++;
int nextDelay = 5;
if (virtualAttempt > 5) nextDelay = 10;
if (virtualAttempt > 15) nextDelay = 15;
remainingSeconds -= nextDelay;
}
// Adiciona uma pequena margem de segurança de 2 tentativas adicionais
calculatedMaxAttempts = virtualAttempt + 2;
} catch (_) {
calculatedMaxAttempts = 50;
}
}
int attempt = 0;
Future<void> pollStatus() async {
// Verifica se o usuário mudou de aba, cancelou ou saiu do fluxo
if (!isPixPollingActive(token)) return;
// 3. PEGA OS SEGUNDOS REAIS RESTANTES ANTES DE APLICAR O DELAY
int remainingSeconds = 9999;
if (pixExpirationDate != null) {
try {
final expiryTime = DateTime.parse(pixExpirationDate!);
final now = DateTime.now();
remainingSeconds = expiryTime.difference(now).inSeconds;
// Se bateu ou estourou o tempo, encerra imediatamente
if (remainingSeconds <= 0) {
_encerrarPorTimeout(paymentId, token);
return;
}
} catch (_) {}
}
attempt++;
// VALIDAÇÃO POR LIMITE DINÂMICO DE TENTATIVAS
if (attempt > calculatedMaxAttempts) {
_encerrarPorTimeout(paymentId, token);
return;
}
// DETERMINA O INTERVALO ATUAL SEGUINDO O BACKOFF
int backoffWaitTime = 5;
if (attempt > 5) backoffWaitTime = 10;
if (attempt > 15) backoffWaitTime = 15;
// 🔥 O PULO DO GATO: Se o tempo restante for menor que o backoff,
// o app espera apenas o tempo exato que falta para o PIX expirar!
final actualWaitTime = (remainingSeconds < backoffWaitTime)
? remainingSeconds
: backoffWaitTime;
// Aguarda o intervalo exato calculado
await Future.delayed(Duration(seconds: actualWaitTime));
// Checa novamente após sair do delay
if (!isPixPollingActive(token)) return;
try {
// Bate na API pra checar se já mudou o status
final statusResult = await _checkoutService.getPixStatus(paymentId);
if (!isPixPollingActive(token)) return;
if (statusResult.isSuccess) {
final status = statusResult.value?.toLowerCase() ?? '';
if (status == 'success' || status == 'paid' || status == 'approved') {
cancelPixPolling();
onSuccess(); // Sucesso, muda o widget para verde e fecha a conta
return;
} else if (status == 'expired' ||
status == 'cancelled' ||
status == 'failed') {
cancelPixPolling();
setError('Pagamento PIX expirado ou cancelado.');
return;
}
}
} catch (e) {
// Erros oscilantes de rede ou internet piscando não quebram o fluxo,
// apenas deixam agendar a próxima rodada
}
// 4. RECURSIVIDADE COERENTE
if (isPixPollingActive(token)) {
// Se aplicamos uma espera curta final, o tempo provavelmente zerou agora.
// Forçamos uma checagem rápida no relógio antes de disparar a próxima chamada de rede à toa.
if (pixExpirationDate != null) {
try {
if (DateTime.now().isAfter(DateTime.parse(pixExpirationDate!))) {
_encerrarPorTimeout(paymentId, token);
return;
}
} catch (_) {}
}
await pollStatus();
}
}
// Inicializa o primeiro disparo
pollStatus();
}