feat(platform): Add confirmation modal for email verification

- Create reusable ConfirmationModal component with variants (info, warning, danger, success)
- Replace browser confirm() dialogs with styled modal for email verification
- Update PlatformBusinesses and PlatformUsers to use the new modal
- Add translation keys for verification messages
- Unverify test@example.com for testing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-02 11:26:47 -05:00
parent 42988c0f88
commit dc3210927a
4 changed files with 226 additions and 19 deletions

View File

@@ -9,6 +9,7 @@ import PlatformListing from './components/PlatformListing';
import PlatformTable from './components/PlatformTable';
import PlatformListRow from './components/PlatformListRow';
import EditPlatformEntityModal from './components/EditPlatformEntityModal';
import ConfirmationModal from '../../components/ConfirmationModal';
import { useQueryClient } from '@tanstack/react-query';
interface PlatformBusinessesProps {
@@ -25,6 +26,8 @@ const PlatformBusinesses: React.FC<PlatformBusinessesProps> = ({ onMasquerade })
const [showInviteModal, setShowInviteModal] = useState(false);
const [editingBusiness, setEditingBusiness] = useState<PlatformBusiness | null>(null);
const [showInactiveBusinesses, setShowInactiveBusinesses] = useState(false);
const [verifyEmailUser, setVerifyEmailUser] = useState<{ id: number; email: string; name: string } | null>(null);
const [isVerifying, setIsVerifying] = useState(false);
// Filter and separate businesses
const filteredBusinesses = (businesses || []).filter(b =>
@@ -47,14 +50,22 @@ const PlatformBusinesses: React.FC<PlatformBusinessesProps> = ({ onMasquerade })
}
};
const handleVerifyEmail = async (userId: number) => {
if (confirm(t('platform.confirmVerifyEmail'))) {
try {
await verifyUserEmail(userId);
queryClient.invalidateQueries({ queryKey: ['platform', 'businesses'] });
} catch (error) {
alert(t('errors.generic'));
}
const handleVerifyEmailClick = (userId: number, email: string, name: string) => {
setVerifyEmailUser({ id: userId, email, name });
};
const handleVerifyEmailConfirm = async () => {
if (!verifyEmailUser) return;
setIsVerifying(true);
try {
await verifyUserEmail(verifyEmailUser.id);
queryClient.invalidateQueries({ queryKey: ['platform', 'businesses'] });
setVerifyEmailUser(null);
} catch (error) {
alert(t('errors.generic'));
} finally {
setIsVerifying(false);
}
};
@@ -70,9 +81,13 @@ const PlatformBusinesses: React.FC<PlatformBusinessesProps> = ({ onMasquerade })
tertiaryText={business.owner?.email || '-'}
actions={
<>
{business.owner && !business.owner.email_verified && ( // Assuming PlatformBusiness owner object has email_verified, if not we might need to fetch it or update interface
{business.owner && !business.owner.email_verified && (
<button
onClick={() => handleVerifyEmail(business.owner!.id)}
onClick={() => handleVerifyEmailClick(
business.owner!.id,
business.owner!.email,
business.owner!.full_name || business.owner!.username || business.name
)}
className="text-green-600 hover:text-green-500 dark:text-green-400 dark:hover:text-green-300 font-medium text-xs inline-flex items-center gap-1 px-3 py-1 border border-green-200 dark:border-green-800 rounded-lg hover:bg-green-50 dark:hover:bg-green-900/30 transition-colors"
title={t('platform.verifyEmail')}
>
@@ -194,6 +209,30 @@ const PlatformBusinesses: React.FC<PlatformBusinessesProps> = ({ onMasquerade })
isOpen={!!editingBusiness}
onClose={() => setEditingBusiness(null)}
/>
<ConfirmationModal
isOpen={!!verifyEmailUser}
onClose={() => setVerifyEmailUser(null)}
onConfirm={handleVerifyEmailConfirm}
title={t('platform.verifyEmail')}
message={
<div className="space-y-3">
<p>{t('platform.confirmVerifyEmailMessage')}</p>
{verifyEmailUser && (
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3">
<p className="font-medium text-gray-900 dark:text-white">{verifyEmailUser.name}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{verifyEmailUser.email}</p>
</div>
)}
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('platform.verifyEmailNote')}
</p>
</div>
}
confirmText={t('platform.verify')}
cancelText={t('common.cancel')}
variant="success"
isLoading={isVerifying}
/>
</div>
);
};

View File

@@ -7,6 +7,7 @@ import { useQueryClient } from '@tanstack/react-query';
import PlatformListing from './components/PlatformListing';
import PlatformListRow from './components/PlatformListRow';
import EditPlatformEntityModal from './components/EditPlatformEntityModal';
import ConfirmationModal from '../../components/ConfirmationModal';
interface PlatformUsersProps {
onMasquerade: (targetUser: { id: number; username?: string; name?: string; email?: string; role?: string }) => void;
@@ -19,6 +20,8 @@ const PlatformUsers: React.FC<PlatformUsersProps> = ({ onMasquerade }) => {
const [roleFilter, setRoleFilter] = useState<string>('all');
const { data: users, isLoading, error } = usePlatformUsers();
const [editingUser, setEditingUser] = useState<PlatformUser | null>(null);
const [verifyEmailUser, setVerifyEmailUser] = useState<{ id: number; email: string; name: string } | null>(null);
const [isVerifying, setIsVerifying] = useState(false);
const filteredUsers = (users || []).filter(u => {
const isPlatformUser = ['superuser', 'platform_manager', 'platform_sales', 'platform_support'].includes(u.role);
@@ -51,14 +54,22 @@ const PlatformUsers: React.FC<PlatformUsersProps> = ({ onMasquerade }) => {
});
};
const handleVerifyEmail = async (userId: number) => {
if (confirm(t('platform.confirmVerifyEmail'))) {
try {
await verifyUserEmail(userId);
queryClient.invalidateQueries({ queryKey: ['platform', 'users'] });
} catch (error) {
alert(t('errors.generic'));
}
const handleVerifyEmailClick = (userId: number, email: string, name: string) => {
setVerifyEmailUser({ id: userId, email, name });
};
const handleVerifyEmailConfirm = async () => {
if (!verifyEmailUser) return;
setIsVerifying(true);
try {
await verifyUserEmail(verifyEmailUser.id);
queryClient.invalidateQueries({ queryKey: ['platform', 'users'] });
setVerifyEmailUser(null);
} catch (error) {
alert(t('errors.generic'));
} finally {
setIsVerifying(false);
}
};
@@ -80,7 +91,7 @@ const PlatformUsers: React.FC<PlatformUsersProps> = ({ onMasquerade }) => {
<>
{!u.email_verified && (
<button
onClick={() => handleVerifyEmail(u.id)}
onClick={() => handleVerifyEmailClick(u.id, u.email, u.name || u.username)}
className="text-green-600 hover:text-green-500 dark:text-green-400 dark:hover:text-green-300 font-medium text-xs inline-flex items-center gap-1 px-3 py-1 border border-green-200 dark:border-green-800 rounded-lg hover:bg-green-50 dark:hover:bg-green-900/30 transition-colors"
title={t('platform.verifyEmail')}
>
@@ -137,6 +148,30 @@ const PlatformUsers: React.FC<PlatformUsersProps> = ({ onMasquerade }) => {
isOpen={!!editingUser}
onClose={() => setEditingUser(null)}
/>
<ConfirmationModal
isOpen={!!verifyEmailUser}
onClose={() => setVerifyEmailUser(null)}
onConfirm={handleVerifyEmailConfirm}
title={t('platform.verifyEmail')}
message={
<div className="space-y-3">
<p>{t('platform.confirmVerifyEmailMessage')}</p>
{verifyEmailUser && (
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3">
<p className="font-medium text-gray-900 dark:text-white">{verifyEmailUser.name}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{verifyEmailUser.email}</p>
</div>
)}
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('platform.verifyEmailNote')}
</p>
</div>
}
confirmText={t('platform.verify')}
cancelText={t('common.cancel')}
variant="success"
isLoading={isVerifying}
/>
</>
);
};