feat(onboarding): Add animated loading indicator and fix completion

- Add multi-step animated loading indicator during tenant creation
- Fix blank completion screen (was checking wrong step number)
- Auto-verify email for users accepting tenant invitations
- Show progress bar and step-by-step status during database setup

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-03 16:26:11 -05:00
parent 5244e16279
commit 567fe0604a
2 changed files with 143 additions and 20 deletions

View File

@@ -1,9 +1,17 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { CheckCircle, Mail, Lock, User, Building2, CreditCard, ArrowRight, ArrowLeft, Loader } from 'lucide-react';
import { CheckCircle, Mail, Lock, User, Building2, CreditCard, ArrowRight, ArrowLeft, Loader, Database, Settings, Sparkles } from 'lucide-react';
import { useInvitationByToken, useAcceptInvitation } from '../hooks/usePlatform';
import { getBaseDomain, buildSubdomainUrl } from '../utils/domain';
// Loading step configuration for the animated progress
const LOADING_STEPS = [
{ id: 'account', label: 'Creating your account', icon: User, duration: 800 },
{ id: 'database', label: 'Setting up your database', icon: Database, duration: 3000 },
{ id: 'config', label: 'Configuring your workspace', icon: Settings, duration: 1500 },
{ id: 'features', label: 'Activating features', icon: Sparkles, duration: 1000 },
];
const TenantOnboardPage: React.FC = () => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
@@ -13,6 +21,10 @@ const TenantOnboardPage: React.FC = () => {
const acceptInvitationMutation = useAcceptInvitation();
const [currentStep, setCurrentStep] = useState(1);
const [isCreating, setIsCreating] = useState(false);
const [creationComplete, setCreationComplete] = useState(false);
const [loadingStepIndex, setLoadingStepIndex] = useState(0);
const loadingIntervalRef = useRef<NodeJS.Timeout | null>(null);
const [formData, setFormData] = useState({
// Step 1: Account
email: '',
@@ -113,10 +125,37 @@ const TenantOnboardPage: React.FC = () => {
setCurrentStep(prev => prev - 1);
};
// Cleanup loading interval on unmount
useEffect(() => {
return () => {
if (loadingIntervalRef.current) {
clearTimeout(loadingIntervalRef.current);
}
};
}, []);
// Animate through loading steps
const startLoadingAnimation = () => {
setLoadingStepIndex(0);
let stepIndex = 0;
const advanceStep = () => {
stepIndex++;
if (stepIndex < LOADING_STEPS.length) {
setLoadingStepIndex(stepIndex);
loadingIntervalRef.current = setTimeout(advanceStep, LOADING_STEPS[stepIndex].duration);
}
};
loadingIntervalRef.current = setTimeout(advanceStep, LOADING_STEPS[0].duration);
};
const handleSubmit = () => {
if (!token) return;
setAcceptError(null);
setIsCreating(true);
startLoadingAnimation();
const data = {
email: formData.email,
@@ -133,9 +172,23 @@ const TenantOnboardPage: React.FC = () => {
{ token, data },
{
onSuccess: () => {
setCurrentStep(4); // Go to completion step
// Clear the loading animation
if (loadingIntervalRef.current) {
clearTimeout(loadingIntervalRef.current);
}
// Show all steps as complete, then show success
setLoadingStepIndex(LOADING_STEPS.length);
setTimeout(() => {
setIsCreating(false);
setCreationComplete(true);
}, 500);
},
onError: (error: any) => {
// Clear the loading animation
if (loadingIntervalRef.current) {
clearTimeout(loadingIntervalRef.current);
}
setIsCreating(false);
setAcceptError(
error.response?.data?.detail ||
error.response?.data?.subdomain?.[0] ||
@@ -238,7 +291,7 @@ const TenantOnboardPage: React.FC = () => {
)}
{/* Step 1: Account Setup */}
{currentStep === 1 && (
{currentStep === 1 && !isCreating && !creationComplete && (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
@@ -340,7 +393,7 @@ const TenantOnboardPage: React.FC = () => {
)}
{/* Step 2: Business Details */}
{currentStep === 2 && (
{currentStep === 2 && !isCreating && !creationComplete && (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
@@ -426,7 +479,7 @@ const TenantOnboardPage: React.FC = () => {
)}
{/* Step 3: Payment Setup (conditional) */}
{currentStep === 3 && invitation.permissions?.can_accept_payments && (
{currentStep === 3 && invitation.permissions?.can_accept_payments && !isCreating && !creationComplete && (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
@@ -452,8 +505,85 @@ const TenantOnboardPage: React.FC = () => {
</div>
)}
{/* Step 4: Complete */}
{currentStep === totalSteps && (
{/* Creating Business - Animated Loading */}
{isCreating && (
<div className="space-y-8 py-4">
<div className="text-center">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Creating Your Business
</h2>
<p className="text-gray-600 dark:text-gray-400">
Please wait while we set everything up for you...
</p>
</div>
<div className="space-y-4">
{LOADING_STEPS.map((step, index) => {
const StepIcon = step.icon;
const isComplete = index < loadingStepIndex;
const isCurrent = index === loadingStepIndex;
return (
<div
key={step.id}
className={`flex items-center gap-4 p-4 rounded-lg transition-all duration-300 ${
isComplete
? 'bg-green-50 dark:bg-green-900/20'
: isCurrent
? 'bg-indigo-50 dark:bg-indigo-900/20'
: 'bg-gray-50 dark:bg-gray-700/50'
}`}
>
<div
className={`w-10 h-10 rounded-full flex items-center justify-center transition-all duration-300 ${
isComplete
? 'bg-green-500 text-white'
: isCurrent
? 'bg-indigo-500 text-white'
: 'bg-gray-300 dark:bg-gray-600 text-gray-500 dark:text-gray-400'
}`}
>
{isComplete ? (
<CheckCircle size={20} />
) : isCurrent ? (
<Loader size={20} className="animate-spin" />
) : (
<StepIcon size={20} />
)}
</div>
<div className="flex-1">
<p
className={`font-medium transition-colors duration-300 ${
isComplete
? 'text-green-700 dark:text-green-400'
: isCurrent
? 'text-indigo-700 dark:text-indigo-400'
: 'text-gray-500 dark:text-gray-400'
}`}
>
{step.label}
{isComplete && ' '}
</p>
</div>
</div>
);
})}
</div>
{/* Progress bar */}
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
<div
className="bg-indigo-600 h-2 rounded-full transition-all duration-500 ease-out"
style={{
width: `${((loadingStepIndex + 1) / LOADING_STEPS.length) * 100}%`,
}}
/>
</div>
</div>
)}
{/* Complete */}
{creationComplete && (
<div className="space-y-6 text-center">
<div className="w-20 h-20 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto">
<CheckCircle size={48} className="text-green-600 dark:text-green-400" />
@@ -489,7 +619,7 @@ const TenantOnboardPage: React.FC = () => {
)}
{/* Navigation Buttons */}
{currentStep < totalSteps && (
{currentStep < totalSteps && !isCreating && !creationComplete && (
<div className="flex items-center justify-between mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
<button
onClick={handleBack}
@@ -505,17 +635,8 @@ const TenantOnboardPage: React.FC = () => {
disabled={acceptInvitationMutation.isPending}
className="flex items-center gap-2 px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
{acceptInvitationMutation.isPending ? (
<>
<Loader className="animate-spin" size={18} />
Creating...
</>
) : (
<>
{currentStep === 2 && !invitation.permissions?.can_accept_payments ? 'Create Business' : 'Continue'}
<ArrowRight size={18} />
</>
)}
{currentStep === 2 && !invitation.permissions?.can_accept_payments ? 'Create Business' : 'Continue'}
<ArrowRight size={18} />
</button>
</div>
)}