startPixPollingWithBackoff method

  1. @action
void startPixPollingWithBackoff(
  1. String paymentId, {
  2. required VoidCallback onSuccess,
})

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();
}