Skip to content

Instantly share code, notes, and snippets.

@slicksammy
Created August 28, 2025 13:18
Show Gist options
  • Select an option

  • Save slicksammy/896d34a81133e5b33d15847da66b0847 to your computer and use it in GitHub Desktop.

Select an option

Save slicksammy/896d34a81133e5b33d15847da66b0847 to your computer and use it in GitHub Desktop.
bt.ts
import React, { useEffect, useRef, useState } from 'react';
import { PaymentMethod, CheckoutData } from '../../types';
import { API_ENDPOINTS, BACKEND_URL } from '../../config';
import { captureException, addBreadcrumb } from '../../utils/sentry';
import { usePostHogTracking } from '../../hooks/usePostHogTracking';
import { useBasisTheory, CardNumberElement, CardExpirationDateElement, CardVerificationCodeElement } from '@basis-theory/react-elements';
interface BasisTheoryCardFormProps {
checkoutData: CheckoutData;
onNewPaymentMethodCreated: (newMethod?: PaymentMethod) => void;
createPath: string;
}
interface BasisTheorySettings {
tokenization_api_key: string;
}
const BasisTheoryCardForm: React.FC<BasisTheoryCardFormProps> = ({
checkoutData,
onNewPaymentMethodCreated,
createPath,
}) => {
const { trackEvent } = usePostHogTracking(checkoutData);
const [isSubmitting, setIsSubmitting] = useState(false);
const [saveCard, setSaveCard] = useState(true);
const [errorMessage, setErrorMessage] = useState("");
const [isFormLoading, setIsFormLoading] = useState(true);
const [isFormValid, setIsFormValid] = useState(false);
const [basisTheorySettings, setBasisTheorySettings] = useState<BasisTheorySettings | null>(null);
const [error, setError] = useState<string>("");
// State for non-sensitive form fields
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [postalCode, setPostalCode] = useState("");
const cardNumberRef = useRef<any>(null);
const cardExpiryRef = useRef<any>(null);
const cardCvcRef = useRef<any>(null);
// Track validation state for each field
const [fieldValidation, setFieldValidation] = useState({
cardNumber: false,
cardExpiry: false,
cardCvc: false
});
// Only initialize Basis Theory when we have the API key
const { bt } = useBasisTheory(basisTheorySettings?.tokenization_api_key);
// Add breadcrumb and track page load when component mounts
useEffect(() => {
if (checkoutData) {
addBreadcrumb(
'Basis Theory card form loaded',
'payment',
{
checkout_id: checkoutData.id,
business_id: checkoutData.business_id,
business_customer_id: checkoutData.business_customer_id,
type: checkoutData.type
}
);
trackEvent('credit_card_form_loaded');
}
}, [checkoutData]);
// Fetch Basis Theory settings
useEffect(() => {
const fetchBasisTheorySettings = async () => {
try {
const response = await fetch(
API_ENDPOINTS.BASIS_THEORY_CARD_SETTINGS +
`?client_secret=${checkoutData.client_secret}`
);
if (!response.ok) {
throw new Error(`Failed to fetch settings: ${response.status} ${response.statusText}`);
}
const data = await response.json();
setBasisTheorySettings(data);
} catch (error) {
console.error('Error fetching Basis Theory settings:', error);
captureException(
error instanceof Error ? error : new Error('Failed to fetch Basis Theory settings'),
checkoutData?.business_id,
checkoutData?.business_customer_id,
'BasisTheoryCardForm',
'basis_theory',
{ error_type: 'basis_theory_settings_fetch' }
);
setError("Failed to load payment configuration");
} finally {
setIsFormLoading(false);
}
};
fetchBasisTheorySettings();
}, [checkoutData.client_secret, checkoutData?.business_id, checkoutData?.business_customer_id]);
// Update overall form validity when individual fields change
useEffect(() => {
const allFieldsValid = Object.values(fieldValidation).every(valid => valid) &&
firstName.trim().length > 0 &&
lastName.trim().length > 0 &&
postalCode.trim().length >= 5;
setIsFormValid(allFieldsValid);
}, [fieldValidation, firstName, lastName, postalCode]);
// Handle card element events
const handleCardNumberReady = () => {
console.log("Card number element ready");
};
const handleCardNumberChange = (event: any) => {
setFieldValidation(prev => ({
...prev,
cardNumber: event.complete && !event.error
}));
if (event.error) {
console.log("Card number validation error:", event.error);
}
};
const handleCardExpiryChange = (event: any) => {
setFieldValidation(prev => ({
...prev,
cardExpiry: event.complete && !event.error
}));
if (event.error) {
console.log("Card expiry validation error:", event.error);
}
};
const handleCardCvcChange = (event: any) => {
setFieldValidation(prev => ({
...prev,
cardCvc: event.complete && !event.error
}));
if (event.error) {
console.log("Card CVC validation error:", event.error);
}
};
// Handle regular input field changes
const handleFirstNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFirstName(e.target.value);
};
const handleLastNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setLastName(e.target.value);
};
const handlePostalCodeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPostalCode(e.target.value);
};
const handleSaveCardChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSaveCard(e.target.checked);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setErrorMessage("");
if (!bt || !cardNumberRef.current || !cardExpiryRef.current || !cardCvcRef.current || !firstName || !lastName || !postalCode || !basisTheorySettings) {
console.error("Basis Theory is not initialized yet or form fields are missing.");
return;
}
// Validate form before submission
if (!isFormValid) {
setErrorMessage("Please complete all required fields before submitting.");
return;
}
setIsSubmitting(true);
try {
console.log('Starting tokenization...');
// Tokenize card information only
const tokenIntent = await bt.tokenIntents.create({
type: "card",
data: {
number: cardNumberRef.current,
expiration_month: cardExpiryRef.current.month(),
expiration_year: cardExpiryRef.current.year(),
cvc: cardCvcRef.current,
},
// containers: ['/general/high/']
});
// Validate that tokenization was successful
if (!tokenIntent) {
throw new Error('Failed to tokenize payment information - no token returned');
}
// All tokenization successful - proceed with backend request
const createResponse = await fetch(
`${BACKEND_URL}${createPath}?client_secret=${checkoutData.client_secret}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
saved: saveCard,
token_intent: tokenIntent,
first_name: firstName,
last_name: lastName,
postal_code: postalCode
}),
}
);
if (!createResponse.ok) {
const error = await createResponse.json();
captureException(
new Error(error.error || 'Failed to create payment method'),
checkoutData?.business_id,
checkoutData?.business_customer_id,
'BasisTheoryCardForm',
'basis_theory',
{ error_type: 'payment_method_creation' },
{ error_response: error }
);
setErrorMessage(error.error || 'Failed to create payment method');
} else {
const { id, last_four, payment_type, card_brand } = await createResponse.json();
onNewPaymentMethodCreated({
id,
type: payment_type,
last4: last_four,
card_brand: card_brand,
apple_pay: false // Basis Theory card form never creates Apple Pay methods
});
}
} catch (error: any) {
console.error("Error submitting Basis Theory payment form:", error);
captureException(
error instanceof Error ? error : new Error('Failed to submit payment form'),
checkoutData?.business_id,
checkoutData?.business_customer_id,
'BasisTheoryCardForm',
'basis_theory',
{ error_type: 'payment_form_submission' },
{ error_message: error.message }
);
setErrorMessage(error.message || "Failed to process payment information. Please try again.");
} finally {
setIsSubmitting(false);
}
};
// Show loading while fetching settings
if (isFormLoading) {
return (
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-5 sm:p-8 my-4 sm:my-6 transition-all max-w-2xl mx-auto">
<div className="flex flex-col justify-center items-center h-[220px]" aria-live="polite">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-blue-100 border-t-blue-600 mb-3"></div>
<p className="font-medium text-gray-700">Loading payment configuration...</p>
</div>
</div>
);
}
// Show error if we couldn't fetch settings
if (error || !basisTheorySettings) {
return (
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-5 sm:p-8 my-4 sm:my-6 transition-all max-w-2xl mx-auto">
<div className="bg-red-50 text-red-600 p-4 rounded-xl text-sm sm:text-base border border-red-200 flex items-start" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-3 flex-shrink-0 mt-0.5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
<span>{error || "Failed to load payment configuration"}</span>
</div>
</div>
);
}
// Only render the form when we have both settings and bt instance
if (!bt) {
return (
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-5 sm:p-8 my-4 sm:my-6 transition-all max-w-2xl mx-auto">
<div className="flex flex-col justify-center items-center h-[220px]" aria-live="polite">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-blue-100 border-t-blue-600 mb-3"></div>
<p className="font-medium text-gray-700">Initializing payment form...</p>
</div>
</div>
);
}
const elementStyle = {
base: {
color: '#374151',
fontFamily: 'system-ui, -apple-system, sans-serif',
fontSize: '16px',
fontWeight: '400',
'::placeholder': {
color: '#9CA3AF',
},
},
invalid: {
color: '#374151',
},
complete: {
color: '#374151',
},
};
return (
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-5 sm:p-8 my-4 sm:my-6 transition-all max-w-2xl mx-auto">
<h3 className="text-xl sm:text-2xl font-bold mb-5 text-gray-800 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mr-3 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
<span className="relative">
Credit / Debit Card
</span>
</h3>
{errorMessage && (
<div className="bg-red-50 text-red-600 p-4 rounded-xl mb-5 text-sm sm:text-base border border-red-200 flex items-start" role="alert" aria-live="assertive">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-3 flex-shrink-0 mt-0.5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
<span>{errorMessage}</span>
</div>
)}
<form className="space-y-6" onSubmit={handleSubmit}>
<div className="min-h-[220px] relative bg-gray-50 rounded-xl p-4 sm:p-6 border border-gray-200 shadow-inner">
{/* Name fields row */}
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
First Name *
</label>
<div className="bg-white border border-gray-300 rounded-lg p-3">
<input
type="text"
value={firstName}
placeholder="First Name"
onChange={handleFirstNameChange}
className="w-full bg-transparent border-none outline-none"
style={{
color: '#374151',
fontSize: '16px',
fontFamily: 'system-ui, -apple-system, sans-serif',
fontWeight: '400'
}}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Last Name *
</label>
<div className="bg-white border border-gray-300 rounded-lg p-3">
<input
type="text"
value={lastName}
placeholder="Last Name"
onChange={handleLastNameChange}
className="w-full bg-transparent border-none outline-none"
style={{
color: '#374151',
fontSize: '16px',
fontFamily: 'system-ui, -apple-system, sans-serif',
fontWeight: '400'
}}
/>
</div>
</div>
</div>
{/* Card number - full width */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Card Number *
</label>
<div className="bg-white border border-gray-300 rounded-lg p-3">
<CardNumberElement
id="card-number"
bt={bt}
ref={cardNumberRef}
placeholder="1234 1234 1234 1234"
onReady={handleCardNumberReady}
onChange={handleCardNumberChange}
style={elementStyle}
/>
</div>
</div>
{/* Expiration and CVV row */}
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Expiration *
</label>
<div className="bg-white border border-gray-300 rounded-lg p-3">
<CardExpirationDateElement
id="card-expiry"
bt={bt}
ref={cardExpiryRef}
placeholder="MM/YY"
onChange={handleCardExpiryChange}
style={elementStyle}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
CVV *
</label>
<div className="bg-white border border-gray-300 rounded-lg p-3">
<CardVerificationCodeElement
id="card-cvc"
bt={bt}
ref={cardCvcRef}
placeholder="123"
onChange={handleCardCvcChange}
style={elementStyle}
/>
</div>
</div>
</div>
{/* Zip code and Country row */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Zip Code *
</label>
<div className="bg-white border border-gray-300 rounded-lg p-3">
<input
type="text"
value={postalCode}
placeholder="12345"
maxLength={5}
onChange={handlePostalCodeChange}
className="w-full bg-transparent border-none outline-none"
style={{
color: '#374151',
fontSize: '16px',
fontFamily: 'system-ui, -apple-system, sans-serif',
fontWeight: '400'
}}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Country
</label>
<div className="bg-white border border-gray-300 rounded-lg p-3">
<input
type="text"
value="US"
disabled
className="w-full bg-transparent border-none outline-none cursor-not-allowed"
style={{
color: '#374151',
fontSize: '16px',
fontFamily: 'system-ui, -apple-system, sans-serif',
fontWeight: '400'
}}
/>
</div>
</div>
</div>
</div>
<div className="flex items-center mt-3 bg-blue-50 p-3 rounded-lg border border-blue-100 hover:bg-blue-100 transition-colors duration-200">
<input
id="save-card"
type="checkbox"
checked={saveCard}
onChange={handleSaveCardChange}
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 cursor-pointer"
/>
<label htmlFor="save-card" className="ml-2 block text-xs sm:text-sm text-gray-600 font-medium cursor-pointer select-none">
Save payment method for future use
</label>
</div>
<button
type="submit"
disabled={!bt || isSubmitting || !isFormValid}
className={`w-full py-4 px-6 rounded-xl text-white font-semibold text-base sm:text-lg transition-all duration-200 transform hover:scale-[1.02] active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${
!bt || isSubmitting || !isFormValid
? 'bg-gray-400 cursor-not-allowed opacity-70'
: 'bg-blue-600 hover:bg-blue-700 shadow-md hover:shadow-lg'
}`}
aria-live="polite"
>
{!bt
? 'Loading Payment Form...'
: isSubmitting
? (
<span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Processing...
</span>
)
: !isFormValid
? 'Please Complete Form'
: 'Add Card'}
</button>
<div className="flex items-center justify-center space-x-4 mt-4 text-xs sm:text-sm text-gray-500">
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<span>Secure Transaction</span>
</div>
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<span>256-bit Encryption</span>
</div>
</div>
</form>
</div>
);
};
export default BasisTheoryCardForm;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment