Penanganan Kesalahan & Pemecahan Masalah

Panduan lengkap untuk menangani kesalahan dengan anggun dan men-debug masalah umum dengan AnySpend untuk memberikan pengalaman pengguna terbaik.

šŸ“Š Siklus Status Pesanan

Memahami status pesanan sangat penting untuk penanganan kesalahan yang tepat dan pengalaman pengguna.

Tipe Status Pesanan

Order Status Enum
enum OrderStatus {
  // Initial States
  SCANNING_DEPOSIT_TRANSACTION = "scanning_deposit_transaction",
  WAITING_STRIPE_PAYMENT = "waiting_stripe_payment",
  EXPIRED = "expired",

  // Processing States
  SENDING_TOKEN_FROM_VAULT = "sending_token_from_vault",
  RELAY = "relay",

  // Success States
  EXECUTED = "executed",

  // Failure States
  REFUNDING = "refunding",
  REFUNDED = "refunded",
  FAILURE = "failure",
}

Deskripsi Status

StatusDeskripsiTindakan Pengguna yang Diperlukan
scanning_deposit_transactionMenunggu konfirmasi pembayaranTidak ada - tunggu konfirmasi blockchain
waiting_stripe_paymentMemproses pembayaran kartu kreditMungkin perlu menyelesaikan 3D Secure
sending_token_from_vaultMengirim token untuk swapTidak ada - proses otomatis
relayTransaksi lintas rantai sedang berlangsungTidak ada - tunggu hingga selesai
executedTransaksi berhasil diselesaikanTidak ada - sukses!
expiredPesanan kedaluwarsa sebelum selesaiBuat pesanan baru
refundingPengembalian dana otomatis sedang berlangsungTidak ada - tunggu pengembalian dana
refundedPengembalian dana selesaiPeriksa dompet untuk token yang dikembalikan
failureTransaksi gagalReview detail kesalahan, coba lagi

āš ļø Kode Kesalahan Umum

Kesalahan Pembayaran

Deskripsi: Pengguna tidak memiliki cukup token untuk transaksiSolusi: Minta pengguna untuk menambah dana ke dompet mereka atau memilih token pembayaran yang berbedaContoh:
if (error.message === "INSUFFICIENT_BALANCE") {
  toast.error("Saldo tidak cukup. Silakan tambah dana ke dompet Anda.");
  // Opsional mengarahkan ke fiat onramp
  openFiatOnramp();
}
Deskripsi: Kontrak token tidak didukung di rantai targetSolusi: Verifikasi token didukung dan berikan opsi alternatifContoh:
if (error.message === "INVALID_TOKEN_ADDRESS") {
  toast.error("Token ini tidak didukung. Silakan pilih yang lain.");
  showSupportedTokens();
}
Deskripsi: Jumlah transaksi di bawah ambang minimumSolusi: Tingkatkan jumlah transaksi atau informasikan pengguna tentang persyaratan minimumContoh:
if (error.message === "MINIMUM_AMOUNT_NOT_MET") {
  toast.error(`Jumlah minimum adalah $${minimumAmount}. Silakan tingkatkan jumlah Anda.`);
}
Deskripsi: Jumlah transaksi melebihi batas maksimumSolusi: Kurangi jumlah atau bagi menjadi beberapa transaksiContoh:
if (error.message === "MAXIMUM_AMOUNT_EXCEEDED") {
  toast.error(`Jumlah maksimum adalah $${maximumAmount}. Silakan kurangi jumlah Anda.`);
}

Kesalahan Jaringan

Deskripsi: Harga bergerak di luar toleransi yang dapat diterima selama eksekusiSolusi: Coba lagi dengan toleransi slippage yang lebih tinggi atau tunggu stabilitas hargaContoh:
if (error.message === "SLIPPAGE") {
  toast.warning("Harga bergerak tidak menguntungkan. Mencoba lagi dengan pengaturan yang disesuaikan...");
  retryWithHigherSlippage();
}
Deskripsi: Masalah koneksi RPC atau kemacetan blockchainSolusi: Coba lagi setelah jeda atau beralih ke RPC alternatifContoh:
if (error.message === "NETWORK_ERROR") {
  toast.error("Masalah jaringan terdeteksi. Silakan periksa koneksi dan coba lagi.");
  scheduleRetry();
}
Deskripsi: Kutipan harga tidak lagi validSolusi: Dapatkan kutipan baru dan coba transaksi lagiContoh:
if (error.message === "QUOTE_EXPIRED") {
  toast.info("Kutipan harga kedaluwarsa. Mendapatkan kutipan baru...");
  refreshQuoteAndRetry();
}
Deskripsi: Blockchain yang diminta tidak didukungSolusi: Gunakan rantai yang didukung atau terapkan fallbackContoh:
if (error.message === "CHAIN_NOT_SUPPORTED") {
  toast.error("Blockchain ini tidak didukung. Silakan pilih yang lain.");
  showSupportedChains();
}

