feat: add full billing address collection to Stripe payment forms (refs #55)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m4s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 38s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m4s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 38s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<DonationSectionMobileProps> = ({ cu
|
||||
const [error, setError] = useState<string | null>(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<DonationSectionMobileProps> = ({ 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<DonationSectionMobileProps> = ({ 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<DonationSectionMobileProps> = ({ 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<DonationSectionMobileProps> = ({ 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<DonationSectionMobileProps> = ({ cu
|
||||
}
|
||||
};
|
||||
|
||||
const isFormValid = amount && parseFloat(amount) >= 0.5 && cardComplete && !processing;
|
||||
const isFormValid = amount && parseFloat(amount) >= 0.5 && cardComplete && addressComplete && !processing;
|
||||
|
||||
return (
|
||||
<GlassCard>
|
||||
@@ -151,6 +179,25 @@ export const DonationSectionMobile: React.FC<DonationSectionMobileProps> = ({ cu
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-avus mb-2">
|
||||
Billing Address
|
||||
</label>
|
||||
<div className="border border-slate-200 dark:border-grigio rounded-xl p-3 bg-white dark:bg-nero">
|
||||
<AddressElement
|
||||
options={{
|
||||
mode: 'billing',
|
||||
defaultValues: {
|
||||
address: {
|
||||
country: 'US',
|
||||
},
|
||||
},
|
||||
}}
|
||||
onChange={handleAddressChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-avus mb-2">
|
||||
Card Details
|
||||
|
||||
Reference in New Issue
Block a user