- Add CustomerBilling page for customers to view payment history and manage cards - Create AddPaymentMethodModal with Stripe Elements for secure card saving - Support both Stripe Connect and direct API payment modes - Auto-set first payment method as default when no default exists - Add dark mode support for Stripe card input styling - Add customer billing API endpoints for payment history and saved cards - Add stripe_customer_id field to User model for Stripe customer tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
220 lines
6.9 KiB
TypeScript
220 lines
6.9 KiB
TypeScript
import { useState } from 'react';
|
||
import apiClient from '../api/client';
|
||
import { setCookie } from '../utils/cookies';
|
||
import { useQueryClient } from '@tanstack/react-query';
|
||
import { getBaseDomain, buildSubdomainUrl } from '../utils/domain';
|
||
|
||
export interface TestUser {
|
||
email: string;
|
||
password: string;
|
||
role: string;
|
||
label: string;
|
||
color: string;
|
||
}
|
||
|
||
const testUsers: TestUser[] = [
|
||
{
|
||
email: 'superuser@platform.com',
|
||
password: 'test123',
|
||
role: 'SUPERUSER',
|
||
label: 'Platform Superuser',
|
||
color: 'bg-purple-600 hover:bg-purple-700',
|
||
},
|
||
{
|
||
email: 'manager@platform.com',
|
||
password: 'test123',
|
||
role: 'PLATFORM_MANAGER',
|
||
label: 'Platform Manager',
|
||
color: 'bg-blue-600 hover:bg-blue-700',
|
||
},
|
||
{
|
||
email: 'sales@platform.com',
|
||
password: 'test123',
|
||
role: 'PLATFORM_SALES',
|
||
label: 'Platform Sales',
|
||
color: 'bg-green-600 hover:bg-green-700',
|
||
},
|
||
{
|
||
email: 'support@platform.com',
|
||
password: 'test123',
|
||
role: 'PLATFORM_SUPPORT',
|
||
label: 'Platform Support',
|
||
color: 'bg-yellow-600 hover:bg-yellow-700',
|
||
},
|
||
{
|
||
email: 'owner@demo.com',
|
||
password: 'test123',
|
||
role: 'TENANT_OWNER',
|
||
label: 'Business Owner',
|
||
color: 'bg-indigo-600 hover:bg-indigo-700',
|
||
},
|
||
{
|
||
email: 'manager@demo.com',
|
||
password: 'test123',
|
||
role: 'TENANT_MANAGER',
|
||
label: 'Business Manager',
|
||
color: 'bg-pink-600 hover:bg-pink-700',
|
||
},
|
||
{
|
||
email: 'staff@demo.com',
|
||
password: 'test123',
|
||
role: 'TENANT_STAFF',
|
||
label: 'Staff Member',
|
||
color: 'bg-teal-600 hover:bg-teal-700',
|
||
},
|
||
{
|
||
email: 'customer@demo.com',
|
||
password: 'test123',
|
||
role: 'CUSTOMER',
|
||
label: 'Customer',
|
||
color: 'bg-orange-600 hover:bg-orange-700',
|
||
},
|
||
];
|
||
|
||
interface DevQuickLoginProps {
|
||
embedded?: boolean;
|
||
}
|
||
|
||
export function DevQuickLogin({ embedded = false }: DevQuickLoginProps) {
|
||
const queryClient = useQueryClient();
|
||
const [loading, setLoading] = useState<string | null>(null);
|
||
const [isMinimized, setIsMinimized] = useState(false);
|
||
|
||
// Only show in development
|
||
if (import.meta.env.PROD) {
|
||
return null;
|
||
}
|
||
|
||
const handleQuickLogin = async (user: TestUser) => {
|
||
setLoading(user.email);
|
||
try {
|
||
// Call custom login API that supports email login
|
||
const response = await apiClient.post('/auth/login/', {
|
||
email: user.email,
|
||
password: user.password,
|
||
});
|
||
|
||
// Store token in cookie (use 'access_token' to match what client.ts expects)
|
||
setCookie('access_token', response.data.access, 7);
|
||
|
||
// Clear any existing masquerade stack - this is a fresh login
|
||
localStorage.removeItem('masquerade_stack');
|
||
|
||
// Fetch user data to determine redirect
|
||
const userResponse = await apiClient.get('/auth/me/');
|
||
const userData = userResponse.data;
|
||
|
||
// Determine the correct subdomain based on user role
|
||
const currentHostname = window.location.hostname;
|
||
const currentPort = window.location.port;
|
||
let targetSubdomain: string | null = null;
|
||
|
||
// Platform users (superuser, platform_manager, platform_support)
|
||
if (['superuser', 'platform_manager', 'platform_support'].includes(userData.role)) {
|
||
targetSubdomain = 'platform';
|
||
}
|
||
// Business users - redirect to their business subdomain
|
||
else if (userData.business_subdomain) {
|
||
targetSubdomain = userData.business_subdomain;
|
||
}
|
||
|
||
// Check if we need to redirect to a different subdomain
|
||
const baseDomain = getBaseDomain();
|
||
const isOnTargetSubdomain = currentHostname === (targetSubdomain ? `${targetSubdomain}.${baseDomain}` : baseDomain);
|
||
const needsRedirect = targetSubdomain && !isOnTargetSubdomain;
|
||
|
||
if (needsRedirect) {
|
||
// Redirect to the correct subdomain
|
||
window.location.href = buildSubdomainUrl(targetSubdomain, '/');
|
||
return;
|
||
}
|
||
|
||
// Already on correct subdomain - just reload to update auth state
|
||
window.location.reload();
|
||
} catch (error: any) {
|
||
console.error('Quick login failed:', error);
|
||
alert(`Failed to login as ${user.label}: ${error.message || 'Unknown error'}`);
|
||
} finally {
|
||
setLoading(null);
|
||
}
|
||
};
|
||
|
||
if (!embedded && isMinimized) {
|
||
return (
|
||
<div className="fixed bottom-4 right-4 z-50">
|
||
<button
|
||
onClick={() => setIsMinimized(false)}
|
||
className="bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg hover:bg-gray-700 transition-colors"
|
||
>
|
||
🔓 Quick Login
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const containerClasses = embedded
|
||
? "w-full bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 mt-6"
|
||
: "fixed bottom-4 right-4 z-50 bg-white rounded-lg shadow-2xl border-2 border-gray-300 p-4 max-w-md";
|
||
|
||
return (
|
||
<div className={containerClasses}>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h3 className="font-bold text-gray-800 dark:text-white flex items-center gap-2">
|
||
<span>🔓</span>
|
||
<span>Quick Login (Dev Only)</span>
|
||
</h3>
|
||
{!embedded && (
|
||
<button
|
||
onClick={() => setIsMinimized(true)}
|
||
className="text-gray-500 hover:text-gray-700 text-xl leading-none"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-2">
|
||
{testUsers.map((user) => (
|
||
<button
|
||
key={user.email}
|
||
onClick={() => handleQuickLogin(user)}
|
||
disabled={loading !== null}
|
||
className={`${user.color} text-white px-3 py-2 rounded text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed`}
|
||
>
|
||
{loading === user.email ? (
|
||
<span className="flex items-center justify-center">
|
||
<svg className="animate-spin h-4 w-4 mr-2" viewBox="0 0 24 24">
|
||
<circle
|
||
className="opacity-25"
|
||
cx="12"
|
||
cy="12"
|
||
r="10"
|
||
stroke="currentColor"
|
||
strokeWidth="4"
|
||
fill="none"
|
||
/>
|
||
<path
|
||
className="opacity-75"
|
||
fill="currentColor"
|
||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||
/>
|
||
</svg>
|
||
Logging in...
|
||
</span>
|
||
) : (
|
||
<div className="text-left">
|
||
<div className="font-semibold">{user.label}</div>
|
||
<div className="text-xs opacity-90">{user.role}</div>
|
||
</div>
|
||
)}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="mt-3 text-xs text-gray-500 dark:text-gray-400 text-center">
|
||
Password for all: <code className="bg-gray-100 dark:bg-gray-700 px-1 rounded">test123</code>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|