Kesalahan Kontrak

Deskripsi: Eksekusi kontrak pintar gagalSolusi: Periksa parameter dan status kontrakContoh:
if (error.message === "CONTRACT_CALL_FAILED") {
  toast.error("Interaksi kontrak gagal. Silakan verifikasi parameter.");
  logContractError(error);
}
Deskripsi: Batas gas ditetapkan terlalu rendah untuk transaksiSolusi: Tingkatkan batas gas atau sarankan optimasi gasContoh:
if (error.message === "INSUFFICIENT_GAS") {
  toast.error("Transaksi memerlukan lebih banyak gas. Meningkatkan batas gas...");
  retryWithHigherGas();
}
Deskripsi: Konflik nonce transaksiSolusi: Tunggu transaksi tertunda selesaiContoh:
if (error.message === "NONCE_TOO_LOW") {
  toast.info("Silakan tunggu transaksi tertunda selesai.");
  waitAndRetry();
}
Deskripsi: Kontrak membatalkan transaksiSolusi: Periksa status dan parameter kontrakContoh:
if (error.message === "TRANSACTION_REVERTED") {
  toast.error("Transaksi ditolak oleh kontrak. Silakan periksa persyaratan.");
  showTransactionDetails();
}

šŸ› ļø Pola Penanganan Kesalahan

Penanganan Kesalahan Tingkat Komponen

Payment Component with Error Handling
import { useAnyspendCreateOrder } from "@b3dotfun/sdk/anyspend";

function PaymentComponent() {
  const [error, setError] = useState<string | null>(null);
  const [retryCount, setRetryCount] = useState(0);

  const { createOrder, isCreatingOrder } = useAnyspendCreateOrder({
    onError: error => {
      console.error("Pembayaran gagal:", error);

      // Menangani kesalahan spesifik
      switch (error.message) {
        case "INSUFFICIENT_BALANCE":
          setError("Saldo tidak cukup. Silakan tambah dana ke dompet Anda.");
          break;

        case "SLIPPAGE":
          if (retryCount < 3) {
            setError("Harga bergerak tidak menguntungkan. Mencoba lagi...");
            setTimeout(() => {
              setRetryCount(prev => prev + 1);
              retryPayment();
            }, 2000);
          } else {
            setError("Harga terlalu fluktuatif. Silakan coba lagi nanti.");
          }
          break;

        case "NETWORK_ERROR":
          setError("Masalah jaringan. Silakan periksa koneksi Anda dan coba lagi.");
          break;

        case "QUOTE_EXPIRED":
          setError("Kutipan harga kedaluwarsa. Mendapatkan kutipan baru...");
          refreshQuote();
          break;

        default:
          setError("Pembayaran gagal. Silakan coba lagi atau hubungi dukungan.");
      }

      // Melacak kesalahan untuk pemantauan
      analytics.track("payment_error", {
        error: error.message,
        retryCount,
        timestamp: new Date().toISOString(),
      });
    },

    onSuccess: () => {
      setError(null);
      setRetryCount(0);
    },
  });

  return (
    <div className="payment-component">
      {error && (
        <div className="error-banner">
          <span className="error-icon">āš ļø</span>
          <span>{error}</span>
          <button onClick={() => setError(null)}>Tutup</button>
        </div>
      )}

      <button onClick={handlePayment} disabled={isCreatingOrder}>
        {isCreatingOrder ? "Memproses..." : "Bayar Sekarang"}
      </button>
    </div>
  );
}

Pemantauan Status Pesanan

Order Status Monitor
import { useAnyspendOrderAndTransactions } from "@b3dotfun/sdk/anyspend";

