High Wire Payments
Guides

React and Next.js

Integrate highwire.js in client-rendered React and Next.js checkout components.

Client-side requirement

collect() must run in the cardholder's browser. In the Next.js App Router, call it from a client component with the 'use client' directive; in other React stacks, call it from a browser event handler.

Importing the ESM module and constructing HighWire() do not access DOM APIs, collect context, or make network requests, but do not call collect() from a server component, Node.js API route, or getServerSideProps. Modern Node.js can produce valid-looking server context instead of an obvious failure. See the API reference for the complete browser-only collection boundary.

Vendor and import the ESM build

Place the delivered ESM build somewhere your frontend bundler can import, such as src/lib/highwire/highwire-v<version>.es.js. If the project uses TypeScript, keep the matching TypeScript declaration delivered with the release beside the ESM file.

Import the factory and instance type from the vendored module:

import {
  HighWire,
  type HighWireInstance,
} from '@/lib/highwire/highwire-v<version>.es.js';

Bundled imports do not use the separately delivered SRI hash. For the differences between the ESM and UMD handoffs, see Choose a build.

Create and retain an instance

Create the instance after the component mounts and retain it in a ref. The submit handler can then use the same instance that observed the form fields:

const hwRef = useRef<HighWireInstance | null>(null);

useEffect(() => {
  const hw = HighWire();
  hwRef.current = hw;

  return () => {
    if (hwRef.current === hw) hwRef.current = null;
  };
}, []);

No publishable key or other required configuration is needed.

Observe fields after mount

Call observeField() only after the checkout inputs exist in the DOM. Observe each supported field that your form contains, and keep the cleanup functions returned by those calls:

const cardEl = document.querySelector<HTMLInputElement>('#card-number');
const cvcEl = document.querySelector<HTMLInputElement>('#card-cvc');

const unobserves = [
  cardEl && hw.observeField(cardEl, 'number'),
  cvcEl && hw.observeField(cvcEl, 'cvc'),
].filter(Boolean) as Array<() => void>;

Observation records paste events only; it never reads an input value. Review the accepted field names before wiring the rest of the form.

Collect on submission

Keep the merchant's existing request builder as the source of the complete, already-valid payment request. Call this focused helper from the existing submit handler after the merchant has built and validated existingPaymentRequest; the helper adds only optional deviceData:

async function collectDeviceDataOrNull(hw: HighWireInstance) {
  try {
    return await hw.collect();
  } catch {
    return null;
  }
}

async function submitCheckout(
  existingPaymentRequest: Record<string, unknown>,
) {
  const hw = hwRef.current;
  const deviceData = hw ? await collectDeviceDataOrNull(hw) : null;

  await fetch('/checkout', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      ...existingPaymentRequest,
      ...(deviceData ? { deviceData } : {}),
    }),
  });
}

Do not serialize the form or enumerate gateway payment fields in the highwire.js wiring. The builder remains merchant-owned and must return the request the checkout already validates and sends. If collection returns null, omit the field and continue the existing payment flow as described in When collection returns null.

If collect() rejects unexpectedly, the helper returns null and the checkout continues without deviceData.

Clean up observers

Return a cleanup function from the effect that registered the observers. Call every retained function when the form unmounts, then clear the retained instance:

return () => {
  unobserves.forEach((unobserve) => unobserve());
  if (hwRef.current === hw) hwRef.current = null;
};

When React mounts a new checkout form, create an instance and observe the new DOM elements again. The API reference defines the observer's return value and cleanup behavior.

Complete example

This component accepts the merchant's request builder and form children. The merchant keeps ownership of the payment DTO, validation, inputs, and submit control; the wrapper owns only the highwire.js lifecycle and optional deviceData addition.

'use client';

import {
  useEffect,
  useRef,
  type FormEvent,
  type ReactNode,
} from 'react';
import {
  HighWire,
  type HighWireInstance,
} from '@/lib/highwire/highwire-v<version>.es.js';

async function collectDeviceDataOrNull(hw: HighWireInstance) {
  try {
    return await hw.collect();
  } catch {
    return null;
  }
}

interface CheckoutFormProps {
  buildExistingPaymentRequest: () => Record<string, unknown>;
  children: ReactNode;
}

export function CheckoutForm({
  buildExistingPaymentRequest,
  children,
}: CheckoutFormProps) {
  const formRef = useRef<HTMLFormElement | null>(null);
  const hwRef = useRef<HighWireInstance | null>(null);

  useEffect(() => {
    const form = formRef.current;
    if (!form) return;

    const hw = HighWire();
    hwRef.current = hw;

    const cardEl = form.querySelector<HTMLInputElement>('#card-number');
    const cvcEl = form.querySelector<HTMLInputElement>('#card-cvc');
    const expEl = form.querySelector<HTMLInputElement>('#card-exp');
    const zipEl = form.querySelector<HTMLInputElement>('#billing-zip');
    const unobserves = [
      cardEl && hw.observeField(cardEl, 'number'),
      cvcEl && hw.observeField(cvcEl, 'cvc'),
      expEl && hw.observeField(expEl, 'exp'),
      zipEl && hw.observeField(zipEl, 'zip'),
    ].filter(Boolean) as Array<() => void>;

    return () => {
      unobserves.forEach((unobserve) => unobserve());
      if (hwRef.current === hw) hwRef.current = null;
    };
  }, []);

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();

    const existingPaymentRequest = buildExistingPaymentRequest();
    const hw = hwRef.current;
    const deviceData = hw ? await collectDeviceDataOrNull(hw) : null;
    const response = await fetch('/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        ...existingPaymentRequest,
        ...(deviceData ? { deviceData } : {}),
      }),
    });

    if (!response.ok) throw new Error('Payment request failed');
  }

  return (
    <form ref={formRef} onSubmit={onSubmit}>
      {children}
    </form>
  );
}

The browser request still goes to your own backend. Follow the integration flow to pass the collected object through on the HighWire request.

On this page