基于Vanilla JS+Node的Stripe美加用户购买限制方案咨询
Hey there! I’ve dealt with exactly this scenario after Stripe deprecated some older georestriction APIs, so let me break down the current, supported approaches for your T-shirt store. The key here is to combine frontend guardrails (for better user experience) with strict backend/webhook validation—since frontend checks can easily be bypassed.
Option 1: Use Stripe Checkout (Recommended)
Stripe Checkout handles most of the heavy lifting for you, including enforcing country restrictions directly in its hosted checkout page. This is the simplest and most secure approach if you don’t need a fully custom payment form.
Backend (Node.js)
When creating a Checkout Session, specify the shipping_address_collection.allowed_countries parameter to limit shipping to US and Canada. You can also require billing addresses to double-verify the country:
const stripe = require('stripe')('YOUR_STRIPE_SECRET_KEY'); const express = require('express'); const app = express(); app.post('/create-checkout-session', async (req, res) => { try { const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], line_items: [ { price: 'YOUR_TSHIRT_PRICE_ID', // Replace with your actual Price ID from Stripe Dashboard quantity: 1, }, ], mode: 'payment', success_url: 'https://your-domain.com/success?session_id={CHECKOUT_SESSION_ID}', cancel_url: 'https://your-domain.com/cancel', // Restrict shipping to US/CA only shipping_address_collection: { allowed_countries: ['US', 'CA'], }, // Optional: Require billing address to cross-verify country billing_address_collection: 'required', }); res.json({ url: session.url }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.listen(3000, () => console.log('Server running on port 3000'));
Frontend (Vanilla JS)
Add a button to trigger the Checkout flow. Stripe will redirect users to its hosted page, which will block any non-US/CA shipping addresses automatically:
<button id="buy-tshirt-btn">Buy T-Shirt</button> <script src="https://js.stripe.com/v3/"></script> <script> document.getElementById('buy-tshirt-btn').addEventListener('click', async () => { const stripe = Stripe('YOUR_STRIPE_PUBLISHABLE_KEY'); // Fetch the Checkout Session from your backend const response = await fetch('/create-checkout-session', { method: 'POST' }); const session = await response.json(); // Redirect to Stripe Checkout const { error } = await stripe.redirectToCheckout({ sessionId: session.id }); // Handle any errors (e.g., network issues) if (error) { alert(`Oops: ${error.message}`); } }); </script>
Option 2: Custom Payment Flow (Payment Intents + Stripe Elements)
If you need a fully custom payment form, use Stripe Elements to collect payment details and validate the country both frontend and backend.
Frontend Validation (Vanilla JS)
Add a country dropdown to your form and validate it before submitting to guide users:
<form id="payment-form"> <div> <label>Country</label> <select id="country"> <option value="US">United States</option> <option value="CA">Canada</option> <!-- Hide other countries or include them but validate --> </select> </div> <div id="card-element"></div> <button type="submit">Buy T-Shirt</button> </form> <script src="https://js.stripe.com/v3/"></script> <script> const stripe = Stripe('YOUR_STRIPE_PUBLISHABLE_KEY'); const elements = stripe.elements(); const cardElement = elements.create('card'); cardElement.mount('#card-element'); const form = document.getElementById('payment-form'); form.addEventListener('submit', async (e) => { e.preventDefault(); const country = document.getElementById('country').value; // Frontend check to prevent unnecessary API calls if (!['US', 'CA'].includes(country)) { alert('Sorry, we only accept orders from the United States and Canada.'); return; } // Fetch Payment Intent from backend const response = await fetch('/create-payment-intent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 2000, country: country }), // $20.00 in cents }); const { clientSecret } = await response.json(); // Confirm payment with Stripe const { error } = await stripe.confirmCardPayment(clientSecret, { payment_method: { card: cardElement, billing_details: { country: country, }, }, }); if (error) { alert(`Payment failed: ${error.message}`); } else { window.location.href = '/success'; } }); </script>
Backend Validation (Node.js)
Validate the country before creating a Payment Intent, and add metadata for later verification:
app.post('/create-payment-intent', async (req, res) => { const { amount, country } = req.body; // Strict backend validation (critical to block malicious requests) if (!['US', 'CA'].includes(country.toUpperCase())) { return res.status(403).json({ error: 'We only accept orders from US and Canada.' }); } try { const paymentIntent = await stripe.paymentIntents.create({ amount: amount, currency: country === 'US' ? 'usd' : 'cad', // Auto-switch currency for better UX metadata: { customer_country: country, }, }); res.json({ clientSecret: paymentIntent.client_secret }); } catch (error) { res.status(500).json({ error: error.message }); } });
Critical Final Step: Webhook Validation
Even with frontend and backend checks, malicious users can bypass them. Use Stripe webhooks to verify the country after payment is processed, and refund if needed:
app.post('/stripe-webhook', express.raw({ type: 'application/json' }), async (req, res) => { const sig = req.headers['stripe-signature']; const webhookSecret = 'YOUR_STRIPE_WEBHOOK_SECRET'; let event; try { event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret); } catch (err) { return res.status(400).send(`Webhook Error: ${err.message}`); } if (event.type === 'payment_intent.succeeded') { const paymentIntent = event.data.object; const billingCountry = paymentIntent.billing_details.address?.country?.toUpperCase(); const shippingCountry = paymentIntent.shipping?.address?.country?.toUpperCase(); const allowedCountries = ['US', 'CA']; // Check if either billing or shipping country is invalid if (!allowedCountries.includes(billingCountry) && !allowedCountries.includes(shippingCountry)) { // Refund the invalid payment await stripe.refunds.create({ payment_intent: paymentIntent.id, }); // Log or notify your team about the suspicious order console.log(`Refunded payment for invalid country: ${billingCountry || shippingCountry}`); } } res.json({ received: true }); });
Key Notes
- Never rely solely on frontend checks: Users can modify frontend code or send direct API requests to bypass them. Backend and webhook checks are non-negotiable.
- Test with Stripe’s test cards: Use test cards like
4242 4242 4242 4242and set test addresses in US/CA to verify restrictions work. - Currency optimization: Auto-switch between USD and CAD based on the user’s country to improve checkout experience.
内容的提问来源于stack exchange,提问作者StephD

