Skip to main content
The customer flow scans an NFC invoice from the merchant device and processes the payment via the TapRails backend.

High-Level: PaymentFlowManager

The recommended approach — handles all UI states automatically.
CustomerScreen.tsx
import { PaymentFlowManager } from '@taprails/tap-to-pay';
import { useState } from 'react';
import { TouchableOpacity, Text } from 'react-native';

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

  return (
    <>
      <TouchableOpacity onPress={() => setShowFlow(true)}>
        <Text>Pay Now</Text>
      </TouchableOpacity>

      {showFlow && (
        <PaymentFlowManager
          config={{
            type: 'customer',
            onComplete: (data) => {
              const { processedPayment } = data as CustomerFlowData;
              console.log('Payment sent!', processedPayment?.transactionId);
              setShowFlow(false);
            },
            onCancel: () => setShowFlow(false),
            onError: (error) => {
              console.error('Payment failed:', error.message);
              setShowFlow(false);
            },
          }}
          onCustomerCancel={() => setShowFlow(false)}
        />
      )}
    </>
  );
}

Customer flow states

idle → scanning → confirming → processing → success
           ↓                        ↓
         error                    error
       (NFC timeout,           (API error,
        invalid tag)          insufficient funds)
StateUI shown
scanning”Hold your phone near the payment terminal” with animated NFC icon
confirmingPayment details: amount, merchant, currency
processingStep-by-step progress indicators
successReceipt: amount, transaction ID, timestamp
errorContextual message + retry / cancel

Low-Level: useNFCCustomer

Use the low-level hook when you need full control over the payment UI.

Step 1 — Scan the NFC invoice

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

const {
  scanPaymentRequest,
  processPaymentRequest,
  isReading,
  isProcessing,
  paymentRequest,
  processedPayment,
  error,
  clearPayment,
} = useNFCCustomer();

const handleScan = async () => {
  const payment = await scanPaymentRequest();
  // payment is null if an error occurred (check `error` state)
};
scanPaymentRequest() internally calls readSignedInvoice() which:
  1. Enables the NFC reader
  2. Waits up to 30 seconds for a tag
  3. Validates the ECDSA signature against the merchant’s registered device keys
  4. Enforces a 5-minute timestamp window (replay-attack protection)
  5. Checks the invoice hasn’t expired
  6. Returns a typed PaymentRequest object

Step 2 — Show payment details to user

After successful scan, paymentRequest contains:
{
  paymentId: 'pay_abc123',
  amount: '25.00',           // USDC as decimal string
  merchantWallet: '0xabcd…',
  currency: 'USDC',
  network: 'base',
  timestamp: 1712345678000,
}

Step 3 — Process the payment

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

  const result = await processPaymentRequest(
    paymentRequest.paymentId,
    '',               // txHash — provided by backend in most flows
    userWalletAddress // optional: only needed in SESSION_KEY mode
  );

  if (result) {
    console.log('Status:', result.status);
    console.log('Transaction ID:', result.transactionId);
  }
};

Complete Low-Level Example

CustomCustomerFlow.tsx
import { useState } from 'react';
import { View, Text, Button, ActivityIndicator } from 'react-native';
import { useNFCCustomer, PaymentStatus } from '@taprails/tap-to-pay';

export function CustomCustomerFlow() {
  const {
    scanPaymentRequest,
    processPaymentRequest,
    isReading,
    isProcessing,
    paymentRequest,
    processedPayment,
    error,
    clearPayment,
  } = useNFCCustomer();

  const handleScan = async () => {
    await scanPaymentRequest();
  };

  const handlePay = async () => {
    if (!paymentRequest) return;
    await processPaymentRequest(paymentRequest.paymentId, '');
  };

  // Success
  if (processedPayment?.status === PaymentStatus.CONFIRMED) {
    return (
      <View>
        <Text>✅ Payment confirmed!</Text>
        <Text>Tx: {processedPayment.transactionId}</Text>
        <Button title="Done" onPress={clearPayment} />
      </View>
    );
  }

  // Processing
  if (isProcessing) {
    return <ActivityIndicator />;
  }

  // Scanned — show confirmation
  if (paymentRequest) {
    return (
      <View>
        <Text>Amount: {paymentRequest.amount} USDC</Text>
        <Text>To: {paymentRequest.merchantWallet.slice(0, 8)}</Text>
        <Button title="Confirm & Pay" onPress={handlePay} />
        <Button title="Cancel" onPress={clearPayment} />
      </View>
    );
  }

  // Error
  if (error) {
    return (
      <View>
        <Text style={{ color: 'red' }}>{error.message}</Text>
        <Button title="Try Again" onPress={handleScan} />
      </View>
    );
  }

  // Idle / Scanning
  return (
    <View>
      {isReading
        ? <Text>📡 Hold phone near payment terminal…</Text>
        : <Button title="Tap to Pay" onPress={handleScan} />
      }
    </View>
  );
}

SESSION_KEY Mode

When using SESSION_KEY mode, the payment is funded from the user’s own wallet. You don’t need to pass anything extra to processPaymentRequest — the SDK automatically signs the payment with the stored session key. If no session key exists, TapRailsThemeProvider will show the setup UI before the payment proceeds. See Session Key Setup.