Spaces:
Running
Running
| // Wallet Component for Trust Economy | |
| class Wallet { | |
| static init() { | |
| console.log('Wallet component initialized'); | |
| this.setupWalletInterface(); | |
| this.loadWalletData(); | |
| this.setupRealTimeUpdates(); | |
| } | |
| static setupWalletInterface() { | |
| // Setup wallet interaction handlers | |
| const walletButtons = document.querySelectorAll('[data-wallet-action]'); | |
| walletButtons.forEach(button => { | |
| button.addEventListener('click', (e) => { | |
| const action = e.target.dataset.walletAction; | |
| this.handleWalletAction(action); | |
| }); | |
| }); | |
| } | |
| static handleWalletAction(action) { | |
| switch (action) { | |
| case 'view-transactions': | |
| this.showTransactionHistory(); | |
| break; | |
| case 'withdraw': | |
| this.showWithdrawModal(); | |
| break; | |
| case 'add-payment-method': | |
| this.showPaymentMethodModal(); | |
| break; | |
| default: | |
| console.log('Unknown wallet action:', action); | |
| } | |
| } | |
| static async processReferralReward(qrId, conversionData) { | |
| try { | |
| // Calculate reward based on conversion value and recommender trust | |
| const baseReward = this.calculateBaseReward(conversionData.salePrice); | |
| const trustMultiplier = this.getTrustMultiplier(conversionData.recommenderTrust); | |
| const wageBonus = this.getWageTransparencyBonus(conversionData.merchantId); | |
| const totalReward = baseReward * trustMultiplier * wageBonus; | |
| const reward = { | |
| id: this.generateTransactionId(), | |
| type: 'referral_reward', | |
| amount: totalReward, | |
| currency: 'USD', | |
| status: 'pending_maturation', | |
| qrId, | |
| conversionId: conversionData.id, | |
| recipientId: conversionData.recommenderId, | |
| metadata: { | |
| baseReward, | |
| trustMultiplier, | |
| wageBonus, | |
| salePrice: conversionData.salePrice | |
| }, | |
| createdAt: new Date().toISOString(), | |
| maturationDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() // 7 days | |
| }; | |
| // Store pending reward | |
| this.storePendingReward(reward); | |
| return { success: true, reward }; | |
| } catch (error) { | |
| console.error('Referral reward processing failed:', error); | |
| return { success: false, error: error.message }; | |
| } | |
| } | |
| static calculateBaseReward(salePrice) { | |
| // Base reward calculation: 5-10% of sale price | |
| const rate = Math.max(0.05, Math.min(0.10, salePrice / 1000)); | |
| return salePrice * rate; | |
| } | |
| static getTrustMultiplier(trustLevel) { | |
| // Trust level affects reward multiplier | |
| const multipliers = { | |
| 'low': 0.8, | |
| 'medium': 1.0, | |
| 'high': 1.2, | |
| 'verified': 1.5 | |
| }; | |
| return multipliers[trustLevel] || 1.0; | |
| } | |
| static getWageTransparencyBonus(merchantId) { | |
| // Bonus for supporting wage-transparent merchants | |
| // In real implementation, this would check merchant's wage transparency score | |
| const wageTransparent = this.isWageTransparent(merchantId); | |
| return wageTransparent ? 1.02 : 1.0; // 2% bonus | |
| } | |
| static isWageTransparent(merchantId) { | |
| // Check if merchant meets wage transparency requirements | |
| const transparentMerchants = JSON.parse(localStorage.getItem('wageTransparentMerchants') || '[]'); | |
| return transparentMerchants.includes(merchantId); | |
| } | |
| static async processReviewBonus(reviewId, bonusAmount) { | |
| const review = this.getReviewById(reviewId); | |
| if (!review) { | |
| throw new Error('Review not found'); | |
| } | |
| // Check if review meets bonus criteria | |
| if (!this.isReviewBonusEligible(review)) { | |
| throw new Error('Review not eligible for bonus'); | |
| } | |
| const bonus = { | |
| id: this.generateTransactionId(), | |
| type: 'review_bonus', | |
| amount: bonusAmount, | |
| currency: 'USD', | |
| status: 'completed', | |
| reviewId, | |
| recipientId: review.userId, | |
| metadata: { | |
| reviewRating: review.rating, | |
| dimensions: review.dimensions, | |
| maturityDays: this.getDaysSinceReview(review.createdAt) | |
| }, | |
| createdAt: new Date().toISOString() | |
| }; | |
| this.addToWallet(review.userId, bonus.amount); | |
| this.storeTransaction(bonus); | |
| return { success: true, bonus }; | |
| } | |
| static isReviewBonusEligible(review) { | |
| const maturityDays = this.getDaysSinceReview(review.createdAt); | |
| const hasMinimumRating = review.rating >= 4; | |
| const hasDetailedContent = review.content && review.content.length >= 100; | |
| const hasAllDimensions = Object.keys(review.dimensions || {}).length >= 3; | |
| return maturityDays >= 14 && hasMinimumRating && hasDetailedContent && hasAllDimensions; | |
| } | |
| static getDaysSinceReview(createdAt) { | |
| return Math.floor((new Date() - new Date(createdAt)) / (1000 * 60 * 60 * 24)); | |
| } | |
| static async processDisputeRefund(disputeData) { | |
| // Handle refund processing due to disputes | |
| const refund = { | |
| id: this.generateTransactionId(), | |
| type: 'dispute_refund', | |
| amount: -Math.abs(disputeData.refundAmount), | |
| currency: 'USD', | |
| status: 'completed', | |
| originalTransactionId: disputeData.originalTxId, | |
| recipientId: disputeData.userId, | |
| metadata: { | |
| disputeReason: disputeData.reason, | |
| resolvedAt: new Date().toISOString(), | |
| merchantId: disputeData.merchantId | |
| }, | |
| createdAt: new Date().toISOString() | |
| }; | |
| // Subtract from merchant payout if applicable | |
| if (disputeData.merchantId) { | |
| await this.adjustMerchantPayout(disputeData.merchantId, refund.amount); | |
| } | |
| // Add back to user wallet | |
| this.addToWallet(disputeData.userId, Math.abs(refund.amount)); | |
| this.storeTransaction(refund); | |
| // Trigger score recalculation due to refund | |
| this.triggerScoreRecalculation(disputeData.productId); | |
| return { success: true, refund }; | |
| } | |
| static async adjustMerchantPayout(merchantId, adjustmentAmount) { | |
| // Adjust merchant payout for refunds/disputes | |
| const merchantPayouts = JSON.parse(localStorage.getItem(`merchantPayouts_${merchantId}`) || '[]'); | |
| const adjustment = { | |
| id: this.generateTransactionId(), | |
| type: 'payout_adjustment', | |
| amount: adjustmentAmount, | |
| currency: 'USD', | |
| status: 'pending', | |
| merchantId, | |
| reason: 'dispute_refund', | |
| createdAt: new Date().toISOString() | |
| }; | |
| merchantPayouts.push(adjustment); | |
| localStorage.setItem(`merchantPayouts_${merchantId}`, JSON.stringify(merchantPayouts)); | |
| } | |
| static addToWallet(userId, amount) { | |
| const wallets = JSON.parse(localStorage.getItem('wallets') || '{}'); | |
| if (!wallets[userId]) { | |
| wallets[userId] = { | |
| balance: 0, | |
| transactions: [], | |
| createdAt: new Date().toISOString() | |
| }; | |
| } | |
| wallets[userId].balance += amount; | |
| wallets[userId].lastUpdated = new Date().toISOString(); | |
| localStorage.setItem('wallets', JSON.stringify(wallets)); | |
| } | |
| static getWalletBalance(userId) { | |
| const wallets = JSON.parse(localStorage.getItem('wallets') || '{}'); | |
| return wallets[userId]?.balance || 0; | |
| } | |
| static storeTransaction(transaction) { | |
| const allTransactions = JSON.parse(localStorage.getItem('allTransactions') || '[]'); | |
| allTransactions.push(transaction); | |
| localStorage.setItem('allTransactions', JSON.stringify(allTransactions)); | |
| } | |
| static storePendingReward(reward) { | |
| const pendingRewards = JSON.parse(localStorage.getItem('pendingRewards') || '[]'); | |
| pendingRewards.push(reward); | |
| localStorage.setItem('pendingRewards', JSON.stringify(pendingRewards)); | |
| } | |
| static checkPendingMaturation() { | |
| const pendingRewards = JSON.parse(localStorage.getItem('pendingRewards') || '[]'); | |
| const now = new Date(); | |
| pendingRewards.forEach(reward => { | |
| if (new Date(reward.maturationDate) <= now && reward.status === 'pending_maturation') { | |
| this.matureReward(reward); | |
| } | |
| }); | |
| } | |
| static matureReward(reward) { | |
| // Move pending reward to completed wallet transaction | |
| reward.status = 'completed'; | |
| reward.maturedAt = new Date().toISOString(); | |
| this.addToWallet(reward.recipientId, reward.amount); | |
| this.storeTransaction(reward); | |
| // Update pending rewards list | |
| const pendingRewards = JSON.parse(localStorage.getItem('pendingRewards') || '[]'); | |
| const updatedRewards = pendingRewards.filter(r => r.id !== reward.id); | |
| localStorage.setItem('pendingRewards', JSON.stringify(updatedRewards)); | |
| // Notify user | |
| if (window.OneSearchApp) { | |
| window.OneSearchApp.showNotification( | |
| `Referral reward of $${reward.amount.toFixed(2)} has matured`, | |
| 'success' | |
| ); | |
| } | |
| } | |
| static loadWalletData() { | |
| const userId = window.OneSearchApp?.user?.id; | |
| if (!userId) return; | |
| // Initialize demo wallet data if not exists | |
| const wallets = JSON.parse(localStorage.getItem('wallets') || '{}'); | |
| if (!wallets[userId]) { | |
| wallets[userId] = { | |
| balance: 47.23, | |
| transactions: [ | |
| { | |
| id: 'tx_1', | |
| type: 'referral_reward', | |
| amount: 3.45, | |
| currency: 'USD', | |
| status: 'completed', | |
| description: 'Kitchen organizer purchase', | |
| createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString() | |
| }, | |
| { | |
| id: 'tx_2', | |
| type: 'review_bonus', | |
| amount: 1.20, | |
| currency: 'USD', | |
| status: 'completed', | |
| description: 'Verified product review', | |
| createdAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString() | |
| } | |
| ], | |
| createdAt: new Date().toISOString() | |
| }; | |
| localStorage.setItem('wallets', JSON.stringify(wallets)); | |
| } | |
| this.updateWalletDisplay(userId); | |
| } | |
| static updateWalletDisplay(userId) { | |
| const wallets = JSON.parse(localStorage.getItem('wallets') || '{}'); | |
| const wallet = wallets[userId]; | |
| if (!wallet) return; | |
| // Update balance display | |
| const balanceElements = document.querySelectorAll('.wallet-balance'); | |
| balanceElements.forEach(el => { | |
| el.textContent = `$${wallet.balance.toFixed(2)}`; | |
| }); | |
| // Update transaction history | |
| this.updateTransactionDisplay(wallet.transactions); | |
| } | |
| static updateTransactionDisplay(transactions) { | |
| const container = document.querySelector('#wallet .space-y-3'); | |
| if (!container) return; | |
| // Clear existing transactions | |
| container.innerHTML = ''; | |
| // Add current transactions | |
| transactions.forEach(tx => { | |
| const txElement = this.createTransactionElement(tx); | |
| container.appendChild(txElement); | |
| }); | |
| } | |
| static createTransactionElement(transaction) { | |
| const element = document.createElement('div'); | |
| element.className = 'flex justify-between items-center p-3 border rounded-lg'; | |
| const typeColors = { | |
| 'referral_reward': 'text-green-600', | |
| 'review_bonus': 'text-green-600', | |
| 'dispute_refund': 'text-blue-600', | |
| 'withdrawal': 'text-red-600' | |
| }; | |
| const iconMap = { | |
| 'referral_reward': 'share-2', | |
| 'review_bonus': 'star', | |
| 'dispute_refund': 'rotate-ccw', | |
| 'withdrawal': 'arrow-up' | |
| }; | |
| const timeAgo = this.getTimeAgo(transaction.createdAt); | |
| const amountColor = transaction.amount >= 0 ? 'text-green-600' : 'text-red-600'; | |
| element.innerHTML = ` | |
| <div> | |
| <div class="text-sm font-medium">${this.getTransactionDescription(transaction)}</div> | |
| <div class="text-xs text-gray-600">${timeAgo}</div> | |
| </div> | |
| <div class="text-right"> | |
| <div class="font-medium ${amountColor}">${transaction.amount >= 0 ? '+' : ''}$${Math.abs(transaction.amount).toFixed(2)}</div> | |
| <div class="text-xs text-gray-500">${transaction.status}</div> | |
| </div> | |
| `; | |
| return element; | |
| } | |
| static getTransactionDescription(transaction) { | |
| const descriptions = { | |
| 'referral_reward': 'Referral Reward', | |
| 'review_bonus': 'Review Bonus', | |
| 'dispute_refund': 'Refund', | |
| 'withdrawal': 'Withdrawal' | |
| }; | |
| return descriptions[transaction.type] || transaction.type; | |
| } | |
| static getTimeAgo(timestamp) { | |
| const now = new Date(); | |
| const time = new Date(timestamp); | |
| const diffInMs = now - time; | |
| const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); | |
| const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60)); | |
| if (diffInDays > 0) { | |
| return `${diffInDays} day${diffInDays > 1 ? 's' : ''} ago`; | |
| } else if (diffInHours > 0) { | |
| return `${diffInHours} hour${diffInHours > 1 ? 's' : ''} ago`; | |
| } else { | |
| return 'Just now'; | |
| } | |
| } | |
| static setupRealTimeUpdates() { | |
| // Check for matured rewards every minute | |
| setInterval(() => { | |
| this.checkPendingMaturation(); | |
| }, 60000); | |
| // Update display every 30 seconds | |
| setInterval(() => { | |
| const userId = window.OneSearchApp?.user?.id; | |
| if (userId) { | |
| this.updateWalletDisplay(userId); | |
| } | |
| }, 30000); | |
| } | |
| static showTransactionHistory() { | |
| // Show detailed transaction history modal | |
| const userId = window.OneSearchApp?.user?.id; | |
| if (!userId) return; | |
| const wallets = JSON.parse(localStorage.getItem('wallets') || '{}'); | |
| const transactions = wallets[userId]?.transactions || []; | |
| const modal = this.createTransactionHistoryModal(transactions); | |
| document.body.appendChild(modal); | |
| } | |
| static createTransactionHistoryModal(transactions) { | |
| const modal = document.createElement('div'); | |
| modal.className = 'fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50'; | |
| modal.innerHTML = ` | |
| <div class="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-96 overflow-y-auto"> | |
| <div class="px-6 py-4 border-b"> | |
| <div class="flex justify-between items-center"> | |
| <h3 class="text-lg font-semibold">Transaction History</h3> | |
| <button class="text-gray-400 hover:text-gray-600" onclick="this.closest('.fixed').remove()"> | |
| <i data-feather="x" class="h-6 w-6"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="p-6"> | |
| <div class="space-y-4"> | |
| ${transactions.map(tx => this.createDetailedTransactionElement(tx)).join('')} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| setTimeout(() => { | |
| if (window.feather) { | |
| window.feather.replace(); | |
| } | |
| }, 0); | |
| return modal; | |
| } | |
| static createDetailedTransactionElement(transaction) { | |
| return ` | |
| <div class="border rounded-lg p-4"> | |
| <div class="flex justify-between items-start mb-2"> | |
| <div> | |
| <div class="font-medium">${this.getTransactionDescription(transaction)}</div> | |
| <div class="text-sm text-gray-600">${new Date(transaction.createdAt).toLocaleString()}</div> | |
| </div> | |
| <div class="text-right"> | |
| <div class="font-bold ${transaction.amount >= 0 ? 'text-green-600' : 'text-red-600'}"> | |
| ${transaction.amount >= 0 ? '+' : ''}$${Math.abs(transaction.amount).toFixed(2)} | |
| </div> | |
| <div class="text-xs text-gray-500">${transaction.status}</div> | |
| </div> | |
| </div> | |
| ${transaction.description ? `<div class="text-sm text-gray-700">${transaction.description}</div>` : ''} | |
| </div> | |
| `; | |
| } | |
| static showWithdrawModal() { | |
| // Show withdrawal modal | |
| const userId = window.OneSearchApp?.user?.id; | |
| const balance = this.getWalletBalance(userId); | |
| if (balance < 10) { | |
| if (window.OneSearchApp) { | |
| window.OneSearchApp.showNotification('Minimum withdrawal amount is $10.00', 'warning'); | |
| } | |
| return; | |
| } | |
| const modal = this.createWithdrawModal(balance); | |
| document.body.appendChild(modal); | |
| } | |
| static createWithdrawModal(balance) { | |
| const modal = document.createElement('div'); | |
| modal.className = 'fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50'; | |
| modal.innerHTML = ` | |
| <div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4"> | |
| <div class="px-6 py-4 border-b"> | |
| <div class="flex justify-between items-center"> | |
| <h3 class="text-lg font-semibold">Withdraw Funds</h3> | |
| <button class="text-gray-400 hover:text-gray-600" onclick="this.closest('.fixed').remove()"> | |
| <i data-feather="x" class="h-6 w-6"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="p-6"> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Available Balance</label> | |
| <div class="text-2xl font-bold">$${balance.toFixed(2)}</div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Withdrawal Amount</label> | |
| <input type="number" class="w-full border border-gray-300 rounded-md px-3 py-2" | |
| min="10" max="${balance}" step="0.01" placeholder="Enter amount"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Withdrawal Method</label> | |
| <select class="w-full border border-gray-300 rounded-md px-3 py-2"> | |
| <option>Bank Transfer</option> | |
| <option>PayPal</option> | |
| <option>Check</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="px-6 py-4 border-t bg-gray-50"> | |
| <div class="flex space-x-3"> | |
| <button class="flex-1 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"> | |
| Cancel | |
| </button> | |
| <button class="flex-1 px-4 py-2 text-sm font-medium text-white bg-blue-500 border border-transparent rounded-md hover:bg-blue-600"> | |
| Withdraw | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| return modal; | |
| } | |
| static generateTransactionId() { | |
| return 'tx_' + Date.now().toString(36) + Math.random().toString(36).substring(2, 8); | |
| } | |
| static getReviewById(reviewId) { | |
| const reviews = JSON.parse(localStorage.getItem('reviews') || '[]'); | |
| return reviews.find(review => review.id === reviewId); | |
| } | |
| static triggerScoreRecalculation(productId) { | |
| // Trigger trust score recalculation due to refund/dispute | |
| const event = new CustomEvent('scoreRecalculationRequested', { | |
| detail: { productId, reason: 'dispute_refund' } | |
| }); | |
| document.dispatchEvent(event); | |
| } | |
| static exportWalletData(userId) { | |
| // Export wallet data for user records | |
| const wallets = JSON.parse(localStorage.getItem('wallets') || '{}'); | |
| const wallet = wallets[userId]; | |
| return { | |
| userId, | |
| balance: wallet?.balance || 0, | |
| transactions: wallet?.transactions || [], | |
| exportDate: new Date().toISOString(), | |
| walletVersion: '1.0' | |
| }; | |
| } | |
| } | |
| // Export for use in main app | |
| if (typeof window !== 'undefined') { | |
| window.Wallet = Wallet; | |
| } | |
| if (typeof module !== 'undefined' && module.exports) { | |
| module.exports = Wallet; | |
| } |