Skip to main content
Hooks for the wallet surface of a customer portal — list saved methods and set-default / detach them, preflight what a detach would break, capture a new method, and atomically swap an in-use default. Together they cover the “you can’t just delete the card that’s paying for things” flow safely. Every hook returns loading/error state — reads via isLoading: boolean, mutations via isPending: boolean — plus error: Error | null. See Introduction. The sections below list each hook’s specific returns.

usePaymentMethods

Loads the customer’s saved payment methods and exposes the two write actions a wallet UI needs: setDefault(id) and detach(id). Both invalidate the customer scope on success so the list re-renders. Takes no arguments; the customer comes from <UnitPayProvider> context. Returns
  • paymentMethodsPaymentMethod[] — the customer’s saved methods, or [] while loading or when none exist. Card fields (cardLast4, cardBrand, expMonth, expYear) are null for non-card types.
  • setDefault(paymentMethodId)(id: string) => Promise<void> — points the customer default at this method. Invalidates the customer scope on success.
  • detach(paymentMethodId)(id: string) => Promise<void> — removes the method. Invalidates the customer scope on success. Throws PmInUseError when the method is a default still powering an active sub or auto-topup rule (see the warning below).
  • isActingbooleantrue while a setDefault or detach mutation is in flight.
import { usePaymentMethods } from '@unitpay/react';

export default function Wallet() {
  const { paymentMethods, setDefault, detach, isLoading, isActing } = usePaymentMethods();

  if (isLoading) return <p>Loading…</p>;

  return (
    <ul>
      {paymentMethods.map((pm) => (
        <li key={pm.id}>
          {pm.cardBrand} •••• {pm.cardLast4}
          <button onClick={() => setDefault(pm.id)} disabled={isActing}>Make default</button>
          <button onClick={() => detach(pm.id)} disabled={isActing}>Remove</button>
        </li>
      ))}
    </ul>
  );
}
detach() throws PmInUseError when the method is the customer default and is still powering an active subscription or auto-topup rule (server 409 pm_in_use). The error carries paymentMethodId, activeSubscriptionIds, autoTopupAccountIds, and requestId from error.details — render the block reason and route the user to the replace flow. Preflight it with usePaymentMethodDependencies so you never show a “Remove” button that’s doomed to throw.
import { usePaymentMethods, PmInUseError } from '@unitpay/react';

async function remove(detach: (id: string) => Promise<void>, id: string) {
  try {
    await detach(id);
  } catch (e) {
    if (e instanceof PmInUseError) {
      // e.activeSubscriptionIds / e.autoTopupAccountIds → offer the replace flow.
      console.warn('Still in use:', e.activeSubscriptionIds);
      return;
    }
    throw e;
  }
}

usePaymentMethodDependencies

The preflight query for a “remove card” confirmation modal. It returns whether the method is the customer default and, if so, the active subscriptions and auto-topup credit accounts that a detach would orphan. data.blocksDetach mirrors the server-side guardrail — when true, route the user to the replace flow instead of letting them click “Delete”. Parameters
paymentMethodId
string
required
The method to inspect. The query is disabled while this is null / undefined.
options.enabled
boolean
When false, the query is disabled — useful for a modal that fetches on open (enabled: isOpen). Defaults to true.
Returns — a TanStack UseQueryResult<PaymentMethodDependencies, Error> (short staleTime of 10s, because dependency state changes whenever subs / auto-topup rules change). data once loaded is PaymentMethodDependencies:
  • paymentMethodId string
  • isDefault boolean — whether this is the customer default.
  • blocksDetach booleantrue when detaching would orphan an active sub or auto-topup rule.
  • activeSubscriptionsArray<{ id, status, planId, collectionMethod, currentBillingPeriodEnd }>.
  • autoTopupAccountsArray<{ id, creditCurrencyId, autoTopupThreshold, autoTopupAmount, autoTopupPackageId }>.
import { usePaymentMethodDependencies } from '@unitpay/react';

