Files
smoothschedule/frontend/src/components/DevQuickLogin.tsx
poduck b0512a660c feat(billing): Add customer billing page with payment method management
- 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>
2025-12-04 13:06:30 -05:00

220 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}