Zype Consumer Subscription Wizard: Streamlining Subscriber Onboarding and Provisioning Zype Consumer Subscription Wizard: Streamlining Subscriber Onboarding and Provisioning

Zype Consumer Subscription Wizard: Streamlining Subscriber Onboarding and Provisioning

Introducing the Zype Consumer Subscription Wizard

A smarter, faster way to onboard and provision subscribers at scale

In the world of streaming and direct-to-consumer platforms, speed and accuracy matter. Whether you're onboarding VIP subscribers, migrating legacy users, running a marketing campaign, or reconciling subscription states, manual workflows can quickly become a bottleneck.

That’s why the Consumer Subscription Wizard was built.

This lightweight, single-page application simplifies the entire lifecycle of creating Zype consumers and provisioning subscriptions — all within a guided, operator-friendly workflow.

 

What Is the Consumer Subscription Wizard?

The Consumer Subscription Wizard is a browser-based tool that streamlines bulk onboarding and subscription assignment using the Zype Admin API.

It guides operators through:

  1. API credential validation

  2. Plan discovery and selection

  3. Email ingestion and validation

  4. Consumer creation or update

  5. Subscription provisioning and reconciliation

  6. Results logging and CSV export

Instead of writing custom scripts or manually executing API calls, operators get a structured, safe, and transparent experience.

 

Key Features

1. Guided 5-Step Wizard Interface

The wizard uses a visual stepper with live validation to keep operators oriented and prevent common mistakes.

Each step builds on the previous one:

  • Credentials must validate before plans load

  • Plans must be selected before subscription-dependent options activate

  • Email limits are enforced before processing begins

This reduces operational risk and ensures clean execution.


2. Dynamic Plan Discovery

Using the Zype Plans API, the tool dynamically fetches available subscription plans.

Features include:

  • Automatic plan loading

  • Contextual summaries

  • A built-in “Skip Plan” card for consumer-only runs

  • Smart enable/disable logic tied to plan selection

This eliminates hardcoded plan IDs and ensures operators are always working with current configurations.


3. Flexible Processing Modes

Operators can choose how they want to run the batch:

  • Create Only – Adds new consumers

  • Update Only – Updates existing consumers

  • Create & Update – Hybrid mode for mixed datasets

Plan-dependent options remain disabled until a plan is selected, preventing accidental misconfiguration.


4. Intelligent Email Ingestion Safeguards

Bulk operations are only as clean as the input data.

The wizard automatically:

  • Trims whitespace

  • Deduplicates (case-insensitive)

  • Validates email format

  • Tracks a live 0 / 100 counter

  • Enforces a strict 100-email batch limit

This ensures clean API calls and predictable outcomes.


5. Automated Consumer Lifecycle Handling

Instead of failing when an email already exists, the wizard:

  • Checks for existing consumers

  • Reuses existing consumer IDs

  • Creates new consumers when needed

  • Proceeds intelligently based on processing mode

This allows true reconciliation — not just blind creation.


6. Subscription Reconciliation (Not Just Creation)

If a consumer already has the selected plan:

  • The wizard updates the subscription instead of failing the batch

  • Expiration timestamps are recalculated

  • Assigned day counts are recorded in results

This is critical for:

  • VIP extensions

  • Promotional renewals

  • Reactivation campaigns

  • Subscription migrations


7. Real-Time Processing Console

During execution, Step 4 displays a scrolling log that shows:

  • Consumer created

  • Consumer reused

  • Subscription created

  • Subscription updated

  • API errors

  • Validation warnings

Operators get immediate transparency into what’s happening.


8. Results Table + CSV Export

Step 5 presents a structured summary table that highlights:

  • Success

  • Warning

  • Failure

Additionally, operators can download a CSV that includes:

  • Email address

  • Consumer ID

  • Subscription ID

  • Assigned duration (days)

  • Status message

Perfect for:

  • Audit records

  • Reporting

  • CRM reconciliation

  • Support documentation

 

API Endpoints Utilized

The wizard integrates directly with the Zype Admin API:

This makes it a practical reference implementation for anyone building against the Zype API.

 

Prerequisites

Before using the tool, you’ll need:

  • A valid Zype Admin API Key

  • Permissions to:

    • List plans

    • Create consumers

    • Manage subscriptions

  • A modern desktop browser with JavaScript enabled

Security Note:
API keys are stored only in memory and cleared upon refresh or reset.

 

Step-by-Step Usage

  1. Open index.html in your browser.

  2. Enter your Admin API key to load plans.

  3. Select a plan (or choose “Skip plan”).

  4. Choose subscription duration if applicable.

  5. Select processing mode.

  6. Paste up to 100 comma- or newline-separated emails.

  7. Confirm and run.

  8. Monitor live processing.

  9. Download CSV or reset.

 

Built-In Safeguards

The wizard enforces:

  • 100-email batch limit

  • Plan dependency validation

  • Email formatting checks

  • Subscription duration calculations

  • Clear error reporting in both console and summary

Error states are never silent. Every API failure, duplication issue, or subscription conflict is surfaced.

 

Use Cases

This tool is ideal for:

  • VIP access provisioning

  • Marketing campaign redemptions

  • Bulk subscription migrations

  • Manual CSR operations

  • Account recovery campaigns

  • Beta tester onboarding

  • Sports event pass activation

  • Limited-time promotional extensions

For teams managing sports streaming, FAST-channel upsells, or direct-to-consumer subscription bundles, this wizard dramatically reduces manual effort and API scripting overhead.

 

Why This Matters for Zype Operators

If you’re already working in the Zype ecosystem — especially in operational or solution-engineering roles — this tool demonstrates how to:

  • Orchestrate multiple API endpoints cleanly

  • Build guardrails around bulk operations

  • Implement reconciliation logic

  • Deliver operator-friendly UX around backend workflows

It’s not just a provisioning tool.
It’s a practical blueprint for API-driven workflow design.

 

Technical Structure

The project is intentionally lightweight and modular:

index.html # Wizard markup styles.css # Responsive styling scripts.js # Step logic, API integration, CSV export README.md # Documentation

The page is optimized down to roughly 440px width, making it usable even on smaller screens.

Code Examples

HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="Expires" content="0" />
    <title>Zype Consumer Subscription Wizard</title>
    <link rel="stylesheet" href="styles.css?cache=1" />
  </head>
  <body>
    <main class="app-shell">
      <section class="panel">
        <header class="panel__header">
          <h1>Consumer Subscription Wizard</h1>
          <p class="panel__subtitle">
            Follow the steps to create consumers or reuse existing ones, then assign them to a subscription plan.
          </p>
        </header>

        <div class="stepper" id="stepper">
          <div class="stepper__item stepper__item--active" data-stepper-index="1">
            <span>1</span>
            <p>Keys</p>
          </div>
          <div class="stepper__item" data-stepper-index="2">
            <span>2</span>
            <p>Plans</p>
          </div>
          <div class="stepper__item" data-stepper-index="3">
            <span>3</span>
            <p>Emails</p>
          </div>
          <div class="stepper__item" data-stepper-index="4">
            <span>4</span>
            <p>Process</p>
          </div>
          <div class="stepper__item" data-stepper-index="5">
            <span>5</span>
            <p>Results</p>
          </div>
        </div>

        <section class="steps">
          <form class="step step--active" id="step-1-form" data-step="1">
            <h2>Step 1: Provide API Key</h2>
            <div class="form-field">
              <label for="admin-key">Admin API Key</label>
              <div class="input-with-toggle">
                <input
                  type="password"
                  id="admin-key"
                  name="api_key"
                  autocomplete="off"
                  required
                  placeholder="Paste your API key"
                />
                <button
                  type="button"
                  class="input-toggle"
                  id="api-key-toggle"
                  aria-label="Show API key"
                  title="Show API key"
                >
                  <svg class="input-toggle__icon input-toggle__icon--show" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
                  <svg class="input-toggle__icon input-toggle__icon--hide" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" hidden><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
                </button>
              </div>
            </div>
            <div class="form-actions">
              <button type="reset" class="button button--secondary">Cancel</button>
              <button type="submit" class="button button--primary" disabled>Continue</button>
            </div>
          </form>

          <form class="step" id="step-2-form" data-step="2">
            <h2>Step 2: Choose Subscription Plan</h2>
            <fieldset class="form-field">
              <legend>Available Plans</legend>
              <p class="hint">Selecting a plan is optional. Skip to create consumers only.</p>
              <div id="plans-container" class="plans"></div>
            </fieldset>
            <div class="form-field">
              <label for="period-select">Subscription Duration</label>
              <select id="period-select" name="current_period_end_at" disabled>
                <option value="">Select duration</option>
                <option value="1-hour">1 Hour</option>
                <option value="1-day">1 Day (24 Hours)</option>
                <option value="1-week">1 Week (7 Days)</option>
                <option value="1-month">1 Month (30 Days)</option>
                <option value="1-year">1 Year (365 Days)</option>
              </select>
            </div>
            <div class="form-actions">
              <button type="button" class="button button--ghost" data-prev>Back</button>
              <button type="submit" class="button button--primary" disabled>Continue</button>
            </div>
          </form>

          <section class="step" data-step="3" id="step-3">
            <div class="plan-summary" id="plan-summary">
              <h3 class="plan-summary__title">Consumer Creation Only</h3>
              <p class="plan-summary__meta">No plan selected</p>
            </div>
            <div class="process-toggle" role="group" aria-label="Processing Mode">
              <label class="process-toggle__option" title="Create New Consumers and Subscriptions">
                <input type="radio" name="do_process" value="create" checked />
                <span>Create</span>
              </label>
              <label class="process-toggle__option" title="Update Existing Consumer Subscriptions Only">
                <input type="radio" name="do_process" value="update" data-plan-required />
                <span>Update</span>
              </label>
              <label class="process-toggle__option" title="Create New Consumers, Subscriptions and Update Existing Consumer Subscriptions">
                <input type="radio" name="do_process" value="full" data-plan-required />
                <span>Create &amp; Update</span>
              </label>
            </div>

            <form id="step-3-form">
              <h2>Step 3: Paste Email Addresses</h2>
              <div class="form-field">
                <label for="emails-input">Email Addresses (max 100 per batch)</label>
                <textarea
                  id="emails-input"
                  name="emails"
                  rows="8"
                  placeholder="Paste comma separated emails or one per line"
                  required
                ></textarea>
                <p class="hint" id="email-count-hint">0 / 100 emails</p>
              </div>
              <div class="form-actions">
                <button type="button" class="button button--ghost" data-prev>Back</button>
                <button type="submit" class="button button--primary" disabled>Process Emails</button>
              </div>
            </form>
          </section>

          <section class="step" id="step-4" data-step="4">
            <h2>Step 4: Processing</h2>
            <div class="status-log" id="processing-log">
              <p class="muted">Processing will begin after the emails are submitted.</p>
            </div>
            <div class="loader" id="processing-loader" hidden>
              <span class="loader__dot"></span>
              <span class="loader__dot"></span>
              <span class="loader__dot"></span>
            </div>
            <div class="form-actions">
              <button type="button" class="button button--ghost" data-prev disabled>Back</button>
              <button type="button" class="button button--primary" data-next disabled>Continue</button>
            </div>
          </section>

          <section class="step" id="step-5" data-step="5">
            <h2>Step 5: Results</h2>
            <p class="muted">
              Below is a summary of consumer creation and subscription assignment for each email.
            </p>
            <div class="results" id="results-table">
              <div class="results__header">
                <span>Email</span>
                <span>Consumer ID</span>
                <span>Status</span>
                <span>Days Assigned</span>
              </div>
              <div class="results__body" id="results-body"></div>
            </div>
            <div class="form-actions">
              <button type="button" class="button button--secondary" id="download-results" disabled>
                <span aria-hidden="true">⬇️</span> Download CSV
              </button>
              <button type="button" class="button button--primary" id="reset-wizard">
                Start Over
              </button>
            </div>
          </section>
        </section>
      </section>
    </main>

    <template id="plan-template">
      <label class="plan-card">
        <input type="radio" name="plan_id" />
        <div>
          <h3 class="plan-card__title"></h3>
          <p class="plan-card__meta"></p>
        </div>
      </label>
    </template>

    <script src="scripts.js?cache=1" defer></script>
  </body>
</html>

 

CSS

:root {
  --bg: #0f172a;
  --panel: #1e293b;
  --accent: #38bdf8;
  --accent-dark: #0284c7;
  --text: #e2e8f0;
  --muted: #94a3b8;
  --danger: #f87171;
  --success: #34d399;
  --warning: #fbbf24;
  --border: rgba(148, 163, 184, 0.25);
  --radius: 8px;
  --shadow: 0 12px 30px rgba(15, 23, 42, 0.55);

  font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  min-width: 0;
  width: 100%;
  overflow-x: hidden;
  background: radial-gradient(circle at top, rgba(56, 189, 248, 0.2), transparent 55%),
    radial-gradient(circle at bottom, rgba(2, 132, 199, 0.3), transparent 60%),
    var(--bg);
  color: var(--text);
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding: 3rem 20px 20px;
}

.app-shell {
  width: min(100%, 960px);
}

.panel {
  background-color: var(--panel);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: clamp(24px, 4vw, 40px);
  display: grid;
  gap: 32px;
  width: 100%;
}