function OrderStatusMonitor({ orderId }: { orderId: string }) {
  const { orderAndTransactions, getOrderAndTransactionsError } = useAnyspendOrderAndTransactions(orderId);

  if (getOrderAndTransactionsError) {
    return (
      <div className="error-state">
        <h3>Tidak dapat memuat status pesanan</h3>
        <p>Silakan periksa koneksi Anda dan coba lagi.</p>
        <button onClick={() => window.location.reload()}>Coba Lagi</button>
      </div>
    );
  }

  if (!orderAndTransactions) {
    return <div>Memuat status pesanan...</div>;
  }

  const { order, depositTxs, executeTx, refundTxs } = orderAndTransactions.data;

  const renderStatusMessage = () => {
    switch (order.status) {
      case "scanning_deposit_transaction":
        return (
          <div className="status-pending">
            <div className="spinner" />
            <div>
              <h3>ā³ Menunggu konfirmasi pembayaran</h3>
              <p>Ini biasanya memakan waktu 1-2 menit. Silakan jangan tutup jendela ini.</p>
              {depositTxs.length > 0 && (
                <a
                  href={getExplorerUrl(depositTxs[0].txHash, depositTxs[0].chainId)}
                  target="_blank"
                  rel="noopener noreferrer"
                >
                  Lihat transaksi pembayaran
                </a>
              )}
            </div>
          </div>
        );

      case "relay":
        return (
          <div className="status-processing">
            <div className="spinner" />
            <div>
              <h3>šŸ”„ Memproses transaksi lintas rantai</h3>
              <p>Pembayaran Anda sedang diproses. Ini mungkin memakan waktu beberapa menit.</p>
            </div>
          </div>
        );

      case "executed":
        return (
          <div className="status-success">
            <div className="success-icon">āœ…</div>
            <div>
              <h3>Transaksi berhasil diselesaikan!</h3>
              <p>Pesanan Anda telah diproses.</p>
              {executeTx && (
                <a href={getExplorerUrl(executeTx.txHash, executeTx.chainId)} target="_blank" rel="noopener noreferrer">
                  Lihat transaksi
                </a>
              )}
            </div>
          </div>
        );

      case "failure":
      case "obtain_failed":
        return (
          <div className="status-error">
            <div className="error-icon">āŒ</div>
            <div>
              <h3>Transaksi gagal</h3>
              <p>{order.errorDetails || "Terjadi kesalahan saat memproses pesanan Anda."}</p>
              <div className="error-actions">
                <button onClick={() => createNewOrder()}>Coba Lagi</button>
                <button onClick={() => contactSupport(orderId)}>Hubungi Dukungan</button>
              </div>
            </div>
          </div>
        );

      case "refunded":
        return (
          <div className="status-refunded">
            <div className="refund-icon">ā†©ļø</div>
            <div>
              <h3>Pengembalian dana diproses</h3>
              <p>Pembayaran Anda telah dikembalikan secara otomatis.</p>
              {refundTxs.length > 0 && (
                <a
                  href={getExplorerUrl(refundTxs[0].txHash, refundTxs[0].chainId)}
                  target="_blank"
                  rel="noopener noreferrer"
                >
                  Lihat transaksi pengembalian dana
                </a>
              )}
            </div>
          </div>
        );

      case "expired":
        return (
          <div className="status-expired">
            <div className="expired-icon">ā°</div>
            <div>
              <h3>Pesanan kedaluwarsa</h3>
              <p>Pesanan ini kedaluwarsa sebelum pembayaran diterima.</p>
              <button onClick={() => createNewOrder()}>Buat Pesanan Baru</button>
            </div>
          </div>
        );

      default:
        return (
          <div className="status-unknown">
            <div className="spinner" />
            <div>
              <h3>Memproses...</h3>
              <p>Status pesanan: {order.status}</p>
            </div>
          </div>
        );
    }
  };

  return (
    <div className="order-status-monitor">
      <div className="order-header">
        <h2>Pesanan #{orderId.slice(0, 8)}</h2>
        <div className="order-meta">
          <span>Dibuat: {new Date(order.createdAt).toLocaleString()}</span>
          <span>Status: {order.status}</span>
        </div>
      </div>

      {renderStatusMessage()}

      {/* Informasi debug dalam pengembangan */}
      {process.env.NODE_ENV === "development" && (
        <details className="debug-info">
          <summary>Informasi Debug</summary>
          <pre>{JSON.stringify(order, null, 2)}</pre>
        </details>
      )}
    </div>
  );
}

Batas Kesalahan Global

Error Boundary Component
import React, { Component, ErrorInfo } from "react";

interface Props {
  children: React.ReactNode;
  fallback?: React.ComponentType<{ error: Error; resetError: () => void }>;
}

interface State {
  hasError: boolean;
  error?: Error;
}

class AnySpendErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error("AnySpend Error Boundary caught an error:", error, errorInfo);

    // Laporkan ke layanan pelacakan kesalahan
    if (typeof window !== "undefined") {
      // Contoh: Sentry.captureException(error, { contexts: { errorInfo } });
    }
  }

  resetError = () => {
    this.setState({ hasError: false, error: undefined });
  };

  render() {
    if (this.state.hasError) {
      const FallbackComponent = this.props.fallback || DefaultErrorFallback;
      return <FallbackComponent error={this.state.error!} resetError={this.resetError} />;
    }

    return this.props.children;
  }
}

function DefaultErrorFallback({ error, resetError }: { error: Error; resetError: () => void }) {
  return (
    <div className="error-fallback">
      <h2>Terjadi kesalahan</h2>
      <p>Terjadi kesalahan tak terduga pada komponen pembayaran.</p>
      <details className="error-details">
        <summary>Detail kesalahan</summary>
        <pre>{error.message}</pre>