Skip to main content

Payment Form Examples

This guide demonstrates how to build secure payment forms using the Easy React SDK with PCI-compliant form elements.

Overview

The Easy SDK provides secure, pre-built form elements that handle sensitive payment data without it ever touching your application. This ensures PCI compliance and protects your users' payment information.

Complete Card Payment Form

This example shows a complete payment form with all card information in a single element.

import React, { useRef, useState } from "react";
import { useEasy, CardElement } from "@easylabs/react";
import type { ICardElement } from "@easylabs/react";

function CompleteCardPaymentForm() {
const { checkout } = useEasy();
const cardRef = useRef<ICardElement>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
setSuccess(false);

try {
const result = await checkout({
customer_creation: true,
customer_details: {
first_name: "John",
last_name: "Doe",
email: "[email protected]",
personal_address: {
line1: "123 Main St",
city: "Anytown",
state: "CA",
postal_code: "12345",
country: "US",
},
},
source: {
type: "PAYMENT_CARD",
cardElement: cardRef,
name: "John's Card",
},
line_items: [{ price_id: "price_abc123", quantity: 1 }],
metadata: {
order_source: "web",
},
});

console.log("Payment successful:", result);
setSuccess(true);
} catch (err) {
setError(err instanceof Error ? err.message : "Payment failed");
console.error("Payment error:", err);
} finally {
setLoading(false);
}
};

return (
<form onSubmit={handleSubmit} className="payment-form">
<h2>Complete Your Payment</h2>

{error && (
<div className="error-message" role="alert">
{error}
</div>
)}

{success && (
<div className="success-message" role="alert">
Payment successful! Thank you for your purchase.
</div>
)}

<div className="form-group">
<label htmlFor="card-element">Card Information</label>
<CardElement
ref={cardRef}
id="card-element"
style={{
base: {
fontSize: "16px",
color: "#424770",
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
fontSmoothing: "antialiased",
"::placeholder": {
color: "#aab7c4",
},
},
invalid: {
color: "#9e2146",
},
}}
/>
</div>

<button type="submit" disabled={loading || success}>
{loading ? "Processing..." : "Pay $25.00"}
</button>
</form>
);
}

export default CompleteCardPaymentForm;

Separate Card Elements

For more control over the layout, you can use separate elements for card number, expiration date, and CVC.

import React, { useRef, useState } from "react";
import {
useEasy,
CardNumberElement,
CardExpirationDateElement,
CardVerificationCodeElement,
} from "@easylabs/react";
import type {
ICardNumberElement,
ICardExpirationDateElement,
ICardVerificationCodeElement,
} from "@easylabs/react";

function SeparateCardElementsForm() {
const { checkout } = useEasy();
const cardNumberRef = useRef<ICardNumberElement>(null);
const expiryRef = useRef<ICardExpirationDateElement>(null);
const cvcRef = useRef<ICardVerificationCodeElement>(null);

const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);

try {
const result = await checkout({
customer_creation: true,
customer_details: {
first_name: "Jane",
last_name: "Smith",
email: "[email protected]",
},
source: {
type: "PAYMENT_CARD",
cardNumberElement: cardNumberRef,
cardExpirationDateElement: expiryRef,
cardVerificationCodeElement: cvcRef,
name: "Jane's Card",
},
line_items: [{ price_id: "price_xyz789", quantity: 1 }],
});

console.log("Payment successful:", result);
} catch (err) {
setError(err instanceof Error ? err.message : "Payment failed");
console.error("Payment error:", err);
} finally {
setLoading(false);
}
};

const elementStyle = {
base: {
fontSize: "16px",
color: "#424770",
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
"::placeholder": {
color: "#aab7c4",
},
},
invalid: {
color: "#9e2146",
},
};

return (
<form onSubmit={handleSubmit} className="payment-form">
<h2>Payment Details</h2>

{error && (
<div className="error-message" role="alert">
{error}
</div>
)}

<div className="form-group">
<label htmlFor="card-number">Card Number</label>
<CardNumberElement
ref={cardNumberRef}
id="card-number"
style={elementStyle}
/>
</div>

<div className="form-row">
<div className="form-group">
<label htmlFor="expiry-date">Expiry Date</label>
<CardExpirationDateElement
ref={expiryRef}
id="expiry-date"
style={elementStyle}
/>
</div>

<div className="form-group">
<label htmlFor="cvc">CVC</label>
<CardVerificationCodeElement
ref={cvcRef}
id="cvc"
style={elementStyle}
/>
</div>
</div>

<button type="submit" disabled={loading}>
{loading ? "Processing..." : "Pay Now"}
</button>
</form>
);
}

export default SeparateCardElementsForm;

Bank Account Payment Form

Accept ACH payments with bank account elements.

import React, { useRef, useState } from "react";
import { useEasy, TextElement } from "@easylabs/react";
import type { ITextElement } from "@easylabs/react";

function BankPaymentForm() {
const { checkout } = useEasy();
const routingRef = useRef<ITextElement>(null);
const accountRef = useRef<ITextElement>(null);

const [accountType, setAccountType] = useState<"CHECKING" | "SAVINGS">(
"CHECKING",
);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);

try {
const result = await checkout({
customer_creation: true,
customer_details: {
first_name: "Bob",
last_name: "Johnson",
email: "[email protected]",
},
source: {
type: "BANK_ACCOUNT",
accountType,
routingElement: routingRef,
accountElement: accountRef,
name: "Primary Checking",
},
line_items: [{ price_id: "price_def456", quantity: 1 }],
});

console.log("Payment successful:", result);
} catch (err) {
setError(err instanceof Error ? err.message : "Payment failed");
console.error("Payment error:", err);
} finally {
setLoading(false);
}
};

const elementStyle = {
base: {
fontSize: "16px",
color: "#424770",
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
"::placeholder": {
color: "#aab7c4",
},
},
};

return (
<form onSubmit={handleSubmit} className="payment-form">
<h2>Bank Account Payment</h2>

{error && (
<div className="error-message" role="alert">
{error}
</div>
)}

<div className="form-group">
<label>Account Type</label>
<div className="radio-group">
<label>
<input
type="radio"
value="CHECKING"
checked={accountType === "CHECKING"}
onChange={(e) => setAccountType(e.target.value as "CHECKING")}
/>
Checking
</label>
<label>
<input
type="radio"
value="SAVINGS"
checked={accountType === "SAVINGS"}
onChange={(e) => setAccountType(e.target.value as "SAVINGS")}
/>
Savings
</label>
</div>
</div>

<div className="form-group">
<label htmlFor="routing-number">Routing Number</label>
<TextElement
ref={routingRef}
id="routing-number"
placeholder="123456789"
style={elementStyle}
/>
</div>

<div className="form-group">
<label htmlFor="account-number">Account Number</label>
<TextElement
ref={accountRef}
id="account-number"
placeholder="0123456789"
style={elementStyle}
/>
</div>

<button type="submit" disabled={loading}>
{loading ? "Processing..." : "Pay with Bank Account"}
</button>
</form>
);
}

export default BankPaymentForm;

Form with Customer Information

A complete form that collects customer information along with payment details.

import React, { useRef, useState } from "react";
import { useEasy, CardElement } from "@easylabs/react";
import type { ICardElement } from "@easylabs/react";

interface CustomerFormData {
firstName: string;
lastName: string;
email: string;
phone: string;
address: {
line1: string;
line2: string;
city: string;
state: string;
postalCode: string;
country: string;
};
}

