Aunty-Sparkles-Website/components/square-payment-form.tsx

410 lines
13 KiB
TypeScript

"use client"
import { useEffect, useState, useRef } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Alert, AlertDescription } from "@/components/ui/alert"
import type { CartItem, CustomerInfo, OrderResult } from "@/lib/square-types"
interface PaymentFormProps {
items: CartItem[]
onPaymentSuccess: (result: OrderResult) => void
onPaymentError: (error: string) => void
}
declare global {
interface Window {
Square: any
}
}
export default function SquarePaymentForm({ items, onPaymentSuccess, onPaymentError }: PaymentFormProps) {
const [isLoading, setIsLoading] = useState(false)
const [squareLoaded, setSquareLoaded] = useState(false)
const [loadingError, setLoadingError] = useState<string>("")
const [customerInfo, setCustomerInfo] = useState<CustomerInfo>({
name: "",
email: "",
phone: "",
address: {
street: "",
city: "",
state: "",
zipCode: "",
country: "US"
},
})
const paymentsRef = useRef<any>(null)
const cardRef = useRef<any>(null)
const totalAmount = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
useEffect(() => {
loadSquareSDK()
return () => {
// Cleanup on unmount
if (cardRef.current) {
try {
cardRef.current.destroy()
} catch (e) {
console.warn("Error destroying card:", e)
}
}
}
}, [])
const loadSquareSDK = async () => {
try {
setLoadingError("")
// Check if Square is already loaded
if (window.Square) {
await initializeSquare()
return
}
// Validate required environment variables
if (!process.env.NEXT_PUBLIC_SQUARE_APPLICATION_ID) {
throw new Error("Square Application ID not configured")
}
if (!process.env.NEXT_PUBLIC_SQUARE_LOCATION_ID) {
throw new Error("Square Location ID not configured")
}
// Load Square Web Payments SDK
const script = document.createElement("script")
script.src = process.env.SQUARE_ENVIRONMENT === "production"
? "https://web.squarecdn.com/v1/square.js"
: "https://sandbox.web.squarecdn.com/v1/square.js"
script.async = true
script.onload = async () => {
try {
await initializeSquare()
} catch (error) {
setLoadingError(`Square initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
script.onerror = () => {
setLoadingError("Failed to load Square payment system")
}
document.head.appendChild(script)
} catch (error) {
setLoadingError(error instanceof Error ? error.message : "Failed to load payment system")
}
}
const initializeSquare = async () => {
try {
console.log("Initializing Square Web Payments SDK...")
// Initialize Square Payments
paymentsRef.current = window.Square.payments(
process.env.NEXT_PUBLIC_SQUARE_APPLICATION_ID,
process.env.NEXT_PUBLIC_SQUARE_LOCATION_ID
)
// Create and attach card payment method
cardRef.current = await paymentsRef.current.card({
style: {
input: {
fontSize: '16px',
fontFamily: 'Arial, sans-serif',
color: '#373F4A',
backgroundColor: '#FFFFFF',
borderRadius: '6px',
borderColor: '#E0E2E5',
borderWidth: '1px',
padding: '12px'
},
'.input-container': {
borderRadius: '6px',
borderColor: '#E0E2E5',
borderWidth: '1px'
},
'.input-container.is-focus': {
borderColor: '#4A90E2',
boxShadow: '0 0 0 1px #4A90E2'
},
'.input-container.is-error': {
borderColor: '#E02F2F'
},
'.message-text': {
color: '#E02F2F',
fontSize: '14px'
}
}
})
await cardRef.current.attach('#card-container')
setSquareLoaded(true)
console.log("Square Web Payments SDK initialized successfully")
} catch (error) {
console.error("Square initialization error:", error)
setLoadingError(`Payment system initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
const handleCustomerInfoChange = (field: string, value: string) => {
if (field.includes(".")) {
const [parent, child] = field.split(".")
setCustomerInfo((prev) => ({
...prev,
[parent]: {
...prev[parent as keyof CustomerInfo],
[child]: value,
},
}))
} else {
setCustomerInfo((prev) => ({
...prev,
[field]: value,
}))
}
}
const validateForm = (): string | null => {
if (!customerInfo.name.trim()) return "Name is required"
if (!customerInfo.email.trim()) return "Email is required"
if (!/\S+@\S+\.\S+/.test(customerInfo.email)) return "Valid email is required"
return null
}
const handlePayment = async () => {
if (!squareLoaded || !cardRef.current) {
onPaymentError("Payment system not ready")
return
}
const validationError = validateForm()
if (validationError) {
onPaymentError(validationError)
return
}
setIsLoading(true)
try {
console.log("Starting payment process...")
// Step 1: Create order
console.log("Creating order...")
const orderResponse = await fetch("/api/square/orders/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items, customerInfo }),
})
if (!orderResponse.ok) {
const errorText = await orderResponse.text()
throw new Error(`Order creation failed: ${errorText}`)
}
const orderResult = await orderResponse.json()
if (!orderResult.success) {
throw new Error(orderResult.error || "Failed to create order")
}
console.log("Order created:", orderResult.order.id)
// Step 2: Tokenize the card
console.log("Tokenizing card...")
const tokenResult = await cardRef.current.tokenize()
if (tokenResult.status !== "OK") {
const errorMessage = tokenResult.errors?.[0]?.detail || "Card tokenization failed"
throw new Error(errorMessage)
}
console.log("Card tokenized successfully")
// Step 3: Process payment
console.log("Processing payment...")
const paymentResponse = await fetch("/api/square/payments/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sourceId: tokenResult.token,
orderId: orderResult.order.id,
amount: totalAmount,
currency: "USD",
customerInfo,
items
}),
})
if (!paymentResponse.ok) {
const errorText = await paymentResponse.text()
throw new Error(`Payment processing failed: ${errorText}`)
}
const paymentResult = await paymentResponse.json()
if (!paymentResult.success) {
throw new Error(paymentResult.error || "Payment failed")
}
console.log("Payment processed successfully:", paymentResult.payment.id)
// Success!
onPaymentSuccess({
orderId: orderResult.order.id,
paymentId: paymentResult.payment.id,
status: paymentResult.payment.status,
totalAmount: totalAmount,
receiptUrl: paymentResult.payment.receiptUrl
})
} catch (error) {
console.error("Payment error:", error)
onPaymentError(error instanceof Error ? error.message : "Payment failed")
} finally {
setIsLoading(false)
}
}
return (
<div className="max-w-2xl mx-auto space-y-6">
{/* Loading Error */}
{loadingError && (
<Alert variant="destructive">
<AlertDescription>{loadingError}</AlertDescription>
</Alert>
)}
{/* Order Summary */}
<Card>
<CardHeader>
<CardTitle>Order Summary</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{items.map((item) => (
<div key={`${item.id}-${item.variationId}`} className="flex justify-between items-center">
<div>
<h4 className="font-medium">{item.name}</h4>
<p className="text-sm text-gray-600">Quantity: {item.quantity}</p>
{item.description && <p className="text-sm text-gray-500">{item.description}</p>}
</div>
<div className="text-right">
<p className="font-medium">${(item.price * item.quantity).toFixed(2)}</p>
</div>
</div>
))}
<div className="border-t pt-4">
<div className="flex justify-between items-center font-bold text-lg">
<span>Total:</span>
<span>${totalAmount.toFixed(2)}</span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Customer Information */}
<Card>
<CardHeader>
<CardTitle>Customer Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<Input
placeholder="Full Name *"
value={customerInfo.name}
onChange={(e) => handleCustomerInfoChange("name", e.target.value)}
required
/>
<Input
type="email"
placeholder="Email Address *"
value={customerInfo.email}
onChange={(e) => handleCustomerInfoChange("email", e.target.value)}
required
/>
</div>
<Input
type="tel"
placeholder="Phone Number"
value={customerInfo.phone}
onChange={(e) => handleCustomerInfoChange("phone", e.target.value)}
/>
<div className="space-y-2">
<h4 className="font-medium">Shipping Address</h4>
<Input
placeholder="Street Address"
value={customerInfo.address?.street || ""}
onChange={(e) => handleCustomerInfoChange("address.street", e.target.value)}
/>
<div className="grid grid-cols-3 gap-2">
<Input
placeholder="City"
value={customerInfo.address?.city || ""}
onChange={(e) => handleCustomerInfoChange("address.city", e.target.value)}
/>
<Input
placeholder="State"
value={customerInfo.address?.state || ""}
onChange={(e) => handleCustomerInfoChange("address.state", e.target.value)}
/>
<Input
placeholder="ZIP Code"
value={customerInfo.address?.zipCode || ""}
onChange={(e) => handleCustomerInfoChange("address.zipCode", e.target.value)}
/>
</div>
</div>
</CardContent>
</Card>
{/* Payment Form */}
<Card>
<CardHeader>
<CardTitle>Payment Information</CardTitle>
</CardHeader>
<CardContent>
{!squareLoaded && !loadingError && (
<div className="text-center py-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-yellow-400 mx-auto mb-2"></div>
<p className="text-sm text-gray-600">Loading secure payment form...</p>
</div>
)}
<div
id="card-container"
className={`mb-4 border rounded-lg min-h-[120px] ${!squareLoaded ? 'opacity-50' : ''}`}
style={{ minHeight: '120px' }}
>
{/* Square card form will be inserted here */}
</div>
<Button
onClick={handlePayment}
disabled={isLoading || !squareLoaded || !!loadingError}
className="w-full bg-yellow-400 text-black hover:bg-yellow-300 disabled:opacity-50"
>
{isLoading ? (
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-black mr-2"></div>
Processing Payment...
</div>
) : (
`Pay $${totalAmount.toFixed(2)}`
)}
</Button>
<p className="text-xs text-gray-500 mt-2 text-center">
Secure payment powered by Square PCI DSS Compliant
</p>
</CardContent>
</Card>
</div>
)
}