Add subcription feature 3
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user