function CompleteCheckoutForm() {
const { checkout } = useEasy();
const cardRef = useRef<ICardElement>(null);

const [formData, setFormData] = useState<CustomerFormData>({
firstName: "",
lastName: "",
email: "",
phone: "",
address: {
line1: "",
line2: "",
city: "",
state: "",
postalCode: "",
country: "US",
},
});

const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [validationErrors, setValidationErrors] = useState<
Record<string, string>
>({});

const validateForm = (): boolean => {
const errors: Record<string, string> = {};

if (!formData.firstName.trim()) {
errors.firstName = "First name is required";
}
if (!formData.lastName.trim()) {
errors.lastName = "Last name is required";
}
if (!formData.email.includes("@")) {
errors.email = "Valid email is required";
}
if (!formData.address.line1.trim()) {
errors.addressLine1 = "Address is required";
}
if (!formData.address.city.trim()) {
errors.city = "City is required";
}
if (!formData.address.state.trim()) {
errors.state = "State is required";
}
if (!formData.address.postalCode.trim()) {
errors.postalCode = "Postal code is required";
}

setValidationErrors(errors);
return Object.keys(errors).length === 0;
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

if (!validateForm()) {
return;
}

setLoading(true);
setError(null);

try {
const result = await checkout({
customer_creation: true,
customer_details: {
first_name: formData.firstName,
last_name: formData.lastName,
email: formData.email,
phone: formData.phone || undefined,
personal_address: {
line1: formData.address.line1,
line2: formData.address.line2 || undefined,
city: formData.address.city,
state: formData.address.state,
postal_code: formData.address.postalCode,
country: formData.address.country,
},
},
source: {
type: "PAYMENT_CARD",
cardElement: cardRef,
name: `${formData.firstName}'s Card`,
address: {
line1: formData.address.line1,
line2: formData.address.line2 || undefined,
city: formData.address.city,
state: formData.address.state,
postal_code: formData.address.postalCode,
country: formData.address.country,
},
},
line_items: [{ price_id: "price_abc123", quantity: 1 }],
});

console.log("Checkout successful:", result);
// Reset form or redirect to success page
} catch (err) {
setError(err instanceof Error ? err.message : "Checkout failed");
console.error("Checkout error:", err);
} finally {
setLoading(false);
}
};

const handleInputChange = (
field: keyof CustomerFormData | string,
value: string,
) => {
if (field.startsWith("address.")) {
const addressField = field.split(
".",
)[1] as keyof CustomerFormData["address"];
setFormData((prev) => ({
...prev,
address: {
...prev.address,
[addressField]: value,
},
}));
} else {
setFormData((prev) => ({
...prev,
[field]: value,
}));
}
};

return (
<form onSubmit={handleSubmit} className="checkout-form">
<h2>Checkout</h2>

{error && (
<div className="error-message" role="alert">
{error}
</div>
)}

{/* Customer Information */}
<section>
<h3>Customer Information</h3>

<div className="form-row">
<div className="form-group">
<label htmlFor="firstName">First Name *</label>
<input
id="firstName"
type="text"
value={formData.firstName}
onChange={(e) => handleInputChange("firstName", e.target.value)}
className={validationErrors.firstName ? "error" : ""}
/>
{validationErrors.firstName && (
<span className="error-text">{validationErrors.firstName}</span>
)}
</div>

<div className="form-group">
<label htmlFor="lastName">Last Name *</label>
<input
id="lastName"
type="text"
value={formData.lastName}
onChange={(e) => handleInputChange("lastName", e.target.value)}
className={validationErrors.lastName ? "error" : ""}
/>
{validationErrors.lastName && (
<span className="error-text">{validationErrors.lastName}</span>
)}
</div>
</div>

<div className="form-group">
<label htmlFor="email">Email *</label>
<input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
className={validationErrors.email ? "error" : ""}
/>
{validationErrors.email && (
<span className="error-text">{validationErrors.email}</span>
)}
</div>

<div className="form-group">
<label htmlFor="phone">Phone</label>
<input
id="phone"
type="tel"
value={formData.phone}
onChange={(e) => handleInputChange("phone", e.target.value)}
/>
</div>
</section>

{/* Billing Address */}
<section>
<h3>Billing Address</h3>

<div className="form-group">
<label htmlFor="addressLine1">Street Address *</label>
<input
id="addressLine1"
type="text"
value={formData.address.line1}
onChange={(e) => handleInputChange("address.line1", e.target.value)}
className={validationErrors.addressLine1 ? "error" : ""}
/>
{validationErrors.addressLine1 && (
<span className="error-text">{validationErrors.addressLine1}</span>
)}
</div>

<div className="form-group">
<label htmlFor="addressLine2">Apartment, suite, etc.</label>
<input
id="addressLine2"
type="text"
value={formData.address.line2}
onChange={(e) => handleInputChange("address.line2", e.target.value)}
/>
</div>

<div className="form-row">
<div className="form-group">
<label htmlFor="city">City *</label>
<input
id="city"
type="text"
value={formData.address.city}
onChange={(e) =>
handleInputChange("address.city", e.target.value)
}
className={validationErrors.city ? "error" : ""}
/>
{validationErrors.city && (
<span className="error-text">{validationErrors.city}</span>
)}
</div>

<div className="form-group">
<label htmlFor="state">State *</label>
<input
id="state"
type="text"
value={formData.address.state}
onChange={(e) =>
handleInputChange("address.state", e.target.value)
}
className={validationErrors.state ? "error" : ""}
/>
{validationErrors.state && (
<span className="error-text">{validationErrors.state}</span>
)}
</div>

<div className="form-group">
<label htmlFor="postalCode">Postal Code *</label>
<input
id="postalCode"
type="text"
value={formData.address.postalCode}
onChange={(e) =>
handleInputChange("address.postalCode", e.target.value)
}
className={validationErrors.postalCode ? "error" : ""}
/>
{validationErrors.postalCode && (
<span className="error-text">{validationErrors.postalCode}</span>
)}
</div>
</div>
</section>

{/* Payment Information */}
<section>
<h3>Payment Information</h3>

<div className="form-group">
<label htmlFor="card-element">Card Details</label>
<CardElement
ref={cardRef}
id="card-element"
style={{
base: {
fontSize: "16px",
color: "#424770",
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
"::placeholder": {
color: "#aab7c4",
},
},
invalid: {
color: "#9e2146",
},
}}
/>
</div>
</section>

<button type="submit" disabled={loading} className="submit-button">
{loading ? "Processing..." : "Complete Purchase"}
</button>
</form>
);
}

