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
|
# Commerce API
|
||||||
# Get these from your Stripe dashboard: https://dashboard.stripe.com/apikeys
|
VITE_COMMERCE_API_URL=https://commerce.hanzo.ai/api/v1
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Other environment variables
|
# Other environment variables
|
||||||
VITE_API_URL=http://localhost:3000
|
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-toggle-group": "^1.1.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@rollup/rollup-darwin-x64": "^4.37.0",
|
"@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",
|
"@tanstack/react-query": "^5.69.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.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 { Label } from '@/components/ui/label';
|
||||||
import { Check, Lock, Mail, Loader2, CreditCard, Coins, Landmark } from 'lucide-react';
|
import { Check, Lock, Mail, Loader2, CreditCard, Coins, Landmark } from 'lucide-react';
|
||||||
import { useLocation, Link } from 'react-router-dom';
|
import { useLocation, Link } from 'react-router-dom';
|
||||||
import { createAgencyCheckout } from '@/lib/stripe';
|
import { createCommerceCheckout, type PaymentMethod, type CommerceCheckoutResult } from '@/lib/commerce';
|
||||||
import { createCommerceCheckout, type PaymentMethod, type CommerceCheckoutResult } from '@/lib/stripe';
|
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
const PAYMENT_METHODS: { value: PaymentMethod; label: string; icon: React.ElementType; desc: string }[] = [
|
const PAYMENT_METHODS: { value: PaymentMethod; label: string; icon: React.ElementType; desc: string }[] = [
|
||||||
@@ -58,18 +57,6 @@ const Payment = () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Checkout error:', 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({
|
toast({
|
||||||
title: 'Checkout Failed',
|
title: 'Checkout Failed',
|
||||||
description: 'Unable to start checkout. Please try again.',
|
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 { Button } from '@/components/ui/button';
|
||||||
import Footer from '@/components/Footer';
|
import Footer from '@/components/Footer';
|
||||||
import { CheckCircle, ArrowRight, Loader2, AlertCircle } from 'lucide-react';
|
import { CheckCircle, ArrowRight, Loader2, AlertCircle } from 'lucide-react';
|
||||||
import { trackPurchaseSuccess } from '@/lib/stripe';
|
import { trackPurchaseSuccess } from '@/lib/commerce';
|
||||||
import { analytics } from '@/utils/analytics';
|
import { analytics } from '@/utils/analytics';
|
||||||
|
|
||||||
const PaymentSuccess = () => {
|
const PaymentSuccess = () => {
|
||||||
@@ -47,7 +47,7 @@ const PaymentSuccess = () => {
|
|||||||
amount: details.amount,
|
amount: details.amount,
|
||||||
description: `Purchased ${details.credits} credits`,
|
description: `Purchased ${details.credits} credits`,
|
||||||
date: new Date().toISOString(),
|
date: new Date().toISOString(),
|
||||||
stripeSessionId: sessionId
|
checkoutSessionId: sessionId
|
||||||
});
|
});
|
||||||
localStorage.setItem('creditHistory', JSON.stringify(history));
|
localStorage.setItem('creditHistory', JSON.stringify(history));
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
TrendingUp,
|
TrendingUp,
|
||||||
Shield
|
Shield
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { createCheckoutSession, STRIPE_PRICE_IDS } from '@/lib/stripe';
|
import { formatCurrency } from '@/lib/commerce';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { analytics } from '@/utils/analytics';
|
import { analytics } from '@/utils/analytics';
|
||||||
|
|
||||||
@@ -40,7 +40,6 @@ const PurchaseCredits = () => {
|
|||||||
price: 100,
|
price: 100,
|
||||||
perCredit: 1.00,
|
perCredit: 1.00,
|
||||||
icon: Zap,
|
icon: Zap,
|
||||||
priceId: STRIPE_PRICE_IDS.STARTER_100_CREDITS,
|
|
||||||
features: [
|
features: [
|
||||||
'100 credits',
|
'100 credits',
|
||||||
'$1 per credit',
|
'$1 per credit',
|
||||||
@@ -55,7 +54,6 @@ const PurchaseCredits = () => {
|
|||||||
perCredit: 0.90,
|
perCredit: 0.90,
|
||||||
savings: 50,
|
savings: 50,
|
||||||
icon: TrendingUp,
|
icon: TrendingUp,
|
||||||
priceId: STRIPE_PRICE_IDS.PROFESSIONAL_500_CREDITS,
|
|
||||||
features: [
|
features: [
|
||||||
'500 credits',
|
'500 credits',
|
||||||
'$0.90 per credit',
|
'$0.90 per credit',
|
||||||
@@ -70,7 +68,6 @@ const PurchaseCredits = () => {
|
|||||||
perCredit: 0.75,
|
perCredit: 0.75,
|
||||||
savings: 500,
|
savings: 500,
|
||||||
icon: Star,
|
icon: Star,
|
||||||
priceId: STRIPE_PRICE_IDS.ENTERPRISE_2000_CREDITS,
|
|
||||||
features: [
|
features: [
|
||||||
'2,000 credits',
|
'2,000 credits',
|
||||||
'$0.75 per credit',
|
'$0.75 per credit',
|
||||||
@@ -111,38 +108,9 @@ const PurchaseCredits = () => {
|
|||||||
const pack = creditPacks.find(p => p.id === selectedPack);
|
const pack = creditPacks.find(p => p.id === selectedPack);
|
||||||
if (!pack) return;
|
if (!pack) return;
|
||||||
|
|
||||||
try {
|
// Redirect to billing portal for credit purchases
|
||||||
// Save purchase info for the success page
|
window.location.href = `https://billing.hanzo.ai?pack=${selectedPack}&quantity=${quantity}`;
|
||||||
const purchaseData = {
|
setIsLoading(false);
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedPackDetails = creditPacks.find(p => p.id === selectedPack);
|
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">
|
<div className="mt-4 flex items-center justify-center gap-2 text-sm text-gray-400">
|
||||||
<Shield className="h-4 w-4" />
|
<Shield className="h-4 w-4" />
|
||||||
<span>Secure payment powered by Stripe</span>
|
<span>Secure payment via Hanzo Billing</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user