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

# Merchant Flow

> Accept contactless USDC payments using NFC tap-to-pay on Android.

The merchant flow creates a signed NFC invoice and waits for a customer to tap their phone to receive it.

<Warning>
  The merchant device **must be Android** due to iOS restrictions on Host Card Emulation (HCE).
</Warning>

***

## High-Level: PaymentFlowManager

The `PaymentFlowManager` handles the complete merchant UI flow with zero boilerplate. This is the recommended approach for most integrations.

```tsx MerchantScreen.tsx theme={null}
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)
```

| State      | UI shown                                              |
| ---------- | ----------------------------------------------------- |
| `creating` | Loading — creating payment on backend                 |
| `waiting`  | Animated NFC icon + payment details + countdown timer |
| `success`  | Transaction receipt with amount and ID                |
| `error`    | Contextual 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

```tsx theme={null}
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:

```tsx theme={null}
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);
      }
    }
  );
};
```

<Note>
  `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.
</Note>

### Dual-Path Flow (Web Fallback)

When the merchant taps **"Generate Tap Payment"** , the SDK automatically activates **two independent HCE services** behind the scenes. No extra integration is required — this is built-in.

#### Path 1 — Primary HCE (Integrated Partner App)

The merchant device responds to your unique **partner AID** (e.g., `F0xxxxxxxx`). When a customer with a TapRails-integrated app taps their phone, Android routes the AID selection to your app's registered HCE service. The customer's app reads the signed invoice directly over APDU — no internet connection required for the data exchange itself.

#### Path 2 — NDEF Fallback (Web-based)

A second HCE service responds to the standard **NDEF AID** (`D2760000850101`). When a customer taps with **any device that does not have a partner app** — including all **iOS devices** — their phone reads a standard NFC URL tag. The browser opens automatically and the customer completes payment via the TapRails web pay page.

The URL served by the NDEF service is signed with the same Ed25519 device key used for the native path. The backend verifies this signature before displaying any payment details — the customer cannot see or act on the payment without a valid signature.

```
https://pay.taprails.xyz/p?id={paymentId}&sig={hexSignature}&t={unixTimestamp}
```

<Note>
  Both services run in parallel. Android's NFC stack routes to the correct one based on which AID the customer's device selects. The partner AID always wins if the customer has the integrated app installed, because a more specific AID match takes priority over the generic NDEF AID.
</Note>

<Warning>
  The NDEF fallback URL is valid for **35 minutes** — matching the 30-minute payment window plus 5 minutes of clock skew tolerance. Customers opening the URL after this window will see an "expired" error.
</Warning>

### Step 3 — Poll for confirmation (optional)

```tsx theme={null}
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

```tsx CustomMerchantFlow.tsx theme={null}
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>
  );
}
```
