feat: Complete Pennyzo frontend with live waitlist tracking
- Live waitlist integration with backend API - Real-time polling every 60 seconds - Comprehensive API logging and error handling - WaitlistContext for global state management - Responsive UI with live data updates - Registration form with automatic waitlist refresh - Modern React/TypeScript architecture - Tailwind CSS styling with luxury theme
This commit is contained in:
424
src/services/PennyzoApiService.ts
Normal file
424
src/services/PennyzoApiService.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
|
||||
import { API_CONFIG } from '../config';
|
||||
import { logger } from '../lib/logger';
|
||||
import type {
|
||||
HealthResponse,
|
||||
RegistrationHealthResponse,
|
||||
RegisterRequest,
|
||||
RegisterResponse,
|
||||
LoginResponse,
|
||||
VerifyEmailResponse,
|
||||
ResendOtpResponse,
|
||||
ForgotPasswordResponse,
|
||||
PasswordResponse,
|
||||
UserMeResponse,
|
||||
UserProfile,
|
||||
UpdateUserProfileRequest,
|
||||
UpdateCompanyRequest,
|
||||
UpdateGstRegistrationRequest,
|
||||
UpgradeMembershipRequest,
|
||||
MembershipInfo,
|
||||
UserStats,
|
||||
PennyzoApiError,
|
||||
ErrorSchema
|
||||
} from '../types/api/pennyzo-api-types';
|
||||
|
||||
/**
|
||||
* PennyzoApiService - A comprehensive TypeScript class for handling all API interactions
|
||||
* with the Pennyzo General API
|
||||
*/
|
||||
export class PennyzoApiService {
|
||||
private apiClient: AxiosInstance;
|
||||
private accessToken: string | null = null;
|
||||
private readonly TOKEN_STORAGE_KEY = 'pennyzo_access_token';
|
||||
private readonly BASE_URL = API_CONFIG.BASE_URL;
|
||||
|
||||
constructor() {
|
||||
console.log('🔧 PennyzoApiService: Initializing API service...');
|
||||
// Create Axios instance with base configuration
|
||||
this.apiClient = axios.create({
|
||||
baseURL: this.BASE_URL,
|
||||
timeout: API_CONFIG.TIMEOUT,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Load token from storage if available
|
||||
this.loadTokenFromStorage();
|
||||
|
||||
// Setup request interceptor for authentication
|
||||
this.setupRequestInterceptor();
|
||||
|
||||
// Setup response interceptor for error handling
|
||||
this.setupResponseInterceptor();
|
||||
|
||||
console.log('✅ PennyzoApiService: API service initialized successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the request interceptor to attach the auth token to protected API calls
|
||||
*/
|
||||
private setupRequestInterceptor(): void {
|
||||
this.apiClient.interceptors.request.use((config) => {
|
||||
// Add start time for duration calculation
|
||||
(config as any).startTime = Date.now();
|
||||
|
||||
// Log outgoing request
|
||||
logger.apiRequest(
|
||||
config.method?.toUpperCase() || 'GET',
|
||||
`${config.baseURL}${config.url}`,
|
||||
{
|
||||
headers: { ...config.headers, Authorization: config.headers.Authorization ? '[REDACTED]' : undefined },
|
||||
data: config.data
|
||||
}
|
||||
);
|
||||
|
||||
// If we have a token, add it to the request header
|
||||
if (this.accessToken) {
|
||||
config.headers.Authorization = `Bearer ${this.accessToken}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the response interceptor to handle errors consistently
|
||||
*/
|
||||
private setupResponseInterceptor(): void {
|
||||
this.apiClient.interceptors.response.use(
|
||||
(response) => {
|
||||
// Log successful response
|
||||
logger.apiResponse(
|
||||
response.status,
|
||||
response.config.method?.toUpperCase() || 'GET',
|
||||
response.config.url || '',
|
||||
response.data,
|
||||
Date.now() - (response.config as any).startTime
|
||||
);
|
||||
return response;
|
||||
},
|
||||
(error: AxiosError<ErrorSchema>) => {
|
||||
// Log error response
|
||||
logger.apiError(
|
||||
error.response?.status || 'NETWORK',
|
||||
error.config?.method?.toUpperCase() || 'GET',
|
||||
error.config?.url || '',
|
||||
{
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
data: error.response?.data,
|
||||
message: error.message
|
||||
}
|
||||
);
|
||||
|
||||
const apiError = this.createApiError(error);
|
||||
|
||||
// Handle 401 Unauthorized - Clear token and trigger authentication flow
|
||||
if (error.response?.status === 401) {
|
||||
this.clearToken();
|
||||
}
|
||||
|
||||
return Promise.reject(apiError);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a standardized error object from API errors
|
||||
*/
|
||||
private createApiError(error: AxiosError<ErrorSchema>): PennyzoApiError {
|
||||
const apiError: PennyzoApiError = new Error(
|
||||
error.response?.data?.error?.message || error.message
|
||||
);
|
||||
|
||||
apiError.name = 'PennyzoApiError';
|
||||
apiError.status = error.response?.status;
|
||||
apiError.code = error.response?.data?.error?.type;
|
||||
apiError.details = error.response?.data?.error?.details;
|
||||
apiError.originalError = error;
|
||||
|
||||
return apiError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the authentication token from local storage
|
||||
*/
|
||||
private loadTokenFromStorage(): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem(this.TOKEN_STORAGE_KEY);
|
||||
if (token) {
|
||||
this.accessToken = token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the authentication token in local storage
|
||||
*/
|
||||
private storeToken(token: string): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(this.TOKEN_STORAGE_KEY, token);
|
||||
this.accessToken = token;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the authentication token from local storage and memory
|
||||
*/
|
||||
private clearToken(): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem(this.TOKEN_STORAGE_KEY);
|
||||
}
|
||||
this.accessToken = null;
|
||||
}
|
||||
|
||||
// ======== SYSTEM & REGISTRATION METHODS (PUBLIC) ========
|
||||
|
||||
/**
|
||||
* Check the health status of the API
|
||||
* GET /system/health
|
||||
*/
|
||||
async checkHealth(): Promise<HealthResponse> {
|
||||
const response = await this.apiClient.get<HealthResponse>('/system/health');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the health status of the registration service
|
||||
* GET /registration/health
|
||||
*/
|
||||
async checkRegistrationHealth(): Promise<RegistrationHealthResponse> {
|
||||
const response = await this.apiClient.get<RegistrationHealthResponse>('/registration/health');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new user
|
||||
* POST /registration/register
|
||||
*/
|
||||
async registerUser(registrationData: RegisterRequest): Promise<RegisterResponse> {
|
||||
const response = await this.apiClient.post<RegisterResponse>(
|
||||
'/registration/register',
|
||||
registrationData
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ======== AUTHENTICATION & VERIFICATION METHODS (PUBLIC) ========
|
||||
|
||||
/**
|
||||
* Login with email and password
|
||||
* POST /auth/login
|
||||
* Content-Type: application/x-www-form-urlencoded
|
||||
*/
|
||||
async login(email: string, password: string): Promise<LoginResponse> {
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('username', email); // API uses username field for email
|
||||
formData.append('password', password);
|
||||
|
||||
const config: AxiosRequestConfig = {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
};
|
||||
|
||||
const response = await this.apiClient.post<LoginResponse>(
|
||||
'/auth/login',
|
||||
formData,
|
||||
config
|
||||
);
|
||||
|
||||
// Store the token
|
||||
this.storeToken(response.data.access_token);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify user's email with OTP
|
||||
* POST /auth/verify-email
|
||||
*/
|
||||
async verifyEmail(email: string, otp_code: string): Promise<VerifyEmailResponse> {
|
||||
const response = await this.apiClient.post<VerifyEmailResponse>(
|
||||
'/auth/verify-email',
|
||||
{ email, otp_code }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend verification OTP to email
|
||||
* POST /auth/resend-otp
|
||||
*/
|
||||
async resendVerificationOtp(email: string): Promise<ResendOtpResponse> {
|
||||
const response = await this.apiClient.post<ResendOtpResponse>(
|
||||
'/auth/resend-otp',
|
||||
{ email }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ======== PASSWORD MANAGEMENT METHODS (PUBLIC/AUTHENTICATED) ========
|
||||
|
||||
/**
|
||||
* Request a password reset (forgot password)
|
||||
* POST /auth/forgot-password
|
||||
*/
|
||||
async forgotPassword(email: string): Promise<ForgotPasswordResponse> {
|
||||
const response = await this.apiClient.post<ForgotPasswordResponse>(
|
||||
'/auth/forgot-password',
|
||||
{ email }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password using OTP
|
||||
* POST /auth/reset-password
|
||||
*/
|
||||
async resetPasswordWithOtp(
|
||||
email: string,
|
||||
otp_code: string,
|
||||
new_password: string
|
||||
): Promise<PasswordResponse> {
|
||||
const response = await this.apiClient.post<PasswordResponse>(
|
||||
'/auth/reset-password',
|
||||
{ email, otp_code, new_password }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change password (requires authentication)
|
||||
* POST /auth/change-password
|
||||
*/
|
||||
async changePassword(
|
||||
current_password: string,
|
||||
new_password: string
|
||||
): Promise<PasswordResponse> {
|
||||
const response = await this.apiClient.post<PasswordResponse>(
|
||||
'/auth/change-password',
|
||||
{ current_password, new_password }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ======== USER & PROFILE MANAGEMENT METHODS (AUTHENTICATED) ========
|
||||
|
||||
/**
|
||||
* Get current user information
|
||||
* GET /auth/me
|
||||
*/
|
||||
async getCurrentUser(): Promise<UserMeResponse> {
|
||||
const response = await this.apiClient.get<UserMeResponse>('/auth/me');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user profile details
|
||||
* GET /users/profile
|
||||
*/
|
||||
async getUserProfile(): Promise<UserProfile> {
|
||||
const response = await this.apiClient.get<UserProfile>('/users/profile');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user profile information
|
||||
* PUT /users/profile
|
||||
*/
|
||||
async updateUserProfile(profileUpdateData: UpdateUserProfileRequest): Promise<UserProfile> {
|
||||
const response = await this.apiClient.put<UserProfile>(
|
||||
'/users/profile',
|
||||
profileUpdateData
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update company information
|
||||
* PUT /users/company
|
||||
*/
|
||||
async updateCompanyInfo(companyUpdateData: UpdateCompanyRequest): Promise<UserProfile> {
|
||||
const response = await this.apiClient.put<UserProfile>(
|
||||
'/users/company',
|
||||
companyUpdateData
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update GST registration information
|
||||
* PUT /users/gst-registration
|
||||
*/
|
||||
async updateGstRegistration(gstUpdateData: UpdateGstRegistrationRequest): Promise<UserProfile> {
|
||||
const response = await this.apiClient.put<UserProfile>(
|
||||
'/users/gst-registration',
|
||||
gstUpdateData
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ======== MEMBERSHIP & STATS METHODS (AUTHENTICATED) ========
|
||||
|
||||
/**
|
||||
* Upgrade user membership
|
||||
* POST /users/upgrade-membership
|
||||
*/
|
||||
async upgradeMembership(membershipData: UpgradeMembershipRequest): Promise<UserProfile> {
|
||||
const response = await this.apiClient.post<UserProfile>(
|
||||
'/users/upgrade-membership',
|
||||
membershipData
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get membership information
|
||||
* GET /users/membership-info
|
||||
*/
|
||||
async getMembershipInfo(): Promise<MembershipInfo> {
|
||||
const response = await this.apiClient.get<MembershipInfo>('/users/membership-info');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user statistics
|
||||
* GET /users/stats
|
||||
*/
|
||||
async getUserStats(): Promise<UserStats> {
|
||||
const response = await this.apiClient.get<UserStats>('/users/stats');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ======== SESSION MANAGEMENT ========
|
||||
|
||||
/**
|
||||
* Logout user by clearing the access token
|
||||
*/
|
||||
logout(): void {
|
||||
this.clearToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is currently authenticated
|
||||
*/
|
||||
isAuthenticated(): boolean {
|
||||
return !!this.accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current access token (if any)
|
||||
*/
|
||||
getAccessToken(): string | null {
|
||||
return this.accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance for easy import and usage
|
||||
export const pennyzoApi = new PennyzoApiService();
|
||||
|
||||
// Default export for convenient import
|
||||
export default PennyzoApiService;
|
||||
187
src/services/README.md
Normal file
187
src/services/README.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Pennyzo API Service
|
||||
|
||||
A comprehensive TypeScript class for handling all API interactions with the Pennyzo General API.
|
||||
|
||||
## Features
|
||||
|
||||
- Complete JWT authentication lifecycle management
|
||||
- Automatic token handling for authenticated requests using Axios interceptors
|
||||
- Comprehensive error handling
|
||||
- TypeScript interfaces for request bodies and response payloads
|
||||
- Environment-aware API URL configuration
|
||||
- Singleton pattern for easy usage throughout the application
|
||||
|
||||
## Installation
|
||||
|
||||
The service is already part of the Pennyzo Web project. It requires Axios, which is installed as a project dependency.
|
||||
|
||||
## Configuration
|
||||
|
||||
The service automatically detects the environment and uses the appropriate API URL:
|
||||
|
||||
- **Development**: `http://localhost:8000` (when `import.meta.env.DEV` is true)
|
||||
- **Production**: `https://api.pennyzo.com/api/v1/general` (when `import.meta.env.PROD` is true)
|
||||
|
||||
You can override the default by setting the `VITE_API_BASE_URL` environment variable in your `.env` file.
|
||||
|
||||
## API Methods
|
||||
|
||||
### System & Registration (Public)
|
||||
|
||||
```typescript
|
||||
// Check health status of the API
|
||||
const healthStatus = await pennyzoApi.checkHealth();
|
||||
|
||||
// Register a new user
|
||||
const registeredUser = await pennyzoApi.registerUser({
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: "john.doe@example.com",
|
||||
// other required fields...
|
||||
});
|
||||
```
|
||||
|
||||
### Authentication & Verification (Public)
|
||||
|
||||
```typescript
|
||||
// Login a user
|
||||
const loginResult = await pennyzoApi.login("user@example.com", "password123");
|
||||
|
||||
// Verify email with OTP
|
||||
const verificationResult = await pennyzoApi.verifyEmail(
|
||||
"user@example.com",
|
||||
"123456"
|
||||
);
|
||||
|
||||
// Resend verification OTP
|
||||
const resendResult = await pennyzoApi.resendVerificationOtp("user@example.com");
|
||||
```
|
||||
|
||||
### Password Management (Public/Authenticated)
|
||||
|
||||
```typescript
|
||||
// Request password reset
|
||||
const forgotPasswordResult = await pennyzoApi.forgotPassword(
|
||||
"user@example.com"
|
||||
);
|
||||
|
||||
// Reset password with OTP
|
||||
const resetResult = await pennyzoApi.resetPasswordWithOtp(
|
||||
"user@example.com",
|
||||
"123456",
|
||||
"newPassword123"
|
||||
);
|
||||
|
||||
// Change password (authenticated)
|
||||
const changePasswordResult = await pennyzoApi.changePassword(
|
||||
"currentPassword123",
|
||||
"newPassword123"
|
||||
);
|
||||
```
|
||||
|
||||
### User & Profile Management (Authenticated)
|
||||
|
||||
```typescript
|
||||
// Get current user information
|
||||
const currentUser = await pennyzoApi.getCurrentUser();
|
||||
|
||||
// Get user profile
|
||||
const userProfile = await pennyzoApi.getUserProfile();
|
||||
|
||||
// Update user profile
|
||||
const updatedProfile = await pennyzoApi.updateUserProfile({
|
||||
first_name: "Updated",
|
||||
last_name: "Name",
|
||||
// other fields to update
|
||||
});
|
||||
|
||||
// Update company information
|
||||
const updatedCompany = await pennyzoApi.updateCompanyInfo({
|
||||
company_name: "New Company Name",
|
||||
industry: "Technology",
|
||||
});
|
||||
|
||||
// Update GST registration
|
||||
const updatedGst = await pennyzoApi.updateGstRegistration({
|
||||
gst_registered: true,
|
||||
gst_number: "GSTIN12345678",
|
||||
});
|
||||
```
|
||||
|
||||
### Membership & Stats (Authenticated)
|
||||
|
||||
```typescript
|
||||
// Upgrade membership
|
||||
const upgradedMembership = await pennyzoApi.upgradeMembership({
|
||||
plan_type: "ELITE",
|
||||
membership_type: "PAID",
|
||||
billing_cycle: "yearly",
|
||||
});
|
||||
|
||||
// Get membership information
|
||||
const membershipInfo = await pennyzoApi.getMembershipInfo();
|
||||
|
||||
// Get user statistics
|
||||
const userStats = await pennyzoApi.getUserStats();
|
||||
```
|
||||
|
||||
### Session Management
|
||||
|
||||
```typescript
|
||||
// Logout user (clears stored token)
|
||||
pennyzoApi.logout();
|
||||
|
||||
// Check if user is authenticated
|
||||
const isAuthenticated = pennyzoApi.isAuthenticated();
|
||||
|
||||
// Get the current access token (if any)
|
||||
const accessToken = pennyzoApi.getAccessToken();
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The service handles errors gracefully and provides meaningful error objects with details:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const profile = await pennyzoApi.getUserProfile();
|
||||
} catch (error) {
|
||||
console.error("Error status:", error.status);
|
||||
console.error("Error message:", error.message);
|
||||
console.error("Error code:", error.code);
|
||||
console.error("Error details:", error.details);
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
- Uses Axios for all HTTP requests
|
||||
- Base URL is set to `https://api.pennyzo.com/api/v1/general`
|
||||
- Automatically attaches Bearer token to authenticated requests
|
||||
- Handles token storage in localStorage
|
||||
- Special handling for form-urlencoded content type (login endpoint)
|
||||
- Automatic 401 error handling to clear invalid tokens
|
||||
|
||||
## Usage Example
|
||||
|
||||
```typescript
|
||||
// Import the singleton instance
|
||||
import { pennyzoApi } from "../services";
|
||||
|
||||
// Use in async/await context
|
||||
async function fetchUserData() {
|
||||
try {
|
||||
// First authenticate
|
||||
await pennyzoApi.login("user@example.com", "password");
|
||||
|
||||
// Then make authenticated calls
|
||||
const profile = await pennyzoApi.getUserProfile();
|
||||
const stats = await pennyzoApi.getUserStats();
|
||||
|
||||
return { profile, stats };
|
||||
} catch (error) {
|
||||
console.error("API Error:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
2
src/services/index.ts
Normal file
2
src/services/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { PennyzoApiService, pennyzoApi } from './PennyzoApiService';
|
||||
export { default } from './PennyzoApiService';
|
||||
Reference in New Issue
Block a user