Stripe Integration (Web)
Guida completa per integrare i pagamenti Stripe nella web app con i 30 piani Visla GPS.
π¦ Setup Inizialeβ
1. Creare i 30 Prodotti in Stripeβ
Vai su Stripe Dashboard β Products β Add product
Script per creare prodotti (opzionale)β
# scripts/create_stripe_products.py
import stripe
stripe.api_key = "sk_test_xxx"
# Pricing structure
PRICING = {
"monthly": {
1: 4.90, 2: 9.80, # Tier 1-2: β¬4.90/device
3: 11.70, 4: 15.60, 5: 19.50, 6: 23.40, 7: 27.30, 8: 31.20, 9: 35.10, 10: 39.00 # β¬3.90/device
},
"semiannual": {
1: 23.50, 2: 47.00, # Tier 1-2
3: 56.10, 4: 74.80, 5: 93.50, 6: 112.20, 7: 130.90, 8: 149.60, 9: 168.30, 10: 187.00
},
"annual": {
1: 35.00, 2: 70.00, # Tier 1-2
3: 84.00, 4: 112.00, 5: 140.00, 6: 168.00, 7: 196.00, 8: 224.00, 9: 252.00, 10: 280.00
}
}
INTERVALS = {
"monthly": {"interval": "month", "interval_count": 1},
"semiannual": {"interval": "month", "interval_count": 6},
"annual": {"interval": "year", "interval_count": 1}
}
for period, prices in PRICING.items():
for devices, amount in prices.items():
product = stripe.Product.create(
name=f"Visla GPS - {devices} Device{'s' if devices > 1 else ''} ({period.capitalize()})",
metadata={"devices": str(devices), "period": period}
)
price = stripe.Price.create(
product=product.id,
unit_amount=int(amount * 100), # In cents
currency="eur",
recurring=INTERVALS[period],
lookup_key=f"{period}_{devices}" # monthly_5, annual_10, etc.
)
print(f"Created: {period}_{devices} = β¬{amount} β {price.id}")
2. Tabella Price IDsβ
Dopo aver creato i prodotti, salva gli ID:
| Piano | NΒ° Devices | Price ID |
|---|---|---|
| monthly_1 | 1 | price_xxx1 |
| monthly_2 | 2 | price_xxx2 |
| ... | ... | ... |
| annual_10 | 10 | price_xxx30 |
3. API Keysβ
# secrets/stripe_api_key
sk_live_xxx
# secrets/stripe_webhook_secret
whsec_xxx
4. Webhookβ
Developers β Webhooks β Add endpoint:
- URL:
https://api.vislagps.com/webhooks/stripe - Eventi:
customer.subscription.*,invoice.*
π₯οΈ Frontend Implementationβ
Price Configurationβ
// lib/pricing.ts
interface PlanPricing {
monthly: number;
semiannual: number;
annual: number;
}
// Prezzo per-device
const PRICE_PER_DEVICE: Record<string, PlanPricing> = {
tier1: { monthly: 4.90, semiannual: 3.92, annual: 2.92 }, // 1-2 devices
tier2: { monthly: 3.90, semiannual: 3.12, annual: 2.33 }, // 3-10 devices
};
export function calculatePrice(devices: number, period: 'monthly' | 'semiannual' | 'annual'): number {
const tier = devices <= 2 ? 'tier1' : 'tier2';
const pricePerDevice = PRICE_PER_DEVICE[tier][period];
const months = period === 'monthly' ? 1 : period === 'semiannual' ? 6 : 12;
return pricePerDevice * devices * months;
}
export function getPriceId(devices: number, period: 'monthly' | 'semiannual' | 'annual'): string {
return `${period}_${devices}`;
}
Subscription Pageβ
// app/subscription/page.tsx
'use client';
import { useState } from 'react';
import { calculatePrice, getPriceId } from '@/lib/pricing';
import { createCheckoutSession } from '@/lib/stripe';
export default function SubscriptionPage() {
const [devices, setDevices] = useState(3);
const [period, setPeriod] = useState<'monthly' | 'semiannual' | 'annual'>('annual');
const price = calculatePrice(devices, period);
const priceId = getPriceId(devices, period);
const handleSubscribe = async () => {
await createCheckoutSession(priceId);
};
return (
<div className="max-w-2xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-8">Scegli il tuo Piano</h1>
{/* Device Selector */}
<div className="mb-8">
<h2 className="text-xl mb-4">Quanti dispositivi vuoi tracciare?</h2>
<div className="grid grid-cols-5 gap-2">
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(n => (
<button
key={n}
onClick={() => setDevices(n)}
className={`p-4 rounded-lg border-2 font-bold ${
devices === n
? 'border-blue-500 bg-blue-50 text-blue-600'
: 'border-gray-200 hover:border-gray-300'
}`}
>
{n}
</button>
))}
</div>
</div>
{/* Period Selector */}
<div className="space-y-3 mb-8">
<PeriodOption
period="monthly"
label="Mensile"
price={calculatePrice(devices, 'monthly')}
selected={period === 'monthly'}
onSelect={() => setPeriod('monthly')}
/>
<PeriodOption
period="semiannual"
label="Semestrale"
price={calculatePrice(devices, 'semiannual')}
discount="-20%"
selected={period === 'semiannual'}
onSelect={() => setPeriod('semiannual')}
/>
<PeriodOption
period="annual"
label="Annuale"
price={calculatePrice(devices, 'annual')}
discount="-40%"
selected={period === 'annual'}
onSelect={() => setPeriod('annual')}
/>
</div>
{/* Summary & CTA */}
<div className="bg-gray-50 rounded-xl p-6">
<div className="flex justify-between items-center mb-4">
<span className="text-gray-600">
{devices} dispositivo{devices > 1 ? 'i' : ''} β’ {period === 'monthly' ? 'Mensile' : period === 'semiannual' ? 'Semestrale' : 'Annuale'}
</span>
<span className="text-3xl font-bold">β¬{price.toFixed(2)}</span>
</div>
<button
onClick={handleSubscribe}
className="w-full bg-blue-600 text-white py-4 rounded-lg font-semibold hover:bg-blue-700"
>
Abbonati Ora
</button>
</div>
</div>
);
}
function PeriodOption({ period, label, price, discount, selected, onSelect }) {
return (
<button
onClick={onSelect}
className={`w-full p-4 rounded-lg border-2 flex justify-between items-center ${
selected ? 'border-blue-500 bg-blue-50' : 'border-gray-200'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-5 h-5 rounded-full border-2 ${
selected ? 'border-blue-500 bg-blue-500' : 'border-gray-300'
}`} />
<span className="font-medium">{label}</span>
{discount && (
<span className="bg-green-100 text-green-700 px-2 py-1 rounded text-sm">
{discount}
</span>
)}
</div>
<span className="font-bold">β¬{price.toFixed(2)}</span>
</button>
);
}
Checkout APIβ
// lib/stripe.ts
import { loadStripe } from '@stripe/stripe-js';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_KEY!);
export async function createCheckoutSession(priceId: string) {
const response = await fetch('/api/checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getAccessToken()}`
},
body: JSON.stringify({ priceId })
});
const { sessionId } = await response.json();
const stripe = await stripePromise;
await stripe!.redirectToCheckout({ sessionId });
}
π§ Backend APIβ
Checkout Endpointβ
# routes/checkout.py
import stripe
from flask import Blueprint, request, jsonify, g
bp = Blueprint('checkout', __name__, url_prefix='/api')
# Price IDs mapping (from Stripe Dashboard)
PRICE_IDS = {
"monthly_1": "price_xxx", "monthly_2": "price_xxx", # ... etc
"semiannual_1": "price_xxx", # ...
"annual_1": "price_xxx", # ...
}
@bp.route('/checkout', methods=['POST'])
def create_checkout_session():
"""Create Stripe Checkout for subscription."""
user_id = g.user_id
data = request.get_json()
plan_key = data.get('priceId') # e.g., "monthly_5"
if plan_key not in PRICE_IDS:
return jsonify({'error': 'Invalid plan'}), 400
price_id = PRICE_IDS[plan_key]
# Extract device count from plan key
devices = int(plan_key.split('_')[1])
customer = get_or_create_customer(user_id)
session = stripe.checkout.Session.create(
mode='subscription',
customer=customer.id,
line_items=[{'price': price_id, 'quantity': 1}],
success_url='https://app.vislagps.com/subscription/success',
cancel_url='https://app.vislagps.com/subscription',
subscription_data={
'metadata': {
'user_id': str(user_id),
'devices': str(devices),
'plan_key': plan_key
}
}
)
return jsonify({'sessionId': session.id})
Upgrade Subscriptionβ
@bp.route('/subscription/upgrade', methods=['POST'])
def upgrade_subscription():
"""Upgrade to a higher device plan."""
user_id = g.user_id
data = request.get_json()
new_plan_key = data.get('newPlan') # e.g., "monthly_10"
# Get current subscription
subscription = get_user_subscription(user_id)
if not subscription:
return jsonify({'error': 'No active subscription'}), 404
new_price_id = PRICE_IDS[new_plan_key]
# Modify subscription with prorations
stripe.Subscription.modify(
subscription.provider_subscription_id,
items=[{
'id': get_subscription_item_id(subscription),
'price': new_price_id,
}],
proration_behavior='create_prorations'
)
return jsonify({'success': True, 'message': 'Plan upgraded'})
β Checklistβ
- Esegui script per creare 30 prodotti in Stripe
- Salva tutti i Price IDs
- Configura webhook
- Implementa UI selezione dispositivi
- Implementa API checkout
- Implementa upgrade flow
- Testa con Stripe CLI
π§ͺ Testingβ
# Forward webhooks
stripe listen --forward-to localhost:8088/webhooks/stripe
# Test specific plans
stripe trigger customer.subscription.created --override subscription:items[0][price]=price_xxx
| Scenario | Carta |
|---|---|
| Successo | 4242 4242 4242 4242 |
| 3D Secure | 4000 0025 0000 3155 |
| Rifiutata | 4000 0000 0000 0002 |