mirror of
https://github.com/hanzoai/agency.git
synced 2026-09-30 20:39:59 +00:00
refactor: remove all Stripe references — Commerce is payment provider
Stripe banned Hanzo. Removed Stripe SDK, webhook handlers, hardcoded keys, and StripePricingTable. Billing redirects to billing.hanzo.ai.
This commit is contained in:
+2
-15
@@ -1,18 +1,5 @@
|
||||
# Stripe Configuration
|
||||
# Get these from your Stripe dashboard: https://dashboard.stripe.com/apikeys
|
||||
|
||||
# Publishable key (safe for client-side)
|
||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
|
||||
|
||||
# Price IDs for credit packs
|
||||
# Create these in Stripe Dashboard > Products
|
||||
VITE_STRIPE_STARTER_PRICE_ID=price_1234567890starter
|
||||
VITE_STRIPE_PROFESSIONAL_PRICE_ID=price_1234567890professional
|
||||
VITE_STRIPE_ENTERPRISE_PRICE_ID=price_1234567890enterprise
|
||||
|
||||
# Price IDs for services
|
||||
VITE_STRIPE_AGENCY_PRICE_ID=price_1234567890agency
|
||||
VITE_STRIPE_INSTANT_SITE_PRICE_ID=price_1234567890instant
|
||||
# Commerce API
|
||||
VITE_COMMERCE_API_URL=https://commerce.hanzo.ai/api/v1
|
||||
|
||||
# Other environment variables
|
||||
VITE_API_URL=http://localhost:3000
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
# Stripe Payment Integration
|
||||
|
||||
This guide explains how to set up and use the Stripe payment integration for the Hanzo Agency website.
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Create a Stripe Account
|
||||
|
||||
1. Go to [https://stripe.com](https://stripe.com) and create an account
|
||||
2. Once logged in, navigate to the [Stripe Dashboard](https://dashboard.stripe.com)
|
||||
|
||||
### 2. Get Your API Keys
|
||||
|
||||
1. In the Stripe Dashboard, go to **Developers** > **API keys**
|
||||
2. Copy your **Publishable key** (starts with `pk_test_` or `pk_live_`)
|
||||
3. Keep your **Secret key** safe (you'll need it for backend integration)
|
||||
|
||||
### 3. Create Products and Prices
|
||||
|
||||
In the Stripe Dashboard, go to **Products** and create the following:
|
||||
|
||||
#### Credit Packs:
|
||||
|
||||
1. **Starter Pack**
|
||||
- Name: "100 Credits - Starter Pack"
|
||||
- Price: $100.00 (one-time)
|
||||
- Copy the price ID (starts with `price_`)
|
||||
|
||||
2. **Professional Pack**
|
||||
- Name: "500 Credits - Professional Pack"
|
||||
- Price: $450.00 (one-time)
|
||||
- Copy the price ID
|
||||
|
||||
3. **Enterprise Pack**
|
||||
- Name: "2000 Credits - Enterprise Pack"
|
||||
- Price: $1,500.00 (one-time)
|
||||
- Copy the price ID
|
||||
|
||||
#### Services:
|
||||
|
||||
1. **Agency Service**
|
||||
- Name: "Full-Service Creative Agency"
|
||||
- Price: $5,000.00/month (recurring)
|
||||
- Copy the price ID
|
||||
|
||||
2. **Instant Site**
|
||||
- Name: "Instant Site - 24hr Delivery"
|
||||
- Price: $500.00 (one-time)
|
||||
- Copy the price ID
|
||||
|
||||
### 4. Configure Environment Variables
|
||||
|
||||
1. Create a `.env` file in the project root (copy from `.env.example`)
|
||||
2. Add your Stripe configuration:
|
||||
|
||||
```env
|
||||
# Stripe Configuration
|
||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_your_key_here
|
||||
|
||||
# Credit Pack Price IDs
|
||||
VITE_STRIPE_STARTER_PRICE_ID=price_xxxxx
|
||||
VITE_STRIPE_PROFESSIONAL_PRICE_ID=price_xxxxx
|
||||
VITE_STRIPE_ENTERPRISE_PRICE_ID=price_xxxxx
|
||||
|
||||
# Service Price IDs
|
||||
VITE_STRIPE_AGENCY_PRICE_ID=price_xxxxx
|
||||
VITE_STRIPE_INSTANT_SITE_PRICE_ID=price_xxxxx
|
||||
```
|
||||
|
||||
### 5. Install Dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
# or
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Testing the Integration
|
||||
|
||||
### Test Credit Cards
|
||||
|
||||
Use these test card numbers in test mode:
|
||||
|
||||
- **Success**: 4242 4242 4242 4242
|
||||
- **Decline**: 4000 0000 0000 0002
|
||||
- **Requires Authentication**: 4000 0025 0000 3155
|
||||
|
||||
Use any future date for expiry and any 3 digits for CVC.
|
||||
|
||||
### Testing Flow
|
||||
|
||||
1. Go to `/login` and create a test account
|
||||
2. Navigate to `/dashboard`
|
||||
3. Click "Purchase Credits"
|
||||
4. Select a credit pack and quantity
|
||||
5. Click "Continue to Payment"
|
||||
6. You'll be redirected to Stripe Checkout
|
||||
7. Use a test card to complete payment
|
||||
8. You'll be redirected back to `/success` or `/cancel`
|
||||
|
||||
## Current Implementation
|
||||
|
||||
### Client-Side Only
|
||||
The current implementation uses Stripe's client-side checkout, which is suitable for:
|
||||
- Simple payment flows
|
||||
- Fixed-price products
|
||||
- Public pricing
|
||||
|
||||
### Limitations
|
||||
- No server-side validation
|
||||
- Credits are managed in localStorage (not secure for production)
|
||||
- No webhook handling for payment confirmation
|
||||
- No customer management
|
||||
|
||||
## Production Recommendations
|
||||
|
||||
For production use, you should:
|
||||
|
||||
1. **Add Backend API**
|
||||
- Create checkout sessions server-side
|
||||
- Validate payments with webhooks
|
||||
- Store credits in a database
|
||||
- Manage customer records
|
||||
|
||||
2. **Security Improvements**
|
||||
- Never expose secret keys
|
||||
- Validate all payments server-side
|
||||
- Use webhooks to confirm payments
|
||||
- Implement proper authentication
|
||||
|
||||
3. **Enhanced Features**
|
||||
- Customer portal for managing subscriptions
|
||||
- Invoice generation
|
||||
- Email receipts
|
||||
- Refund handling
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib/
|
||||
│ └── stripe.ts # Stripe configuration and helpers
|
||||
├── pages/
|
||||
│ ├── PurchaseCredits.tsx # Credit purchase page
|
||||
│ ├── PaymentSuccess.tsx # Success redirect page
|
||||
│ ├── PaymentCancel.tsx # Cancel redirect page
|
||||
│ └── Payment.tsx # General payment page
|
||||
└── App.tsx # Routes configuration
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Stripe not loaded" error
|
||||
- Check that your publishable key is correct
|
||||
- Ensure you're using the correct environment (test/live)
|
||||
|
||||
### Payment fails
|
||||
- Check the browser console for errors
|
||||
- Verify price IDs are correct
|
||||
- Ensure products are active in Stripe Dashboard
|
||||
|
||||
### Success page shows error
|
||||
- This demo uses localStorage, so make sure it's not blocked
|
||||
- In production, implement proper backend validation
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Set up a backend server (Node.js, Python, etc.)
|
||||
2. Implement Stripe webhooks
|
||||
3. Create a database for user and credit management
|
||||
4. Add customer portal integration
|
||||
5. Implement subscription management for agency services
|
||||
|
||||
## Support
|
||||
|
||||
- [Stripe Documentation](https://stripe.com/docs)
|
||||
- [Stripe Support](https://support.stripe.com)
|
||||
- [API Reference](https://stripe.com/docs/api)
|
||||
@@ -1,78 +0,0 @@
|
||||
# Stripe Product Configuration
|
||||
|
||||
## Products Created in Stripe Dashboard
|
||||
|
||||
The following products have been created in your live Stripe account:
|
||||
|
||||
### Credit Packs (One-time payments)
|
||||
|
||||
1. **100 Credits - Starter Pack**
|
||||
- Price ID: `price_1QvKqDJ03IK6WYmUxGzK5DXg`
|
||||
- Amount: $100.00 USD
|
||||
- Type: One-time payment
|
||||
|
||||
2. **500 Credits - Professional Pack**
|
||||
- Price ID: `price_1QvKqWJ03IK6WYmUJX9PqvHM`
|
||||
- Amount: $450.00 USD
|
||||
- Type: One-time payment
|
||||
- Savings: $50 (10% off)
|
||||
|
||||
3. **2000 Credits - Enterprise Pack**
|
||||
- Price ID: `price_1QvKqiJ03IK6WYmU7vwMNGLC`
|
||||
- Amount: $1,500.00 USD
|
||||
- Type: One-time payment
|
||||
- Savings: $500 (25% off)
|
||||
|
||||
### Services
|
||||
|
||||
4. **Agency Service**
|
||||
- Price ID: `price_1QvKr3J03IK6WYmUbCwK4LFp`
|
||||
- Amount: $5,000.00 USD
|
||||
- Type: Monthly subscription
|
||||
|
||||
5. **Instant Site**
|
||||
- Price ID: `price_1QvKrHJ03IK6WYmUYKq0Zx8n`
|
||||
- Amount: $500.00 USD
|
||||
- Type: One-time payment
|
||||
|
||||
## Webhook Configuration
|
||||
|
||||
Your webhook endpoint has been configured in Stripe:
|
||||
- Endpoint URL: `https://your-domain.vercel.app/api/stripe-webhook`
|
||||
- Webhook Secret: `PURGED`
|
||||
|
||||
## Environment Variables for Vercel
|
||||
|
||||
Add these to your Vercel project settings:
|
||||
|
||||
```
|
||||
STRIPE_SECRET_KEY=PURGED
|
||||
STRIPE_WEBHOOK_SECRET=PURGED
|
||||
```
|
||||
|
||||
## Analytics Events Tracked
|
||||
|
||||
- Page views
|
||||
- Product selection
|
||||
- Add to cart
|
||||
- Checkout initiation
|
||||
- Purchase completion
|
||||
- User registration
|
||||
- User login
|
||||
- Form submissions
|
||||
|
||||
## Testing
|
||||
|
||||
To test the integration:
|
||||
1. Go to `/purchase-credits`
|
||||
2. Select a credit pack
|
||||
3. Click "Continue to Payment"
|
||||
4. Complete the Stripe checkout
|
||||
5. You'll be redirected back to `/success`
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Secret keys are only used in serverless functions
|
||||
- All payment processing happens server-side
|
||||
- Webhook signatures are verified
|
||||
- Analytics track all key conversion events
|
||||
@@ -1,82 +0,0 @@
|
||||
import { VercelRequest, VercelResponse } from '@vercel/node';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
// Initialize Stripe with your secret key
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || 'PURGED', {
|
||||
apiVersion: '2023-10-16',
|
||||
});
|
||||
|
||||
// Plan-to-price mapping (server-side source of truth)
|
||||
const PLAN_PRICES: Record<string, { priceId: string; mode: 'payment' | 'subscription' }> = {
|
||||
'agency': {
|
||||
priceId: 'price_1QvKr3J03IK6WYmUbCwK4LFp',
|
||||
mode: 'subscription',
|
||||
},
|
||||
'instant-site': {
|
||||
priceId: 'price_1QvKrHJ03IK6WYmUYKq0Zx8n',
|
||||
mode: 'payment',
|
||||
},
|
||||
'enterprise': {
|
||||
priceId: 'price_1QvKrZJ03IK6WYmUeNtP9Kx2',
|
||||
mode: 'subscription',
|
||||
},
|
||||
};
|
||||
|
||||
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
// Handle CORS preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
return res.status(200).json({});
|
||||
}
|
||||
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { plan, priceId, mode, quantity, successUrl, cancelUrl, metadata, customerEmail } = req.body;
|
||||
|
||||
// Determine price ID and mode from plan name or explicit params
|
||||
let resolvedPriceId = priceId;
|
||||
let resolvedMode: 'payment' | 'subscription' = mode || 'payment';
|
||||
|
||||
if (plan && PLAN_PRICES[plan]) {
|
||||
resolvedPriceId = PLAN_PRICES[plan].priceId;
|
||||
resolvedMode = PLAN_PRICES[plan].mode;
|
||||
}
|
||||
|
||||
if (!resolvedPriceId) {
|
||||
return res.status(400).json({ error: 'Missing priceId or valid plan name' });
|
||||
}
|
||||
|
||||
// Build session params
|
||||
const sessionParams: Stripe.Checkout.SessionCreateParams = {
|
||||
payment_method_types: ['card'],
|
||||
line_items: [{
|
||||
price: resolvedPriceId,
|
||||
quantity: quantity || 1,
|
||||
}],
|
||||
mode: resolvedMode,
|
||||
success_url: successUrl || `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: cancelUrl || `${req.headers.origin}/cancel`,
|
||||
metadata: metadata || {},
|
||||
billing_address_collection: 'required',
|
||||
};
|
||||
|
||||
// Attach customer email if provided
|
||||
if (customerEmail || metadata?.email) {
|
||||
sessionParams.customer_email = customerEmail || metadata.email;
|
||||
}
|
||||
|
||||
const session = await stripe.checkout.sessions.create(sessionParams);
|
||||
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.status(200).json({ sessionId: session.id, url: session.url });
|
||||
} catch (error: any) {
|
||||
console.error('Stripe error:', error);
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { VercelRequest, VercelResponse } from '@vercel/node';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || 'PURGED', {
|
||||
apiVersion: '2023-10-16',
|
||||
});
|
||||
|
||||
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET || 'PURGED';
|
||||
|
||||
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
const sig = req.headers['stripe-signature'] as string;
|
||||
let event: Stripe.Event;
|
||||
|
||||
try {
|
||||
// Verify webhook signature
|
||||
const rawBody = JSON.stringify(req.body);
|
||||
event = stripe.webhooks.constructEvent(rawBody, sig, endpointSecret);
|
||||
} catch (err: any) {
|
||||
console.error('Webhook signature verification failed:', err.message);
|
||||
return res.status(400).send(`Webhook Error: ${err.message}`);
|
||||
}
|
||||
|
||||
// Handle the event
|
||||
switch (event.type) {
|
||||
case 'checkout.session.completed':
|
||||
const session = event.data.object as Stripe.Checkout.Session;
|
||||
|
||||
// Process the successful payment
|
||||
console.log('Payment successful:', {
|
||||
sessionId: session.id,
|
||||
customerId: session.customer,
|
||||
amount: session.amount_total,
|
||||
metadata: session.metadata
|
||||
});
|
||||
|
||||
// Here you would typically:
|
||||
// 1. Update user credits in your database
|
||||
// 2. Send confirmation email
|
||||
// 3. Log the transaction
|
||||
|
||||
// For now, we'll just log it
|
||||
break;
|
||||
|
||||
case 'payment_intent.succeeded':
|
||||
const paymentIntent = event.data.object as Stripe.PaymentIntent;
|
||||
console.log('Payment intent succeeded:', paymentIntent.id);
|
||||
break;
|
||||
|
||||
case 'payment_intent.payment_failed':
|
||||
const failedPayment = event.data.object as Stripe.PaymentIntent;
|
||||
console.log('Payment failed:', failedPayment.id);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`Unhandled event type ${event.type}`);
|
||||
}
|
||||
|
||||
// Return a 200 response to acknowledge receipt of the event
|
||||
res.status(200).json({ received: true });
|
||||
}
|
||||
@@ -41,8 +41,6 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@rollup/rollup-darwin-x64": "^4.37.0",
|
||||
"@stripe/react-stripe-js": "^2.4.0",
|
||||
"@stripe/stripe-js": "^2.4.0",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { analytics } from '@/utils/analytics';
|
||||
|
||||
// --- Commerce API (Square, crypto, wire) ---
|
||||
|
||||
const COMMERCE_API = 'https://commerce.hanzo.ai/api/v1';
|
||||
|
||||
export type PaymentMethod = 'card' | 'crypto' | 'wire';
|
||||
|
||||
export type CommerceCheckoutResult =
|
||||
| { type: 'redirect'; url: string; sessionId: string }
|
||||
| { type: 'wire'; instructions: Record<string, unknown> };
|
||||
|
||||
// Plan configuration for agency checkout
|
||||
const PLAN_CONFIG: Record<string, {
|
||||
name: string;
|
||||
price: number;
|
||||
amount: number;
|
||||
}> = {
|
||||
'agency': {
|
||||
name: 'Agency Service',
|
||||
price: 9999,
|
||||
amount: 999900,
|
||||
},
|
||||
'instant-site': {
|
||||
name: 'Instant Site',
|
||||
price: 500,
|
||||
amount: 50000,
|
||||
},
|
||||
'enterprise': {
|
||||
name: 'Enterprise',
|
||||
price: 9999,
|
||||
amount: 999900,
|
||||
},
|
||||
};
|
||||
|
||||
export async function createCommerceCheckout(
|
||||
plan: string,
|
||||
options: { email: string; name: string; paymentMethod?: PaymentMethod }
|
||||
): Promise<CommerceCheckoutResult> {
|
||||
const { email, name, paymentMethod = 'card' } = options;
|
||||
|
||||
if (paymentMethod === 'wire') {
|
||||
const res = await fetch(`${COMMERCE_API}/checkout/wire/instructions?org=hanzo`);
|
||||
if (!res.ok) throw new Error(`Wire instructions request failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return { type: 'wire' as const, instructions: data };
|
||||
}
|
||||
|
||||
const config = PLAN_CONFIG[plan];
|
||||
const res = await fetch(`${COMMERCE_API}/checkout/sessions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
org: 'hanzo',
|
||||
providerHint: paymentMethod === 'crypto' ? 'ethereum' : 'square',
|
||||
currency: 'USD',
|
||||
customer: { email, name },
|
||||
items: [{
|
||||
name: config?.name || plan,
|
||||
amount: config?.amount || 999900,
|
||||
quantity: 1,
|
||||
}],
|
||||
successUrl: `${window.location.origin}/onboarding-success`,
|
||||
cancelUrl: `${window.location.origin}/pricing`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
throw new Error(`Commerce checkout failed: ${body}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return { type: 'redirect' as const, url: data.checkoutUrl, sessionId: data.sessionId };
|
||||
}
|
||||
|
||||
// Helper to format currency
|
||||
export const formatCurrency = (amount: number): string => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
}).format(amount / 100);
|
||||
};
|
||||
|
||||
// Track successful purchase (to be called on success page)
|
||||
export const trackPurchaseSuccess = (sessionId: string, products: any[], totalAmount: number) => {
|
||||
analytics.trackPurchase(sessionId, products, totalAmount);
|
||||
};
|
||||
@@ -1,236 +0,0 @@
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { analytics } from '@/utils/analytics';
|
||||
|
||||
// Initialize Stripe with your publishable key
|
||||
const stripePromise = loadStripe(
|
||||
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY ||
|
||||
'PURGED'
|
||||
);
|
||||
|
||||
// API endpoint (will use relative URL in production)
|
||||
const API_URL = import.meta.env.VITE_API_URL || '';
|
||||
|
||||
// Helper function to create a Stripe Checkout session via API
|
||||
export const createCheckoutSession = async (
|
||||
priceId: string,
|
||||
quantity: number = 1,
|
||||
metadata?: any
|
||||
) => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/create-checkout-session`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
priceId,
|
||||
quantity,
|
||||
metadata,
|
||||
successUrl: `${window.location.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancelUrl: `${window.location.origin}/cancel`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create checkout session');
|
||||
}
|
||||
|
||||
const { sessionId, url } = await response.json();
|
||||
|
||||
// Track checkout initiation
|
||||
analytics.trackBeginCheckout([{
|
||||
id: priceId,
|
||||
name: metadata?.productName || 'Credit Pack',
|
||||
category: 'credits',
|
||||
price: metadata?.price || 0,
|
||||
quantity: quantity
|
||||
}], metadata?.totalPrice || 0);
|
||||
|
||||
// Redirect to Stripe Checkout
|
||||
if (url) {
|
||||
window.location.href = url;
|
||||
} else {
|
||||
// Fallback to client-side redirect
|
||||
const stripe = await stripePromise;
|
||||
if (!stripe) throw new Error('Stripe failed to load');
|
||||
|
||||
const { error } = await stripe.redirectToCheckout({ sessionId });
|
||||
if (error) throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Checkout error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Price IDs - Create these in your Stripe Dashboard
|
||||
export const STRIPE_PRICE_IDS = {
|
||||
// Credit Packs (create these as one-time products in Stripe)
|
||||
STARTER_100_CREDITS: 'price_1QvKqDJ03IK6WYmUxGzK5DXg', // $100
|
||||
PROFESSIONAL_500_CREDITS: 'price_1QvKqWJ03IK6WYmUJX9PqvHM', // $450
|
||||
ENTERPRISE_2000_CREDITS: 'price_1QvKqiJ03IK6WYmU7vwMNGLC', // $1,500
|
||||
|
||||
// Services (create these in Stripe)
|
||||
AGENCY_MONTHLY: 'price_1QvKr3J03IK6WYmUbCwK4LFp', // $9,999/month recurring
|
||||
INSTANT_SITE: 'price_1QvKrHJ03IK6WYmUYKq0Zx8n', // $500 one-time
|
||||
ENTERPRISE_MONTHLY: 'price_1QvKrZJ03IK6WYmUeNtP9Kx2', // $9,999/month recurring
|
||||
};
|
||||
|
||||
// Plan configuration for agency checkout
|
||||
const PLAN_CONFIG: Record<string, {
|
||||
priceId: string;
|
||||
mode: 'subscription' | 'payment';
|
||||
name: string;
|
||||
price: number;
|
||||
amount: number;
|
||||
}> = {
|
||||
'agency': {
|
||||
priceId: STRIPE_PRICE_IDS.AGENCY_MONTHLY,
|
||||
mode: 'subscription' as const,
|
||||
name: 'Agency Service',
|
||||
price: 9999,
|
||||
amount: 999900,
|
||||
},
|
||||
'instant-site': {
|
||||
priceId: STRIPE_PRICE_IDS.INSTANT_SITE,
|
||||
mode: 'payment' as const,
|
||||
name: 'Instant Site',
|
||||
price: 500,
|
||||
amount: 50000,
|
||||
},
|
||||
'enterprise': {
|
||||
priceId: STRIPE_PRICE_IDS.ENTERPRISE_MONTHLY,
|
||||
mode: 'subscription' as const,
|
||||
name: 'Enterprise',
|
||||
price: 9999,
|
||||
amount: 999900,
|
||||
},
|
||||
};
|
||||
|
||||
// --- Commerce API (Square, crypto, wire) ---
|
||||
|
||||
const COMMERCE_API = 'https://commerce.hanzo.ai/api/v1';
|
||||
|
||||
export type PaymentMethod = 'card' | 'crypto' | 'wire';
|
||||
|
||||
export type CommerceCheckoutResult =
|
||||
| { type: 'redirect'; url: string; sessionId: string }
|
||||
| { type: 'wire'; instructions: Record<string, unknown> };
|
||||
|
||||
export async function createCommerceCheckout(
|
||||
plan: string,
|
||||
options: { email: string; name: string; paymentMethod?: PaymentMethod }
|
||||
): Promise<CommerceCheckoutResult> {
|
||||
const { email, name, paymentMethod = 'card' } = options;
|
||||
|
||||
if (paymentMethod === 'wire') {
|
||||
const res = await fetch(`${COMMERCE_API}/checkout/wire/instructions?org=hanzo`);
|
||||
if (!res.ok) throw new Error(`Wire instructions request failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return { type: 'wire' as const, instructions: data };
|
||||
}
|
||||
|
||||
const config = PLAN_CONFIG[plan];
|
||||
const res = await fetch(`${COMMERCE_API}/checkout/sessions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
org: 'hanzo',
|
||||
providerHint: paymentMethod === 'crypto' ? 'ethereum' : 'square',
|
||||
currency: 'USD',
|
||||
customer: { email, name },
|
||||
items: [{
|
||||
name: config?.name || plan,
|
||||
amount: config?.amount || 999900,
|
||||
quantity: 1,
|
||||
}],
|
||||
successUrl: `${window.location.origin}/onboarding-success`,
|
||||
cancelUrl: `${window.location.origin}/pricing`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
throw new Error(`Commerce checkout failed: ${body}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return { type: 'redirect' as const, url: data.checkoutUrl, sessionId: data.sessionId };
|
||||
}
|
||||
|
||||
// Create a Stripe Checkout session for agency plans
|
||||
export const createAgencyCheckout = async (
|
||||
plan: keyof typeof PLAN_CONFIG,
|
||||
customer: { email: string; name: string }
|
||||
) => {
|
||||
const config = PLAN_CONFIG[plan];
|
||||
if (!config) throw new Error(`Unknown plan: ${plan}`);
|
||||
|
||||
try {
|
||||
const successPath = plan === 'instant-site' ? '/instant-site-form' : '/onboarding-success';
|
||||
|
||||
const response = await fetch(`${API_URL}/api/create-checkout-session`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
plan,
|
||||
priceId: config.priceId,
|
||||
mode: config.mode,
|
||||
quantity: 1,
|
||||
metadata: {
|
||||
plan,
|
||||
productName: config.name,
|
||||
email: customer.email,
|
||||
customerName: customer.name,
|
||||
},
|
||||
customerEmail: customer.email,
|
||||
successUrl: `${window.location.origin}${successPath}?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancelUrl: `${window.location.origin}/payment?plan=${plan}`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errBody = await response.text();
|
||||
throw new Error(`Checkout session failed: ${errBody}`);
|
||||
}
|
||||
|
||||
const { sessionId, url } = await response.json();
|
||||
|
||||
// Track checkout initiation
|
||||
analytics.trackBeginCheckout([{
|
||||
id: config.priceId,
|
||||
name: config.name,
|
||||
category: 'agency_services',
|
||||
price: config.price,
|
||||
quantity: 1,
|
||||
}], config.price);
|
||||
|
||||
// Redirect to Stripe Checkout
|
||||
if (url) {
|
||||
window.location.href = url;
|
||||
} else {
|
||||
const stripe = await stripePromise;
|
||||
if (!stripe) throw new Error('Stripe failed to load');
|
||||
const { error } = await stripe.redirectToCheckout({ sessionId });
|
||||
if (error) throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Agency checkout error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to format currency
|
||||
export const formatCurrency = (amount: number): string => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
}).format(amount / 100);
|
||||
};
|
||||
|
||||
// Track successful purchase (to be called on success page)
|
||||
export const trackPurchaseSuccess = (sessionId: string, products: any[], totalAmount: number) => {
|
||||
analytics.trackPurchase(sessionId, products, totalAmount);
|
||||
};
|
||||
+1
-14
@@ -5,8 +5,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Check, Lock, Mail, Loader2, CreditCard, Coins, Landmark } from 'lucide-react';
|
||||
import { useLocation, Link } from 'react-router-dom';
|
||||
import { createAgencyCheckout } from '@/lib/stripe';
|
||||
import { createCommerceCheckout, type PaymentMethod, type CommerceCheckoutResult } from '@/lib/stripe';
|
||||
import { createCommerceCheckout, type PaymentMethod, type CommerceCheckoutResult } from '@/lib/commerce';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
const PAYMENT_METHODS: { value: PaymentMethod; label: string; icon: React.ElementType; desc: string }[] = [
|
||||
@@ -58,18 +57,6 @@ const Payment = () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Checkout error:', error);
|
||||
// Fall back to legacy Stripe checkout for card payments
|
||||
if (paymentMethod === 'card') {
|
||||
try {
|
||||
await createAgencyCheckout(plan, {
|
||||
email: formData.email,
|
||||
name: formData.name,
|
||||
});
|
||||
return; // Stripe will redirect
|
||||
} catch (fallbackError) {
|
||||
console.error('Stripe fallback error:', fallbackError);
|
||||
}
|
||||
}
|
||||
toast({
|
||||
title: 'Checkout Failed',
|
||||
description: 'Unable to start checkout. Please try again.',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNavigate, useSearchParams, Link } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Footer from '@/components/Footer';
|
||||
import { CheckCircle, ArrowRight, Loader2, AlertCircle } from 'lucide-react';
|
||||
import { trackPurchaseSuccess } from '@/lib/stripe';
|
||||
import { trackPurchaseSuccess } from '@/lib/commerce';
|
||||
import { analytics } from '@/utils/analytics';
|
||||
|
||||
const PaymentSuccess = () => {
|
||||
@@ -47,7 +47,7 @@ const PaymentSuccess = () => {
|
||||
amount: details.amount,
|
||||
description: `Purchased ${details.credits} credits`,
|
||||
date: new Date().toISOString(),
|
||||
stripeSessionId: sessionId
|
||||
checkoutSessionId: sessionId
|
||||
});
|
||||
localStorage.setItem('creditHistory', JSON.stringify(history));
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
TrendingUp,
|
||||
Shield
|
||||
} from 'lucide-react';
|
||||
import { createCheckoutSession, STRIPE_PRICE_IDS } from '@/lib/stripe';
|
||||
import { formatCurrency } from '@/lib/commerce';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { analytics } from '@/utils/analytics';
|
||||
|
||||
@@ -40,7 +40,6 @@ const PurchaseCredits = () => {
|
||||
price: 100,
|
||||
perCredit: 1.00,
|
||||
icon: Zap,
|
||||
priceId: STRIPE_PRICE_IDS.STARTER_100_CREDITS,
|
||||
features: [
|
||||
'100 credits',
|
||||
'$1 per credit',
|
||||
@@ -55,7 +54,6 @@ const PurchaseCredits = () => {
|
||||
perCredit: 0.90,
|
||||
savings: 50,
|
||||
icon: TrendingUp,
|
||||
priceId: STRIPE_PRICE_IDS.PROFESSIONAL_500_CREDITS,
|
||||
features: [
|
||||
'500 credits',
|
||||
'$0.90 per credit',
|
||||
@@ -70,7 +68,6 @@ const PurchaseCredits = () => {
|
||||
perCredit: 0.75,
|
||||
savings: 500,
|
||||
icon: Star,
|
||||
priceId: STRIPE_PRICE_IDS.ENTERPRISE_2000_CREDITS,
|
||||
features: [
|
||||
'2,000 credits',
|
||||
'$0.75 per credit',
|
||||
@@ -111,38 +108,9 @@ const PurchaseCredits = () => {
|
||||
const pack = creditPacks.find(p => p.id === selectedPack);
|
||||
if (!pack) return;
|
||||
|
||||
try {
|
||||
// Save purchase info for the success page
|
||||
const purchaseData = {
|
||||
packId: selectedPack,
|
||||
credits: pack.credits * quantity,
|
||||
amount: pack.price * quantity,
|
||||
quantity: quantity,
|
||||
productName: `${pack.credits} Credits Pack`,
|
||||
unitPrice: pack.price
|
||||
};
|
||||
|
||||
localStorage.setItem('pendingPurchase', JSON.stringify(purchaseData));
|
||||
|
||||
// Create checkout session via API
|
||||
await createCheckoutSession(pack.priceId, quantity, {
|
||||
userId: localStorage.getItem('userId'),
|
||||
email: localStorage.getItem('userEmail'),
|
||||
packId: selectedPack,
|
||||
credits: pack.credits * quantity,
|
||||
productName: `${pack.credits} Credits Pack`,
|
||||
price: pack.price,
|
||||
totalPrice: pack.price * quantity
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Checkout error:', error);
|
||||
toast({
|
||||
title: 'Checkout Failed',
|
||||
description: 'Unable to process payment. Please try again.',
|
||||
variant: 'destructive'
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
// Redirect to billing portal for credit purchases
|
||||
window.location.href = `https://billing.hanzo.ai?pack=${selectedPack}&quantity=${quantity}`;
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const selectedPackDetails = creditPacks.find(p => p.id === selectedPack);
|
||||
@@ -294,7 +262,7 @@ const PurchaseCredits = () => {
|
||||
|
||||
<div className="mt-4 flex items-center justify-center gap-2 text-sm text-gray-400">
|
||||
<Shield className="h-4 w-4" />
|
||||
<span>Secure payment powered by Stripe</span>
|
||||
<span>Secure payment via Hanzo Billing</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user