.panel__header h1 {
  margin: 0;
  font-size: clamp(1.8rem, 2.5vw, 2.4rem);
  font-weight: 600;
}

.panel__subtitle {
  margin: 8px 0 0;
  color: var(--muted);
  font-size: 0.95rem;
}

.stepper {
  display: flex;
  justify-content: space-between;
  align-items: stretch;
  gap: 8px;
  flex-wrap: nowrap;
  padding-bottom: 8px;
  overflow: hidden;
}

.stepper__item {
  flex: 1 1 0;
  min-width: 0;
  min-height: 70px;
  padding: clamp(8px, 2vw, 12px);
  border: 1px solid transparent;
  border-radius: var(--radius);
  display: grid;
  place-items: center;
  background: rgba(15, 23, 42, 0.4);
  transition: border 0.2s ease, background 0.2s ease, color 0.2s ease;
}

.stepper__item span {
  display: inline-flex;
  width: clamp(24px, 3vw, 32px);
  height: clamp(24px, 3vw, 32px);
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  background-color: rgba(148, 163, 184, 0.2);
  margin-bottom: 6px;
  font-weight: 600;
}

.stepper__item p {
  margin: 0;
  font-size: 0.85rem;
  color: var(--muted);
}

.stepper__item--active {
  border-color: var(--accent);
  background: rgba(56, 189, 248, 0.12);
}

.stepper__item--active span {
  background-color: var(--accent);
  color: var(--bg);
}

.stepper__item--active p {
  color: var(--text);
}

.stepper__item--complete {
  border-color: var(--accent-dark);
  background: rgba(2, 132, 199, 0.15);
}

.steps {
  display: grid;
  margin-top: 3rem;
}

.process-toggle {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 12px;
  margin-bottom: 24px;
}

.plan-summary {
  text-align: center;
  margin-bottom: 16px;
}

.plan-summary__title {
  margin: 0;
  font-size: 1.25rem;
  font-weight: 600;
}

.plan-summary__meta {
  margin: 4px 0 0;
  color: var(--muted);
  font-size: 0.85rem;
}

.process-toggle__option {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: rgba(15, 23, 42, 0.55);
  cursor: pointer;
  transition: border 0.2s ease, background 0.2s ease, color 0.2s ease;
}

.process-toggle__option input {
  accent-color: var(--accent);
}

.process-toggle__option span {
  font-weight: 600;
}

.process-toggle__option:hover,
.process-toggle__option input:focus-visible + span,
.process-toggle__option input:checked + span {
  border-color: var(--accent);
  color: var(--text);
}

.process-toggle__option:is([disabled], .is-disabled) {
  cursor: default;
  border-color: var(--border);
  background: rgba(15, 23, 42, 0.35);
}

.process-toggle__option:is([disabled], .is-disabled):hover,
.process-toggle__option.is-disabled input:focus-visible + span {
  border-color: var(--border);
  color: var(--muted);
}

.step {
  display: none;
  animation: fade-in 0.4s ease;
}

.step--active {
  display: grid;
  gap: 24px;
}

.step h2 {
  margin: 0;
  font-size: 1.4rem;
}

.form-field {
  display: grid;
  gap: 8px;
}

.form-field label,
.form-field legend {
  font-weight: 500;
}

.form-field input[type="password"],
.form-field input[type="text"],
.form-field select,
.form-field textarea {
  background-color: rgba(15, 23, 42, 0.6);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 14px;
  color: var(--text);
  font-size: 1rem;
  transition: border 0.2s ease, box-shadow 0.2s ease;
}

.form-field input[type="password"]:focus,
.form-field input[type="text"]:focus,
.form-field select:focus,
.form-field textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.2);
}

.input-with-toggle {
  position: relative;
  display: block;
  width: 100%;
}

.input-with-toggle input {
  width: 100%;
  box-sizing: border-box;
  padding-right: 48px;
}

.input-with-toggle .input-toggle {
  position: absolute;
  top: 50%;
  right: 10px;
  transform: translateY(-50%);
  width: 36px;
  height: 36px;
  padding: 0;
  border: none;
  border-radius: var(--radius);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 0.2s ease, background 0.2s ease;
}

.input-with-toggle .input-toggle:hover {
  color: var(--accent);
  background: rgba(56, 189, 248, 0.12);
}

.input-with-toggle .input-toggle:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.4);
}

.input-with-toggle .input-toggle__icon--hide[hidden],
.input-with-toggle .input-toggle__icon--show[hidden] {
  display: none !important;
}

.form-actions {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
}

.button {
  appearance: none;
  border: none;
  border-radius: var(--radius);
  font-size: 0.95rem;
  padding: 12px 20px;
  cursor: pointer;
  font-weight: 600;
  transition: transform 0.15s ease, box-shadow 0.2s ease, background 0.2s ease;
}

.button:disabled {
  cursor: not-allowed;
  opacity: 0.55;
  transform: none;
  box-shadow: none;
}

.button--primary {
  background: linear-gradient(135deg, var(--accent), var(--accent-dark));
  color: var(--bg);
  box-shadow: 0 6px 20px rgba(56, 189, 248, 0.25);
}

.button--primary:not(:disabled):hover {
  transform: translateY(-1px);
}

.button--secondary {
  background-color: rgba(148, 163, 184, 0.2);
  color: var(--text);
}

.button--ghost {
  background-color: transparent;
  border: 1px solid var(--border);
  color: var(--text);
}

.plans {
  display: grid;
  gap: 12px;
}

.plan-card {
  border: 1px solid var(--border);
  background: rgba(15, 23, 42, 0.55);
  border-radius: var(--radius);
  display: flex;
  gap: 16px;
  padding: 16px;
  cursor: pointer;
  transition: border 0.2s ease, background 0.2s ease;
  align-items: center;
}

.plan-card:hover {
  border-color: var(--accent);
}

.plan-card input {
  accent-color: var(--accent);
  width: 1.2rem;
  height: 1.2rem;
}

.plan-card__title {
  margin: 0;
  font-size: 1.1rem;
  font-weight: 600;
}

.plan-card__meta {
  margin: 4px 0 0;
  color: var(--muted);
  font-size: 0.85rem;
}

.hint {
  margin: 0;
  font-size: 0.85rem;
  color: var(--muted);
}

.hint--danger {
  color: var(--danger);
}

.status-log {
  max-height: 240px;
  overflow-y: auto;
  background: rgba(15, 23, 42, 0.4);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
  display: grid;
  gap: 8px;
}

.log {
  margin: 0;
  font-size: 0.9rem;
  color: var(--text);
}

.log--muted {
  color: var(--muted);
}

.log--success {
  color: var(--success);
}

