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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -1059,6 +1059,7 @@ class TenantInvitationViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
# Create Owner User
|
||||
# Email is verified since they received the invitation email
|
||||
owner_user = User.objects.create_user(
|
||||
username=serializer.validated_data['email'],
|
||||
email=serializer.validated_data['email'],
|
||||
@@ -1067,6 +1068,7 @@ class TenantInvitationViewSet(viewsets.ModelViewSet):
|
||||
last_name=serializer.validated_data['last_name'],
|
||||
role=User.Role.TENANT_OWNER,
|
||||
tenant=tenant,
|
||||
email_verified=True,
|
||||
)
|
||||
|
||||
# Mark invitation as accepted
|
||||
|
||||
Reference in New Issue
Block a user