> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rinne.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Live Demo

> Try Apple Pay, Google Pay, and card collection in an interactive sandbox checkout.

export const RinneUnifiedCheckoutDemo = () => {
  const demoOrder = {
    amount: 314,
    currency: 'BRL',
    country: 'BR',
    lineItems: [{
      label: 'Piizza Margherita',
      amount: 314
    }]
  };
  const readIsDarkMode = () => {
    const html = document.documentElement;
    if (html.classList.contains('dark') || html.getAttribute('data-theme') === 'dark') {
      return true;
    }
    if (html.classList.contains('light') || html.getAttribute('data-theme') === 'light') {
      return false;
    }
    return window.matchMedia('(prefers-color-scheme: dark)').matches;
  };
  const loadRinneScript = () => {
    return new Promise((resolve, reject) => {
      if (window.Rinne) {
        resolve(window.Rinne);
        return;
      }
      const existing = document.querySelector('script[data-rinne-umd="true"]');
      if (existing) {
        existing.addEventListener('load', () => resolve(window.Rinne), {
          once: true
        });
        existing.addEventListener('error', () => reject(new Error('Failed to load Rinne script')), {
          once: true
        });
        return;
      }
      const script = document.createElement('script');
      script.src = 'https://pkgs.rinne.com.br/rinne-js';
      script.async = true;
      script.dataset.rinneUmd = 'true';
      script.onload = () => {
        if (window.Rinne) {
          resolve(window.Rinne);
        } else {
          reject(new Error('Rinne not found after script load'));
        }
      };
      script.onerror = () => reject(new Error('Failed to load Rinne script'));
      document.head.appendChild(script);
    });
  };
  const addDemoStyles = () => {
    const existing = document.querySelector('style[data-rinne-checkout-demo="true"]');
    if (existing) return;
    const sheet = document.createElement('style');
    sheet.dataset.rinneCheckoutDemo = 'true';
    sheet.textContent = `
      @keyframes rinne-checkout-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
      @keyframes rinne-checkout-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
    `;
    document.head.appendChild(sheet);
  };
  const applePayRef = useRef(null);
  const googlePayRef = useRef(null);
  const cardRef = useRef(null);
  const applePayElementRef = useRef(null);
  const googlePayElementRef = useRef(null);
  const mountedCardRef = useRef(null);
  const [isDark, setIsDark] = useState(() => typeof window === 'undefined' ? false : readIsDarkMode());
  const [isLoading, setIsLoading] = useState(true);
  const [applePayReady, setApplePayReady] = useState(false);
  const [googlePayReady, setGooglePayReady] = useState(false);
  const [cardMounted, setCardMounted] = useState(false);
  const [cardState, setCardState] = useState({
    isValid: false,
    isComplete: false,
    errors: {}
  });
  const [status, setStatus] = useState(null);
  const [capturedPayload, setCapturedPayload] = useState(null);
  const palette = isDark ? {
    shellBg: 'linear-gradient(180deg, #0f1117 0%, #181c25 100%)',
    panelBg: 'rgba(255, 255, 255, 0.03)',
    elevatedBg: 'linear-gradient(180deg, #1b2230 0%, #161d2a 100%)',
    border: 'rgba(255, 255, 255, 0.08)',
    borderStrong: 'rgba(99, 102, 241, 0.35)',
    text: '#e4e4e7',
    textMuted: '#a1a1aa',
    textDim: '#6b7280',
    summaryRow: 'rgba(255, 255, 255, 0.02)',
    inputBg: 'rgba(15, 23, 42, 0.45)',
    primaryBtn: '#4f46e5',
    primaryBtnText: '#f8fafc',
    disabledBg: 'rgba(255, 255, 255, 0.04)',
    disabledText: '#71717a',
    successBg: 'rgba(16, 185, 129, 0.15)',
    successBorder: 'rgba(16, 185, 129, 0.35)',
    successText: '#34d399',
    errorBg: 'rgba(239, 68, 68, 0.12)',
    errorBorder: 'rgba(239, 68, 68, 0.35)',
    errorText: '#f87171',
    codeBg: 'rgba(2, 6, 23, 0.6)',
    codeText: '#a7f3d0'
  } : {
    shellBg: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)',
    panelBg: 'rgba(2, 6, 23, 0.02)',
    elevatedBg: 'linear-gradient(180deg, #ffffff 0%, #f7f9ff 100%)',
    border: 'rgba(2, 6, 23, 0.1)',
    borderStrong: 'rgba(79, 70, 229, 0.25)',
    text: '#0f172a',
    textMuted: '#475569',
    textDim: '#94a3b8',
    summaryRow: 'rgba(2, 6, 23, 0.02)',
    inputBg: '#ffffff',
    primaryBtn: '#4f46e5',
    primaryBtnText: '#eef2ff',
    disabledBg: 'rgba(2, 6, 23, 0.05)',
    disabledText: '#94a3b8',
    successBg: 'rgba(16, 185, 129, 0.1)',
    successBorder: 'rgba(16, 185, 129, 0.25)',
    successText: '#059669',
    errorBg: 'rgba(239, 68, 68, 0.08)',
    errorBorder: 'rgba(239, 68, 68, 0.25)',
    errorText: '#dc2626',
    codeBg: 'rgba(255, 255, 255, 0.95)',
    codeText: '#047857'
  };
  const cardTheme = {
    preset: 'minimal',
    styles: {
      ':root': {
        colorScheme: isDark ? 'dark' : 'light'
      },
      fieldset: {
        border: `1px solid ${palette.border}`,
        borderRadius: '10px',
        backgroundColor: palette.inputBg,
        padding: '10px'
      },
      'label[ev-name], .field:first-child label': {
        display: 'none'
      },
      '.field': {
        position: 'relative'
      },
      '.field[ev-name="expiry"]': {
        marginRight: '5px'
      },
      '.field[ev-name="cvc"]': {
        marginLeft: '5px'
      },
      '.field[ev-name="number"]': {
        position: 'relative'
      },
      'fieldset[ev-valid=false]': {
        paddingBottom: '10px'
      },
      '.field input': {
        marginTop: 6,
        height: '40px',
        padding: '9px 12px',
        border: `1px solid ${palette.border}`,
        borderRadius: '10px',
        backgroundColor: palette.inputBg,
        color: palette.text,
        fontSize: 14
      },
      '[ev-component] .field input': {
        borderTopLeftRadius: '10px !important',
        borderTopRightRadius: '10px !important',
        borderBottomLeftRadius: '10px !important',
        borderBottomRightRadius: '10px !important'
      },
      '.field[ev-name="number"] input': {
        paddingLeft: '56px'
      },
      '.field[ev-name="number"] .icon': {
        position: 'absolute',
        left: '12px',
        top: 'calc(50% + 4px)',
        transform: 'translateY(-50%)',
        pointerEvents: 'none'
      },
      '.field[ev-name="number"] .field-icon': {
        position: 'absolute',
        left: '12px',
        top: 'calc(50% + 4px)',
        transform: 'translateY(-50%)',
        pointerEvents: 'none'
      },
      '.field[ev-name="number"] .brand-icon': {
        position: 'absolute',
        left: '12px',
        top: 'calc(50% + 4px)',
        transform: 'translateY(-50%)',
        pointerEvents: 'none'
      },
      '.field[ev-name="number"] [class*="icon"]': {
        left: '12px !important',
        top: 'calc(50% + 4px) !important',
        transform: 'translateY(-50%) !important',
        pointerEvents: 'none'
      },
      '.field[ev-name="number"] img': {
        position: 'absolute',
        left: '12px',
        top: 'calc(50% + 4px)',
        transform: 'translateY(-50%)',
        pointerEvents: 'none'
      },
      '.field[ev-name="number"] svg': {
        position: 'absolute',
        left: '12px',
        top: 'calc(50% + 4px)',
        transform: 'translateY(-50%)',
        pointerEvents: 'none'
      },
      '.field:focus-within input': {
        borderColor: isDark ? '#818cf8' : '#4f46e5',
        boxShadow: isDark ? '0 0 0 3px rgba(129, 140, 248, 0.2)' : '0 0 0 3px rgba(79, 70, 229, 0.16)'
      },
      '.field[ev-has-value="true"] input': {
        backgroundColor: palette.inputBg
      },
      '.field[ev-valid="false"] input': {
        borderColor: '#dc2626'
      },
      '.error': {
        display: 'none !important',
        margin: '0 !important',
        maxHeight: '0 !important',
        opacity: '0 !important',
        overflow: 'hidden !important'
      },
      '.field .error': {
        display: 'none !important',
        margin: '0 !important',
        maxHeight: '0 !important',
        opacity: '0 !important',
        overflow: 'hidden !important'
      },
      '.field [class*="error"]': {
        display: 'none !important',
        margin: '0 !important',
        maxHeight: '0 !important',
        opacity: '0 !important',
        overflow: 'hidden !important'
      },
      '.field [ev-error]': {
        display: 'none !important'
      },
      '.field [data-error]': {
        display: 'none !important'
      },
      '.field [aria-live]': {
        display: 'none !important'
      }
    }
  };
  const formatAmount = cents => new Intl.NumberFormat('pt-BR', {
    style: 'currency',
    currency: demoOrder.currency
  }).format(cents / 100);
  const redactPayload = payload => {
    if (!payload) return payload;
    const safe = JSON.parse(JSON.stringify(payload));
    if (safe.card_data?.number) safe.card_data.number = '[ENCRYPTED]';
    if (safe.card_data?.cvv) safe.card_data.cvv = '[ENCRYPTED]';
    if (safe.card_data?.network_token) safe.card_data.network_token = '[REDACTED]';
    if (safe.card_data?.cryptogram) safe.card_data.cryptogram = '[REDACTED]';
    return safe;
  };
  const handleCardSubmit = () => {
    const mountedCard = mountedCardRef.current;
    if (!mountedCard || !mountedCard.isComplete || !mountedCard.isValid) {
      setStatus({
        type: 'error',
        message: 'Complete all card fields before preparing the payload.'
      });
      setCapturedPayload(null);
      return;
    }
    const payload = {
      method: 'card',
      amount: demoOrder.amount,
      currency: demoOrder.currency,
      country: demoOrder.country,
      customer: {
        name: mountedCard.values?.card?.name || ''
      },
      card_data: mountedCard.values.card
    };
    setStatus({
      type: 'success',
      message: 'Card payload ready. Send it to your backend transaction endpoint.'
    });
    setCapturedPayload(payload);
  };
  useEffect(() => {
    const syncTheme = () => setIsDark(readIsDarkMode());
    syncTheme();
    const observer = new MutationObserver(syncTheme);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['class', 'data-theme']
    });
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    const mediaHandler = () => syncTheme();
    mediaQuery.addEventListener('change', mediaHandler);
    return () => {
      observer.disconnect();
      mediaQuery.removeEventListener('change', mediaHandler);
    };
  }, []);
  useEffect(() => {
    let active = true;
    addDemoStyles();
    const initDemo = async () => {
      try {
        setIsLoading(true);
        setStatus(null);
        setCapturedPayload(null);
        const RinneClass = await loadRinneScript();
        if (!active) return;
        const rinne = new RinneClass({
          merchantId: '730d1a4f-85ba-42c4-bf15-035dea51057c',
          environment: 'sandbox',
          rinneUrl: 'https://api-dev.rinne.com.br/core/v1'
        });
        const transaction = await rinne.transaction.create({
          amount: demoOrder.amount,
          currency: demoOrder.currency,
          country: demoOrder.country,
          lineItems: demoOrder.lineItems.map(({label, amount}) => ({
            label,
            amount
          }))
        });
        if (!active) return;
        const colorScheme = readIsDarkMode() ? 'dark' : 'light';
        const walletHandlers = {
          onCapture: async payload => {
            setStatus({
              type: 'success',
              message: 'Wallet payment authorized successfully.'
            });
            setCapturedPayload(payload);
          },
          onError: error => {
            setStatus({
              type: 'error',
              message: error?.message || 'A wallet payment error occurred.'
            });
            setCapturedPayload(null);
          },
          onCancel: () => {
            setStatus({
              type: 'error',
              message: 'Wallet payment was cancelled by the user.'
            });
            setCapturedPayload(null);
          }
        };
        if (applePayRef.current) {
          try {
            const applePay = await rinne.elements.applePay(transaction, {
              button: {
                type: 'plain',
                color: 'black',
                locale: 'pt',
                borderRadius: 10,
                size: {
                  width: '100%',
                  height: '46px'
                }
              },
              ...walletHandlers
            });
            await applePay.mount(applePayRef.current);
            applePayElementRef.current = applePay;
            if (active) setApplePayReady(true);
          } catch {
            if (active) setApplePayReady(false);
          }
        }
        if (googlePayRef.current) {
          try {
            const googlePay = await rinne.elements.googlePay(transaction, {
              button: {
                type: 'plain',
                color: 'black',
                locale: 'pt',
                borderRadius: 10,
                size: {
                  width: '100%',
                  height: '46px'
                }
              },
              colorScheme,
              ...walletHandlers
            });
            await googlePay.mount(googlePayRef.current);
            googlePayElementRef.current = googlePay;
            if (active) setGooglePayReady(true);
          } catch {
            if (active) setGooglePayReady(false);
          }
        }
        if (cardRef.current) {
          try {
            const cardElement = await rinne.elements.card({
              theme: cardTheme,
              colorScheme,
              locale: 'pt',
              fields: ['name', 'number', 'expiry', 'cvc'],
              icons: true,
              onReady: () => {
                if (!active) return;
                setCardMounted(true);
              },
              onChange: nextState => {
                if (!active) return;
                setCardState(nextState);
              }
            });
            const mountedCard = await cardElement.mount(cardRef.current);
            if (!active) {
              mountedCard.unmount();
              return;
            }
            mountedCardRef.current = mountedCard;
            setCardMounted(true);
          } catch {
            if (active) setCardMounted(false);
          }
        }
      } catch (error) {
        setStatus({
          type: 'error',
          message: error instanceof Error ? error.message : 'Failed to initialize checkout demo.'
        });
      } finally {
        if (active) setIsLoading(false);
      }
    };
    initDemo();
    return () => {
      active = false;
      try {
        applePayElementRef.current?.unmount?.();
      } catch {}
      applePayElementRef.current = null;
      try {
        googlePayElementRef.current?.unmount?.();
      } catch {}
      googlePayElementRef.current = null;
      if (mountedCardRef.current) {
        try {
          mountedCardRef.current.unmount();
        } catch {}
        mountedCardRef.current = null;
      }
    };
  }, [isDark]);
  const renderShimmer = height => <div style={{
    height,
    width: '100%',
    borderRadius: '10px',
    background: isDark ? 'linear-gradient(90deg, rgba(255,255,255,0.06) 25%, rgba(255,255,255,0.12) 50%, rgba(255,255,255,0.06) 75%)' : 'linear-gradient(90deg, rgba(2,6,23,0.04) 25%, rgba(2,6,23,0.08) 50%, rgba(2,6,23,0.04) 75%)',
    backgroundSize: '200% 100%',
    animation: 'rinne-checkout-shimmer 1.5s infinite'
  }} />;
  const cardCanSubmit = cardState.isComplete && cardState.isValid;
  return <div style={{
    width: '100%',
    maxWidth: '100%',
    margin: '0 0 24px',
    background: palette.shellBg,
    borderRadius: '16px',
    border: `1px solid ${palette.border}`,
    overflow: 'hidden',
    boxShadow: isDark ? '0 20px 40px -16px rgba(0, 0, 0, 0.6)' : '0 20px 40px -20px rgba(15, 23, 42, 0.25)'
  }}>
      <div style={{
    padding: '12px 16px',
    borderBottom: `1px solid ${palette.border}`,
    background: palette.panelBg,
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between'
  }}>
        <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '10px'
  }}>
          <span style={{
    width: '7px',
    height: '7px',
    borderRadius: '50%',
    background: '#10b981',
    animation: 'rinne-checkout-pulse 2s infinite'
  }} />
          <span style={{
    color: palette.text,
    fontSize: '12px',
    fontWeight: '700',
    letterSpacing: '0.45px',
    textTransform: 'uppercase'
  }}>
            Checkout Simulation
          </span>
        </div>
        <span style={{
    color: '#10b981',
    fontSize: '11px',
    fontWeight: '700',
    textTransform: 'uppercase',
    letterSpacing: '0.45px',
    background: 'rgba(16, 185, 129, 0.12)',
    border: '1px solid rgba(16, 185, 129, 0.25)',
    borderRadius: '999px',
    padding: '4px 8px'
  }}>
          Sandbox
        </span>
      </div>

      <div style={{
    padding: '16px',
    display: 'grid',
    gap: '14px'
  }}>
        <section style={{
    background: palette.elevatedBg,
    border: `1px solid ${palette.borderStrong}`,
    borderRadius: '12px',
    padding: '12px'
  }}>
          <p style={{
    margin: '0 0 8px',
    color: palette.textMuted,
    fontSize: '11px',
    fontWeight: '600',
    textTransform: 'uppercase',
    letterSpacing: '0.4px'
  }}>
            Order Summary
          </p>
          {demoOrder.lineItems.map((item, index) => <div key={`${item.label}-${index}`} style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    borderRadius: '8px',
    background: palette.summaryRow,
    padding: '8px 10px',
    marginBottom: '8px'
  }}>
              <span style={{
    color: palette.text,
    fontSize: '13px'
  }}>{item.label}</span>
              <span style={{
    color: palette.textMuted,
    fontSize: '13px',
    fontFamily: 'ui-monospace, monospace'
  }}>
                {formatAmount(item.amount)}
              </span>
            </div>)}
          <div style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between',
    paddingTop: '4px'
  }}>
            <span style={{
    color: palette.text,
    fontSize: '13px',
    fontWeight: '600'
  }}>Total</span>
            <span style={{
    color: '#10b981',
    fontSize: '18px',
    fontWeight: '700',
    fontFamily: 'ui-monospace, monospace'
  }}>
              {formatAmount(demoOrder.amount)}
            </span>
          </div>
        </section>

        <section style={{
    border: `1px solid ${palette.border}`,
    borderRadius: '12px',
    background: palette.panelBg,
    padding: '12px',
    display: 'grid',
    gap: '10px'
  }}>
          <p style={{
    margin: 0,
    color: palette.textMuted,
    fontSize: '11px',
    fontWeight: '600',
    textTransform: 'uppercase',
    letterSpacing: '0.35px'
  }}>
            Express Checkout
          </p>

          <div ref={applePayRef} style={isLoading ? {
    minHeight: '46px'
  } : applePayReady ? {
    height: '46px',
    overflow: 'hidden'
  } : {
    minHeight: '46px',
    borderRadius: '10px',
    border: `1px solid ${palette.border}`,
    background: palette.inputBg,
    color: palette.textDim,
    fontSize: '12px',
    display: 'flex',
    alignItems: 'center',
    padding: '0 12px'
  }}>
            {isLoading && renderShimmer('46px')}
            {!isLoading && !applePayReady && <span>Apple Pay is unavailable in this browser context.</span>}
          </div>

          <div ref={googlePayRef} style={isLoading ? {
    minHeight: '46px'
  } : googlePayReady ? {
    height: '46px',
    overflow: 'hidden'
  } : {
    minHeight: '46px',
    borderRadius: '10px',
    border: `1px solid ${palette.border}`,
    background: palette.inputBg,
    color: palette.textDim,
    fontSize: '12px',
    display: 'flex',
    alignItems: 'center',
    padding: '0 12px'
  }}>
            {isLoading && renderShimmer('46px')}
            {!isLoading && !googlePayReady && <span>Google Pay is unavailable in this browser context.</span>}
          </div>
        </section>

        <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '10px',
    color: palette.textDim,
    fontSize: '11px',
    textTransform: 'uppercase',
    letterSpacing: '0.35px',
    fontWeight: '600'
  }}>
          <span style={{
    flex: 1,
    height: '1px',
    background: palette.border
  }} />
          or pay with card
          <span style={{
    flex: 1,
    height: '1px',
    background: palette.border
  }} />
        </div>

        <section style={{
    border: `1px solid ${palette.border}`,
    borderRadius: '12px',
    background: palette.panelBg,
    padding: '12px',
    display: 'grid',
    gap: '10px'
  }}>
          <div style={{
    display: 'grid',
    gap: '6px'
  }}>
            <span style={{
    color: palette.textMuted,
    fontSize: '11px',
    fontWeight: '600',
    textTransform: 'uppercase',
    letterSpacing: '0.35px'
  }}>
              Card Details
            </span>
            <div ref={cardRef} style={{
    minHeight: '160px',
    borderRadius: '10px',
    border: 'none',
    background: 'transparent',
    padding: 0
  }}>
              {isLoading && renderShimmer('160px')}
            </div>
          </div>

          <button type="button" onClick={handleCardSubmit} disabled={!cardMounted || !cardCanSubmit} style={{
    width: '100%',
    height: '42px',
    borderRadius: '10px',
    border: 'none',
    background: cardMounted && cardCanSubmit ? palette.primaryBtn : palette.disabledBg,
    color: cardMounted && cardCanSubmit ? palette.primaryBtnText : palette.disabledText,
    fontSize: '13px',
    fontWeight: '700',
    letterSpacing: '0.3px',
    cursor: cardMounted && cardCanSubmit ? 'pointer' : 'not-allowed'
  }}>
            Pay {formatAmount(demoOrder.amount)}
          </button>
        </section>

        {status && <div style={{
    borderRadius: '10px',
    border: `1px solid ${status.type === 'success' ? palette.successBorder : palette.errorBorder}`,
    background: status.type === 'success' ? palette.successBg : palette.errorBg,
    color: status.type === 'success' ? palette.successText : palette.errorText,
    padding: '10px 12px',
    fontSize: '13px'
  }}>
            {status.message}
          </div>}

        {capturedPayload && <section style={{
    border: `1px solid ${palette.successBorder}`,
    borderRadius: '10px',
    background: palette.successBg,
    padding: '10px 12px'
  }}>
            <p style={{
    margin: '0 0 8px',
    color: palette.successText,
    fontSize: '11px',
    fontWeight: '700',
    textTransform: 'uppercase',
    letterSpacing: '0.35px'
  }}>
              Captured Payload
            </p>
            <pre style={{
    margin: 0,
    fontSize: '11px',
    lineHeight: 1.45,
    color: palette.codeText,
    WebkitTextFillColor: palette.codeText,
    background: palette.codeBg,
    border: `1px solid ${palette.border}`,
    borderRadius: '8px',
    padding: '10px',
    overflow: 'auto',
    maxHeight: '220px',
    whiteSpace: 'pre-wrap',
    wordBreak: 'break-word'
  }}>
              {JSON.stringify(redactPayload(capturedPayload), null, 2)}
            </pre>
          </section>}
      </div>
    </div>;
};

See rinne-js in action. This interactive demo renders the same payment elements you'll use in production — Apple Pay, Google Pay, and card collection — all powered by a single SDK instance running in sandbox mode.

Use it to explore the checkout experience, inspect element behavior, and understand the flow before you start integrating.

<RinneUnifiedCheckoutDemo />

<Info>
  This demo runs in sandbox mode and is intended for integration testing only. No data or payments are processed. Use `environment: 'sandbox'` while developing and switch to `production` only after wallet, card, and 3DS paths are fully tested.
</Info>
