Add subcription feature 3

This commit is contained in:
Urtzi Alfaro
2026-01-15 20:45:49 +01:00
parent a4c3b7da3f
commit b674708a4c
83 changed files with 9451 additions and 6828 deletions

View File

@@ -40,14 +40,9 @@ export interface AuthState {
// Actions
login: (email: string, password: string) => Promise<void>;
register: (userData: {
email: string;
password: string;
full_name: string;
tenant_name?: string;
subscription_plan?: string;
payment_method_id?: string;
}) => Promise<void>;
logout: () => void;
refreshAuth: () => Promise<void>;
updateUser: (updates: Partial<User>) => void;
@@ -56,6 +51,41 @@ export interface AuthState {
setDemoAuth: (token: string, demoUser: Partial<User>, subscriptionTier?: string) => void;
setPendingSubscriptionId: (subscriptionId: string | null) => void; // Store subscription ID from registration
// Registration Flow - Atomic Registration with 3DS Support
createRegistrationPaymentSetup: (registrationData: {
email: string;
full_name: string;
password: string;
subscription_plan: string;
billing_cycle: string;
payment_method_id: string;
coupon_code?: string;
terms_accepted: boolean;
privacy_accepted: boolean;
marketing_consent: boolean;
analytics_consent: boolean;
}) => Promise<any>;
verifySetupIntent: (setupIntentId: string) => Promise<any>;
completeRegistrationAfterPayment: (registrationData: {
email: string;
full_name: string;
password: string;
subscription_plan: string;
billing_cycle: string;
payment_customer_id: string;
payment_method_id: string;
subscription_id: string;
coupon_code?: string;
terms_accepted: boolean;
privacy_accepted: boolean;
marketing_consent: boolean;
analytics_consent: boolean;
threeds_completed: boolean;
setup_intent_id: string;
}) => Promise<any>;
// Permission helpers
hasPermission: (permission: string) => boolean;
hasRole: (role: string) => boolean;
@@ -125,191 +155,6 @@ export const useAuthStore = create<AuthState>()(
}
},
register: async (userData: {
email: string;
password: string;
full_name: string;
tenant_name?: string;
subscription_plan?: string;
payment_method_id?: string;
}) => {
try {
set({ isLoading: true, error: null });
const response = await authService.register(userData);
if (response && response.access_token) {
// Set the auth tokens on the API client immediately
apiClient.setAuthToken(response.access_token);
if (response.refresh_token) {
apiClient.setRefreshToken(response.refresh_token);
}
set({
user: response.user || null,
token: response.access_token,
refreshToken: response.refresh_token || null,
isAuthenticated: true,
isLoading: false,
error: null,
});
} else {
throw new Error('Registration failed');
}
} catch (error) {
set({
user: null,
token: null,
refreshToken: null,
isAuthenticated: false,
isLoading: false,
error: error instanceof Error ? error.message : 'Error de registro',
});
throw error;
}
},
registerWithSubscription: async (userData: {
email: string;
password: string;
full_name: string;
tenant_name?: string;
subscription_plan?: string;
payment_method_id?: string;
billing_cycle?: 'monthly' | 'yearly';
coupon_code?: string;
address?: string;
postal_code?: string;
city?: string;
country?: string;
}) => {
try {
set({ isLoading: true, error: null });
const response = await authService.registerWithSubscription(userData);
// NEW ARCHITECTURE: Check if SetupIntent verification is required
if (response && response.requires_action) {
// SetupIntent required - NO user created yet, NO tokens returned
// Store registration data for post-3DS completion
const pendingRegistrationData = {
email: userData.email,
password: userData.password,
full_name: userData.full_name,
setup_intent_id: response.setup_intent_id,
plan_id: response.plan_id,
payment_method_id: response.payment_method_id,
billing_interval: response.billing_interval,
coupon_code: response.coupon_code,
customer_id: response.customer_id,
payment_customer_id: response.payment_customer_id,
trial_period_days: response.trial_period_days,
client_secret: response.client_secret
};
// Store in session storage for post-3DS completion
sessionStorage.setItem('pending_registration_data', JSON.stringify(pendingRegistrationData));
set({
isLoading: false,
error: null,
pendingRegistrationData,
});
// Return the SetupIntent data for frontend to handle 3DS
return response;
}
// OLD FLOW: No SetupIntent required - user created and authenticated
if (response && response.access_token) {
// Set the auth tokens on the API client immediately
apiClient.setAuthToken(response.access_token);
if (response.refresh_token) {
apiClient.setRefreshToken(response.refresh_token);
}
// Store subscription ID in state for onboarding flow (instead of localStorage for security)
const pendingSubscriptionId = response.subscription_id || null;
set({
user: response.user || null,
token: response.access_token,
refreshToken: response.refresh_token || null,
isAuthenticated: true,
isLoading: false,
error: null,
pendingSubscriptionId,
});
} else {
throw new Error('Registration with subscription failed');
}
} catch (error) {
set({
user: null,
token: null,
refreshToken: null,
isAuthenticated: false,
isLoading: false,
error: error instanceof Error ? error.message : 'Error de registro con suscripción',
});
throw error;
}
},
completeRegistrationAfterSetupIntent: async (completionData: {
email: string;
password: string;
full_name: string;
setup_intent_id: string;
plan_id: string;
payment_method_id: string;
billing_interval: 'monthly' | 'yearly';
coupon_code?: string;
}) => {
try {
set({ isLoading: true, error: null });
const response = await authService.completeRegistrationAfterSetupIntent(completionData);
if (response && response.access_token) {
// Set the auth tokens on the API client immediately
apiClient.setAuthToken(response.access_token);
if (response.refresh_token) {
apiClient.setRefreshToken(response.refresh_token);
}
// Store subscription ID in state for onboarding flow
const pendingSubscriptionId = response.subscription_id || null;
// Clear pending registration data from session storage
sessionStorage.removeItem('pending_registration_data');
set({
user: response.user || null,
token: response.access_token,
refreshToken: response.refresh_token || null,
isAuthenticated: true,
isLoading: false,
error: null,
pendingSubscriptionId,
pendingRegistrationData: null,
});
} else {
throw new Error('Registration completion failed');
}
} catch (error) {
set({
user: null,
token: null,
refreshToken: null,
isAuthenticated: false,
isLoading: false,
error: error instanceof Error ? error.message : 'Error completando el registro',
});
throw error;
}
},
logout: () => {
// Clear the auth tokens from API client
apiClient.setAuthToken(null);
@@ -441,6 +286,249 @@ export const useAuthStore = create<AuthState>()(
console.log('✅ [Auth Store] Demo auth state updated (no JWT token)', { subscriptionTier });
},
// ===================================================================
// REGISTRATION FLOW - Atomic Registration with 3DS Support
// ===================================================================
/**
* Create registration payment setup (Step 1 of atomic registration)
* This initiates the SetupIntent-first registration flow
*/
createRegistrationPaymentSetup: async (registrationData: {
email: string;
full_name: string;
password: string;
subscription_plan: string;
billing_cycle: string;
payment_method_id: string;
coupon_code?: string;
terms_accepted: boolean;
privacy_accepted: boolean;
marketing_consent: boolean;
analytics_consent: boolean;
}) => {
try {
set({ isLoading: true, error: null });
// Map frontend data to backend UserRegistration schema
const userData: any = {
email: registrationData.email,
password: registrationData.password,
full_name: registrationData.full_name,
subscription_plan: registrationData.subscription_plan,
billing_cycle: registrationData.billing_cycle,
payment_method_id: registrationData.payment_method_id,
coupon_code: registrationData.coupon_code,
terms_accepted: registrationData.terms_accepted,
privacy_accepted: registrationData.privacy_accepted,
marketing_consent: registrationData.marketing_consent,
analytics_consent: registrationData.analytics_consent,
};
// Call the auth service to start registration
const response = await authService.startRegistration(userData);
if (response.requires_action) {
// 3DS required - return SetupIntent data for frontend handling
return {
requires_action: true,
action_type: response.action_type,
client_secret: response.client_secret,
setup_intent_id: response.setup_intent_id,
customer_id: response.customer_id,
payment_customer_id: response.payment_customer_id,
plan_id: response.plan_id,
payment_method_id: response.payment_method_id,
billing_cycle: response.billing_cycle,
email: response.email,
};
} else {
// No 3DS required - registration completed immediately
if (response.user) {
// Set the auth tokens on the API client
if (response.access_token) {
apiClient.setAuthToken(response.access_token);
}
if (response.refresh_token) {
apiClient.setRefreshToken(response.refresh_token);
}
// Extract subscription from JWT if available
const jwtSubscription = response.access_token ? getSubscriptionFromJWT(response.access_token) : null;
const jwtTenantAccess = response.access_token ? getTenantAccessFromJWT(response.access_token) : null;
const primaryTenantId = response.access_token ? getPrimaryTenantIdFromJWT(response.access_token) : null;
set({
user: {
id: response.user.id,
email: response.user.email,
full_name: response.user.full_name,
is_active: response.user.is_active,
is_verified: response.user.is_verified,
created_at: response.user.created_at,
tenant_id: response.user.tenant_id,
role: response.user.role,
},
token: response.access_token || null,
refreshToken: response.refresh_token || null,
isAuthenticated: true,
isLoading: false,
error: null,
jwtSubscription,
jwtTenantAccess,
primaryTenantId,
pendingSubscriptionId: response.subscription_id || null,
});
return {
success: true,
requires_action: false,
user: response.user,
subscription_id: response.subscription_id,
payment_customer_id: response.payment_customer_id,
message: response.message,
};
}
}
return response;
} catch (error) {
set({
isLoading: false,
error: error instanceof Error ? error.message : 'Registration setup failed',
});
throw error;
}
},
/**
* Verify SetupIntent status (optional step in registration flow)
*/
verifySetupIntent: async (setupIntentId: string) => {
try {
set({ isLoading: true, error: null });
// This would call a backend endpoint to verify SetupIntent status
// For now, we'll return a mock response
return {
status: 'succeeded',
setup_intent_id: setupIntentId,
requires_action: false,
};
} catch (error) {
set({
isLoading: false,
error: error instanceof Error ? error.message : 'SetupIntent verification failed',
});
throw error;
}
},
/**
* Complete registration after payment verification (Step 2 of atomic registration)
* This is called after frontend confirms SetupIntent and handles 3DS
*/
completeRegistrationAfterPayment: async (registrationData: {
email: string;
full_name: string;
password: string;
subscription_plan: string;
billing_cycle: string;
payment_customer_id: string;
payment_method_id: string;
subscription_id: string;
coupon_code?: string;
terms_accepted: boolean;
privacy_accepted: boolean;
marketing_consent: boolean;
analytics_consent: boolean;
threeds_completed: boolean;
setup_intent_id: string;
}) => {
try {
set({ isLoading: true, error: null });
// Prepare verification data for backend
const verificationData = {
setup_intent_id: registrationData.setup_intent_id,
user_data: {
email: registrationData.email,
password: registrationData.password,
full_name: registrationData.full_name,
subscription_plan: registrationData.subscription_plan,
billing_cycle: registrationData.billing_cycle,
payment_method_id: registrationData.payment_method_id,
coupon_code: registrationData.coupon_code,
terms_accepted: registrationData.terms_accepted,
privacy_accepted: registrationData.privacy_accepted,
marketing_consent: registrationData.marketing_consent,
analytics_consent: registrationData.analytics_consent,
},
};
// Call the auth service to complete registration
const response = await authService.completeRegistration(verificationData);
if (response.success && response.user) {
// Set the auth tokens on the API client
if (response.access_token) {
apiClient.setAuthToken(response.access_token);
}
if (response.refresh_token) {
apiClient.setRefreshToken(response.refresh_token);
}
// Extract subscription from JWT if available
const jwtSubscription = response.access_token ? getSubscriptionFromJWT(response.access_token) : null;
const jwtTenantAccess = response.access_token ? getTenantAccessFromJWT(response.access_token) : null;
const primaryTenantId = response.access_token ? getPrimaryTenantIdFromJWT(response.access_token) : null;
set({
user: {
id: response.user.id,
email: response.user.email,
full_name: response.user.full_name,
is_active: response.user.is_active,
is_verified: response.user.is_verified,
created_at: response.user.created_at,
tenant_id: response.user.tenant_id,
role: response.user.role,
},
token: response.access_token || null,
refreshToken: response.refresh_token || null,
isAuthenticated: true,
isLoading: false,
error: null,
jwtSubscription,
jwtTenantAccess,
primaryTenantId,
pendingSubscriptionId: response.subscription_id || null,
});
return {
success: true,
user: response.user,
subscription_id: response.subscription_id,
payment_customer_id: response.payment_customer_id,
message: response.message,
access_token: response.access_token,
refresh_token: response.refresh_token,
};
}
return {
success: false,
message: response.message || 'Registration completion failed',
};
} catch (error) {
set({
isLoading: false,
error: error instanceof Error ? error.message : 'Registration completion failed',
});
throw error;
}
},
// Permission helpers - Global user permissions only
hasPermission: (_permission: string): boolean => {
const { user } = get();
@@ -531,9 +619,6 @@ export const usePermissions = () => useAuthStore((state) => ({
// Hook for auth actions
export const useAuthActions = () => useAuthStore((state) => ({
login: state.login,
register: state.register,
registerWithSubscription: state.registerWithSubscription,
completeRegistrationAfterSetupIntent: state.completeRegistrationAfterSetupIntent,
logout: state.logout,
refreshAuth: state.refreshAuth,
updateUser: state.updateUser,