export default function ConfirmDeletePM({
  paymentMethodId,
  isOpen,
}: {
  paymentMethodId: string;
  isOpen: boolean;
}) {
  const { data, isLoading } = usePaymentMethodDependencies(paymentMethodId, {
    enabled: isOpen,
  });

  if (isLoading || !data) return <p>Checking…</p>;

  if (data.blocksDetach) {
    return (
      <p>
        This card powers {data.activeSubscriptions.length} subscription(s) and{' '}
        {data.autoTopupAccounts.length} auto-topup rule(s). Replace it instead.
      </p>
    );
  }

  return <button>Delete card</button>;
}

useSetupPaymentMethod

Mutation that captures a new payment method via a PSP SetupIntent. It always resolves to a requires_form SettleOutcome — the server mints a SetupIntent and returns the PSP client envelope, which you mount in <PaymentForm mode="save_only">. After the customer completes setup and the PSP webhook attaches the method, the payment-methods cache is invalidated so the next render shows it. Parameters
options
HandleSettlementOptions
Settlement callbacks dispatched after the mutation resolves — wire onRequiresForm to mount the inline form. See the Settlement model.
The setupPaymentMethod(input?) alias accepts an optional input:
input.successUrl
string
Where to send the customer after a successful hosted-mode setup.
input.cancelUrl
string
Where to send the customer if they abandon a hosted-mode setup.
input.uiMode
'embedded' | 'hosted'
Whether to render the form inline (embedded) or redirect to the PSP-hosted page (hosted).
Returns — a TanStack UseMutationResult extended with setupPaymentMethod(input?), which triggers the SetupIntent and resolves to a requires_form outcome whose client.token is the Stripe Elements clientSecret.
import { useSetupPaymentMethod } from '@unitpay/react';
import { useState } from 'react';

export default function AddCard() {
  const [clientSecret, setClientSecret] = useState<string | null>(null);

  const { setupPaymentMethod, isPending } = useSetupPaymentMethod({
    onRequiresForm: (result) => setClientSecret(result.client.token),
  });

  if (clientSecret) {
    // Mount Stripe Elements in save-only mode with this client secret.
    return <PaymentForm mode="save_only" clientSecret={clientSecret} />;
  }

  return (
    <button onClick={() => setupPaymentMethod()} disabled={isPending}>
      {isPending ? 'Starting…' : 'Add a card'}
    </button>
  );
}

useReplacePaymentMethod

Mutation that performs an atomic “swap A for B” — it flips the customer default to the new method and detaches the old one in one server transaction. It’s the resolution path when usePaymentMethodDependencies reports blocksDetach: the old default is still powering active subs / auto-topup rules, so you can’t just detach it. Parameters — the hook takes no arguments; the replacement pair goes to the replacePaymentMethod(input) alias:
input.oldPaymentMethodId
string
required
The method being replaced — typically the customer’s current default.
input.newPaymentMethodId
string
required
The method to become the new default. Must already be attached and active (capture one first with useSetupPaymentMethod).
Returns — a TanStack UseMutationResult extended with replacePaymentMethod(input), which runs the swap and resolves to a ReplacePaymentMethodResult. On success it invalidates all customer-scoped queries (the PM list, default pointer, sub references). The result carries:
  • customerId string
  • oldPaymentMethodId string
  • newPaymentMethodId string
  • becameDefault booleantrue iff the default pointer actually moved this call.
  • clearedOldDefault booleantrue iff the old method was the default (and is now cleared).
import { useReplacePaymentMethod } from '@unitpay/react';

export default function SwapCard({ oldId, newId }: { oldId: string; newId: string }) {
  const { replacePaymentMethod, isPending } = useReplacePaymentMethod();

  return (
    <button
      disabled={isPending}
      onClick={async () => {
        const result = await replacePaymentMethod({
          oldPaymentMethodId: oldId,
          newPaymentMethodId: newId,
        });
        if (result.becameDefault) alert('Card replaced.');
      }}
    >
      {isPending ? 'Replacing…' : 'Replace card'}
    </button>
  );
}

See also

Invoices

Collect payment with usePayInvoice once a method is on file.

Settlement model

The requires_form outcome and client envelope behind card capture.

Error handling

The SDK error model and PmInUseError.

Customer

setupPayment() on useCustomer mints a hosted-link alternative.