Spaces:
Running
Running
| // QR Generation and Trust Network Component | |
| class QRGenerator { | |
| static init() { | |
| console.log('QRGenerator component initialized'); | |
| this.setupQRValidation(); | |
| this.loadTrustNetwork(); | |
| } | |
| static generateTrustQR(productId, recommenderId, reason, expirationDays = 30) { | |
| const qrId = this.generateQRId(); | |
| const issuedAt = new Date().toISOString(); | |
| const expiresAt = new Date(Date.now() + expirationDays * 24 * 60 * 60 * 1000).toISOString(); | |
| const payload = { | |
| v: 1, | |
| p: recommenderId, | |
| m: this.getMerchantId(productId), | |
| pr: productId, | |
| q: qrId, | |
| t: issuedAt, | |
| exp: expiresAt, | |
| r: reason | |
| }; | |
| const payloadString = `TSQR:v1|p:${payload.p}|m:${payload.m}|pr:${payload.pr}|q:${payload.q}|t:${payload.t}|exp:${payload.exp}|r:${encodeURIComponent(payload.r)}`; | |
| // In a real implementation, this would be signed with the user's private key | |
| const signature = this.signPayload(payloadString, recommenderId); | |
| const signedPayload = `${payloadString}|sig:${signature}`; | |
| return { | |
| qrId, | |
| payload: signedPayload, | |
| expiresAt, | |
| status: 'active' | |
| }; | |
| } | |
| static signPayload(payload, userId) { | |
| // In a real implementation, this would use WebCrypto API or similar | |
| // to sign the payload with the user's private key | |
| const message = `${payload}|user:${userId}|nonce:${Math.random().toString(36).substring(7)}`; | |
| // Simulate signature generation | |
| return btoa(message).substring(0, 44); // Base64 encoded signature | |
| } | |
| static validateQRScan(scannedPayload, scannerContext = {}) { | |
| try { | |
| // Parse the QR payload | |
| const parsed = this.parseQRPpayload(scannedPayload); | |
| if (!parsed) { | |
| return { valid: false, error: 'Invalid QR format' }; | |
| } | |
| // Check expiration | |
| if (new Date(parsed.exp) < new Date()) { | |
| return { valid: false, error: 'QR code has expired' }; | |
| } | |
| // Validate signature | |
| if (!this.validateSignature(scannedPayload, parsed)) { | |
| return { valid: false, error: 'Invalid signature' }; | |
| } | |
| // Check usage limits | |
| const usageCount = this.getUsageCount(parsed.q); | |
| const maxUses = this.getMaxUses(parsed); | |
| if (usageCount >= maxUses) { | |
| return { valid: false, error: 'QR code usage limit reached' }; | |
| } | |
| // Record the scan | |
| this.recordScan(parsed, scannerContext); | |
| return { | |
| valid: true, | |
| data: parsed, | |
| recommendation: { | |
| productId: parsed.pr, | |
| recommenderId: parsed.p, | |
| reason: parsed.r, | |
| issuedAt: parsed.t | |
| } | |
| }; | |
| } catch (error) { | |
| console.error('QR validation error:', error); | |
| return { valid: false, error: 'Validation failed' }; | |
| } | |
| } | |
| static parseQRPpayload(payload) { | |
| try { | |
| // Parse TSQR format | |
| const parts = payload.split('|'); | |
| const data = {}; | |
| parts.forEach(part => { | |
| const [key, value] = part.split(':'); | |
| if (key && value) { | |
| data[key] = value; | |
| } | |
| }); | |
| // Validate required fields | |
| const required = ['v', 'p', 'm', 'pr', 'q', 't', 'sig']; | |
| const missing = required.filter(field => !data[field]); | |
| if (missing.length > 0) { | |
| return null; | |
| } | |
| return data; | |
| } catch (error) { | |
| return null; | |
| } | |
| } | |
| static validateSignature(payload, parsed) { | |
| // In a real implementation, this would verify the signature | |
| // against the user's public key | |
| try { | |
| const payloadWithoutSig = payload.split('|sig:')[0]; | |
| const providedSig = parsed.sig; | |
| // Simulate signature validation | |
| const expectedSig = btoa(payloadWithoutSig).substring(0, 44); | |
| return providedSig === expectedSig; | |
| } catch (error) { | |
| return false; | |
| } | |
| } | |
| static recordScan(parsed, context) { | |
| // Record the QR scan for attribution tracking | |
| const scanRecord = { | |
| qrId: parsed.q, | |
| productId: parsed.pr, | |
| recommenderId: parsed.p, | |
| scannerUserId: context.userId || null, | |
| timestamp: new Date().toISOString(), | |
| geolocation: context.geolocation || null, | |
| userAgent: navigator.userAgent, | |
| ip: context.ip || null | |
| }; | |
| // In a real implementation, this would save to the database | |
| console.log('QR scan recorded:', scanRecord); | |
| // Store in local storage for demo purposes | |
| const scans = JSON.parse(localStorage.getItem('qrScans') || '[]'); | |
| scans.push(scanRecord); | |
| localStorage.setItem('qrScans', JSON.stringify(scans)); | |
| } | |
| static getUsageCount(qrId) { | |
| // Get how many times this QR has been used | |
| const scans = JSON.parse(localStorage.getItem('qrScans') || '[]'); | |
| return scans.filter(scan => scan.qrId === qrId).length; | |
| } | |
| static getMaxUses(parsed) { | |
| // Different products may have different usage limits | |
| // For now, return a default of 10 uses | |
| return 10; | |
| } | |
| static getMerchantId(productId) { | |
| // In a real implementation, this would lookup the merchant ID | |
| // For demo purposes, return a mock merchant ID | |
| return `merchant_${productId.substring(0, 8)}`; | |
| } | |
| static generateQRId() { | |
| return 'QR' + Date.now().toString(36) + Math.random().toString(36).substring(2, 8); | |
| } | |
| static setupQRValidation() { | |
| // Setup QR scanner validation handlers | |
| const scannerInputs = document.querySelectorAll('input[placeholder*="QR code"]'); | |
| scannerInputs.forEach(input => { | |
| input.addEventListener('input', (e) => { | |
| if (e.target.value.trim()) { | |
| this.validateManualQR(e.target.value.trim()); | |
| } | |
| }); | |
| }); | |
| } | |
| static async validateManualQR(payload) { | |
| const result = this.validateQRScan(payload, { manualEntry: true }); | |
| if (result.valid) { | |
| this.showValidationSuccess(result.recommendation); | |
| } else { | |
| this.showValidationError(result.error); | |
| } | |
| } | |
| static showValidationSuccess(recommendation) { | |
| // Show success message with recommendation details | |
| const message = ` | |
| Valid QR from trusted recommender | |
| Product: ${recommendation.productId} | |
| Reason: ${recommendation.reason} | |
| `; | |
| if (window.OneSearchApp) { | |
| window.OneSearchApp.showNotification('Trust QR validated successfully', 'success'); | |
| } | |
| } | |
| static showValidationError(error) { | |
| // Show error message | |
| if (window.OneSearchApp) { | |
| window.OneSearchApp.showNotification(`QR validation failed: ${error}`, 'error'); | |
| } | |
| } | |
| static loadTrustNetwork() { | |
| // Load recent recommendations from the trust network | |
| const networkData = [ | |
| { | |
| id: 'rec_1', | |
| productId: '1', | |
| recommenderId: 'user_123', | |
| recommenderName: 'Sarah M.', | |
| productTitle: 'Kitchen Storage Solutions', | |
| timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), | |
| trustLevel: 'verified', | |
| reason: 'Perfect for small kitchens with excellent build quality' | |
| }, | |
| { | |
| id: 'rec_2', | |
| productId: '2', | |
| recommenderId: 'user_456', | |
| recommenderName: 'Mike R.', | |
| productTitle: 'Smart Home Automation', | |
| timestamp: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), | |
| trustLevel: 'high', | |
| reason: 'Seamless integration and reliable performance' | |
| }, | |
| { | |
| id: 'rec_3', | |
| productId: '3', | |
| recommenderId: 'user_789', | |
| recommenderName: 'Lisa K.', | |
| productTitle: 'Space-Saving Organizer', | |
| timestamp: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), | |
| trustLevel: 'verified', | |
| reason: 'Transforms cluttered spaces into organized areas' | |
| } | |
| ]; | |
| this.updateTrustNetworkDisplay(networkData); | |
| } | |
| static updateTrustNetworkDisplay(networkData) { | |
| const container = document.querySelector('#recommendations .space-y-3'); | |
| if (!container) return; | |
| // Update the recent recommendations display | |
| networkData.forEach(rec => { | |
| const existingElement = Array.from(container.children).find(el => | |
| el.textContent.includes(rec.productTitle) | |
| ); | |
| if (!existingElement) { | |
| const newElement = this.createRecommendationElement(rec); | |
| container.insertBefore(newElement, container.firstChild); | |
| } | |
| }); | |
| } | |
| static createRecommendationElement(recommendation) { | |
| const element = document.createElement('div'); | |
| element.className = 'flex items-center space-x-3 p-3 border rounded-lg fade-in'; | |
| const timeAgo = this.getTimeAgo(recommendation.timestamp); | |
| const trustColor = recommendation.trustLevel === 'verified' ? 'green' : 'blue'; | |
| element.innerHTML = ` | |
| <div class="w-8 h-8 bg-${trustColor}-500 rounded-full flex items-center justify-center"> | |
| <i data-feather="check" class="h-4 w-4 text-white"></i> | |
| </div> | |
| <div class="flex-1"> | |
| <div class="text-sm font-medium">${recommendation.productTitle}</div> | |
| <div class="text-xs text-gray-600">by ${recommendation.recommenderName} • ${timeAgo}</div> | |
| </div> | |
| <div class="text-xs text-${trustColor}-600 font-medium">${recommendation.trustLevel}</div> | |
| `; | |
| // Replace icons | |
| setTimeout(() => { | |
| if (window.feather) { | |
| window.feather.replace(); | |
| } | |
| }, 0); | |
| return element; | |
| } | |
| 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)); | |
| if (diffInDays === 0) { | |
| return 'today'; | |
| } else if (diffInDays === 1) { | |
| return 'yesterday'; | |
| } else { | |
| return `${diffInDays} days ago`; | |
| } | |
| } | |
| static createQRTemplate(productId, productTitle) { | |
| return { | |
| productId, | |
| productTitle, | |
| recommendationReasons: [ | |
| 'Excellent build quality', | |
| 'Great value for money', | |
| 'Solved my specific problem', | |
| 'Highly recommended by community', | |
| 'Verified by multiple users' | |
| ], | |
| defaultExpirationDays: 30, | |
| maxUsages: 10 | |
| }; | |
| } | |
| static exportQRData(qrId) { | |
| // Export QR generation data for auditing | |
| const scans = JSON.parse(localStorage.getItem('qrScans') || '[]'); | |
| const qrScans = scans.filter(scan => scan.qrId === qrId); | |
| return { | |
| qrId, | |
| generatedAt: new Date().toISOString(), | |
| totalScans: qrScans.length, | |
| scans: qrScans, | |
| conversions: qrScans.filter(scan => scan.conversion === true).length | |
| }; | |
| } | |
| } | |
| // Export for use in main app | |
| if (typeof window !== 'undefined') { | |
| window.QRGenerator = QRGenerator; | |
| } | |
| if (typeof module !== 'undefined' && module.exports) { | |
| module.exports = QRGenerator; | |
| } |