Add subcription feature 5
This commit is contained in:
@@ -279,6 +279,7 @@ export const BasicInfoStep: React.FC<BasicInfoStepProps> = ({
|
||||
}}
|
||||
placeholder={t('auth:register.confirm_password_placeholder', '••••••••')}
|
||||
error={errors.confirmPassword || formData.confirmPasswordError}
|
||||
isValid={formData.confirmPassword.length > 0 && formData.confirmPassword === formData.password}
|
||||
className="w-full"
|
||||
rightIcon={showConfirmPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
onRightIconClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button, Input, Card } from '../../ui';
|
||||
import { PasswordCriteria, validatePassword, getPasswordErrors } from '../../ui/PasswordCriteria';
|
||||
import { useAuthActions } from '../../../stores/auth.store';
|
||||
import { showToast } from '../../../utils/toast';
|
||||
import { useResetPassword } from '../../../api/hooks/auth';
|
||||
import { useResetPasswordWithToken } from '../../../api/hooks/auth';
|
||||
|
||||
interface PasswordResetFormProps {
|
||||
token?: string;
|
||||
@@ -36,7 +36,7 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
|
||||
const passwordInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Password reset mutation hooks
|
||||
const { mutateAsync: resetPasswordMutation, isPending: isResetting } = useResetPassword();
|
||||
const { mutateAsync: resetPasswordMutation, isPending: isResetting } = useResetPasswordWithToken();
|
||||
const isLoading = isResetting;
|
||||
const error = null;
|
||||
|
||||
@@ -184,7 +184,7 @@ export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
|
||||
// Call the reset password API
|
||||
await resetPasswordMutation({
|
||||
token: token,
|
||||
new_password: password
|
||||
newPassword: password
|
||||
});
|
||||
|
||||
showToast.success('¡Tu contraseña ha sido restablecida exitosamente! Ya puedes iniciar sesión.', {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useStartRegistration, useCompleteRegistration } from '@/api/hooks/auth';
|
||||
import { ThreeDSAuthenticationRequired } from '@/api/types/auth';
|
||||
import { Button, Card, Input } from '../../ui';
|
||||
import { Loader2, CreditCard, Lock, AlertCircle, CheckCircle } from 'lucide-react';
|
||||
import { Loader2, CreditCard, Lock, AlertCircle } from 'lucide-react';
|
||||
import { subscriptionService, apiClient } from '../../../api';
|
||||
import { useAuthStore } from '../../../stores/auth.store';
|
||||
import { getSubscriptionFromJWT, getTenantAccessFromJWT, getPrimaryTenantIdFromJWT } from '../../../utils/jwt';
|
||||
@@ -23,17 +23,13 @@ const SUPPORTED_COUNTRIES = [
|
||||
];
|
||||
|
||||
interface PaymentStepProps {
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
onPaymentComplete: (paymentData: any) => void;
|
||||
registrationState: any;
|
||||
updateRegistrationState: (updates: any) => void;
|
||||
}
|
||||
|
||||
export const PaymentStep: React.FC<PaymentStepProps> = ({
|
||||
onNext,
|
||||
onBack,
|
||||
onPaymentComplete,
|
||||
registrationState,
|
||||
updateRegistrationState
|
||||
}) => {
|
||||
@@ -285,20 +281,44 @@ export const PaymentStep: React.FC<PaymentStepProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Payment setup successful
|
||||
updateRegistrationState({
|
||||
paymentSetup: {
|
||||
success: true,
|
||||
customerId: paymentSetupResult.customer_id ?? '',
|
||||
subscriptionId: paymentSetupResult.subscription_id ?? '',
|
||||
paymentMethodId: paymentMethod.id,
|
||||
planId: registrationState.subscription.planId,
|
||||
// Payment setup successful (no 3DS required)
|
||||
// Now call /complete-registration to create user, subscription, and get tokens
|
||||
const completionResult = await completeRegistrationMutation.mutateAsync({
|
||||
setup_intent_id: paymentSetupResult.setup_intent_id || '',
|
||||
user_data: {
|
||||
email: registrationState.basicInfo.email,
|
||||
password: registrationState.basicInfo.password || '',
|
||||
full_name: `${registrationState.basicInfo.firstName} ${registrationState.basicInfo.lastName}`,
|
||||
subscription_plan: registrationState.subscription.planId,
|
||||
billing_cycle: registrationState.subscription.billingInterval,
|
||||
payment_method_id: paymentMethod.id,
|
||||
coupon_code: registrationState.subscription.couponCode,
|
||||
customer_id: paymentSetupResult.customer_id ?? paymentSetupResult.payment_customer_id ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
setSuccess(true);
|
||||
onPaymentComplete(paymentSetupResult);
|
||||
|
||||
if (completionResult.success) {
|
||||
updateRegistrationState({
|
||||
paymentSetup: {
|
||||
success: true,
|
||||
customerId: completionResult.payment_customer_id ?? paymentSetupResult.customer_id ?? '',
|
||||
subscriptionId: completionResult.subscription_id ?? '',
|
||||
paymentMethodId: paymentMethod.id,
|
||||
planId: registrationState.subscription.planId,
|
||||
},
|
||||
});
|
||||
|
||||
// Perform auto-login if tokens are returned
|
||||
performAutoLogin(completionResult);
|
||||
|
||||
// Show success message and navigate to onboarding
|
||||
showToast.success(t('auth:registration_complete', 'Registration complete! Redirecting to onboarding...'));
|
||||
navigate('/app/onboarding');
|
||||
return;
|
||||
} else {
|
||||
setError(completionResult.message || t('auth:registration_failed', 'Registration failed'));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof ThreeDSAuthenticationRequired) {
|
||||
setRequires3DS(true);
|
||||
@@ -384,28 +404,12 @@ export const PaymentStep: React.FC<PaymentStepProps> = ({
|
||||
});
|
||||
|
||||
// Perform auto-login if tokens are returned
|
||||
const autoLoginSuccess = performAutoLogin(verificationResult);
|
||||
performAutoLogin(verificationResult);
|
||||
|
||||
if (autoLoginSuccess) {
|
||||
// Show success message
|
||||
showToast.success(t('auth:registration_complete', 'Registration complete! Redirecting to onboarding...'));
|
||||
|
||||
// Navigate directly to onboarding
|
||||
navigate('/app/onboarding');
|
||||
return;
|
||||
}
|
||||
|
||||
setSuccess(true);
|
||||
setRequires3DS(false);
|
||||
|
||||
// Create a combined result for the parent component (fallback if no tokens)
|
||||
const paymentCompleteResult = {
|
||||
...verificationResult,
|
||||
requires_3ds: true,
|
||||
threeds_completed: true,
|
||||
};
|
||||
|
||||
onPaymentComplete(paymentCompleteResult);
|
||||
// Show success message and navigate to onboarding
|
||||
showToast.success(t('auth:registration_complete', 'Registration complete! Redirecting to onboarding...'));
|
||||
navigate('/app/onboarding');
|
||||
return;
|
||||
} else {
|
||||
setError(verificationResult.message || t('auth:payment_verification_failed'));
|
||||
}
|
||||
@@ -424,30 +428,15 @@ export const PaymentStep: React.FC<PaymentStepProps> = ({
|
||||
setError(null);
|
||||
};
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="text-center py-8">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100 mb-4 mx-auto">
|
||||
<CheckCircle className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2">
|
||||
{t('auth:payment_setup_complete')}
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-6">
|
||||
{t('auth:payment_setup_complete_description')}
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
onClick={onNext}
|
||||
disabled={loading}
|
||||
className="bg-primary hover:bg-primary/90"
|
||||
>
|
||||
{t('auth:continue_to_account_creation')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Auto-redirect on success (safety net - should not normally reach here)
|
||||
useEffect(() => {
|
||||
if (success) {
|
||||
const timer = setTimeout(() => {
|
||||
navigate('/app/onboarding');
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [success, navigate]);
|
||||
|
||||
if (requires3DS) {
|
||||
return (
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { Card, Progress } from '../../ui';
|
||||
import { Card } from '../../ui';
|
||||
import { showToast } from '../../../utils/toast';
|
||||
import { useAuthActions, useAuthLoading, useAuthError, useAuthStore } from '../../../stores/auth.store';
|
||||
import { useAuthStore } from '../../../stores/auth.store';
|
||||
import { usePilotDetection } from '../../../hooks/usePilotDetection';
|
||||
import { subscriptionService, apiClient } from '../../../api';
|
||||
import { apiClient } from '../../../api';
|
||||
import { validateEmail } from '../../../utils/validation';
|
||||
import { getSubscriptionFromJWT, getTenantAccessFromJWT, getPrimaryTenantIdFromJWT } from '../../../utils/jwt';
|
||||
import { loadStripe, Stripe } from '@stripe/stripe-js';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import { useRegistrationState } from './hooks/useRegistrationState';
|
||||
import { useRegistrationState, RegistrationStep } from './hooks/useRegistrationState';
|
||||
import BasicInfoStep from './BasicInfoStep';
|
||||
import SubscriptionStep from './SubscriptionStep';
|
||||
import { PaymentStep } from './PaymentStep';
|
||||
@@ -38,7 +38,6 @@ interface RegistrationContainerProps {
|
||||
}
|
||||
|
||||
export const RegistrationContainer: React.FC<RegistrationContainerProps> = ({
|
||||
onSuccess,
|
||||
onLoginClick,
|
||||
className
|
||||
}) => {
|
||||
@@ -46,15 +45,12 @@ export const RegistrationContainer: React.FC<RegistrationContainerProps> = ({
|
||||
const navigate = useNavigate();
|
||||
const { isPilot, couponCode } = usePilotDetection();
|
||||
const [searchParams] = useSearchParams();
|
||||
const {
|
||||
createRegistrationPaymentSetup,
|
||||
verifySetupIntent,
|
||||
completeRegistrationAfterPayment
|
||||
} = useAuthActions();
|
||||
const isLoading = useAuthLoading();
|
||||
const error = useAuthError();
|
||||
|
||||
// Helper function to perform auto-login after registration
|
||||
// Get auth store functions for 3DS redirect recovery
|
||||
const verifySetupIntent = useAuthStore((state) => state.verifySetupIntent);
|
||||
const completeRegistrationAfterPayment = useAuthStore((state) => state.completeRegistrationAfterPayment);
|
||||
|
||||
// Helper function to perform auto-login after registration (used in 3DS redirect recovery)
|
||||
const performAutoLogin = (result: any) => {
|
||||
if (result.access_token && result.refresh_token) {
|
||||
// Set tokens in API client
|
||||
@@ -92,11 +88,12 @@ export const RegistrationContainer: React.FC<RegistrationContainerProps> = ({
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
// Get URL parameters
|
||||
const planParam = searchParams.get('plan');
|
||||
const VALID_PLANS = ['starter', 'professional', 'enterprise'];
|
||||
const preSelectedPlan = (planParam && VALID_PLANS.includes(planParam)) ? planParam : null;
|
||||
const VALID_PLANS = ['starter', 'professional', 'enterprise'] as const;
|
||||
type PlanType = typeof VALID_PLANS[number];
|
||||
const preSelectedPlan: PlanType | null = (planParam && VALID_PLANS.includes(planParam as PlanType)) ? planParam as PlanType : null;
|
||||
|
||||
const billingParam = searchParams.get('billing_cycle');
|
||||
const urlBillingCycle = (billingParam === 'monthly' || billingParam === 'yearly') ? billingParam : null;
|
||||
@@ -116,21 +113,21 @@ export const RegistrationContainer: React.FC<RegistrationContainerProps> = ({
|
||||
|
||||
const {
|
||||
step,
|
||||
setStep,
|
||||
registrationState,
|
||||
updateRegistrationState,
|
||||
nextStep,
|
||||
prevStep,
|
||||
isSubmitting,
|
||||
setIsSubmitting
|
||||
isSubmitting
|
||||
} = useRegistrationState({
|
||||
initialPlan: preSelectedPlan || 'starter',
|
||||
initialBillingCycle: urlBillingCycle || 'monthly',
|
||||
isPilot: effectivePilotState // Use the effective pilot state
|
||||
});
|
||||
|
||||
|
||||
const [stripeInstance, setStripeInstance] = useState<Stripe | null>(null);
|
||||
// paymentComplete is used for 3DS redirect recovery to prevent re-processing
|
||||
const [paymentComplete, setPaymentComplete] = useState(false);
|
||||
const [paymentData, setPaymentData] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadStripeInstance = async () => {
|
||||
@@ -191,9 +188,9 @@ export const RegistrationContainer: React.FC<RegistrationContainerProps> = ({
|
||||
updateRegistrationState({
|
||||
basicInfo: savedState.basicInfo,
|
||||
subscription: savedState.subscription,
|
||||
paymentSetup: savedState.paymentSetup,
|
||||
step: 'payment'
|
||||
paymentSetup: savedState.paymentSetup
|
||||
});
|
||||
setStep('payment');
|
||||
|
||||
// Verify the SetupIntent status with our backend
|
||||
const verificationResult = await verifySetupIntent(setupIntentParam);
|
||||
@@ -227,6 +224,9 @@ export const RegistrationContainer: React.FC<RegistrationContainerProps> = ({
|
||||
threeds_completed: true
|
||||
};
|
||||
|
||||
// Complete the registration after 3DS verification
|
||||
const result = await completeRegistrationAfterPayment(registrationData);
|
||||
|
||||
if (result.success) {
|
||||
// Clean up saved state
|
||||
sessionStorage.removeItem(savedStateKey);
|
||||
@@ -238,10 +238,15 @@ export const RegistrationContainer: React.FC<RegistrationContainerProps> = ({
|
||||
newUrl.searchParams.delete('redirect_status');
|
||||
window.history.replaceState({}, '', newUrl.toString());
|
||||
|
||||
// Perform auto-login if tokens are returned
|
||||
performAutoLogin(result);
|
||||
|
||||
showToast.success(t('auth:registration_complete', 'Registration complete! Redirecting to onboarding...'));
|
||||
|
||||
// Navigate to onboarding (same as in PaymentStep)
|
||||
// Use a small timeout to ensure state updates are processed before navigation
|
||||
// Mark payment as complete to prevent re-processing
|
||||
setPaymentComplete(true);
|
||||
|
||||
// Navigate to onboarding
|
||||
setTimeout(() => {
|
||||
navigate('/app/onboarding');
|
||||
}, 100);
|
||||
@@ -261,13 +266,7 @@ export const RegistrationContainer: React.FC<RegistrationContainerProps> = ({
|
||||
|
||||
handle3DSRedirectRecovery();
|
||||
}, [setupIntentParam, redirectStatusParam, stripeInstance, paymentComplete, redirectRecoveryInProgress]);
|
||||
|
||||
const handlePaymentComplete = (data: any) => {
|
||||
setPaymentData(data);
|
||||
setPaymentComplete(true);
|
||||
nextStep();
|
||||
};
|
||||
|
||||
|
||||
const handleRegistrationSubmit = async () => {
|
||||
try {
|
||||
if (step === 'basic_info') {
|
||||
@@ -279,13 +278,14 @@ export const RegistrationContainer: React.FC<RegistrationContainerProps> = ({
|
||||
if (preSelectedPlan) {
|
||||
// Ensure subscription data is set with the pre-selected plan
|
||||
updateRegistrationState({
|
||||
step: 'payment',
|
||||
subscription: {
|
||||
...registrationState.subscription,
|
||||
planId: preSelectedPlan,
|
||||
billingInterval: urlBillingCycle || registrationState.subscription.billingInterval
|
||||
}
|
||||
});
|
||||
// Skip directly to payment step
|
||||
setStep('payment');
|
||||
} else {
|
||||
// Go to subscription selection step
|
||||
nextStep();
|
||||
@@ -341,100 +341,22 @@ export const RegistrationContainer: React.FC<RegistrationContainerProps> = ({
|
||||
|
||||
const validateSubscription = (): boolean => {
|
||||
const { subscription } = registrationState;
|
||||
|
||||
|
||||
if (!subscription.planId || !subscription.billingInterval) {
|
||||
updateRegistrationState({
|
||||
errors: { subscription: t('auth:register.select_plan') }
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleCompleteRegistration = async () => {
|
||||
if (!paymentData) {
|
||||
showToast.error(t('auth:alerts.payment_required'), {
|
||||
title: t('auth:alerts.error')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
const registrationData = {
|
||||
email: registrationState.basicInfo.email,
|
||||
full_name: `${registrationState.basicInfo.firstName} ${registrationState.basicInfo.lastName}`,
|
||||
password: registrationState.basicInfo.password,
|
||||
subscription_plan: registrationState.subscription.planId,
|
||||
billing_cycle: registrationState.subscription.billingInterval,
|
||||
payment_customer_id: paymentData.customer_id,
|
||||
payment_method_id: paymentData.payment_method_id,
|
||||
subscription_id: paymentData.subscription_id,
|
||||
coupon_code: isPilot ? couponCode : registrationState.subscription.couponCode,
|
||||
terms_accepted: registrationState.basicInfo.acceptTerms,
|
||||
privacy_accepted: registrationState.basicInfo.acceptTerms,
|
||||
marketing_consent: registrationState.basicInfo.marketingConsent,
|
||||
analytics_consent: registrationState.basicInfo.analyticsConsent,
|
||||
threeds_completed: paymentData.threeds_completed || false,
|
||||
setup_intent_id: paymentData.setup_intent_id,
|
||||
};
|
||||
|
||||
// Complete registration after successful payment
|
||||
const result = await completeRegistrationAfterPayment(registrationData);
|
||||
|
||||
if (result.success) {
|
||||
showToast.success(t('auth:register.registration_complete'), {
|
||||
title: t('auth:alerts.success')
|
||||
});
|
||||
|
||||
// Perform auto-login if tokens are returned
|
||||
const autoLoginSuccess = performAutoLogin(result);
|
||||
|
||||
if (autoLoginSuccess) {
|
||||
// Show success message
|
||||
showToast.success(t('auth:registration_complete', 'Registration complete! Redirecting to onboarding...'));
|
||||
|
||||
// Use a small timeout to ensure state updates are processed before navigation
|
||||
setTimeout(() => {
|
||||
navigate('/app/onboarding');
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: Navigate to onboarding after a delay if auto-login didn't happen
|
||||
setTimeout(() => {
|
||||
navigate('/app/onboarding');
|
||||
}, 1000); // Small delay to allow the success toast to be seen
|
||||
|
||||
onSuccess?.();
|
||||
} else {
|
||||
showToast.error(result.message || t('auth:alerts.registration_error'), {
|
||||
title: t('auth:alerts.error')
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Registration completion error:', error);
|
||||
showToast.error(t('auth:alerts.registration_error'), {
|
||||
title: t('auth:alerts.error')
|
||||
});
|
||||
|
||||
// Even if there's an error, try to navigate to onboarding
|
||||
// The user might still be able to log in manually
|
||||
setTimeout(() => {
|
||||
navigate('/app/onboarding');
|
||||
}, 1000);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate progress percentage
|
||||
const getProgressPercentage = () => {
|
||||
const steps = preSelectedPlan
|
||||
? ['basic_info', 'payment', 'complete']
|
||||
: ['basic_info', 'subscription', 'payment', 'complete'];
|
||||
const steps = preSelectedPlan
|
||||
? ['basic_info', 'payment']
|
||||
: ['basic_info', 'subscription', 'payment'];
|
||||
const currentIndex = steps.indexOf(step);
|
||||
return currentIndex >= 0 ? ((currentIndex + 1) / steps.length) * 100 : 0;
|
||||
};
|
||||
@@ -445,15 +367,14 @@ export const RegistrationContainer: React.FC<RegistrationContainerProps> = ({
|
||||
case 'basic_info': return t('auth:register.step_basic_info', 'Información Básica');
|
||||
case 'subscription': return t('auth:register.step_subscription', 'Suscripción');
|
||||
case 'payment': return t('auth:register.step_payment', 'Pago');
|
||||
case 'complete': return t('auth:register.step_complete', 'Completar');
|
||||
default: return '';
|
||||
}
|
||||
};
|
||||
|
||||
// Determine which steps to show in progress indicator
|
||||
const progressSteps: RegistrationStep[] = preSelectedPlan
|
||||
? ['basic_info', 'payment', 'complete']
|
||||
: ['basic_info', 'subscription', 'payment', 'complete'];
|
||||
const progressSteps: RegistrationStep[] = preSelectedPlan
|
||||
? ['basic_info', 'payment']
|
||||
: ['basic_info', 'subscription', 'payment'];
|
||||
|
||||
// Render current step
|
||||
const renderStep = () => {
|
||||
@@ -489,37 +410,21 @@ export const RegistrationContainer: React.FC<RegistrationContainerProps> = ({
|
||||
);
|
||||
case 'payment':
|
||||
return (
|
||||
<Elements
|
||||
stripe={stripeInstance}
|
||||
<Elements
|
||||
stripe={stripeInstance}
|
||||
options={{
|
||||
mode: 'setup',
|
||||
mode: 'setup',
|
||||
currency: 'eur',
|
||||
paymentMethodCreation: 'manual' // Required for Payment Element
|
||||
}}
|
||||
>
|
||||
<PaymentStep
|
||||
onNext={handleRegistrationSubmit}
|
||||
onBack={prevStep}
|
||||
onPaymentComplete={handlePaymentComplete}
|
||||
registrationState={registrationState}
|
||||
updateRegistrationState={updateRegistrationState}
|
||||
/>
|
||||
</Elements>
|
||||
);
|
||||
case 'complete':
|
||||
return (
|
||||
<div className="registration-complete-step">
|
||||
<h2>{t('auth:register.registration_complete')}</h2>
|
||||
<p>{t('auth:register.registration_complete_description')}</p>
|
||||
<button
|
||||
onClick={handleCompleteRegistration}
|
||||
disabled={isSubmitting}
|
||||
className="complete-registration-button"
|
||||
>
|
||||
{isSubmitting ? t('auth:register.completing') : t('auth:register.complete_registration')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Checkbox } from '../../ui';
|
||||
import { Button } from '../../ui';
|
||||
import { SubscriptionPricingCards } from '../../subscription/SubscriptionPricingCards';
|
||||
import { subscriptionService } from '../../../api';
|
||||
|
||||
@@ -73,9 +73,14 @@ export const SubscriptionStep: React.FC<SubscriptionStepProps> = ({
|
||||
|
||||
fetchPlanMetadata();
|
||||
|
||||
// Automatically apply PILOT2025 coupon for pilot users with trial enabled
|
||||
if (isPilot && useTrial && !couponCode) {
|
||||
updateField('couponCode', 'PILOT2025');
|
||||
// Automatically enable trial and apply PILOT2025 coupon for pilot users
|
||||
if (isPilot) {
|
||||
if (!useTrial) {
|
||||
updateField('useTrial', true);
|
||||
}
|
||||
if (!couponCode) {
|
||||
updateField('couponCode', 'PILOT2025');
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
@@ -130,38 +135,14 @@ export const SubscriptionStep: React.FC<SubscriptionStepProps> = ({
|
||||
mode="selection"
|
||||
selectedPlan={selectedPlan}
|
||||
onPlanSelect={(planKey) => updateField('planId', planKey)}
|
||||
showPilotBanner={false} // Pilot program disabled as requested
|
||||
pilotCouponCode={undefined}
|
||||
pilotTrialMonths={0}
|
||||
showPilotBanner={isPilot}
|
||||
pilotCouponCode={isPilot ? 'PILOT2025' : undefined}
|
||||
pilotTrialMonths={isPilot ? 3 : 0}
|
||||
billingCycle={billingCycle}
|
||||
onBillingCycleChange={(cycle) => updateField('billingInterval', cycle)}
|
||||
className="mb-8"
|
||||
/>
|
||||
|
||||
{/* Pilot trial option - automatically enabled for pilot users */}
|
||||
{isPilot && (
|
||||
<div className="mb-6 p-4 bg-[var(--color-info-50)] rounded-lg">
|
||||
<Checkbox
|
||||
id="useTrial"
|
||||
checked={useTrial}
|
||||
onChange={(e) => {
|
||||
updateField('useTrial', e.target.checked);
|
||||
// Automatically apply PILOT2025 coupon when trial is enabled
|
||||
if (e.target.checked) {
|
||||
updateField('couponCode', 'PILOT2025');
|
||||
} else {
|
||||
updateField('couponCode', undefined);
|
||||
}
|
||||
}}
|
||||
label={t('auth:register.enable_trial', 'Enable 3-month free trial')}
|
||||
className="mb-2 font-medium"
|
||||
/>
|
||||
<p className="text-sm text-[var(--color-info-800)] ml-6">
|
||||
{t('auth:register.trial_description', 'Get full access to Professional plan features for 3 months at no cost.')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex justify-between mt-8">
|
||||
<Button type="button" variant="secondary" size="lg" onClick={onBack}>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { validateEmail } from '../../../../utils/validation';
|
||||
|
||||
export type RegistrationStep = 'basic_info' | 'subscription' | 'payment' | 'complete';
|
||||
export type RegistrationStep = 'basic_info' | 'subscription' | 'payment';
|
||||
|
||||
export type BasicInfoData = {
|
||||
firstName: string;
|
||||
@@ -94,18 +94,17 @@ export const useRegistrationState = ({
|
||||
switch (prev) {
|
||||
case 'basic_info': return 'subscription';
|
||||
case 'subscription': return 'payment';
|
||||
case 'payment': return 'complete';
|
||||
// Payment is the last step - no next step
|
||||
default: return prev;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
const prevStep = useCallback(() => {
|
||||
setStep(prev => {
|
||||
switch (prev) {
|
||||
case 'subscription': return 'basic_info';
|
||||
case 'payment': return 'subscription';
|
||||
case 'complete': return 'payment';
|
||||
default: return prev;
|
||||
}
|
||||
});
|
||||
@@ -210,6 +209,7 @@ export const useRegistrationState = ({
|
||||
|
||||
return {
|
||||
step,
|
||||
setStep,
|
||||
registrationState,
|
||||
updateRegistrationState,
|
||||
nextStep,
|
||||
|
||||
@@ -118,6 +118,7 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const registerBakery = useRegisterBakery();
|
||||
const registerBakeryWithSubscription = useRegisterBakeryWithSubscription();
|
||||
const updateTenant = useUpdateTenant();
|
||||
|
||||
const handleInputChange = (field: keyof BakeryRegistration, value: string) => {
|
||||
@@ -428,7 +429,7 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
|
||||
<div className="flex justify-end pt-4 border-t-2 border-[var(--border-color)]/50">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
isLoading={registerBakery.isPending || updateTenant.isPending}
|
||||
isLoading={registerBakery.isPending || registerBakeryWithSubscription.isPending || updateTenant.isPending}
|
||||
loadingText={t(tenantId
|
||||
? 'onboarding:steps.tenant_registration.loading.updating'
|
||||
: (isEnterprise
|
||||
|
||||
Reference in New Issue
Block a user