273 lines
8.0 KiB
TypeScript
273 lines
8.0 KiB
TypeScript
/**
|
|
* Wallet Service - Platform-aware wallet management
|
|
* Uses Firebase abstraction layer for cross-platform support
|
|
*/
|
|
import { doc, collection, FieldValue } from '../firebase';
|
|
import { useUserWalletStore } from '../stores/userWalletStore';
|
|
import { withGlobalLoading } from '../stores/uiStore';
|
|
|
|
export interface CreditCard {
|
|
id: string;
|
|
cardNumber: string;
|
|
expiryDate: string;
|
|
cardType?: string;
|
|
lastFourDigits: string;
|
|
createdAt: Date;
|
|
isActive: boolean;
|
|
}
|
|
|
|
export interface UserWallet {
|
|
uid: string;
|
|
balance: number;
|
|
currency: string;
|
|
isActive: boolean;
|
|
cards: CreditCard[];
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
export class WalletService {
|
|
private static snapshotExists(docSnap: any): boolean {
|
|
const existsValue = docSnap?.exists;
|
|
if (typeof existsValue === 'function') {
|
|
try {
|
|
return !!existsValue.call(docSnap);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
return !!existsValue;
|
|
}
|
|
|
|
// Create User Wallet
|
|
static async createUserWallet(uid: string): Promise<{ success: boolean; error?: string }> {
|
|
return withGlobalLoading(async () => {
|
|
try {
|
|
const walletData: UserWallet = {
|
|
uid,
|
|
balance: 0,
|
|
currency: 'USD',
|
|
isActive: true,
|
|
cards: [],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
const walletRef = doc('wallets', uid);
|
|
await walletRef.set(walletData);
|
|
console.log('Wallet created successfully for user:', uid);
|
|
|
|
try {
|
|
await useUserWalletStore.getState().invalidateWallet(uid);
|
|
} catch (cacheError) {
|
|
console.warn('Failed to refresh wallet cache after creation', cacheError);
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Wallet creation error:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to create wallet'
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
// Update Wallet Balance
|
|
static async updateWalletBalance(
|
|
uid: string,
|
|
newBalanceInCents: number
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
return withGlobalLoading(async () => {
|
|
try {
|
|
const walletDocRef = doc('wallets', uid);
|
|
await walletDocRef.update({
|
|
balance: newBalanceInCents,
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
console.log('Wallet balance updated successfully');
|
|
|
|
try {
|
|
await useUserWalletStore.getState().invalidateWallet(uid);
|
|
} catch (cacheError) {
|
|
console.warn('Failed to refresh wallet cache after balance update', cacheError);
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Wallet balance update error:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to update wallet balance'
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add Credit Card to Wallet
|
|
static async addCreditCard(
|
|
uid: string,
|
|
cardData: {
|
|
cardNumber: string;
|
|
expiryDate: string;
|
|
cvv: string;
|
|
}
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
return withGlobalLoading(async () => {
|
|
try {
|
|
// Basic validation
|
|
if (!cardData.cardNumber || !cardData.expiryDate || !cardData.cvv) {
|
|
return { success: false, error: 'All card fields are required' };
|
|
}
|
|
|
|
// Validate card number
|
|
const cleanCardNumber = cardData.cardNumber.replace(/\s|-/g, '');
|
|
if (!/^\d{16}$/.test(cleanCardNumber)) {
|
|
return { success: false, error: 'Card number must be 16 digits' };
|
|
}
|
|
|
|
// Validate expiry date
|
|
if (!/^(0[1-9]|1[0-2])\/\d{2}$/.test(cardData.expiryDate)) {
|
|
return { success: false, error: 'Expiry date must be in MM/YY format' };
|
|
}
|
|
|
|
// Validate CVV
|
|
if (!/^\d{3,4}$/.test(cardData.cvv)) {
|
|
return { success: false, error: 'CVV must be 3 or 4 digits' };
|
|
}
|
|
|
|
// Detect card type
|
|
const cardType = WalletService.detectCardType(cleanCardNumber);
|
|
|
|
// Create card object
|
|
const newCard: CreditCard = {
|
|
id: `card_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
cardNumber: WalletService.maskCardNumber(cleanCardNumber),
|
|
expiryDate: cardData.expiryDate,
|
|
cardType,
|
|
lastFourDigits: cleanCardNumber.slice(-4),
|
|
createdAt: new Date(),
|
|
isActive: true,
|
|
};
|
|
|
|
// Get current wallet
|
|
const walletDocRef = doc('wallets', uid);
|
|
const walletDoc = await walletDocRef.get();
|
|
|
|
if (!WalletService.snapshotExists(walletDoc)) {
|
|
return { success: false, error: 'Wallet not found' };
|
|
}
|
|
|
|
const currentWallet = walletDoc.data() as UserWallet;
|
|
const updatedCards = [...(currentWallet.cards || []), newCard];
|
|
|
|
// Update wallet with new card
|
|
await walletDocRef.update({
|
|
cards: updatedCards,
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
console.log('Credit card added successfully');
|
|
|
|
try {
|
|
await useUserWalletStore.getState().invalidateWallet(uid);
|
|
} catch (cacheError) {
|
|
console.warn('Failed to refresh wallet cache after adding card', cacheError);
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Add credit card error:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to add credit card'
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
// Remove Credit Card from Wallet
|
|
static async removeCreditCard(
|
|
uid: string,
|
|
cardId: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
return withGlobalLoading(async () => {
|
|
try {
|
|
const walletDocRef = doc('wallets', uid);
|
|
const walletDoc = await walletDocRef.get();
|
|
|
|
if (!WalletService.snapshotExists(walletDoc)) {
|
|
return { success: false, error: 'Wallet not found' };
|
|
}
|
|
|
|
const currentWallet = walletDoc.data() as UserWallet;
|
|
const updatedCards = (currentWallet.cards || []).filter(card => card.id !== cardId);
|
|
|
|
await walletDocRef.update({
|
|
cards: updatedCards,
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
console.log('Credit card removed successfully');
|
|
|
|
try {
|
|
await useUserWalletStore.getState().invalidateWallet(uid);
|
|
} catch (cacheError) {
|
|
console.warn('Failed to refresh wallet cache after removing card', cacheError);
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Remove credit card error:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to remove credit card'
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
// Helper: Detect card type
|
|
private static detectCardType(cardNumber: string): string {
|
|
if (cardNumber.startsWith('4')) {
|
|
return 'Visa';
|
|
} else if (cardNumber.startsWith('5') || cardNumber.startsWith('2')) {
|
|
return 'MasterCard';
|
|
} else if (cardNumber.startsWith('3')) {
|
|
return 'American Express';
|
|
} else if (cardNumber.startsWith('6')) {
|
|
return 'Discover';
|
|
}
|
|
return 'Unknown';
|
|
}
|
|
|
|
// Helper: Mask card number
|
|
private static maskCardNumber(cardNumber: string): string {
|
|
return '**** **** **** ' + cardNumber.slice(-4);
|
|
}
|
|
|
|
// Get User Wallet
|
|
static async getUserWallet(uid: string): Promise<{ success: boolean; wallet?: UserWallet; error?: string }> {
|
|
try {
|
|
const walletDocRef = doc('wallets', uid);
|
|
const walletDoc = await walletDocRef.get();
|
|
|
|
if (WalletService.snapshotExists(walletDoc)) {
|
|
const wallet = walletDoc.data() as UserWallet;
|
|
return { success: true, wallet };
|
|
}
|
|
return { success: false, error: 'Wallet not found' };
|
|
} catch (error) {
|
|
console.error('Error fetching user wallet:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to fetch wallet'
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
export default WalletService;
|