export default CompleteCheckoutForm;

Styling Form Elements

Customize the appearance of payment form elements to match your design.

const customStyle = {
base: {
fontSize: "16px",
color: "#32325d",
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
fontSmoothing: "antialiased",
"::placeholder": {
color: "#aab7c4",
},
":focus": {
color: "#32325d",
},
},
invalid: {
color: "#fa755a",
iconColor: "#fa755a",
},
complete: {
color: "#32cd32",
},
};

function StyledCardForm() {
const cardRef = useRef(null);

return (
<CardElement
ref={cardRef}
style={customStyle}
options={{
hidePostalCode: false,
iconStyle: "default", // or "solid"
}}
/>
);
}

Best Practices

1. Always Use Refs

Secure elements require refs to tokenize the payment data:

const cardRef = useRef<ICardElement>(null);

<CardElement ref={cardRef} />;

2. Handle Loading States

Provide clear feedback during payment processing:

const [loading, setLoading] = useState(false);

<button disabled={loading}>{loading ? "Processing..." : "Pay Now"}</button>;

3. Validate Customer Input

Validate all customer information before submission:

const validateForm = () => {
const errors = {};

if (!email.includes("@")) {
errors.email = "Valid email required";
}

return Object.keys(errors).length === 0;
};

4. Error Handling

Always catch and display errors appropriately:

try {
await checkout(data);
} catch (error) {
setError(error instanceof Error ? error.message : "Payment failed");
}

5. Security

  • Never store or log card data
  • Always use HTTPS in production
  • Use environment variables for API keys
  • Validate data on both client and server

Common Patterns

Disable Submit Until Form is Complete

const [isFormValid, setIsFormValid] = useState(false);

// Check form validity on input change
useEffect(() => {
const valid =
email.includes("@") && firstName.trim() !== "" && cardRef.current !== null;

setIsFormValid(valid);
}, [email, firstName]);

<button disabled={!isFormValid || loading}>Submit Payment</button>;

Show Different Payment Methods

const [paymentMethod, setPaymentMethod] = useState<"card" | "bank">("card");

return (
<form>
<div>
<button onClick={() => setPaymentMethod("card")}>Credit Card</button>
<button onClick={() => setPaymentMethod("bank")}>Bank Account</button>
</div>

{paymentMethod === "card" ? (
<CardElement ref={cardRef} />
) : (
<>
<TextElement ref={routingRef} placeholder="Routing Number" />
<TextElement ref={accountRef} placeholder="Account Number" />
</>
)}
</form>
);

Remember Me / Save Payment Method

const [savePaymentMethod, setSavePaymentMethod] = useState(false);

<label>
<input
type="checkbox"
checked={savePaymentMethod}
onChange={(e) => setSavePaymentMethod(e.target.checked)}
/>
Save payment method for future purchases
</label>;

Next Steps