React Example

React Example (Loader + Polling)

This example shows an end-to-end integration flow in React:

  1. Request an access token (POST /oauth/token)
  2. Create a try-on job (POST /v1/mirrago/generate-image)
  3. Show a loader while processing
  4. Poll for completion (GET /v1/mirrago/job/{job_id})
  5. Render the final result_url

Security note: Keep client_secret on the server whenever possible. If you must run this demo in a browser, use it only for internal testing and restrict the key.


Prerequisites

  • Node.js 18+
  • A React app (Vite or Next.js)
  • Mirrago client_id and client_secret

Environment Variables

Vite

Create a .env file:

VITE_MIRRAGO_BASE_URL="https://api.mirrago.com"
VITE_MIRRAGO_CLIENT_ID="YOUR_CLIENT_ID"
VITE_MIRRAGO_CLIENT_SECRET="YOUR_CLIENT_SECRET"

Next.js

Create a .env.local file:

NEXT_PUBLIC_MIRRAGO_BASE_URL="https://api.mirrago.com"
NEXT_PUBLIC_MIRRAGO_CLIENT_ID="YOUR_CLIENT_ID"
NEXT_PUBLIC_MIRRAGO_CLIENT_SECRET="YOUR_CLIENT_SECRET"

React Component (Vite-friendly)

Create src/TryOnDemo.tsx:

import React, { useMemo, useState } from "react";

type JobStatus = "queued" | "pending" | "completed" | "failed";

type TokenResponse = {
  access_token: string;
  expires_in: number;
  token_type: "Bearer" | string;
};

type GenerateImageResponse = {
  job_id: string;
  status: JobStatus;
};

type JobStatusResponse = {
  job_id: string;
  status: JobStatus;
  result_image_url?: string;
  error?: string;
};

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

