Skip to main content
The merchant flow creates a signed NFC invoice and waits for a customer to tap their phone to receive it.
The merchant device must be Android due to iOS restrictions on Host Card Emulation (HCE).

High-Level: PaymentFlowManager

The PaymentFlowManager handles the complete merchant UI flow with zero boilerplate. This is the recommended approach for most integrations.
MerchantScreen.tsx
import { PaymentFlowManager } from '@taprails/tap-to-pay';
import { useState } from 'react';
import { TouchableOpacity, Text } from 'react-native';

export function MerchantScreen() {
  const [showFlow, setShowFlow] = useState(false);

  return (
    <>
      <TouchableOpacity onPress={() => setShowFlow(true)}>
        <Text>Accept Payment</Text>
      </TouchableOpacity>

      {showFlow && (
        <PaymentFlowManager
          config={{
            type: 'merchant',
            onComplete: (data) => {
              const { paymentRequest, txHash } = data as MerchantFlowData;
              console.log('Payment received!', paymentRequest?.paymentId, txHash);
              setShowFlow(false);
            },
            onCancel: () => setShowFlow(false),
            onError: (error) => {
              console.error('Payment error:', error.message);
              setShowFlow(false);
            },
          }}
          onMerchantCancel={() => setShowFlow(false)}
        />
      )}
    </>
  );
}

Merchant flow states

idle → creating → waiting → (customer taps) → success

                                 error (timeout, NFC error, API error)
StateUI shown
creatingLoading — creating payment on backend
waitingAnimated NFC icon + payment details + countdown timer
successTransaction receipt with amount and ID
errorContextual error message + retry / cancel buttons

Low-Level: useNFCMerchant + writeSignedInvoice

Use the low-level API when you want to build your own payment UI.

Step 1 — Create the payment request

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

const { createPaymentRequest, isCreating, paymentRequest, error, reset } = useNFCMerchant();

const handleCreatePayment = async () => {
  const request = await createPaymentRequest({
    amount: '25.00',       // USDC amount as decimal string
    merchantId: 'merchant_123',  // Optional if set in SDK config
  });

  if (request) {
    console.log('Payment ID:', request.paymentId);
    console.log('Merchant wallet:', request.merchantWallet);
    console.log('Expires at:', request.expiresAt);
  }
};

Step 2 — Emit the invoice via NFC (HCE)

Once you have a PaymentRequest, write the signed invoice to the HCE layer:
import { writeSignedInvoice } from '@taprails/tap-to-pay';
import { useState } from 'react';

const [hasExchangedData, setHasExchangedData] = useState(false);

const startNFCEmulation = async () => {
  if (!paymentRequest) return;

  await writeSignedInvoice(
    {
      id: paymentRequest.paymentId,
      merchantAddress: paymentRequest.merchantWallet,
      amount: Math.round(parseFloat(paymentRequest.amount) * 1_000_000), // Convert USDC → µUSDC
      expiresAt: paymentRequest.expiresAt!,
    },
    (exchanged) => {
      if (exchanged) {
        console.log('Customer successfully read the invoice!');
        // Customer's app will now process the payment
        setHasExchangedData(true);
      }
    }
  );
};
writeSignedInvoice starts HCE emulation and returns immediately. The setHasExchangedData callback fires when the customer’s device reads the NFC tag. At this point, the customer’s app is processing the payment — you can show a “processing” state on the merchant screen.

Step 3 — Poll for confirmation (optional)

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

const { startPolling, isConfirmed, status } = usePaymentStatus();

useEffect(() => {
  if (hasExchangedData && paymentRequest) {
    startPolling(paymentRequest.paymentId);
  }
}, [hasExchangedData, paymentRequest]);

useEffect(() => {
  if (isConfirmed) {
    console.log('Payment confirmed on-chain!', status?.txHash);
  }
}, [isConfirmed]);

Complete Low-Level Example

CustomMerchantFlow.tsx
import { useState } from 'react';
import { View, Text, TextInput, Button, ActivityIndicator } from 'react-native';
import { useNFCMerchant, writeSignedInvoice, usePaymentStatus } from '@taprails/tap-to-pay';

export function CustomMerchantFlow() {
  const [amount, setAmount] = useState('');
  const [isEmulating, setIsEmulating] = useState(false);
  const [exchanged, setExchanged] = useState(false);

  const { createPaymentRequest, isCreating, paymentRequest, error, reset } = useNFCMerchant();
  const { startPolling, isConfirmed } = usePaymentStatus();

  const handleStart = async () => {
    const request = await createPaymentRequest({ amount });
    if (!request) return;

    setIsEmulating(true);
    await writeSignedInvoice(
      {
        id: request.paymentId,
        merchantAddress: request.merchantWallet,
        amount: Math.round(parseFloat(amount) * 1_000_000),
        expiresAt: request.expiresAt!,
      },
      (didExchange) => {
        setExchanged(didExchange);
        setIsEmulating(false);
        startPolling(request.paymentId);
      }
    );
  };

  if (isConfirmed) return <Text>✅ Payment confirmed!</Text>;
  if (exchanged) return <Text>⏳ Processing payment…</Text>;
  if (isEmulating) return <Text>📡 Waiting for customer to tap…</Text>;
  if (isCreating) return <ActivityIndicator />;

  return (
    <View>
      {error && <Text style={{ color: 'red' }}>{error.message}</Text>}
      <TextInput
        value={amount}
        onChangeText={setAmount}
        placeholder="0.00"
        keyboardType="decimal-pad"
      />
      <Button title="Create Payment" onPress={handleStart} />
    </View>
  );
}