Skip to main content
writeSignedInvoice enables the Android HCE layer to emit a signed payment invoice over NFC. When a customer taps their phone to the merchant device, the invoice is read by the customer’s app.
This function requires an Android device with HCE support. It will not work on iOS.

Import

import { writeSignedInvoice } from '@taprails/tap-to-pay';

Signature

async function writeSignedInvoice(
  invoice: {
    id: string;
    merchantAddress: string;
    amount: number;      // Amount in µUSDC (micro-USDC = USDC × 1,000,000)
    expiresAt: number;   // Unix timestamp in milliseconds
  },
  setHasExchangedData: (value: boolean) => void
): Promise<void>

Parameters

invoice.id
string
required
The paymentId returned from createPayment / useNFCMerchant.
invoice.merchantAddress
string
required
The merchant’s wallet address (merchantWallet from the payment response).
invoice.amount
number
required
Amount in micro-USDC (µUSDC). Multiply the USDC decimal amount by 1,000,000.
// $25.00 USDC → 25,000,000 µUSDC
amount: Math.round(parseFloat('25.00') * 1_000_000)
invoice.expiresAt
number
required
Unix timestamp in milliseconds when the invoice expires. Use expiresAt from the createPayment response directly.
setHasExchangedData
(value: boolean) => void
required
Callback invoked with true when the customer’s device successfully reads the NFC tag. Use this to transition to a “processing” state on the merchant screen.

What it does internally

  1. Signs the invoice payload with the merchant’s ECDSA private key (stored in Keychain)
  2. Serializes the signed payload as JSON
  3. Wraps it in an NDEF Text record
  4. Starts HCE emulation via react-native-hce
  5. Listens for the HCE_STATE_READ event (customer tap)
  6. On read: invokes setHasExchangedData(true), then cancels the session after 1.5s

Example

import { useNFCMerchant, writeSignedInvoice } from '@taprails/tap-to-pay';

const { createPaymentRequest, paymentRequest } = useNFCMerchant();
const [exchanged, setExchanged] = useState(false);

const startPayment = async () => {
  const request = await createPaymentRequest({ amount: '25.00' });
  if (!request) return;

  await writeSignedInvoice(
    {
      id: request.paymentId,
      merchantAddress: request.merchantWallet,
      amount: Math.round(25 * 1_000_000),  // µUSDC
      expiresAt: request.expiresAt!,
    },
    setExchanged
  );
};

useEffect(() => {
  if (exchanged) {
    console.log('Customer tapped — waiting for backend confirmation');
  }
}, [exchanged]);
writeSignedInvoice returns as soon as HCE emulation starts — it does not block until the customer taps. The setHasExchangedData callback fires asynchronously when the tap occurs.