.log--warning {
  color: var(--warning);
}

.log--danger {
  color: var(--danger);
}

.loader {
  display: flex;
  gap: 10px;
  justify-content: center;
}

.loader[hidden] {
  display: none !important;
}

.loader[hidden] .loader__dot {
  animation: none !important;
}

.loader__dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background-color: var(--accent);
  animation: bounce 0.6s infinite alternate;
}

.loader__dot:nth-child(2) {
  animation-delay: 0.2s;
}

.loader__dot:nth-child(3) {
  animation-delay: 0.4s;
}

.results {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  display: grid;
  overflow-x: auto;
}

.results__header,
.results__row {
  display: grid;
  grid-template-columns: 1.4fr 1fr 1.2fr 0.8fr;
  gap: 12px;
  padding: 14px 18px;
}

.results__header {
  background: rgba(56, 189, 248, 0.12);
  font-weight: 600;
}

.results__body {
  display: grid;
  gap: 1px;
  background: rgba(148, 163, 184, 0.2);
}

.results__row {
  background: rgba(15, 23, 42, 0.7);
}

.results__status[data-state="success"] {
  color: var(--success);
}

.results__status[data-state="danger"] {
  color: var(--danger);
}

.results__status[data-state="warning"] {
  color: var(--warning);
}

.results__empty {
  padding: 18px;
}

.muted {
  color: var(--muted);
}

@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes bounce {
  to {
    transform: translateY(-6px);
  }
}

@media (max-width: 760px) {
  body {
    padding: 32px 16px 16px;
  }

  .panel {
    gap: 28px;
  }

  .plan-summary__title {
    font-size: 1.1rem;
  }

  .plan-summary__meta {
    font-size: 0.8rem;
  }

  .steps {
    margin-top: 2rem;
  }

  .step {
    gap: 16px;
  }

  .process-toggle {
    flex-wrap: wrap;
    gap: 10px;
    margin-bottom: 20px;
  }

  .stepper__item {
    min-width: 70px;
    padding: 5px;
  }

  .stepper__item span {
    width: 28px;
    height: 28px;
    font-size: 0.85rem;
  }

  .stepper {
    flex-wrap: wrap;
    justify-content: center;
  }


  .form-actions {
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: 10px;
  }
}

@media (max-width: 560px) {
  body {
    padding: 24px 12px 12px;
  }

  .panel {
    padding: 20px;
    gap: 24px;
  }

  .process-toggle {
    margin-bottom: 16px;
  }

  .plan-summary__title {
    font-size: 1rem;
  }

  .plan-summary__meta {
    font-size: 0.75rem;
  }

  .steps {
    margin-top: 2rem;
  }

  .step {
    gap: 15px;
  }

  .stepper__item {
    min-width: 60px;
    padding: 4px;
  }

  .stepper__item span {
    width: 28px;
    height: 28px;
    font-size: 0.85rem;
  }

  .stepper {
    flex-wrap: wrap;
    justify-content: center;
  }

  .results__header,
  .results__row {
    grid-template-columns: repeat(4, 1fr);
    gap: 6px;
  }

  .results__row {
    border-bottom: 1px solid rgba(148, 163, 184, 0.2);
    padding: 12px 14px;
  }

  .results__row span {
    font-size: 0.9rem;
  }

  .results__status {
    font-weight: 600;
  }
}

@media (max-width: 460px) {
  body {
    padding: 20px 10px 10px;
  }

  .panel {
    padding: 18px;
  }

  .process-toggle__option {
    flex: 1 1 100%;
    justify-content: center;
  }

  .plan-summary__title {
    font-size: 0.95rem;
  }

  .plan-summary__meta {
    font-size: 0.72rem;
  }

  .button {
    width: 100%;
  }

  .form-actions {
    flex-direction: column;
    align-items: stretch;
  }
}

 

Javascript

"use strict";

const faviconUrl = "https://www.zype.com/hs-fs/hubfs/_streaming/_zype/Zype%20Logo%20White.png?width=303&height=38&name=Zype%20Logo%20Black.png";
  
const consumer_already_exists = "Could not save consumer: E-mail is already taken";
let consumer_event_status = [];

const API_BASE_URL = "https://api.zype.com";

const state = {
  read_only_key: "",
  api_key: "",
  plan_id: "",
  plan_name: "",
  current_period_end_at: "",
  plan_duration_days: null,
  process_mode: "create",
  emails: [],
};

const steps = Array.from(document.querySelectorAll(".step"));
const stepperItems = Array.from(document.querySelectorAll(".stepper__item"));
let currentStepIndex = 0;

const step1Form = document.getElementById("step-1-form");
const step2Form = document.getElementById("step-2-form");
const step3Form = document.getElementById("step-3-form");
const plansContainer = document.getElementById("plans-container");
const planTemplate = document.getElementById("plan-template");
const periodSelect = document.getElementById("period-select");
const emailTextarea = document.getElementById("emails-input");
const emailCountHint = document.getElementById("email-count-hint");
const processingLog = document.getElementById("processing-log");
const processingLoader = document.getElementById("processing-loader");
const processingStep = document.getElementById("step-4");
const processingBackBtn = processingStep.querySelector("[data-prev]");
const processingNextBtn = processingStep.querySelector("[data-next]");
const resultsBody = document.getElementById("results-body");
const resetWizardBtn = document.getElementById("reset-wizard");
const downloadResultsBtn = document.getElementById("download-results");
const processRadios = Array.from(document.querySelectorAll('input[name="do_process"]'));
const planSummary = document.getElementById("plan-summary");
const adminKeyInput = document.getElementById("admin-key");
const apiKeyToggleBtn = document.getElementById("api-key-toggle");

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const CREATE_DELAY_MS = 500;

function updatePlanSummary() {
  if (!planSummary) {
    return;
  }
  const titleEl = planSummary.querySelector(".plan-summary__title");
  const metaEl = planSummary.querySelector(".plan-summary__meta");
  if (!titleEl || !metaEl) {
    return;
  }

  if (!state.plan_id) {
    titleEl.textContent = "Consumer Creation Only";
    metaEl.textContent = "No plan selected";
  } else {
    titleEl.textContent = state.plan_name || state.plan_id;
    metaEl.textContent = `Plan ID: ${state.plan_id}`;
  }
}

function setStep(index) {
  currentStepIndex = index;
  steps.forEach((step, i) => {
    step.classList.toggle("step--active", i === index);
  });
  stepperItems.forEach((item, i) => {
    item.classList.toggle("stepper__item--active", i === index);
    item.classList.toggle("stepper__item--complete", i < index);
  });
}

