🎥 Ezzocard StreamElements Widget

Official documentation for adding the Ezzocard live checkout widget to StreamElements. This page explains the setup process, the supported flow, referral tracking with manual or automatic StreamElements user detection, chat command integration, and provides a full copy-ready widget code block.

Platform: StreamElements
Mode: Custom Widget
QR Code Included
Full Checkout Flow
Dark Overlay UI
Referral Tracking
Auto Detect
Chat Commands

Quick Links

Useful public resources related to this integration.

Important: this page is focused specifically on the StreamElements widget setup and not on general API integration.

What This Widget Does

Select card type

The user can choose from the supported Ezzocard products.

Select amount

Only amounts aligned with the configured sell prices are displayed.

Select crypto

The widget supports BTC, ETH, USDT networks, DOGE, LTC, TRX, SOL and BNB.

Generate order

The widget creates the order and displays payment instructions.

QR code payment

Users can scan the QR code directly from the overlay.

Live card delivery

Once payment is confirmed, delivered card data is shown in the widget.

Referral tracking

Each order can be attributed using a manual ID or automatic StreamElements channel detection.

Chat interaction

The PRO widget can react to supported chat commands and show engagement prompts.

How to Add the Widget in StreamElements

UI StreamElements → My Overlays → Add Widget → Custom Widget
  1. Open your StreamElements dashboard.
  2. Go to My Overlays.
  3. Open the overlay where you want the widget.
  4. Add a new Custom Widget.
  5. Open the widget editor.
  6. Delete any example or test code already in the editor.
  7. Paste the full code from the section Final StreamElements Widget Code.
  8. Open the widget code and locate the YOUR STREAMER ID block near the top of the script.
  9. Replace YOUR_STREAMELEMENTS_USER_ID with your own ID, or leave it unchanged if you want to use automatic detection.
  10. Save the widget.
  11. Test the full payment flow.
Recommended: test the widget in preview mode before using it on a live scene.

Streamer ID Setup

The PRO widget supports two referral tracking modes.

Option 1 — Manual setup

Replace YOUR_STREAMELEMENTS_USER_ID in the widget code with your own StreamElements user ID.

Option 2 — Automatic detection

If you leave the placeholder unchanged, the widget will try to automatically detect your StreamElements channel username and use it as the referral ID.

Recommended: set your own ID manually for maximum accuracy.
Manual example:
const EZZO_REF = "YOUR_STREAMELEMENTS_USER_ID";
Optional creator name:
const EZZO_CREATOR = "MyChannel";

Quick Setup for Streamers

  1. Open StreamElements.
  2. Add a Custom Widget to your overlay.
  3. Paste the widget code from this page.
  4. Set your own StreamElements ID in EZZO_REF, or leave the placeholder for automatic detection.
  5. Save and test the widget before going live.
Manual setup example:
const EZZO_REF = "streamer123";

Chat Commands and Viewer Engagement

The PRO widget supports chat-triggered engagement actions through StreamElements event listeners.

!buy

Shows a call-to-action prompt and highlights the creator link.

!card

Promotes the card checkout widget to the audience.

!visa

Can preselect the Visa card option in the PRO widget.

!crypto

Displays the supported crypto payment methods to the viewer.

Note: the widget can react visually to supported chat commands, but automatic chat replies must still be configured separately in the StreamElements bot if you want bot messages in chat.

Recommended Checkout Flow

Step 1

Select a supported card type.

Step 2

Select an available amount.

Step 3

Select the crypto payment method.

Step 4

Click Generate Payment to create the order.

Step 5

Show the user the wallet address, QR code and exact crypto amount.

Step 6

Poll for payment status until the cards are delivered.

What the Final Widget Includes

Final StreamElements Widget Code

Copy the full widget code below and paste it into your StreamElements Custom Widget.

<div id="ezzo-widget-v2">
  <div class="ezzo-v2-shell">
    <div class="ezzo-v2-top">
      <div class="ezzo-v2-brand">EZZOCARD LIVE</div>
      <div class="ezzo-v2-title">Virtual Card Checkout</div>
      <div class="ezzo-v2-sub">Fast crypto payment overlay</div>
      <div class="ezzo-v2-promo">
        Creator: <strong id="v2-refBadge">creator</strong> | Instant delivery | Visa / Mastercard
      </div>
      <div class="v2-top-actions">
        <a id="v2-creatorUrl" class="v2-top-link" href="https://ezzocard.finance/" target="_blank" rel="noopener">Open creator link</a>
        <button id="v2-copyCreatorBtn" class="v2-mini-btn" type="button">Copy ref link</button>
      </div>
      <div id="v2-alertStack" class="v2-alert-stack"></div>
    </div>

    <div id="v2-step-select" class="v2-step active">
      <div class="v2-label">Card type</div>
      <select id="v2-cardType"></select>

      <div class="v2-label">Amount</div>
      <select id="v2-amount"></select>

      <div id="v2-cardInfo" class="v2-card-info"></div>

      <div class="v2-label">Crypto</div>
      <div id="v2-cryptoGrid" class="v2-crypto-grid"></div>

      <div class="v2-summary">
        <div><span>Card</span><strong id="v2-sumCard">-</strong></div>
        <div><span>Amount</span><strong id="v2-sumAmount">-</strong></div>
        <div><span>Crypto</span><strong id="v2-sumCrypto">-</strong></div>
      </div>

      <div class="v2-chat-panel">
        <div class="v2-chat-head">
          <strong>Chat bot triggers</strong>
          <span>Viewer engagement</span>
        </div>
        <div class="v2-chat-commands">
          <span id="v2-cmd-buy">!buy</span>
          <span id="v2-cmd-card">!card</span>
          <span id="v2-cmd-visa">!visa</span>
          <span id="v2-cmd-crypto">!crypto</span>
        </div>
        <div id="v2-chatHint" class="v2-chat-hint">
          Viewers can trigger promo prompts directly from chat.
        </div>
      </div>

      <button id="v2-createBtn" class="v2-main-btn" disabled>
        Generate Payment
      </button>

      <div id="v2-createMsg" class="v2-msg"></div>
    </div>

    <div id="v2-step-pay" class="v2-step">
      <div class="v2-pay-box">
        <div class="v2-pay-head">Payment Details</div>
        <div class="v2-pay-card-brand" id="v2-payCardBrand"></div>

        <div class="v2-pay-row"><span>Order ID</span><strong id="v2-orderId">-</strong></div>
        <div class="v2-pay-row"><span>Secret</span><strong id="v2-secret">-</strong></div>
        <div class="v2-pay-row">
          <span>Crypto</span>
          <strong id="v2-crypto" class="v2-pay-crypto">-</strong>
        </div>
        <div class="v2-pay-row"><span>Total</span><strong id="v2-total">-</strong></div>
      </div>

      <div class="v2-label">Wallet address</div>
      <div class="v2-address-box">
        <div id="v2-address" class="v2-address-text">-</div>
        <button id="v2-copyBtn" class="v2-copy-btn">Copy</button>
      </div>

      <div class="v2-qr-wrap">
        <div class="v2-qr-box">
          <div id="v2-qrCanvas"></div>
        </div>
        <div class="v2-qr-note" id="v2-qrNote">Scan to pay</div>
      </div>

      <div class="v2-link-box">
        <a id="v2-paymentUrl" href="#" target="_blank" rel="noopener">Open payment page</a>
      </div>

      <div class="v2-timer-wrap">
        <div class="v2-timer-head">
          <span id="v2-statusText">Waiting for payment...</span>
          <strong id="v2-timeText">30:00</strong>
        </div>
        <div class="v2-progress">
          <div id="v2-progressBar" class="v2-progress-bar"></div>
        </div>
      </div>

      <div class="v2-actions">
        <button id="v2-checkBtn" class="v2-sec-btn">Check now</button>
        <button id="v2-resetBtn" class="v2-cancel-btn">Cancel</button>
      </div>

      <div id="v2-payMsg" class="v2-msg"></div>
    </div>

    <div id="v2-step-done" class="v2-step">
      <div class="v2-done-title">Payment Confirmed</div>
      <div id="v2-doneMeta" class="v2-msg ok">Card delivered successfully.</div>
      <div id="v2-cardsWrap" class="v2-cards-wrap"></div>
      <button id="v2-resetBtn2" class="v2-main-btn">New Order</button>
    </div>
  </div>
