← Voltar aos tutoriais

Upsell custom em React

Um hook useCeletusUpsell() que encapsula o SDK com useEffect + cleanup. Funciona em Next.js, CRA, Vite e até standalone no navegador via Babel CDN.

Tutoriais › React

Como incluir o SDK

O SDK é exposto globalmente como window.CeletusUpsell — não há pacote npm (v1). Carregue o script uma vez antes do seu componente:

Next.js (App Router ou Pages)

// app/layout.tsx ou pages/_document.tsx
import Script from "next/script";

<Script
  src="https://pay.celetus.com/assets/js/celetus.upsell.sdk.js"
  strategy="afterInteractive"
/>

CRA / Vite

Adicione no public/index.html (CRA) ou index.html (Vite), antes do </body>:

<script src="https://pay.celetus.com/assets/js/celetus.upsell.sdk.js"></script>

Pegue a key no painel

No painel Celetus, em Funil de vendas, clique no ícone </> da etapa e copie o valor da key. Ela fica hardcoded no seu código (ou numa variável de ambiente), igual à key usada hoje em Upsell.Widget.show({ key }).

O sfid vem automaticamente da query string da página (redirect pós-compra) — não precisa passar.

Tipagem (TypeScript)

Declare o tipo global para o autocomplete funcionar:

// src/types/celetus.d.ts
declare global {
  interface Window {
    CeletusUpsell: {
      init(config: { key: string; sfid?: string }): CeletusUpsellInstance;
      toMoneyBRL(value: number): string;
    };
  }
}
interface CeletusUpsellInstance {
  step: StepData | null;
  charge: ChargeData | null;
  loadStep(opts?: { force?: boolean }): Promise<StepData>;
  accept(opts?: { force?: boolean }): Promise<ChargeData>;
  next(opts?: {
    direction?: "positive" | "negative";
    redirectUrl?: string;
    noRedirect?: boolean;
  }): string;
  decline(opts?: { redirectUrl?: string; noRedirect?: boolean }): string;
  pollStatus(opts?: { intervalMs?: number; timeoutMs?: number }): void;
  stopPolling(): void;
  destroy(): void;
  clearStepCache(): void;
  on(event: string, fn: (payload: any) => void): () => void;
  off(event: string, fn: (payload: any) => void): void;
}
export {};

O hook useCeletusUpsell()

// src/hooks/useCeletusUpsell.ts
import { useEffect, useRef, useState } from "react";

type Status =
  | "idle"
  | "loading-step"
  | "ready"
  | "processing"
  | "awaiting-pix"
  | "paid"
  | "failed"
  | "error";

export function useCeletusUpsell(config: { key: string }) {
  const instanceRef = useRef<any>(null);
  const [status, setStatus] = useState<Status>("idle");
  const [step, setStep] = useState<any>(null);
  const [pix, setPix] = useState<{ pixQrCode?: string; pixQrUrl?: string } | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    if (typeof window === "undefined" || !window.CeletusUpsell) return;

    const upsell = window.CeletusUpsell.init({ key: config.key });
    instanceRef.current = upsell;

    const offs: Array<() => void> = [];
    offs.push(upsell.on("qrReady", (p: any) => {
      setPix({ pixQrCode: p.pixQrCode, pixQrUrl: p.pixQrUrl });
      setStatus("awaiting-pix");
    }));
    offs.push(upsell.on("paid",     () => setStatus("paid")));
    offs.push(upsell.on("failed",   (p: any) => { setStatus("failed"); setError(p.reason); }));
    offs.push(upsell.on("error",    (err: Error) => { setError(err.message); }));

    setStatus("loading-step");
    upsell
      .loadStep()
      .then((s: any) => { setStep(s); setStatus("ready"); })
      .catch((err: Error) => { setError(err.message); setStatus("error"); });

    return () => {
      offs.forEach((off) => off());
      upsell.destroy();
    };
  }, []);

  return {
    status,
    step,
    pix,
    error,
    accept: () => {
      setStatus("processing");
      return instanceRef.current?.accept();
    },
    // Avança para a próxima etapa do funil após pagamento confirmado.
    // Chame dentro do handler de `paid` (ou quando quiser, pra dar UX de
    // sucesso antes do redirect).
    next: (opts?: Parameters<CeletusUpsellInstance["next"]>[0]) =>
      instanceRef.current?.next(opts),
    decline: () => instanceRef.current?.decline(),
  };
}

Usando no componente

// src/pages/upsell.tsx
import { useCeletusUpsell } from "../hooks/useCeletusUpsell";

export default function UpsellPage() {
  const { status, step, pix, error, accept, decline, next } = useCeletusUpsell({
    key: "SUA_KEY_AQUI", // ← copiada do painel Celetus
  });

  // Avança para a próxima etapa do funil após mostrar sucesso por 1.5s.
  useEffect(() => {
    if (status === "paid") {
      const t = setTimeout(() => next(), 1500);
      return () => clearTimeout(t);
    }
  }, [status, next]);

  if (status === "loading-step" || status === "idle") return <p>Carregando…</p>;
  if (status === "error") return <p>Erro: {error}</p>;
  if (status === "paid")  return <p>✅ Pagamento confirmado!</p>;

  if (status === "awaiting-pix" && pix) {
    const isImg = pix.pixQrUrl?.startsWith("http");
    return (
      <div>
        <h1>Pague com PIX</h1>
        {isImg ? <img src={pix.pixQrUrl} alt="QR" /> : <code>{pix.pixQrCode}</code>}
        <button onClick={() => navigator.clipboard.writeText(pix.pixQrCode || "")}>
          Copiar chave PIX
        </button>
      </div>
    );
  }

  return (
    <div>
      <h1>{step.productName}</h1>
      <p>{step.valueFormatted}</p>
      <button onClick={accept} disabled={status === "processing"}>
        {step.positiveButtonLabel}
      </button>
      <button onClick={decline}>{step.negativeButtonLabel}</button>
    </div>
  );
}

Exemplo executável

Veja um componente React rodando 100% standalone (Babel + React via CDN, sem build): abrir exemplo.html.

Babel standalone é ótimo pra exemplos e provas de conceito, mas lento em produção. Sempre use Vite/Next.js/CRA em apps reais.

Encadeamento entre etapas do funil

O SDK sabe para onde avançar depois de cada etapa — as URLs (positiva e negativa) vêm do painel Celetus e estão disponíveis em step.positivePageUrl / step.negativePageUrl após o loadStep().

Dentro do componente, chame next() no handler de paid (com delay, se quiser mostrar uma tela de sucesso antes):

useEffect(() => {
  if (status === "paid") {
    const t = setTimeout(() => next(), 1500);
    return () => clearTimeout(t);
  }
}, [status, next]);

Se quiser renderizar um <Link> do Next.js em vez de redirecionar imperativamente, use next({ noRedirect: true }) pra obter a URL de destino:

<Link href={next({ noRedirect: true })}>Continuar</Link>

⚠️ Link de uso único (importante para testes)

Por segurança, cada compra gera um link de upsell que pode ser carregado uma única vez. O SDK cacheia o passo em localStorage por 1 minuto para sobreviver a reloads / hot reloads do dev server dentro dessa janela.

Next.js — cuidados com SSR