function resetWizard() {
  state.read_only_key = "";
  state.api_key = "";
  state.plan_id = "";
  state.plan_name = "";
  state.current_period_end_at = "";
  state.plan_duration_days = null;
  state.process_mode = "create";
  state.emails = [];
  consumer_event_status = [];
  step1Form.reset();
  step2Form.reset();
  step3Form.reset();
  plansContainer.innerHTML = "";
  periodSelect.value = "";
  periodSelect.disabled = true;
  emailCountHint.textContent = "0 / 100 emails";
  processingLog.innerHTML =
    '<p class="muted">Processing will begin after the emails are submitted.</p>';
  processingLoader.hidden = true;
  processingBackBtn.disabled = true;
  processingNextBtn.disabled = true;
  resultsBody.innerHTML = "";
  if (downloadResultsBtn) {
    downloadResultsBtn.disabled = true;
  }
  processRadios.forEach((radio) => {
    radio.checked = radio.value === "create";
  });
  updatePlanSummary();
  updateSubmitButtons();
  setStep(0);
}

function updateSubmitButtons() {
  const step1Submit = step1Form.querySelector('button[type="submit"]');
  const step2Submit = step2Form.querySelector('button[type="submit"]');
  const step3Submit = step3Form.querySelector('button[type="submit"]');

  const adminValid = step1Form.api_key.value.trim().length > 0;
  step1Submit.disabled = !adminValid;

  const planSelected = Boolean(state.plan_id);
  const periodSelected = periodSelect.value !== "";
  if (!planSelected && periodSelect.value !== "") {
    periodSelect.value = "";
  }
  periodSelect.disabled = !planSelected;
  step2Submit.disabled = planSelected ? !periodSelected : false;

  processRadios.forEach((radio) => {
    if (radio.dataset.planRequired !== undefined) {
      radio.disabled = !planSelected;
      const wrapper = radio.closest('.process-toggle__option');
      if (wrapper) {
        if (!planSelected) {
          wrapper.classList.add('is-disabled');
        } else {
          wrapper.classList.remove('is-disabled');
        }
      }
      if (!planSelected && radio.checked) {
        const defaultRadio = processRadios.find((item) => item.value === "create" && !item.disabled);
        if (defaultRadio) {
          defaultRadio.checked = true;
          state.process_mode = "create";
        }
      }
    }
  });

  const emailCount = parseEmailList(emailTextarea.value).length;
  step3Submit.disabled = emailCount === 0 || emailCount > 100;

  state.plan_duration_days = planSelected && periodSelected ? getDaysForPeriod(periodSelect.value) : null;

  updatePlanSummary();
}

function parseEmailList(value) {
  if (!value) return [];
  const raw = value
    .split(/[\n,]+/)
    .map((email) => email.trim().toLowerCase())
    .filter(Boolean);
  const unique = Array.from(new Set(raw));
  return unique.filter((email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email));
}

function showEmailCount() {
  const emails = parseEmailList(emailTextarea.value);
  emailCountHint.textContent = `${emails.length} / 100 emails`;
  if (emails.length > 100) {
    emailCountHint.classList.add("hint--danger");
  } else {
    emailCountHint.classList.remove("hint--danger");
  }
}