</div>

<style>
#ezzo-widget-v2 {
  width: 390px;
  max-width: 390px;
  font-family: Inter, Arial, sans-serif;
  color: #eef3ff;
}

* {
  box-sizing: border-box;
}

.ezzo-v2-shell {
  border-radius: 24px;
  overflow: hidden;
  background:
    radial-gradient(circle at top right, rgba(110, 124, 255, 0.22), transparent 32%),
    linear-gradient(180deg, rgba(10,14,24,0.98), rgba(5,8,16,0.98));
  border: 1px solid rgba(255,255,255,0.08);
  box-shadow: 0 20px 70px rgba(0,0,0,0.5);
  backdrop-filter: blur(10px);
}

.ezzo-v2-top {
  padding: 16px;
  border-bottom: 1px solid rgba(255,255,255,0.08);
}

.ezzo-v2-brand {
  display: inline-block;
  padding: 5px 10px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.12em;
  background: rgba(110,124,255,0.14);
  border: 1px solid rgba(110,124,255,0.24);
  color: #cfd7ff;
}

.ezzo-v2-title {
  margin-top: 10px;
  font-size: 22px;
  font-weight: 800;
}

.ezzo-v2-sub {
  margin-top: 4px;
  font-size: 12px;
  color: #9fb0d0;
}

.ezzo-v2-promo {
  margin-top: 10px;
  font-size: 11px;
  line-height: 1.45;
  color: #dbe4ff;
}

.v2-top-actions {
  display: flex;
  gap: 8px;
  margin-top: 10px;
}

.v2-top-link,
.v2-mini-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 34px;
  padding: 8px 10px;
  border-radius: 12px;
  font-size: 11px;
  font-weight: 800;
  text-decoration: none;
}

.v2-top-link {
  flex: 1;
  color: #eef3ff;
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.08);
}

.v2-top-link:hover {
  background: rgba(255,255,255,0.08);
}

.v2-mini-btn {
  cursor: pointer;
  border: 1px solid rgba(110,124,255,0.25);
  color: #fff;
  background: rgba(110,124,255,0.18);
}

.v2-mini-btn:hover {
  background: rgba(110,124,255,0.28);
}

.v2-alert-stack {
  display: grid;
  gap: 8px;
  margin-top: 12px;
}

.v2-alert {
  padding: 10px 12px;
  border-radius: 14px;
  font-size: 11px;
  line-height: 1.45;
  background: rgba(17,22,35,0.96);
  border: 1px solid rgba(255,255,255,0.08);
  animation: v2SlideIn 0.25s ease;
}

.v2-alert strong {
  display: block;
  font-size: 12px;
  margin-bottom: 3px;
}

.v2-alert.info {
  border-color: rgba(60,123,255,0.25);
}

.v2-alert.success {
  border-color: rgba(32,211,138,0.28);
}

.v2-alert.warn {
  border-color: rgba(255,179,71,0.32);
}

@keyframes v2SlideIn {
  from { opacity: 0; transform: translateY(-6px); }
  to { opacity: 1; transform: translateY(0); }
}

.v2-step {
  display: none;
  padding: 14px;
}

.v2-step.active {
  display: block;
}

.v2-label {
  font-size: 12px;
  color: #9fb0d0;
  margin: 10px 0 6px;
}

#ezzo-widget-v2 select {
  width: 100%;
  background: rgba(20,26,42,0.95);
  border: 1px solid rgba(255,255,255,0.08);
  color: #eef3ff;
  border-radius: 14px;
  padding: 12px;
  outline: none;
}

.v2-card-info,
.v2-summary,
.v2-pay-box,
.v2-address-box,
.v2-link-box,
.v2-timer-wrap,
.v2-card-box,
.v2-qr-box {
  background: rgba(17,22,35,0.96);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: 16px;
}

.v2-card-info {
  padding: 12px;
  color: #bfd0f2;
  font-size: 12px;
  line-height: 1.55;
  margin-top: 10px;
}

.v2-card-info-top {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 10px;
  margin-bottom: 8px;
}

.v2-card-brand-line {
  margin-top: 8px;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.v2-card-brand-label {
  font-size: 11px;
  color: #9fb0d0;
  font-weight: 700;
}

.v2-brand-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: 999px;
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.08);
  color: #eef3ff;
  font-size: 11px;
  font-weight: 800;
}

.v2-brand-chip-large {
  margin-bottom: 10px;
  font-size: 12px;
}

.v2-brand-logo {
  height: 34px;
  max-width: 34px;
  object-fit: contain;
  border-radius: 6px;
  padding: 2px 4px;
}

.v2-pay-card-brand {
  margin-bottom: 10px;
}

.v2-tag {
  display: inline-block;
  margin: 3px 4px 0 0;
  padding: 4px 8px;
  border-radius: 999px;
  background: rgba(255,255,255,0.05);
  font-size: 10px;
  color: #d9e1ff;
}

