Skip to main content

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_KEY or REACT_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:

  1. User selects tournament and registers
  2. Backend creates payment intent: POST /api/tournament/{id}/register
  3. Backend returns stripePaymentIntentId and clientSecret
  4. Frontend confirms payment with Stripe
  5. 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:

  1. User selects charter and dates
  2. Backend creates booking and payment intent
  3. Frontend confirms payment
  4. Webhook updates booking status

Similar implementation to tournament registration

3. Advertiser Payments (Top-up/Subscription)

Flow:

  1. Advertiser selects payment type (top-up or subscription)
  2. Backend creates payment intent or checkout session
  3. Frontend handles payment
  4. 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:

  1. User selects subscription tier
  2. Backend creates checkout session
  3. Frontend redirects to Stripe Checkout
  4. 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

  1. Never expose secret key - Only use publishable key in frontend
  2. Verify webhook signatures - Backend handles this
  3. Use HTTPS - Always in production
  4. Validate on backend - Don't trust frontend validation alone
  5. 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:

  1. Payment Succeeded

    • Show success message
    • Update UI (e.g., mark registration as confirmed)
    • Redirect to success page
  2. Payment Failed

    • Show error message
    • Allow retry
    • Keep registration in pending state
  3. 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_...

Resources