async function fetchPlans(apiKey) {
  const url = `${API_BASE_URL}/plans?api_key=${encodeURIComponent(apiKey)}`;
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Unable to load plans (status ${response.status})`);
  }
  const json = await response.json();
  return json.response || json.data || [];
}

function renderPlans(plans) {
  plansContainer.innerHTML = "";
  const skipLabel = document.createElement("label");
  skipLabel.className = "plan-card plan-card--skip";
  const skipInput = document.createElement("input");
  skipInput.type = "radio";
  skipInput.name = "plan_id";
  skipInput.value = "";
  skipInput.checked = !state.plan_id;
  skipInput.addEventListener("change", () => {
    state.plan_id = "";
    state.plan_name = "";
    state.current_period_end_at = "";
    state.plan_duration_days = null;
    periodSelect.value = "";
    periodSelect.disabled = true;
    updateSubmitButtons();
  });
  skipLabel.appendChild(skipInput);
  const skipDetail = document.createElement("div");
  skipDetail.innerHTML = `
    <strong>No subscription</strong><br />
    <small>Skip creating subscriptions for these consumers.</small>
  `;
  skipLabel.appendChild(skipDetail);
  plansContainer.appendChild(skipLabel);

  if (!Array.isArray(plans) || plans.length === 0) {
    const emptyMessage = document.createElement("p");
    emptyMessage.className = "muted";
    emptyMessage.textContent = "No plans available. Consumers will be created without subscriptions.";
    plansContainer.appendChild(emptyMessage);
    periodSelect.disabled = true;
    state.plan_id = "";
    state.plan_name = "";
    state.plan_duration_days = null;
    updateSubmitButtons();
    updatePlanSummary();
    return;
  }

  plans.forEach((plan) => {
    const template = planTemplate.content.cloneNode(true);
    const input = template.querySelector("input");
    const title = template.querySelector(".plan-card__title");
    const meta = template.querySelector(".plan-card__meta");

    input.value = plan._id || plan.id || "";
    input.checked = Boolean(state.plan_id) && input.value === state.plan_id;
    input.addEventListener("change", () => {
      state.plan_id = input.value;
      state.plan_name = plan.title || plan.name || input.value;
      periodSelect.disabled = false;
      updateSubmitButtons();
    });
    if (input.checked) {
      state.plan_name = plan.title || plan.name || input.value;
    }

    title.textContent = plan.title || plan.name || "Untitled Plan";
    const price = plan.price_in_cents
      ? `$${(plan.price_in_cents / 100).toFixed(2)}`
      : "N/A";
    meta.textContent = `ID: ${plan._id || plan.id || "unknown"} · Price: ${price}`;
    plansContainer.appendChild(template);
  });

  periodSelect.disabled = !Boolean(state.plan_id);
  updatePlanSummary();
  updateSubmitButtons();
}

async function safeFetch(url, options = {}) {
  const response = await fetch(url, {
    ...options,
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
      ...(options.headers || {}),
    },
  });

  const contentType = response.headers.get("content-type");
  const isJson = contentType && contentType.includes("application/json");
  const payload = isJson ? await response.json() : await response.text();

  if (!response.ok) {
    const errorMessage =
      (payload && payload.message) ||
      (typeof payload === "string" ? payload : JSON.stringify(payload));
    const error = new Error(errorMessage || "Request failed");
    error.status = response.status;
    error.payload = payload;
    throw error;
  }

  return payload;
}

async function createConsumer(email) {
  const url = `${API_BASE_URL}/consumers?api_key=${encodeURIComponent(state.api_key)}`;
  const body = {
    consumer: {
      email,
    },
  };
  const payload = await safeFetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  return payload;
}

async function findConsumerByEmail(email) {
  const url = `${API_BASE_URL}/consumers?api_key=${encodeURIComponent(
    state.api_key
  )}&email=${encodeURIComponent(email)}`;
  const payload = await safeFetch(url);
  if (Array.isArray(payload.response) && payload.response.length > 0) {
    return payload.response[0];
  }
  if (Array.isArray(payload.data) && payload.data.length > 0) {
    return payload.data[0];
  }
  return null;
}

async function createSubscription({ consumer_id, plan_id, current_period_end_at }) {
  const url = `${API_BASE_URL}/subscriptions?api_key=${encodeURIComponent(state.api_key)}`;
  const payload = {
    subscription: {
      consumer_id,
      plan_id,
      current_period_end_at,
      third_party_id: plan_id,
    }
  };

  return safeFetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(payload),
  });
}

async function listSubscriptions({ consumer_id, plan_id }) {
  const url = `${API_BASE_URL}/subscriptions?api_key=${encodeURIComponent(state.api_key)}&consumer_id=${encodeURIComponent(consumer_id)}&plan_id=${encodeURIComponent(plan_id)}`;
  return safeFetch(url);
}

async function updateSubscription({ subscription_id, consumer_id, plan_id, current_period_end_at }) {
  const url = `${API_BASE_URL}/subscriptions/${encodeURIComponent(subscription_id)}?api_key=${encodeURIComponent(state.api_key)}`;
  const payload = {
    subscription: {
      consumer_id,
      plan_id,
      current_period_end_at,
    }
  };
  return safeFetch(url, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(payload),
  });
}

function logStatus(message, type = "muted") {
  const entry = document.createElement("p");
  entry.textContent = message;
  entry.className = `log log--${type}`;
  processingLog.appendChild(entry);
  processingLog.scrollTop = processingLog.scrollHeight;
}

function calculateExpiration(option) {
  const now = new Date();
  const msMap = {
    "1-hour": 60 * 60 * 1000,
    "1-day": 24 * 60 * 60 * 1000,
    "1-week": 7 * 24 * 60 * 60 * 1000,
    "1-month": 30 * 24 * 60 * 60 * 1000,
    "1-year": 365 * 24 * 60 * 60 * 1000,
  };
  const offset = msMap[option];
  if (!offset) return now.toISOString();
  const future = new Date(now.getTime() + offset);
  return future.toISOString();
}

function getDaysForPeriod(option) {
  const map = {
    "1-hour": 1 / 24,
    "1-day": 1,
    "1-week": 7,
    "1-month": 30,
    "1-year": 365,
  };
  const value = map[option];
  if (typeof value === "number") {
    return value;
  }
  return null;
}

function formatDays(value) {
  if (value == null) {
    return "—";
  }
  if (Math.abs(value - Math.round(value)) < 1e-6) {
    return String(Math.round(value));
  }
  const fixed = value.toFixed(2);
  return fixed.replace(/\.?0+$/, "");
}

function hydrateResultsTable() {
  resultsBody.innerHTML = "";
  if (consumer_event_status.length === 0) {
    resultsBody.innerHTML =
      '<p class="muted results__empty">No consumer records were processed.</p>';
    if (downloadResultsBtn) {
      downloadResultsBtn.disabled = true;
    }
    return;
  }

  if (downloadResultsBtn) {
    downloadResultsBtn.disabled = false;
  }

  consumer_event_status.forEach((record) => {
    const row = document.createElement("div");
    row.className = "results__row";

    const emailCell = document.createElement("span");
    emailCell.textContent = record.email || "N/A";

    const idCell = document.createElement("span");
    idCell.textContent = record.consumer_id || "—";

    const statusCell = document.createElement("span");
    statusCell.textContent = record.status || "Unknown";
    statusCell.className = "results__status";
    if (record.status?.includes("Failed")) {
      statusCell.dataset.state = "danger";
    } else if (record.status?.includes("Success")) {
      statusCell.dataset.state = "success";
    } else if (record.status?.includes("Added")) {
      statusCell.dataset.state = "success";
    } else if (record.status?.includes("Existed")) {
      statusCell.dataset.state = "warning";
    }

    const daysCell = document.createElement("span");
    daysCell.textContent = formatDays(record.days_assigned);

    row.appendChild(emailCell);
    row.appendChild(idCell);
    row.appendChild(statusCell);
    row.appendChild(daysCell);
    resultsBody.appendChild(row);
  });
}

function convertResultsToCsv() {
  const headers = ["Email", "Consumer ID", "Status", "Days Assigned"];
  const rows = consumer_event_status.map((record) => [
    record.email || "",
    record.consumer_id || "",
    record.status || "",
    formatDays(record.days_assigned).replace("—", ""),
  ]);
  const allRows = [headers, ...rows];

  const sanitize = (value) => {
    const stringValue = value == null ? "" : String(value);
    if (stringValue.includes("\"") || stringValue.includes(",") || stringValue.includes("\n")) {
      return `"${stringValue.replace(/"/g, '""')}"`;
    }
    return stringValue;
  };

  return allRows.map((row) => row.map(sanitize).join(",")).join("\r\n");
}

function downloadResultsCsv() {
  if (!consumer_event_status.length) {
    return;
  }

  const csvContent = convertResultsToCsv();
  const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
  const url = URL.createObjectURL(blob);
  const timestamp = new Date().toISOString().replace(/[:]/g, "-").replace("T", "_").split(".")[0];
  const filename = `consumer-results-${timestamp}.csv`;

  const link = document.createElement("a");
  link.href = url;
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  URL.revokeObjectURL(url);
}