.v2-crypto-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.v2-crypto-btn {
  border: 1px solid rgba(255,255,255,0.08);
  background: rgba(20,26,42,0.96);
  color: #eef3ff;
  border-radius: 12px;
  padding: 10px 8px;
  font-size: 11px;
  font-weight: 700;
  cursor: pointer;
  transition: 0.18s ease;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 92px;
  gap: 4px;
}

.v2-crypto-btn:hover {
  transform: translateY(-1px);
  border-color: rgba(110,124,255,0.4);
}

.v2-crypto-btn.active {
  background: rgba(110,124,255,0.18);
  border-color: rgba(110,124,255,0.45);
}

.v2-crypto-icon {
  width: 28px;
  height: 28px;
  object-fit: contain;
  background: #fff;
  border-radius: 50%;
  padding: 2px;
}

.v2-crypto-symbol {
  font-size: 11px;
  font-weight: 800;
  line-height: 1.1;
}

.v2-crypto-network {
  font-size: 10px;
  color: #9fb0d0;
  background: rgba(255,255,255,0.06);
  border-radius: 999px;
  padding: 2px 6px;
  line-height: 1.1;
}

.v2-summary {
  margin-top: 12px;
  padding: 12px;
}

.v2-chat-panel {
  margin-top: 12px;
  padding: 12px;
  background: rgba(17,22,35,0.96);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: 16px;
}

.v2-chat-head {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  align-items: center;
}

.v2-chat-head strong {
  font-size: 12px;
}

.v2-chat-head span,
.v2-chat-hint {
  font-size: 11px;
  color: #9fb0d0;
}

.v2-chat-commands {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 10px;
}

.v2-chat-commands span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 7px 10px;
  border-radius: 999px;
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.08);
  font-size: 11px;
  font-weight: 800;
  color: #eef3ff;
}

.v2-summary div,
.v2-pay-row {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 6px 0;
  font-size: 12px;
}

.v2-summary span,
.v2-pay-row span {
  color: #9fb0d0;
}

