Frontend Reference

April 17, 2026 · View on GitHub

TL;DR

  • React 19 + TypeScript + Vite + Tailwind
  • App.tsx handles A2A messaging and state
  • Components render UCP data types (Checkout, Product, Payment)

Why a Mock Payment Provider?

Real payment flows involve sensitive credentials and complex integrations. The CredentialProviderProxy mock lets you:

  • Test the full checkout flow without real payment credentials
  • Understand the data contracts before implementing real providers
  • Demonstrate UCP patterns without external dependencies

In production, you'd replace this with your actual payment SDK (Stripe, Adyen, etc.).

Request/Response Cycle

Frontend Request/Response Cycle

Figure 1: Request/response cycle — React App sends POST /api with UCP-Agent header, Vite Proxy rewrites path and forwards to A2A Server at :10999, ADK Agent returns response with parts[] containing text and structured data (checkout, products).

Component Hierarchy

React Component Hierarchy

Figure 2: React component tree — App.tsx manages state and A2A messaging, with Header, ChatMessage, and ChatInput as children. ChatMessage contains UCP data components (green): ProductCard, Checkout, PaymentMethodSelector, and PaymentConfirmation.

App.tsx - State & Handlers

State

const [messages, setMessages] = useState<ChatMessage[]>([initialMessage]);
const [isLoading, setIsLoading] = useState(false);
const [contextId, setContextId] = useState<string | null>(null);
const [taskId, setTaskId] = useState<string | null>(null);

Handler Functions

HandlerPurpose
handleSendMessage(content, options)Send A2A message, parse response
handleAddToCheckout(product)Add product to cart
handleStartPayment()Initiate payment flow
handlePaymentMethodSelection(checkout)Fetch available methods
handlePaymentMethodSelected(method)Get payment token
handleConfirmPayment(instrument)Complete checkout

A2A Communication

Request Format

const request = {
  jsonrpc: "2.0",
  id: crypto.randomUUID(),
  method: "message/send",
  params: {
    message: {
      role: "user",
      parts: [{ type: "text", text: "show me cookies" }],
      contextId: contextId, // From previous response
      taskId: taskId, // For multi-turn tasks
    },
    configuration: { historyLength: 0 },
  },
};

fetch("/api", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-A2A-Extensions":
      "https://ucp.dev/2026-01-23/specification/overview?v=2026-01-23",
    "UCP-Agent": `profile="http://localhost:3000/profile/agent_profile.json"`,
  },
  body: JSON.stringify(request),
});

Response Parsing

const data = await response.json();

// Extract context for next request
setContextId(data.result?.contextId);

// Parse response parts
for (const part of data.result?.status?.message?.parts || []) {
  if (part.text) {
    message.text += part.text;
  }
  if (part.data?.["a2a.product_results"]) {
    message.products = part.data["a2a.product_results"].results;
  }
  if (part.data?.["a2a.ucp.checkout"]) {
    message.checkout = part.data["a2a.ucp.checkout"];
  }
}

Key Components

ComponentPropsRenders
ProductCardproduct, onAddToCartProduct image, name, price, stock
Checkoutcheckout, onCheckout, onCompletePaymentLine items, totals, action buttons
PaymentMethodSelectorpaymentMethods, onSelectRadio list of methods
PaymentConfirmationpaymentInstrument, onConfirmConfirm button
ChatMessagemessage, handlersCombines all above based on data

Types (types.ts)

interface ChatMessage {
  id: string;
  sender: Sender; // USER | MODEL
  text: string;
  products?: Product[];
  isLoading?: boolean;
  paymentMethods?: PaymentMethod[];
  isUserAction?: boolean;
  checkout?: Checkout;
  paymentInstrument?: PaymentInstrument;
}

interface Checkout {
  id: string;
  line_items: CheckoutItem[];
  currency: string;
  continue_url?: string | null;
  status: string; // incomplete | ready_for_complete | completed
  totals: CheckoutTotal[];
  order_id?: string;
  order_permalink_url?: string;
  payment?: Payment;
}

interface Product {
  productID: string;
  name: string;
  image: string[];
  brand: { name: string };
  offers: {
    price: string;
    priceCurrency: string;
    availability: string;
  };
  url: string;
  description: string;
  size: { name: string };
}
interface DisplayInfo {
  brand: string;
  last_digits: string;
  expiry_month: number;
  expiry_year: number;
}

interface PaymentMethod {
  id: string;
  type: string;
  display: DisplayInfo;
}

CredentialProviderProxy

Mock payment provider in mocks/credentialProviderProxy.ts:

class CredentialProviderProxy {
  handler_id = "example_payment_provider";

  // Returns mock payment methods (wrapped in object)
  async getSupportedPaymentMethods(
    user_email: string,
    config: any
  ): Promise<{ payment_method_aliases: PaymentMethod[] }> {
    return {
      payment_method_aliases: [
        {
          id: "instr_1",
          type: "card",
          display: {
            brand: "amex",
            last_digits: "1111",
            expiry_month: 12,
            expiry_year: 2026,
          },
        },
        {
          id: "instr_2",
          type: "card",
          display: {
            brand: "visa",
            last_digits: "8888",
            expiry_month: 12,
            expiry_year: 2026,
          },
        },
      ],
    };
  }

  // Converts method to PaymentInstrument with token
  async getPaymentToken(
    user_email: string,
    payment_method_id: string
  ): Promise<PaymentInstrument | undefined> {
    return {
      ...payment_method,
      handler_id: this.handler_id,
      credential: { type: "token", token: `mock_token_${uuid}` },
    };
  }
}

Configuration

Vite Proxy (vite.config.ts)

server: {
  port: 3000,
  proxy: {
    "/api": {
      target: "http://localhost:10999",
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/api/, "")
    }
  }
}

App Config (config.ts)

export const appConfig = new AppProperties(
  "Business Agent",
  "Your personal shopping assistant.",
  "/images/logo.jpg",
  "Hello, I am your Business Agent...",
  "Shop with Business Agent"
);