async function processConsumersAndSubscriptions() {
  processingLog.innerHTML = "";
  processingLoader.hidden = false;
  processingBackBtn.disabled = true;
  processingNextBtn.disabled = true;

  consumer_event_status = [];

  const emails = state.emails;
  const total = emails.length;
  logStatus(`Starting consumer processing for ${total} email${total === 1 ? "" : "s"}...`);

  for (const email of emails) {
    let consumer_id = null;
    let status = "Consumer: Failed Creation";
    let expiresAt = null;

    try {
      await delay(CREATE_DELAY_MS);
      const response = await createConsumer(email);
      const responseMessage = response?.message;
      const responsePayload = response?.response;
      if (responseMessage && responseMessage.includes(consumer_already_exists)) {
        logStatus(`${email}: already exists. Fetching consumer id...`, "warning");
        try {
          const existing = await findConsumerByEmail(email);
          if (existing && (existing._id || existing.id)) {
            consumer_id = existing._id || existing.id;
            status = "Consumer: Existed Already";
          } else {
            status = "Consumer: Failed Creation (existing consumer not found)";
          }
        } catch (lookupError) {
          console.log("Consumer lookup error:", lookupError);
          status = `Consumer: Failed Creation (${lookupError.message || "Consumer lookup failed"})`;
          logStatus(`${email}: failed to locate existing consumer.`, "danger");
        }
      } else if (responsePayload?._id) {
        consumer_id = responsePayload._id;
        status = "Consumer: Success Creation";
        logStatus(`${email}: consumer created (ID ${consumer_id}).`, "success");
      } else if (response?._id || response?.id || (response?.consumer && (response.consumer._id || response.consumer.id))) {
        consumer_id =
          response?._id ||
          response?.id ||
          response?.consumer?._id ||
          response?.consumer?.id ||
          null;
        status = "Consumer: Success Creation";
        logStatus(`${email}: consumer created (ID ${consumer_id}).`, "success");
      } else {
        logStatus(`${email}: unexpected response format.`, "danger");
      }
    } catch (error) {
      console.log("Consumer creation error:", error);
      if (error.payload?.message && error.payload.message.includes(consumer_already_exists)) {
        logStatus(`${email}: already exists. Fetching consumer id...`, "warning");
        try {
          const existing = await findConsumerByEmail(email);
          if (existing && (existing._id || existing.id)) {
            consumer_id = existing._id || existing.id;
            status = "Consumer: Existed Already";
          } else {
            status = `Consumer: Failed Creation (${error.message})`;
            logStatus(`${email}: failed to locate existing consumer.`, "danger");
          }
        } catch (lookupError) {
          console.log("Consumer lookup error:", lookupError);
          status = `Consumer: Failed Creation (${lookupError.message || "Consumer lookup failed"})`;
          logStatus(`${email}: failed to locate existing consumer.`, "danger");
        }
      } else {
        status = `Consumer: Failed Creation (${error.message || "Unknown error"})`;
        logStatus(`${email}: ${status}`, "danger");
      }
    }

    const temp_array = {
      consumer_id,
      email,
      status,
      plan_id: null,
      expires_at: expiresAt,
      days_assigned: null,
    };

    await delay(CREATE_DELAY_MS);
    consumer_event_status.push(temp_array);
  }

 if (!state.plan_id) {
  logStatus("Consumer processing complete. No plan selected; skipping subscription creation.");
  consumer_event_status.forEach((record) => {
    record.plan_id = null;
    record.status += " - Subscription Plan: Skipped (no plan selected)";
    record.expires_at = null;
  });
  processingLoader.hidden = true;
  processingNextBtn.disabled = false;
  processingNextBtn.focus();
  hydrateResultsTable();
  return;
 }

 logStatus("Consumer processing complete. Creating subscriptions...");

  const expirationIso = calculateExpiration(state.current_period_end_at);

  for (const record of consumer_event_status) {
    if (!record.consumer_id) {
      record.status += " - Subscription Plan: Skipped (no consumer id)";
      logStatus(`${record.email}: unable to create subscription (missing consumer id).`, "danger");
      continue;
    }

    try {
      await delay(CREATE_DELAY_MS);
      const response = await createSubscription({
        consumer_id: record.consumer_id,
        plan_id: state.plan_id,
        current_period_end_at: expirationIso,
      });
      console.log("Subscription request payload:", {
        subscription: {
          consumer_id: record.consumer_id,
          plan_id: state.plan_id,
          current_period_end_at: expirationIso,
          email: record.email,
        } 
      });

      const responsePayload = response?.response;
      let subscription = null;
      if (Array.isArray(responsePayload) && responsePayload.length > 0) {
        subscription = responsePayload[0];
      } else if (responsePayload && typeof responsePayload === "object") {
        subscription = responsePayload;
      }
      if (!subscription && Array.isArray(response?.data) && response.data.length > 0) {
        subscription = response.data[0];
      }
      if (!subscription && response?.subscription) {
        subscription = response.subscription;
      }

      const subscriptionId = subscription?._id || subscription?.id || null;
      if (!subscriptionId && response?.message && response.message.includes("Could not create subscription: Plan is already taken")) {
        try {
          const listResponse = await listSubscriptions({
            consumer_id: record.consumer_id,
            plan_id: state.plan_id,
          });
          const listPayload = Array.isArray(listResponse?.response) ? listResponse.response : [];
          const existingSubscription = listPayload.find((item) => item.plan_id === state.plan_id && item.consumer_id === record.consumer_id);
          if (existingSubscription?._id) {
            const updateResponse = await updateSubscription({
              subscription_id: existingSubscription._id,
              consumer_id: record.consumer_id,
              plan_id: state.plan_id,
              current_period_end_at: expirationIso,
            });
            if (updateResponse?.response?._id || updateResponse?.subscription?._id || updateResponse?._id) {
              record.status += " - Subscription Plan: Updated";
              record.plan_id = state.plan_id;
              record.expires_at = expirationIso;
              record.days_assigned = state.plan_duration_days;
              logStatus(
                `${record.email}: subscription updated (ID ${existingSubscription._id}).`,
                "success"
              );
              continue;
            }
          }
        } catch (listOrUpdateError) {
          console.log("Subscription update after duplicate plan error:", listOrUpdateError);
          record.status += ` - Subscription Plan: Failed (${listOrUpdateError.message || "Update failed"})`;
          logStatus(`${record.email}: subscription update failed after duplicate plan.`, "danger");
          continue;
        }
      }
      record.plan_id = state.plan_id;
      record.expires_at = expirationIso;
      record.days_assigned = state.plan_duration_days;
      if (subscriptionId) {
        record.status += " - Subscription Plan: Added";
        logStatus(
          `${record.email}: subscription created (ID ${subscriptionId}).`,
          "success"
        );
      } else {
        record.status += " - Subscription Plan: Unknown Response";
        logStatus(`${record.email}: subscription response missing id.`, "warning");
      }
    } catch (error) {
      console.log("Subscription creation error:", error);
      const duplicateMessage = "Could not create subscription: Plan is already taken";
      const errorMessage =
        (typeof error?.payload?.message === "string" && error.payload.message) ||
        (typeof error?.message === "string" && error.message) ||
        "";

      if (errorMessage.includes(duplicateMessage)) {
        try {
          await delay(CREATE_DELAY_MS);
          const listResponse = await listSubscriptions({
            consumer_id: record.consumer_id,
            plan_id: state.plan_id,
          });
          const subscriptionList = Array.isArray(listResponse?.response)
            ? listResponse.response
            : Array.isArray(listResponse?.data)
            ? listResponse.data
            : [];
          const existingSubscription = subscriptionList.find(
            (item) => item?.plan_id === state.plan_id && item?.consumer_id === record.consumer_id
          );

          if (existingSubscription?._id) {
            await delay(CREATE_DELAY_MS);
            const updateResponse = await updateSubscription({
              subscription_id: existingSubscription._id,
              consumer_id: record.consumer_id,
              plan_id: state.plan_id,
              current_period_end_at: expirationIso,
            });

            const updateSucceeded =
              updateResponse?.response?._id ||
              updateResponse?.subscription?._id ||
              updateResponse?._id ||
              false;

            if (updateSucceeded) {
              record.status += " - Subscription Plan: Updated";
              record.plan_id = state.plan_id;
              record.expires_at = expirationIso;
              logStatus(
                `${record.email}: subscription updated (ID ${existingSubscription._id}).`,
                "success"
              );
              continue;
            }
          }

          record.status += " - Subscription Plan: Failed (update unavailable)";
          logStatus(`${record.email}: failed to update existing subscription.`, "danger");
          continue;
        } catch (fallbackError) {
          console.log("Subscription fallback error:", fallbackError);
          const fallbackMessage =
            fallbackError?.message || fallbackError?.payload?.message || "Subscription update failed";
          record.status += ` - Subscription Plan: Failed (${fallbackMessage})`;
          logStatus(`${record.email}: subscription update failed after duplicate plan.`, "danger");
          continue;
        }
      }

      record.status += ` - Subscription Plan: Failed (${error.message || "Unknown error"})`;
      logStatus(`${record.email}: subscription creation failed.`, "danger");
    }
  }

  processingLoader.hidden = true;
  processingNextBtn.disabled = false;
  processingNextBtn.focus();
  hydrateResultsTable();
}

step1Form.addEventListener("input", updateSubmitButtons);
step2Form.addEventListener("input", updateSubmitButtons);
step3Form.addEventListener("input", () => {
  showEmailCount();
  updateSubmitButtons();
});

step1Form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const adminValue = step1Form.api_key.value.trim();
  state.api_key = adminValue;
  state.read_only_key = "";
  setStep(1);
  try {
    const plans = await fetchPlans(state.api_key); 
    renderPlans(plans);
  } catch (error) {
    console.log("Plan lookup error:", error);
    plansContainer.innerHTML = `<p class="error">Failed to load plans: ${error.message}</p>`;
  }
});

periodSelect.addEventListener("change", () => {
  updateSubmitButtons();
});

step2Form.addEventListener("submit", (event) => {
  event.preventDefault();
  const planSelected = Boolean(state.plan_id);
  state.current_period_end_at = planSelected ? periodSelect.value : "";
  state.plan_duration_days = planSelected ? getDaysForPeriod(periodSelect.value) : null;
  setStep(2);
  updatePlanSummary();
});

step3Form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const emails = parseEmailList(emailTextarea.value);
  if (emails.length === 0) {
    return;
  }
  const selectedProcess = processRadios.find((radio) => radio.checked)?.value || "create";
  state.process_mode = selectedProcess;
  const willCreateSubscriptions = Boolean(state.plan_id);
  const confirmationMessage = [
    `You are about to process ${emails.length} email${emails.length === 1 ? "" : "s"}.`,
    willCreateSubscriptions
      ? "This will create consumers and assign subscriptions for each address."
      : "This will create consumers without assigning subscriptions.",
    "",
    "Do you want to continue?",
  ].join("\n");
  const confirmed = window.confirm(confirmationMessage);
  if (!confirmed) {
    return;
  }
  state.emails = emails.slice(0, 100);
  setStep(3);
  await processConsumersAndSubscriptions();
});

document.querySelectorAll("[data-prev]").forEach((button) => {
  button.addEventListener("click", () => {
    if (currentStepIndex > 0) {
      setStep(currentStepIndex - 1);
    }
  });
});

document.querySelectorAll("[data-next]").forEach((button) => {
  button.addEventListener("click", () => {
    if (processingNextBtn.disabled) return;
    setStep(4);
  });
});

resetWizardBtn.addEventListener("click", () => resetWizard());
step1Form.addEventListener("reset", () => {
  if (adminKeyInput) adminKeyInput.type = "password";
  if (apiKeyToggleBtn) {
    apiKeyToggleBtn.setAttribute("aria-label", "Show API key");
    apiKeyToggleBtn.setAttribute("title", "Show API key");
    const showIcon = apiKeyToggleBtn.querySelector(".input-toggle__icon--show");
    const hideIcon = apiKeyToggleBtn.querySelector(".input-toggle__icon--hide");
    if (showIcon && hideIcon) {
      showIcon.hidden = false;
      hideIcon.hidden = true;
    }
  }
  setTimeout(() => {
    updateSubmitButtons();
  }, 0);
});

if (adminKeyInput && apiKeyToggleBtn) {
  const showIcon = apiKeyToggleBtn.querySelector(".input-toggle__icon--show");
  const hideIcon = apiKeyToggleBtn.querySelector(".input-toggle__icon--hide");
  apiKeyToggleBtn.addEventListener("click", () => {
    const isPassword = adminKeyInput.type === "password";
    adminKeyInput.type = isPassword ? "text" : "password";
    apiKeyToggleBtn.setAttribute("aria-label", isPassword ? "Hide API key" : "Show API key");
    apiKeyToggleBtn.setAttribute("title", isPassword ? "Hide API key" : "Show API key");
    if (showIcon && hideIcon) {
      showIcon.hidden = isPassword;
      hideIcon.hidden = !isPassword;
    }
  });
}

processingNextBtn.addEventListener("click", () => {
  setStep(4);
});

document.addEventListener("DOMContentLoaded", () => {
  updateSubmitButtons();
  showEmailCount();
  updatePlanSummary();
  let faviconLink = document.querySelector('link[rel="icon"]');
  if (!faviconLink) {
    faviconLink = document.createElement("link");
    faviconLink.rel = "icon";
    faviconLink.type = "image/png";
    document.head.appendChild(faviconLink);
  }
  faviconLink.href = faviconUrl;
});

if (downloadResultsBtn) {
  downloadResultsBtn.addEventListener("click", downloadResultsCsv);
}

processRadios.forEach((radio) => {
  if (radio.dataset.planRequired !== undefined) {
    radio.disabled = !Boolean(state.plan_id);
    const wrapper = radio.closest('.process-toggle__option');
    if (wrapper) {
      if (radio.disabled) {
        wrapper.classList.add('is-disabled');
      } else {
        wrapper.classList.remove('is-disabled');
      }
    }
  }
  radio.addEventListener("change", () => {
    if (radio.checked) {
      state.process_mode = radio.value;
    }
  });
});

 

Please change your favicon as desired:

const faviconUrl = "https://www.zype.com/hs-fs/hubfs/_streaming/_zype/Zype%20Logo%20White.png?width=303&height=38&name=Zype%20Logo%20Black.png";