Stripe Payment Integration - Frontend Guide
Overview
This guide covers integrating Stripe payments into the FishingLog frontend applications (Web App, React Native, and Admin Panel).
Stripe Configuration
Publishable Key
- Test:
pk_test_...(from Stripe Dashboard) - Live:
pk_live_...(for production) - Store in environment variables:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYorREACT_APP_STRIPE_PUBLISHABLE_KEY
API Version
- Current:
2025-11-17.clover - Ensure Stripe.js version is compatible
Installation
Web App (React/Next.js)
npm install @stripe/stripe-js @stripe/react-stripe-js
React Native
npm install @stripe/stripe-react-native
Payment Flow
1. Tournament Registration Payment
Flow:
- User selects tournament and registers
- Backend creates payment intent:
POST /api/tournament/{id}/register - Backend returns
stripePaymentIntentIdandclientSecret - Frontend confirms payment with Stripe
- Webhook updates registration status
Implementation:
// Web App Example
import { loadStripe } from '@stripe/stripe-js';
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
// Tournament Registration Component
function TournamentRegistration({ tournamentId }: { tournamentId: number }) {
const [clientSecret, setClientSecret] = useState<string | null>(null);
// Step 1: Create registration and payment intent
const handleRegister = async () => {
const response = await fetch(`/api/tournament/${tournamentId}/register`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ /* registration data */ })
});
const data = await response.json();
setClientSecret(data.stripeClientSecret);
};
// Step 2: Confirm payment
if (clientSecret) {
return (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<PaymentForm tournamentId={tournamentId} />
</Elements>
);
}
return <button onClick={handleRegister}>Register</button>;
}
function PaymentForm({ tournamentId }: { tournamentId: number }) {
const stripe = useStripe();
const elements = useElements();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) return;
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/tournament/${tournamentId}/success`,
},
redirect: 'if_required',
});
if (error) {
// Handle error
console.error(error);
} else if (paymentIntent.status === 'succeeded') {
// Payment succeeded - webhook will update registration
// Optionally poll for status or redirect
}
};
return (
<form onSubmit={handleSubmit}>
<PaymentElement />
<button type="submit" disabled={!stripe}>
Pay Entry Fee
</button>
</form>
);
}
2. Charter Booking Payment
Flow:
- User selects charter and dates
- Backend creates booking and payment intent
- Frontend confirms payment
- Webhook updates booking status
Similar implementation to tournament registration
3. Advertiser Payments (Top-up/Subscription)
Flow:
- Advertiser selects payment type (top-up or subscription)
- Backend creates payment intent or checkout session
- Frontend handles payment
- Webhook updates account balance or subscription status
Checkout Session Example:
// Create checkout session for subscription
const createCheckoutSession = async (priceId: string) => {
const response = await fetch('/api/advertiser/checkout', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ priceId })
});
const { sessionId } = await response.json();
// Redirect to Stripe Checkout
const stripe = await stripePromise;
const { error } = await stripe!.redirectToCheckout({ sessionId });
if (error) {
console.error(error);
}
};
4. User Subscriptions
Flow:
- User selects subscription tier
- Backend creates checkout session
- Frontend redirects to Stripe Checkout
- Webhook updates user subscription
React Native Implementation
import { initStripe, useStripe } from '@stripe/stripe-react-native';
// Initialize Stripe
initStripe({
publishableKey: process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY!,
merchantIdentifier: 'merchant.com.fishinglog', // iOS only
});
// Payment Sheet Example
function PaymentScreen({ clientSecret }: { clientSecret: string }) {
const { initPaymentSheet, presentPaymentSheet } = useStripe();
useEffect(() => {
initializePaymentSheet();
}, []);
const initializePaymentSheet = async () => {
const { error } = await initPaymentSheet({
paymentIntentClientSecret: clientSecret,
merchantDisplayName: 'FishingLog',
});
if (error) {
console.error(error);
}
};
const handlePay = async () => {
const { error } = await presentPaymentSheet();
if (error) {
console.error(error);
} else {
// Payment succeeded
}
};
return <Button onPress={handlePay}>Pay</Button>;
}
Error Handling
Common Errors
// Handle Stripe errors
const handleStripeError = (error: StripeError) => {
switch (error.type) {
case 'card_error':
// Card was declined
showError(error.message);
break;
case 'validation_error':
// Invalid input
showError('Please check your payment details');
break;
case 'api_error':
// Stripe API error
showError('Payment service temporarily unavailable');
break;
default:
showError('An unexpected error occurred');
}
};
Payment Status Polling
Since webhooks are async, you may want to poll for payment status:
const pollPaymentStatus = async (paymentIntentId: string) => {
const maxAttempts = 10;
let attempts = 0;
const poll = setInterval(async () => {
attempts++;
const response = await fetch(`/api/payment/status/${paymentIntentId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const { status } = await response.json();
if (status === 'succeeded' || attempts >= maxAttempts) {
clearInterval(poll);
if (status === 'succeeded') {
// Handle success
}
}
}, 2000); // Poll every 2 seconds
};
Testing
Test Cards
- Success:
4242 4242 4242 4242 - Decline:
4000 0000 0000 0002 - 3D Secure:
4000 0025 0000 3155
Test Mode
- Use test publishable key:
pk_test_... - All test payments are simulated
- Use Stripe Dashboard to view test payments
Security Best Practices
- Never expose secret key - Only use publishable key in frontend
- Verify webhook signatures - Backend handles this
- Use HTTPS - Always in production
- Validate on backend - Don't trust frontend validation alone
- Handle errors gracefully - Show user-friendly messages
API Endpoints Reference
Payment Intent Creation
- Tournament:
POST /api/tournament/{id}/register - Booking:
POST /api/charter/listings/{id}/book - Advertiser:
POST /api/advertiser/{id}/payments
Payment Status
GET /api/payment/intent/{id}- Get payment intent status
Refunds (Admin Only)
POST /api/admin/payments/tournament-registration/{id}/refund
Webhook Events
Frontend should handle these scenarios:
-
Payment Succeeded
- Show success message
- Update UI (e.g., mark registration as confirmed)
- Redirect to success page
-
Payment Failed
- Show error message
- Allow retry
- Keep registration in pending state
-
Refund Processed
- Show refund notification
- Update registration/booking status
- Handle partial refunds
UI Components Needed
Payment Forms
- Card input component
- Payment method selector
- Billing address form
- Payment summary
Status Indicators
- Payment pending indicator
- Payment success confirmation
- Payment failed error display
- Refund status badge
Admin Components
- Refund form (amount, reason)
- Payment history table
- Failed payments list
- Payment statistics dashboard
Environment Variables
# Web App (.env.local)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
# React Native (.env)
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_...
# Production
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...