.v2-pay-crypto span {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.v2-pay-crypto img {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
  padding: 1px;
}

.v2-main-btn,
.v2-sec-btn,
.v2-cancel-btn,
.v2-copy-btn,
.v2-toggle-btn {
  cursor: pointer;
  border: 0;
  font-weight: 800;
  transition: 0.18s ease;
}

.v2-main-btn {
  width: 100%;
  margin-top: 12px;
  padding: 13px;
  border-radius: 14px;
  color: white;
  background: linear-gradient(135deg, #14b86a, #21d07a);
  box-shadow: 0 10px 30px rgba(20,184,106,0.18);
}

.v2-main-btn:hover {
  transform: translateY(-1px);
  background: linear-gradient(135deg, #12a45f, #1cc46f);
  box-shadow: 0 14px 34px rgba(20,184,106,0.28);
}

.v2-main-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.v2-pay-box {
  padding: 12px;
}

.v2-pay-head {
  font-size: 14px;
  font-weight: 800;
  margin-bottom: 8px;
}

.v2-address-box {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
}

.v2-address-text {
  flex: 1;
  font-family: monospace;
  font-size: 12px;
  word-break: break-all;
}

.v2-copy-btn {
  padding: 8px 12px;
  border-radius: 12px;
  color: #fff;
  background: rgba(110,124,255,0.18);
  border: 1px solid rgba(110,124,255,0.25);
}

.v2-copy-btn:hover {
  background: rgba(110,124,255,0.28);
}

.v2-qr-wrap {
  margin-top: 12px;
  text-align: center;
}

.v2-qr-box {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 12px;
}

#v2-qrCanvas {
  width: 180px;
  height: 180px;
  background: #fff;
  border-radius: 12px;
  overflow: hidden;
}

#v2-qrCanvas img,
#v2-qrCanvas canvas {
  display: block;
  width: 180px !important;
  height: 180px !important;
  border-radius: 12px;
}

.v2-qr-note {
  margin-top: 8px;
  font-size: 12px;
  color: #9fb0d0;
}

.v2-link-box {
  margin-top: 10px;
  padding: 10px 12px;
}

.v2-link-box a {
  color: #cfd7ff;
  text-decoration: none;
  font-size: 12px;
}

.v2-timer-wrap {
  margin-top: 12px;
  padding: 12px;
}

.v2-timer-head {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-size: 12px;
}

.v2-progress {
  margin-top: 10px;
  height: 10px;
  border-radius: 999px;
  background: rgba(255,255,255,0.06);
  overflow: hidden;
}

.v2-progress-bar {
  height: 100%;
  width: 100%;
  background: linear-gradient(90deg, #20d38a, #8fdc6b);
  transition: width 0.8s linear;
}

.v2-actions {
  display: flex;
  gap: 10px;
  margin-top: 12px;
}

.v2-sec-btn,
.v2-cancel-btn {
  flex: 1;
  padding: 12px;
  border-radius: 12px;
}

.v2-sec-btn {
  color: #fff;
  background: linear-gradient(135deg, #2667ff, #3c7bff);
  border: 1px solid rgba(60,123,255,0.35);
  box-shadow: 0 10px 28px rgba(38,103,255,0.16);
}

.v2-sec-btn:hover {
  transform: translateY(-1px);
  background: linear-gradient(135deg, #1f59e2, #2f6df0);
  box-shadow: 0 14px 32px rgba(38,103,255,0.28);
}

.v2-cancel-btn {
  color: #d9e1ff;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
}

.v2-cancel-btn:hover {
  transform: translateY(-1px);
  color: #fff;
  background: linear-gradient(135deg, #c9304f, #ef476f);
  border-color: rgba(239,71,111,0.4);
  box-shadow: 0 14px 32px rgba(239,71,111,0.28);
}

.v2-msg {
  margin-top: 10px;
  font-size: 12px;
  color: #aebddd;
  line-height: 1.5;
  min-height: 18px;
}

.v2-msg.ok {
  color: #9af0c9;
}

.v2-done-title {
  font-size: 16px;
  font-weight: 800;
  color: #20d38a;
}

.v2-cards-wrap {
  display: grid;
  gap: 10px;
  margin-top: 12px;
}

.v2-card-box {
  padding: 12px;
}

.v2-card-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
}

.v2-card-head strong {
  font-size: 13px;
}

.v2-toggle-btn {
  padding: 7px 10px;
  border-radius: 10px;
  color: #fff;
  background: rgba(110,124,255,0.18);
  border: 1px solid rgba(110,124,255,0.22);
}

.v2-toggle-btn:hover {
  background: rgba(110,124,255,0.28);
}

.v2-card-line {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 6px 0;
  font-size: 12px;
}

.v2-card-line span {
  color: #9fb0d0;
}

.v2-secret {
  filter: blur(7px);
  user-select: none;
  transition: 0.18s ease;
}

.v2-secret.show {
  filter: blur(0);
  user-select: text;
}

.v2-status-ok {
  color: #9af0c9;
}

.v2-status-bad {
  color: #ff9aaa;
}
</style>

<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>

<script>
const V2_CREATE = 'https://ezzocard.finance/ai-agent/process.php';
const V2_STATUS = 'https://ezzocard.finance/api/ai-check.php?secret=';

/***********************************************************
###########################
#   YOUR STREAMER ID      #
###########################

Replace with your StreamElements User ID.
If you leave the placeholder unchanged, the widget will try
to auto-detect the channel username from StreamElements.

Examples:
const EZZO_REF = "my_stream_id";
const EZZO_CREATOR = "MyChannel";

***********************************************************/
const EZZO_REF = "YOUR_STREAMELEMENTS_USER_ID";
const EZZO_CREATOR = "";
const EZZO_CAMPAIGN = "streamelements";
const EZZO_SOURCE = "streamelements-widget";

const V2_CRYPTOS = [
  'BTC',
  'ETH',
  'USDT.ERC20',
  'USDT.TRC20',
  'USDT.BEP20',
  'USDT.SOL',
  'DOGE',
  'LTC',
  'TRX',
  'SOL',
  'BNB'
];

const V2_CRYPTO_META = {
  'BTC': { name: 'Bitcoin', symbol: 'BTC', image: 'https://ezzocard.finance/images/bitcoin.svg', network: 'Bitcoin' },
  'ETH': { name: 'Ethereum', symbol: 'ETH', image: 'https://ezzocard.finance/images/ethereum.svg', network: 'ERC20' },
  'USDT.ERC20': { name: 'Tether', symbol: 'USDT', image: 'https://ezzocard.finance/images/usdt_erc20.svg', network: 'ERC20' },
  'USDT.TRC20': { name: 'Tether', symbol: 'USDT', image: 'https://ezzocard.finance/images/usdt_trc20.svg', network: 'TRC20' },
  'USDT.BEP20': { name: 'Tether', symbol: 'USDT', image: 'https://ezzocard.finance/images/usdt_bep20.svg', network: 'BEP20' },
  'USDT.SOL': { name: 'Tether', symbol: 'USDT', image: 'https://ezzocard.finance/images/usdt_sol.svg', network: 'SOL' },
  'DOGE': { name: 'Dogecoin', symbol: 'DOGE', image: 'https://ezzocard.finance/images/dogecoin.svg', network: 'Dogecoin' },
  'LTC': { name: 'Litecoin', symbol: 'LTC', image: 'https://ezzocard.finance/images/litecoin.svg', network: 'Litecoin' },
  'TRX': { name: 'Tron', symbol: 'TRX', image: 'https://ezzocard.finance/images/tron.svg', network: 'TRC20' },
  'SOL': { name: 'Solana', symbol: 'SOL', image: 'https://ezzocard.finance/images/solana.svg', network: 'Solana' },
  'BNB': { name: 'BNB', symbol: 'BNB', image: 'https://ezzocard.finance/images/bnb.svg', network: 'BEP20' }
};

const V2_CARD_BRANDS = {
  'gold_visa': { brand: 'Visa', logo: 'https://ezzocard.finance/images/s-visa.svg' },
  'gold_mastercard': { brand: 'Mastercard', logo: 'https://ezzocard.finance/images/s-mastercard.svg' },
  'violet': { brand: 'Visa', logo: 'https://ezzocard.finance/images/s-visa.svg' },
  'lime-7': { brand: 'Visa', logo: 'https://ezzocard.finance/images/s-visa.svg' },
  'lime-30': { brand: 'Visa', logo: 'https://ezzocard.finance/images/s-visa.svg' },
  'brown': { brand: 'Mastercard', logo: 'https://ezzocard.finance/images/s-mastercard.svg' },
  'orange': { brand: 'Visa', logo: 'https://ezzocard.finance/images/s-visa.svg' },
  'yellow': { brand: 'Mastercard', logo: 'https://ezzocard.finance/images/s-mastercard.svg' },
  'maroon': { brand: 'Visa', logo: 'https://ezzocard.finance/images/s-visa.svg' },
  'teal': { brand: 'Visa', logo: 'https://ezzocard.finance/images/s-visa.svg' }
};

const V2_CARDS = {
  'gold_visa': {
    title: 'Gold Visa',
    currency: 'USD',
    validity: '24 months',
    registration: 'Required',
    tags: ['Visa', '3D Secure', 'Wallet'],
    pricing: { 100: 125.00, 200: 215.00, 500: 509.99, 1000: 1029.99, 2000: 2029.99, 5000: 5029.99, 10000: 10229.99 }
  },
  'gold_mastercard': {
    title: 'Gold Mastercard',
    currency: 'USD',
    validity: '24 months',
    registration: 'Required',
    tags: ['Mastercard', '3D Secure', 'Wallet'],
    pricing: { 100: 125.00, 200: 219.00, 500: 509.99, 1000: 1029.99, 2000: 2029.99, 5000: 5029.99, 10000: 10229.99 }
  },
  'violet': {
    title: 'Violet',
    currency: 'USD',
    validity: '24 months',
    registration: 'No',
    tags: ['Anonymous', 'USD'],
    pricing: { 50: 74.99, 100: 119.99, 250: 275.00, 500: 505.99, 1000: 1019.99, 2000: 2179.99, 5000: 5179.99, 10000: 10179.99 }
  },
  'lime-7': {
    title: 'Lime-7',
    currency: 'USD',
    validity: '24 months',
    registration: 'No',
    tags: ['Anonymous', 'Wallet'],
    pricing: { 100: 109.99, 200: 209.99, 250: 259.99, 500: 504.99, 2000: 2009.99, 5000: 5009.99 }
  },
  'lime-30': {
    title: 'Lime-30',
    currency: 'USD',
    validity: '24 months',
    registration: 'No',
    tags: ['Anonymous', 'US Only'],
    pricing: { 100: 114.99, 200: 219.99, 250: 269.99, 500: 504.99, 2000: 2019.99, 10000: 10019.99 }
  },
  'brown': {
    title: 'Brown',
    currency: 'CAD',
    validity: '6 months - 3 years',
    registration: 'Required',
    tags: ['CAD', 'Mastercard'],
    pricing: { 25: 51.99, 50: 57.99, 100: 99.99, 200: 189.99 }
  },
  'orange': {
    title: 'Orange',
    currency: 'USD',
    validity: '5-8 years',
    registration: 'Required',
    tags: ['US Only', 'Visa'],
    pricing: { 25: 56.99, 50: 79.99, 100: 124.99, 200: 259.99 }
  },
  'yellow': {
    title: 'Yellow',
    currency: 'USD',
    validity: '12 months',
    registration: 'No',
    tags: ['Anonymous', 'Mastercard'],
    pricing: { 50: 54.00, 100: 110.00, 200: 215.00, 250: 260.00, 500: 505.00 }
  },
  'maroon': {
    title: 'Maroon',
    currency: 'USD',
    validity: '6 months',
    registration: 'No',
    tags: ['US Only', 'Visa'],
    pricing: { 25: 49.99, 50: 77.99, 100: 122.99, 200: 249.99, 500: 506.99, 1000: 1199.99 }
  },
  'teal': {
    title: 'Teal',
    currency: 'CAD',
    validity: '6 months',
    registration: 'No',
    tags: ['CAD', 'Anonymous'],
    pricing: { 25: 44.99, 50: 52.99, 100: 89.99, 250: 209.99 }
  }
};

const v2 = {
  cardType: 'gold_visa',
  amount: 100,
  crypto: '',
  order: null,
  expiresIn: 1800,
  startedAt: null,
  poller: null,
  timer: null,
  ref: EZZO_REF,
  campaign: EZZO_CAMPAIGN,
  source: EZZO_SOURCE,
  creatorName: EZZO_CREATOR || 'creator',
  creatorUrl: EZZO_REF && EZZO_REF !== 'YOUR_STREAMELEMENTS_USER_ID'
    ? `https://ezzocard.finance/?ref=${encodeURIComponent(EZZO_REF)}`
    : 'https://ezzocard.finance/',
  commands: {
    buy: '!buy',
    card: '!card',
    visa: '!visa',
    crypto: '!crypto'
  },
  alerts: [],
  activity: [],
  autoDetectedRef: false
};

const $ = (id) => document.getElementById(id);

function esc(v) {
  return String(v ?? '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

function money(v) {
  const n = Number(v || 0);
  return n.toFixed(2);
}

function pickNonEmpty(...values) {
  return values.find(v => String(v || '').trim() !== '') || '';
}

function isPlaceholderRef(value) {
  const v = String(value || '').trim();
  return !v || v === 'YOUR_STREAMELEMENTS_USER_ID' || v === 'creator';
}

function normalizeChannelRef(value) {
  return String(value || '')
    .trim()
    .replace(/^@+/, '')
    .replace(/\s+/g, '');
}

function buildCreatorUrl(refValue) {
  const ref = normalizeChannelRef(refValue);
  return ref ? `https://ezzocard.finance/?ref=${encodeURIComponent(ref)}` : 'https://ezzocard.finance/';
}

function getCardConfig() {
  return V2_CARDS[v2.cardType];
}

function getSellPrice() {
  const cfg = getCardConfig();
  return cfg?.pricing?.[v2.amount] ?? null;
}

function getAmountsForCard(cardType) {
  const cfg = V2_CARDS[cardType];
  return Object.keys(cfg.pricing).map(Number).sort((a, b) => a - b);
}

function v2SetStep(stepId) {
  ['v2-step-select', 'v2-step-pay', 'v2-step-done'].forEach(id => {
    $(id).classList.remove('active');
  });
  $(stepId).classList.add('active');
}

function v2InitCardTypes() {
  $('v2-cardType').innerHTML = Object.entries(V2_CARDS)
    .map(([k, c]) => `<option value="${k}">${c.title} (${c.currency})</option>`)
    .join('');
}

function v2InitAmounts() {
  const cfg = getCardConfig();
  const amounts = getAmountsForCard(v2.cardType);

  $('v2-amount').innerHTML = amounts.map(a => {
    const sellPrice = cfg.pricing[a];
    return `<option value="${a}">${cfg.currency} ${a} - Sell ${cfg.currency} ${money(sellPrice)}</option>`;
  }).join('');

  v2.amount = Number($('v2-amount').value);
}

function v2RenderInfo() {
  const c = getCardConfig();
  const brandMeta = V2_CARD_BRANDS[v2.cardType] || null;

  $('v2-cardInfo').innerHTML = `
    <div class="v2-card-info-top">
      <div>
        <div><strong>${esc(c.title)}</strong></div>
        <div class="v2-card-brand-line">
          <span class="v2-card-brand-label">Card type</span>
          ${brandMeta ? `
            <span class="v2-brand-chip">
              <img src="${brandMeta.logo}" alt="${esc(brandMeta.brand)}" class="v2-brand-logo">
              <span>${esc(brandMeta.brand)}</span>
            </span>
          ` : ''}
        </div>
      </div>
    </div>
    <div>Currency: ${esc(c.currency)}</div>
    <div>Validity: ${esc(c.validity)}</div>
    <div>Registration: ${esc(c.registration)}</div>
    <div style="margin-top:6px;">${c.tags.map(t => `<span class="v2-tag">${esc(t)}</span>`).join('')}</div>
  `;
}

function v2RenderCryptos() {
  $('v2-cryptoGrid').innerHTML = V2_CRYPTOS.map(c => {
    const meta = V2_CRYPTO_META[c] || { symbol: c, image: '', network: '' };

    return `
      <button class="v2-crypto-btn ${v2.crypto === c ? 'active' : ''}" data-crypto="${c}">
        ${meta.image ? `<img src="${meta.image}" alt="${esc(meta.symbol)}" class="v2-crypto-icon">` : ''}
        <span class="v2-crypto-symbol">${esc(meta.symbol)}</span>
        ${meta.network ? `<span class="v2-crypto-network">${esc(meta.network)}</span>` : ''}
      </button>
    `;
  }).join('');

  [...document.querySelectorAll('.v2-crypto-btn')].forEach(btn => {
    btn.onclick = () => {
      v2.crypto = btn.dataset.crypto;
      v2RenderCryptos();
      v2UpdateSummary();
    };
  });
}

function v2UpdateSummary() {
  const cfg = getCardConfig();
  const sellPrice = getSellPrice();
  const cryptoMeta = V2_CRYPTO_META[v2.crypto] || null;
  const brandMeta = V2_CARD_BRANDS[v2.cardType] || null;

  if (brandMeta) {
    $('v2-sumCard').innerHTML = `
      <span style="display:inline-flex;align-items:center;gap:8px;">
        <img src="${brandMeta.logo}" alt="${esc(brandMeta.brand)}" style="height:34px;max-width:34px;object-fit:contain;border-radius:6px;padding:2px;">
        ${esc(cfg.title)}
      </span>
    `;
  } else {
    $('v2-sumCard').textContent = cfg.title;
  }

  $('v2-sumAmount').textContent = `${cfg.currency} ${v2.amount} -> ${cfg.currency} ${money(sellPrice)}`;

  if (v2.crypto && cryptoMeta) {
    $('v2-sumCrypto').textContent = `${cryptoMeta.symbol} (${cryptoMeta.network})`;
  } else {
    $('v2-sumCrypto').textContent = '-';
  }

  $('v2-createBtn').disabled = !v2.crypto || !sellPrice;
}

function v2BuildQrValue(address, crypto, amount) {
  const c = String(crypto || '').toUpperCase();

  if (!address) return '';
  if (c === 'BTC') return `bitcoin:${address}?amount=${amount || ''}`;
  if (c === 'ETH') return `ethereum:${address}`;
  if (c === 'LTC') return `litecoin:${address}?amount=${amount || ''}`;
  if (c === 'DOGE') return `dogecoin:${address}?amount=${amount || ''}`;
  if (c === 'TRX') return address;
  if (c === 'SOL') return address;
  if (c === 'BNB') return address;
  if (c.indexOf('USDT') === 0) return address;

  return address;
}

function v2RenderQr(address, crypto, amount) {
  const qrBox = $('v2-qrCanvas');
  const qrNote = $('v2-qrNote');

  if (!qrBox) return;

  qrBox.innerHTML = '';
  const qrValue = v2BuildQrValue(address, crypto, amount);

  if (!qrValue) {
    qrNote.textContent = 'QR unavailable';
    return;
  }

  new QRCode(qrBox, {
    text: qrValue,
    width: 180,
    height: 180
  });

  qrNote.textContent = `Scan to pay with ${crypto}`;
}

function v2Notify(type, title, message) {
  const stack = $('v2-alertStack');
  if (!stack) return;

  const box = document.createElement('div');
  box.className = `v2-alert ${type || 'info'}`;
  box.innerHTML = `<strong>${esc(title)}</strong><div>${esc(message)}</div>`;
  stack.prepend(box);

  while (stack.children.length > 3) {
    stack.removeChild(stack.lastChild);
  }

  setTimeout(() => {
    if (box.parentNode) box.parentNode.removeChild(box);
  }, 7000);
}

function v2CopyCreatorUrl() {
  if (!v2.creatorUrl) return;
  navigator.clipboard.writeText(v2.creatorUrl).then(() => {
    v2Notify('success', 'Referral link copied', 'Share this creator link in chat, panels, or bot replies.');
  }).catch(() => {
    v2Notify('warn', 'Copy failed', 'Clipboard is not available in this browser context.');
  });
}

function v2SetCardType(cardType) {
  if (!V2_CARDS[cardType]) return;
  v2.cardType = cardType;
  $('v2-cardType').value = cardType;
  v2InitAmounts();
  v2RenderInfo();
  v2UpdateSummary();
}

function v2HandleChatCommand(rawText, username = 'Viewer') {
  const text = String(rawText || '').trim().toLowerCase();
  if (!text) return;

  if (text === v2.commands.buy.toLowerCase() || text.startsWith(v2.commands.buy.toLowerCase() + ' ')) {
    v2Notify('info', `${username} used ${v2.commands.buy}`, `CTA pushed for ${v2.creatorName}. Link: ${v2.creatorUrl}`);
    return;
  }

  if (text === v2.commands.card.toLowerCase() || text.startsWith(v2.commands.card.toLowerCase() + ' ')) {
    v2Notify('info', `${username} asked for cards`, 'Overlay highlighted the active card checkout funnel.');
    return;
  }

  if (text === v2.commands.visa.toLowerCase() || text.startsWith(v2.commands.visa.toLowerCase() + ' ')) {
    v2SetCardType('gold_visa');
    v2Notify('info', `${username} requested Visa`, 'Gold Visa was preselected in the widget.');
    return;
  }

  if (text === v2.commands.crypto.toLowerCase() || text.startsWith(v2.commands.crypto.toLowerCase() + ' ')) {
    v2Notify('warn', `${username} asked for crypto options`, V2_CRYPTOS.join(', '));
  }
}

function v2GetRuntimeConfig(detail = null) {
  const params = new URLSearchParams(window.location.search);
  const seData = detail?.detail?.fieldData || window.fieldData || {};
  const channelData = detail?.detail?.channel || {};
  const custom = window.EZZO_WIDGET_CONFIG || {};

  const channelUsername = normalizeChannelRef(channelData.username || '');
  const manualRef = normalizeChannelRef(EZZO_REF);
  const manualCreator = String(EZZO_CREATOR || '').trim();

  const runtimeRef = normalizeChannelRef(
    pickNonEmpty(seData.ref, custom.ref, params.get('ref'))
  );

  if (!isPlaceholderRef(manualRef)) {
    v2.ref = manualRef;
  } else if (runtimeRef) {
    v2.ref = runtimeRef;
  } else if (channelUsername) {
    v2.ref = channelUsername;
    v2.autoDetectedRef = true;
  } else {
    v2.ref = 'creator';
  }

  const runtimeCampaign = pickNonEmpty(seData.campaign, custom.campaign, params.get('campaign'));
  const runtimeSource = pickNonEmpty(seData.source, custom.source, params.get('source'));
  const runtimeCreator = pickNonEmpty(seData.creatorName, custom.creatorName, params.get('creator'));

  v2.campaign = runtimeCampaign || EZZO_CAMPAIGN;
  v2.source = runtimeSource || EZZO_SOURCE;
  v2.creatorName = runtimeCreator || manualCreator || v2.ref || 'creator';

  const runtimeClickUrl = pickNonEmpty(seData.clickUrl, custom.clickUrl, params.get('url'));
  v2.creatorUrl = runtimeClickUrl || buildCreatorUrl(v2.ref);

  v2.commands.buy = pickNonEmpty(seData.cmdBuy, custom.cmdBuy, params.get('cmd_buy')) || '!buy';
  v2.commands.card = pickNonEmpty(seData.cmdCard, custom.cmdCard, params.get('cmd_card')) || '!card';
  v2.commands.visa = pickNonEmpty(seData.cmdVisa, custom.cmdVisa, params.get('cmd_visa')) || '!visa';
  v2.commands.crypto = pickNonEmpty(seData.cmdCrypto, custom.cmdCrypto, params.get('cmd_crypto')) || '!crypto';
}

function v2RenderRuntimeMeta() {
  $('v2-refBadge').textContent = v2.creatorName;
  $('v2-creatorUrl').href = v2.creatorUrl;
  $('v2-cmd-buy').textContent = v2.commands.buy;
  $('v2-cmd-card').textContent = v2.commands.card;
  $('v2-cmd-visa').textContent = v2.commands.visa;
  $('v2-cmd-crypto').textContent = v2.commands.crypto;
  $('v2-chatHint').textContent = `Affiliate ref: ${v2.ref} | Campaign: ${v2.campaign}`;
}

async function v2CreateOrder() {
  $('v2-createMsg').textContent = 'Creating order...';

  const cfg = getCardConfig();
  const sellPrice = getSellPrice();

  if (!sellPrice) {
    $('v2-createMsg').textContent = 'Selected amount is not available.';
    return;
  }

  const payload = {
    card_type: v2.cardType,
    amount: Number(v2.amount),
    currency: cfg.currency,
    crypto: v2.crypto,
    ref: v2.ref,
    campaign: v2.campaign,
    source: v2.source,
    creator_name: v2.creatorName,
    landing_url: v2.creatorUrl
  };

  try {
    const res = await fetch(V2_CREATE, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    });

    const data = await res.json();

    if (!res.ok || !data.success || !data.secret) {
      throw new Error(data.message || data.error || 'Could not create order.');
    }

    v2.order = data;
    v2.expiresIn = Number(data.expires_in || 1800);
    v2.startedAt = Date.now();

    $('v2-orderId').textContent = data.order_id || '-';
    $('v2-secret').textContent = data.secret || '-';

    const payBrand = $('v2-payCardBrand');
    const brandMeta = V2_CARD_BRANDS[v2.cardType] || null;

    if (brandMeta) {
      payBrand.innerHTML = `
        <span class="v2-brand-chip v2-brand-chip-large">
          <img src="${brandMeta.logo}" alt="${esc(brandMeta.brand)}" class="v2-brand-logo">
          <span>${esc(getCardConfig().title)} | ${esc(brandMeta.brand)}</span>
        </span>
      `;
    } else {
      payBrand.innerHTML = '';
    }

    const payCrypto = $('v2-crypto');
    const cryptoMeta = V2_CRYPTO_META[data.crypto] || null;

    if (cryptoMeta) {
      payCrypto.innerHTML = `
        <span>
          <img src="${cryptoMeta.image}" alt="${esc(cryptoMeta.symbol)}">
          ${esc(data.crypto)}
        </span>
      `;
    } else {
      payCrypto.textContent = data.crypto || '-';
    }

    $('v2-total').textContent = data.amount_crypto ? `${data.amount_crypto} ${data.crypto}` : '-';
    $('v2-address').textContent = data.address || '-';

    v2RenderQr(data.address || '', data.crypto || '', data.amount_crypto || '');

    const link = $('v2-paymentUrl');
    if (data.payment_url) {
      link.href = data.payment_url;
      link.textContent = 'Open payment page';
    } else {
      link.href = '#';
      link.textContent = 'Payment page unavailable';
    }

    $('v2-statusText').textContent = 'Waiting for payment...';
    $('v2-payMsg').textContent = `Selected sell price: ${cfg.currency} ${money(sellPrice)}. Send the exact crypto amount.`;
    v2Notify('info', 'Order created', `${cfg.title} | ${cfg.currency} ${v2.amount} | ${data.crypto} | ref ${v2.ref}`);

    v2SetStep('v2-step-pay');
    $('v2-createMsg').textContent = '';
    v2StartPolling();
  } catch (e) {
    $('v2-createMsg').textContent = e.message || 'Order creation failed.';
  }
}

function v2TimeFormat(sec) {
  sec = Math.max(0, Number(sec || 0));
  const m = String(Math.floor(sec / 60)).padStart(2, '0');
  const s = String(sec % 60).padStart(2, '0');
  return `${m}:${s}`;
}

function v2UpdateCountdown(remaining = null) {
  if (remaining === null && v2.startedAt) {
    const elapsed = Math.floor((Date.now() - v2.startedAt) / 1000);
    remaining = Math.max(0, v2.expiresIn - elapsed);
  }

  if (remaining === null) remaining = v2.expiresIn;

  $('v2-timeText').textContent = v2TimeFormat(remaining);

  const pct = v2.expiresIn ? (remaining / v2.expiresIn) * 100 : 0;
  $('v2-progressBar').style.width = `${Math.max(0, Math.min(100, pct))}%`;

  if (pct <= 25) {
    $('v2-progressBar').style.background = 'linear-gradient(90deg, #ffb347, #ff6b81)';
  } else {
    $('v2-progressBar').style.background = 'linear-gradient(90deg, #20d38a, #8fdc6b)';
  }
}

function v2StartPolling() {
  v2StopPolling();
  v2CheckStatus();

  v2.poller = setInterval(() => {
    v2CheckStatus();
  }, 30000);

  v2.timer = setInterval(() => {
    v2UpdateCountdown();
  }, 1000);
}

function v2StopPolling() {
  if (v2.poller) {
    clearInterval(v2.poller);
    v2.poller = null;
  }
  if (v2.timer) {
    clearInterval(v2.timer);
    v2.timer = null;
  }
}

async function v2CheckStatus() {
  if (!v2.order?.secret) return;

  try {
    const res = await fetch(V2_STATUS + encodeURIComponent(v2.order.secret), {
      method: 'GET'
    });

    const data = await res.json();

    if (!res.ok) {
      throw new Error(data.error || 'Status check failed.');
    }

    if (typeof data.time_remaining_seconds !== 'undefined') {
      v2UpdateCountdown(data.time_remaining_seconds);
    }

    const status = String(data.status || '').toLowerCase();

    if (status === 'confirmed') {
      $('v2-statusText').textContent = 'Payment confirmed';
      $('v2-payMsg').innerHTML = `<span class="v2-status-ok">Card delivered successfully.</span>`;
      v2Notify('success', 'Payment confirmed', `${v2.creatorName} conversion completed successfully.`);
      v2StopPolling();
      v2RenderCards(data);
      v2SetStep('v2-step-done');
      return;
    }

    if (status === 'expired') {
      $('v2-statusText').textContent = 'Order expired';
      $('v2-payMsg').innerHTML = `<span class="v2-status-bad">This order expired. Create a new one.</span>`;
      v2StopPolling();
      return;
    }

    if (status === 'awaiting_payment') {
      $('v2-statusText').textContent = 'Waiting for payment...';
      $('v2-payMsg').textContent = 'No blockchain payment detected yet.';
      return;
    }

    $('v2-statusText').textContent = data.status || 'Waiting for payment...';
    $('v2-payMsg').textContent = data.next_step || 'Polling order status...';
  } catch (e) {
    $('v2-payMsg').innerHTML = `<span class="v2-status-bad">${esc(e.message || 'Status check failed.')}</span>`;
  }
}

function v2RenderCards(data) {
  const cards = Array.isArray(data.cards) ? data.cards : [];

  $('v2-doneMeta').innerHTML = `
    Status: ${esc(data.status || 'confirmed')}<br>
    Delivery: ${esc(data.delivery_status || 'completed')}<br>
    TX: ${esc(data.tx_hash || '-')}
  `;

  if (!cards.length) {
    $('v2-cardsWrap').innerHTML = `
      <div class="v2-card-box">
        No card data found in response.
      </div>
    `;
    return;
  }

  $('v2-cardsWrap').innerHTML = cards.map((card, i) => {
    const number = card.number || '';
    const expiry = card.expiry || '';
    const cvv = card.cvv || '';
    const displayName = card.display_name || `Card ${i + 1}`;
    const system = card.system || 'CARD';

    return `
      <div class="v2-card-box">
        <div class="v2-card-head">
          <strong>${esc(displayName)}</strong>
          <button class="v2-toggle-btn" onclick="v2ToggleCard(${i}, this)">Show</button>
        </div>

        <div class="v2-card-line">
          <span>System</span>
          <strong>${esc(system)}</strong>
        </div>

        <div class="v2-card-line">
          <span>Number</span>
          <strong id="v2-card-number-${i}" class="v2-secret">${esc(number)}</strong>
        </div>

        <div class="v2-card-line">
          <span>Expiry</span>
          <strong id="v2-card-expiry-${i}" class="v2-secret">${esc(expiry)}</strong>
        </div>

        <div class="v2-card-line">
          <span>CVV</span>
          <strong id="v2-card-cvv-${i}" class="v2-secret">${esc(cvv)}</strong>
        </div>
      </div>
    `;
  }).join('');
}

function v2ToggleCard(i, btn) {
  const fields = [
    document.getElementById(`v2-card-number-${i}`),
    document.getElementById(`v2-card-expiry-${i}`),
    document.getElementById(`v2-card-cvv-${i}`)
  ];

  const showing = fields[0]?.classList.contains('show');

  fields.forEach(el => {
    if (!el) return;
    if (showing) el.classList.remove('show');
    else el.classList.add('show');
  });

  if (btn) btn.textContent = showing ? 'Show' : 'Hide';
}

async function v2CopyAddress() {
  const value = $('v2-address').textContent.trim();
  if (!value || value === '-') return;

  try {
    await navigator.clipboard.writeText(value);
    $('v2-payMsg').innerHTML = `<span class="v2-status-ok">Address copied.</span>`;
  } catch (e) {
    $('v2-payMsg').innerHTML = `<span class="v2-status-bad">Copy failed in this browser context.</span>`;
  }
}

function v2Reset() {
  v2StopPolling();
  v2.order = null;
  v2.expiresIn = 1800;
  v2.startedAt = null;
  v2.crypto = '';

  $('v2-createMsg').textContent = '';
  $('v2-payMsg').textContent = '';
  $('v2-cardsWrap').innerHTML = '';
  $('v2-doneMeta').textContent = 'Card delivered successfully.';
  $('v2-qrCanvas').innerHTML = '';
  $('v2-qrNote').textContent = 'Scan to pay';
  $('v2-payCardBrand').innerHTML = '';
  $('v2-crypto').textContent = '-';

  v2SetStep('v2-step-select');
  v2RenderCryptos();
  v2UpdateSummary();
}

function v2Bind() {
  $('v2-cardType').addEventListener('change', (e) => {
    v2.cardType = e.target.value;
    v2InitAmounts();
    v2RenderInfo();
    v2UpdateSummary();
  });

  $('v2-amount').addEventListener('change', (e) => {
    v2.amount = Number(e.target.value);
    v2UpdateSummary();
  });

  $('v2-createBtn').addEventListener('click', v2CreateOrder);
  $('v2-copyBtn').addEventListener('click', v2CopyAddress);
  $('v2-copyCreatorBtn').addEventListener('click', v2CopyCreatorUrl);
  $('v2-checkBtn').addEventListener('click', v2CheckStatus);
  $('v2-resetBtn').addEventListener('click', v2Reset);
  $('v2-resetBtn2').addEventListener('click', v2Reset);
}

function v2Init(detail = null) {
  v2GetRuntimeConfig(detail);
  v2InitCardTypes();
  v2InitAmounts();
  v2RenderInfo();
  v2RenderCryptos();
  v2UpdateSummary();
  v2RenderRuntimeMeta();
  v2Bind();

  if (v2.autoDetectedRef) {
    v2Notify('success', 'Auto-detected StreamElements user', `Using channel username as ref: ${v2.ref}`);
  } else if (!isPlaceholderRef(EZZO_REF)) {
    v2Notify('info', 'Manual ref loaded', `Tracking ref ${v2.ref} from ${v2.source}.`);
  } else {
    v2Notify('warn', 'Set your Streamer ID', 'Replace YOUR_STREAMELEMENTS_USER_ID or let StreamElements auto-detect your channel username.');
  }
}

window.addEventListener('onWidgetLoad', function(obj) {
  v2Init(obj);
});

window.addEventListener('onEventReceived', function(obj) {
  const listener = obj?.detail?.listener || '';
  const event = obj?.detail?.event || {};
  if (listener !== 'message') return;

  const data = event.data || {};
  const text = data.text || event.text || '';
  const username = data.displayName || data.nick || event.nick || 'Viewer';
  v2HandleChatCommand(text, username);
});

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', () => {
    if (!window.SE_API) v2Init();
  });
} else {
  if (!window.SE_API) v2Init();
}
</script>
Note: this widget is designed as a single full block so it can be copied easily.

Testing Checklist

Troubleshooting

QR code does not display

Make sure the QR library script is still included in the widget code and that the address is returned correctly after order creation.

Order is created but no cards appear later

Check the response returned by the status endpoint and confirm that the cards array exists after confirmation.

Widget looks broken in the editor

Make sure the full widget code was pasted without removing any part of the HTML, CSS or JavaScript block.

Referral ID is incorrect

Make sure you replaced YOUR_STREAMELEMENTS_USER_ID correctly in the widget code, or confirm that automatic StreamElements username detection is working properly.

Automatic detection does not work

If the widget cannot detect your StreamElements channel username automatically, set your referral ID manually in the widget code using the EZZO_REF constant.

Chat commands do not trigger anything

Confirm that you are using the PRO widget version with StreamElements event listeners enabled and that the command names match your configured widget commands.

Best Practices