From 9f6832097c6fbdd1b7e246d6459fa8e7288a5262 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Sun, 18 Jan 2026 20:58:49 -0600 Subject: [PATCH] feat: add full billing address collection to Stripe payment forms (refs #55) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace CardElement with PaymentElement + AddressElement in subscription forms - Add AddressElement to donation forms for billing address collection - Now collects: Name, Address Line 1/2, City, State, Postal Code, Country - Card details: Card Number, Expiration, CVC - Both desktop and mobile forms updated 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../components/DonationSection.tsx | 65 +++++++++- .../components/DonationSectionMobile.tsx | 55 +++++++- .../components/PaymentMethodForm.tsx | 120 +++++++++++++----- .../mobile/SubscriptionMobileScreen.tsx | 9 +- .../subscription/pages/SubscriptionPage.tsx | 9 +- 5 files changed, 218 insertions(+), 40 deletions(-) diff --git a/frontend/src/features/subscription/components/DonationSection.tsx b/frontend/src/features/subscription/components/DonationSection.tsx index 2a5d016..2d5d7bd 100644 --- a/frontend/src/features/subscription/components/DonationSection.tsx +++ b/frontend/src/features/subscription/components/DonationSection.tsx @@ -14,12 +14,12 @@ import { TableRow, Chip, } from '@mui/material'; -import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js'; +import { CardElement, AddressElement, useStripe, useElements } from '@stripe/react-stripe-js'; import { format } from 'date-fns'; import toast from 'react-hot-toast'; import { Card } from '../../../shared-minimal/components/Card'; import { useCreateDonation, useDonations } from '../hooks/useSubscription'; -import type { StripeCardElementChangeEvent } from '@stripe/stripe-js'; +import type { StripeCardElementChangeEvent, StripeAddressElementChangeEvent } from '@stripe/stripe-js'; import type { SubscriptionTier } from '../types/subscription.types'; interface DonationSectionProps { @@ -48,6 +48,7 @@ export const DonationSection: React.FC = ({ currentTier }) const [error, setError] = useState(null); const [processing, setProcessing] = useState(false); const [cardComplete, setCardComplete] = useState(false); + const [addressComplete, setAddressComplete] = useState(false); const [showSuccess, setShowSuccess] = useState(false); const createDonationMutation = useCreateDonation(); @@ -60,6 +61,10 @@ export const DonationSection: React.FC = ({ currentTier }) setCardComplete(event.complete); }; + const handleAddressChange = (event: StripeAddressElementChangeEvent) => { + setAddressComplete(event.complete); + }; + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); @@ -72,6 +77,11 @@ export const DonationSection: React.FC = ({ currentTier }) return; } + const addressElement = elements.getElement(AddressElement); + if (!addressElement) { + return; + } + // Validate amount const amountNum = parseFloat(amount); if (isNaN(amountNum) || amountNum < 0.5) { @@ -79,6 +89,13 @@ export const DonationSection: React.FC = ({ currentTier }) return; } + // Get billing address data + const addressData = await addressElement.getValue(); + if (!addressData.complete) { + setError('Please complete the billing address'); + return; + } + setProcessing(true); setError(null); @@ -87,10 +104,21 @@ export const DonationSection: React.FC = ({ currentTier }) const donationResponse = await createDonationMutation.mutateAsync(amountNum); const { clientSecret } = donationResponse.data; - // Confirm payment with Stripe + // Confirm payment with Stripe including billing details const { error: confirmError } = await stripe.confirmCardPayment(clientSecret, { payment_method: { card: cardElement, + billing_details: { + name: addressData.value.name, + address: { + line1: addressData.value.address.line1, + line2: addressData.value.address.line2 || undefined, + city: addressData.value.address.city, + state: addressData.value.address.state, + postal_code: addressData.value.address.postal_code, + country: addressData.value.address.country, + }, + }, }, }); @@ -118,7 +146,7 @@ export const DonationSection: React.FC = ({ currentTier }) } }; - const isFormValid = amount && parseFloat(amount) >= 0.5 && cardComplete && !processing; + const isFormValid = amount && parseFloat(amount) >= 0.5 && cardComplete && addressComplete && !processing; return ( @@ -164,6 +192,35 @@ export const DonationSection: React.FC = ({ currentTier }) /> + + + Billing Address + + + + + + Card Details diff --git a/frontend/src/features/subscription/components/DonationSectionMobile.tsx b/frontend/src/features/subscription/components/DonationSectionMobile.tsx index 6c46769..afbd808 100644 --- a/frontend/src/features/subscription/components/DonationSectionMobile.tsx +++ b/frontend/src/features/subscription/components/DonationSectionMobile.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; -import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js'; +import { CardElement, AddressElement, useStripe, useElements } from '@stripe/react-stripe-js'; import { format } from 'date-fns'; import toast from 'react-hot-toast'; import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard'; import { useCreateDonation, useDonations } from '../hooks/useSubscription'; -import type { StripeCardElementChangeEvent } from '@stripe/stripe-js'; +import type { StripeCardElementChangeEvent, StripeAddressElementChangeEvent } from '@stripe/stripe-js'; import type { SubscriptionTier } from '../types/subscription.types'; interface DonationSectionMobileProps { @@ -33,6 +33,7 @@ export const DonationSectionMobile: React.FC = ({ cu const [error, setError] = useState(null); const [processing, setProcessing] = useState(false); const [cardComplete, setCardComplete] = useState(false); + const [addressComplete, setAddressComplete] = useState(false); const [showSuccess, setShowSuccess] = useState(false); const createDonationMutation = useCreateDonation(); @@ -45,6 +46,10 @@ export const DonationSectionMobile: React.FC = ({ cu setCardComplete(event.complete); }; + const handleAddressChange = (event: StripeAddressElementChangeEvent) => { + setAddressComplete(event.complete); + }; + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); @@ -57,6 +62,11 @@ export const DonationSectionMobile: React.FC = ({ cu return; } + const addressElement = elements.getElement(AddressElement); + if (!addressElement) { + return; + } + // Validate amount const amountNum = parseFloat(amount); if (isNaN(amountNum) || amountNum < 0.5) { @@ -64,6 +74,13 @@ export const DonationSectionMobile: React.FC = ({ cu return; } + // Get billing address data + const addressData = await addressElement.getValue(); + if (!addressData.complete) { + setError('Please complete the billing address'); + return; + } + setProcessing(true); setError(null); @@ -72,10 +89,21 @@ export const DonationSectionMobile: React.FC = ({ cu const donationResponse = await createDonationMutation.mutateAsync(amountNum); const { clientSecret } = donationResponse.data; - // Confirm payment with Stripe + // Confirm payment with Stripe including billing details const { error: confirmError } = await stripe.confirmCardPayment(clientSecret, { payment_method: { card: cardElement, + billing_details: { + name: addressData.value.name, + address: { + line1: addressData.value.address.line1, + line2: addressData.value.address.line2 || undefined, + city: addressData.value.address.city, + state: addressData.value.address.state, + postal_code: addressData.value.address.postal_code, + country: addressData.value.address.country, + }, + }, }, }); @@ -103,7 +131,7 @@ export const DonationSectionMobile: React.FC = ({ cu } }; - const isFormValid = amount && parseFloat(amount) >= 0.5 && cardComplete && !processing; + const isFormValid = amount && parseFloat(amount) >= 0.5 && cardComplete && addressComplete && !processing; return ( @@ -151,6 +179,25 @@ export const DonationSectionMobile: React.FC = ({ cu +
+ +
+ +
+
+