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:
API credential validation
Plan discovery and selection
Email ingestion and validation
Consumer creation or update
Subscription provisioning and reconciliation
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
Open
index.htmlin your browser.Enter your Admin API key to load plans.
Select a plan (or choose “Skip plan”).
Choose subscription duration if applicable.
Select processing mode.
Paste up to 100 comma- or newline-separated emails.
Confirm and run.
Monitor live processing.
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 & 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";
Was this article helpful?
Articles in this section
- Zype Consumer Subscription Wizard: Streamlining Subscriber Onboarding and Provisioning
- How to Download a Video for Offline Playback Using the Zype API
- Integrating a Custom Video Player via API
- Create zobjects with custom attributes using the API
- Creating New Subscriptions with the API
- Assigning Categories to Videos Using the API
- Using the Upload API Endpoint
- Ad Timings with the API
- Importing a Single Video from a URL
- How to Merge a New Video Import to an Existing Video Using the API