export default function TryOnDemo() {
  const baseUrl = import.meta.env.VITE_MIRRAGO_BASE_URL ?? "https://api.mirrago.com";
  const clientId = import.meta.env.VITE_MIRRAGO_CLIENT_ID;
  const clientSecret = import.meta.env.VITE_MIRRAGO_CLIENT_SECRET;

  // Files (multipart/form-data)
  const [modelFile, setModelFile] = useState<File | null>(null);
  const [garmentFile, setGarmentFile] = useState<File | null>(null);

  // Optional fields
  const [background, setBackground] = useState<string>("studio");
  const [username, setUsername] = useState<string>("");

  const [loading, setLoading] = useState(false);
  const [status, setStatus] = useState<JobStatus | null>(null);
  const [jobId, setJobId] = useState<string | null>(null);
  const [resultUrl, setResultUrl] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);

  const canRun = useMemo(() => {
    return Boolean(clientId && clientSecret && modelFile && garmentFile);
  }, [clientId, clientSecret, modelFile, garmentFile]);

  async function getAccessToken(): Promise<string> {
    const res = await fetch(`${baseUrl}/oauth/token`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        client_id: clientId,
        client_secret: clientSecret,
        grant_type: "client_credentials",
      }),
    });

    if (!res.ok) {
      const text = await res.text();
      throw new Error(`Token request failed (${res.status}): ${text}`);
    }

    const data = (await res.json()) as TokenResponse;
    return data.access_token;
  }

  async function createTryOnJob(token: string): Promise<GenerateImageResponse> {
    if (!modelFile || !garmentFile) {
      throw new Error("Please select both model and garment images.");
    }

    const form = new FormData();
    // IMPORTANT: field names must match backend
    form.append("model_image", modelFile);
    form.append("garment_image", garmentFile);

    // Optional fields (only append if present)
    if (background) form.append("background", background);
    if (username) form.append("username", username);

    const res = await fetch(`${baseUrl}/v1/mirrago/generate-image`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        // Do NOT set Content-Type manually for multipart; browser sets boundary.
      },
      body: form,
    });

    if (!res.ok) {
      const text = await res.text();
      throw new Error(`Generate-image request failed (${res.status}): ${text}`);
    }

    return (await res.json()) as GenerateImageResponse;
  }

  async function getJobStatus(token: string, id: string): Promise<JobStatusResponse> {
    const res = await fetch(`${baseUrl}/v1/mirrago/job/${encodeURIComponent(id)}`, {
      method: "GET",
      headers: { Authorization: `Bearer ${token}` },
    });

    if (!res.ok) {
      const text = await res.text();
      throw new Error(`getJobStatus failed (${res.status}): ${text}`);
    }

    return (await res.json()) as JobStatusResponse;
  }

  async function run() {
    setLoading(true);
    setError(null);
    setResultUrl(null);
    setJobId(null);
    setStatus("queued");

    try {
      const token = await getAccessToken();
      const created = await createTryOnJob(token);

      setJobId(created.job_id);
      setStatus(created.status);

      // Poll until completed or failed
      const pollEveryMs = 1000;
      const timeoutMs = 30000;
      const maxAttempts = Math.ceil(timeoutMs / pollEveryMs);

      for (let i = 0; i < maxAttempts; i++) {
        const s = await getJobStatus(token, created.job_id);
        setStatus(s.status);

        if (s.status === "completed") {
          setResultUrl(s.result_image_url ?? null);
          return;
        }

        if (s.status === "failed") {
          setError(s.error ?? "Generation failed.");
          return;
        }

        await sleep(pollEveryMs);
      }

      setError("Timed out waiting for result. Please try again.");
    } catch (e: any) {
      setError(e?.message ?? "Something went wrong.");
    } finally {
      setLoading(false);
    }
  }

  return (
    <div style={{ maxWidth: 900, margin: "40px auto", fontFamily: "system-ui, -apple-system, Segoe UI, Roboto" }}>
      <h1 style={{ marginBottom: 6 }}>Mirrago Try-On (React Demo)</h1>
      <p style={{ marginTop: 0, opacity: 0.85 }}>
        Uploads files to <code>/v1/mirrago/generate-image</code> and polls <code>/v1/getJobStatus</code> until ready.
      </p>

      <div style={{ display: "grid", gap: 12, marginTop: 16 }}>
        <label style={{ display: "grid", gap: 6 }}>
          <span style={{ fontWeight: 600 }}>Model Image (JPG/PNG)</span>
          <input
            type="file"
            accept="image/*"
            onChange={(e) => setModelFile(e.target.files?.[0] ?? null)}
          />
          {modelFile && <small style={{ opacity: 0.85 }}>{modelFile.name}</small>}
        </label>

        <label style={{ display: "grid", gap: 6 }}>
          <span style={{ fontWeight: 600 }}>Garment Image (JPG/PNG)</span>
          <input
            type="file"
            accept="image/*"
            onChange={(e) => setGarmentFile(e.target.files?.[0] ?? null)}
          />
          {garmentFile && <small style={{ opacity: 0.85 }}>{garmentFile.name}</small>}
        </label>

        <label style={{ display: "grid", gap: 6 }}>
          <span style={{ fontWeight: 600 }}>Background (optional)</span>
          <input
            value={background}
            onChange={(e) => setBackground(e.target.value)}
            placeholder="studio"
            style={{ padding: 12, borderRadius: 10, border: "1px solid #ddd" }}
          />
        </label>

        <label style={{ display: "grid", gap: 6 }}>
          <span style={{ fontWeight: 600 }}>Username (optional)</span>
          <input
            value={username}
            onChange={(e) => setUsername(e.target.value)}
            placeholder="bittu"
            style={{ padding: 12, borderRadius: 10, border: "1px solid #ddd" }}
          />
        </label>

        <button
          onClick={run}
          disabled={!canRun || loading}
          style={{
            padding: "12px 14px",
            borderRadius: 12,
            border: "1px solid #ddd",
            fontWeight: 700,
            cursor: !canRun || loading ? "not-allowed" : "pointer",
          }}
        >
          {loading ? "Generating..." : "Generate Try-On"}
        </button>

        {!canRun && (
          <div style={{ opacity: 0.85 }}>
            Set your env vars and select both images.
          </div>
        )}
      </div>

      <div style={{ marginTop: 22, padding: 16, border: "1px solid #eee", borderRadius: 14 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <strong>Status:</strong>
          <span>{status ?? "-"}</span>
          {loading && <Spinner />}
        </div>

        {jobId && (
          <div style={{ marginTop: 10, opacity: 0.9 }}>
            <strong>Job ID:</strong> <code>{jobId}</code>
          </div>
        )}

        {error && (
          <div style={{ marginTop: 12, color: "crimson" }}>
            <strong>Error:</strong> {error}
          </div>
        )}
      </div>

      {resultUrl && (
        <div style={{ marginTop: 18 }}>
          <h2 style={{ marginBottom: 10 }}>Result</h2>
          <img
            src={resultUrl}
            alt="Try-on result"
            style={{ width: "100%", borderRadius: 16, border: "1px solid #eee" }}
          />
        </div>
      )}
    </div>
  );
}

function Spinner() {
  return (
    <span
      aria-label="loading"
      style={{
        width: 14,
        height: 14,
        borderRadius: "50%",
        border: "2px solid #999",
        borderTopColor: "transparent",
        display: "inline-block",
        animation: "spin 0.8s linear infinite",
      }}
    >
      <style>
        {`
          @keyframes spin {
            from { transform: rotate(0deg); }
            to { transform: rotate(360deg); }
          }
        `}
      </style>
    </span>
  );
}

Notes and Best Practices

Polling

  • Recommended: 1 second
  • Timeout: 30 seconds (adjust for your use case)
  • Stop polling when status is completed or failed

Production recommendation (important)

For production apps, do not request tokens in the browser. Instead:

  • Create a small backend endpoint in your system that calls POST /oauth/token
  • Your frontend calls your backend (never exposing client_secret)

Next

  • Integration Notes: image guidelines, retries, and scaling patterns
  • UI Example: add progress messaging and retry controls