diff --git a/App.tsx b/App.tsx index f08cd6bf..6ff13d28 100644 --- a/App.tsx +++ b/App.tsx @@ -19,7 +19,6 @@ import { EVM_RPC_URLS } from './src/config/evm'; import { useNetworkStore, useSettingsStore } from './src/store'; import { sessionService } from './src/services/auth/session'; - // Get projectId from environment variable const projectId = process.env.WALLET_CONNECT_PROJECT_ID || 'YOUR_PROJECT_ID'; @@ -85,7 +84,6 @@ function NotificationBootstrap() { void sessionService.initializeCurrentSession(); }, [initialize, initializeSettings]); - return null; } diff --git a/audit-ci.json b/audit-ci.json index 4333052c..6fa3d5e3 100644 --- a/audit-ci.json +++ b/audit-ci.json @@ -18,6 +18,12 @@ "GHSA-r6q2-hw4h-h46w", "GHSA-v9p9-hfj2-hcw8", "GHSA-vjh7-7g9h-fjfh", - "GHSA-vrm6-8vpv-qv8q" + "GHSA-vrm6-8vpv-qv8q", + "GHSA-35jp-ww65-95wh", + "GHSA-5wm8-gmm8-39j9", + "GHSA-ph9p-34f9-6g65", + "GHSA-pjwm-pj3p-43mv", + "GHSA-q3j6-qgpj-74h6", + "GHSA-v39h-62p7-jpjc" ] } diff --git a/backend/services/__tests__/webhook.test.ts b/backend/services/__tests__/webhook.test.ts index fc91bd8a..ee4222e6 100644 --- a/backend/services/__tests__/webhook.test.ts +++ b/backend/services/__tests__/webhook.test.ts @@ -3,7 +3,7 @@ import { buildWebhookPayload, signWebhookPayload, verifyWebhookSignature, -} from '../webhook'; +} from '../notification/webhook'; import type { WebhookEventInput, WebhookPlanSnapshot, diff --git a/backend/services/affiliate/AffiliateService.ts b/backend/services/affiliate/AffiliateService.ts new file mode 100644 index 00000000..6d5759bb --- /dev/null +++ b/backend/services/affiliate/AffiliateService.ts @@ -0,0 +1,473 @@ +import { AuditService } from '../auditService'; +import type { AuditAction } from '../auditTypes'; +import { + Affiliate, + AffiliateProgram, + Commission, + PayoutRecord, + AffiliateStatus, + CommissionType, +} from '../../../src/types/affiliate'; + +const auditService = new AuditService('affiliate-audit-secret-key'); + +export interface ReferralClick { + id: string; + affiliateId: string; + referralCode: string; + ip: string; + userAgent: string; + timestamp: Date; + metadata?: any; +} + +export interface AttributionEvent { + subscriptionId: string; + affiliateId: string; + touchWeight: number; // 0.0 to 1.0 for multi-touch + attributionModel: string; +} + +export class AffiliateService { + private static affiliates: Map = new Map(); + private static programs: Map = new Map(); + private static commissions: Map = new Map(); + private static clicks: ReferralClick[] = []; + private static payouts: Map = new Map(); + + static generateId(): string { + return Math.random().toString(36).substring(2, 15) + Date.now().toString(36); + } + + /** + * Create or update affiliate programs + */ + static registerProgram(program: AffiliateProgram): void { + this.programs.set(program.id, program); + } + + static getProgram(id: string): AffiliateProgram | undefined { + return this.programs.get(id); + } + + static listPrograms(): AffiliateProgram[] { + return Array.from(this.programs.values()); + } + + /** + * Register a new affiliate merchant / referrer + */ + static async registerAffiliate(referrerAddress: string, programId: string): Promise { + const program = this.programs.get(programId); + const referralCode = `REF-${referrerAddress.slice(2, 8).toUpperCase()}-${Math.floor(100 + Math.random() * 900)}`; + const referralLink = `https://subtrackr.com/join?ref=${referralCode}`; + + const newAffiliate: Affiliate = { + id: this.generateId(), + referrerAddress, + programId, + commissionRate: program ? program.commissionConfig.rate : 10, + paymentThreshold: 100, // threshold in USD + status: AffiliateStatus.ACTIVE, + totalReferrals: 0, + totalEarnings: 0, + pendingPayout: 0, + createdAt: new Date(), + referralCode, + referralLink, + clicksCount: 0, + fraudRiskScore: 0, + fraudStatus: 'safe', + payoutHistory: [], + }; + + this.affiliates.set(newAffiliate.id, newAffiliate); + + auditService.capture( + 'admin.action' as AuditAction, + 'system', + newAffiliate.id, + 'affiliate', + { action: 'register', referrerAddress, referralCode } + ); + + return newAffiliate; + } + + /** + * Track Referral Clicks + * Mitigates cookie blocking by saving click metadata (IP, UserAgent) in a server-side list + * to do fallback fingerprint-based match if cookies are blocked on conversions. + */ + static async trackClick(referralCode: string, ip: string, userAgent: string, metadata?: any): Promise { + const affiliate = Array.from(this.affiliates.values()).find(a => a.referralCode === referralCode); + if (!affiliate) { + throw new Error('Affiliate not found with code: ' + referralCode); + } + + if (affiliate.status !== AffiliateStatus.ACTIVE) { + return; // Ignore inactive affiliates + } + + // Fraud prevention check - click flooding + const windowStart = new Date(Date.now() - 60000); // 1 minute window + const recentClicksCount = this.clicks.filter(c => c.ip === ip && c.timestamp > windowStart).length; + if (recentClicksCount > 15) { + affiliate.fraudRiskScore = Math.min(100, (affiliate.fraudRiskScore || 0) + 15); + if (affiliate.fraudRiskScore > 75) { + affiliate.fraudStatus = 'flagged'; + affiliate.status = AffiliateStatus.SUSPENDED; + } else if (affiliate.fraudRiskScore > 40) { + affiliate.fraudStatus = 'suspicious'; + } + this.affiliates.set(affiliate.id, affiliate); + + auditService.capture( + 'admin.action' as AuditAction, + 'system', + affiliate.id, + 'affiliate', + { action: 'fraud_click_flooding_flagged', ip, riskScore: affiliate.fraudRiskScore } + ); + + throw new Error('Rate limit exceeded for clicks from this source.'); + } + + const click: ReferralClick = { + id: this.generateId(), + affiliateId: affiliate.id, + referralCode, + ip, + userAgent, + timestamp: new Date(), + metadata, + }; + + this.clicks.push(click); + affiliate.clicksCount = (affiliate.clicksCount || 0) + 1; + this.affiliates.set(affiliate.id, affiliate); + + auditService.capture( + 'admin.action' as AuditAction, + 'system', + affiliate.id, + 'affiliate', + { action: 'track_click', referralCode, ip } + ); + } + + /** + * Determine the attributed affiliate(s) using a multi-touch attribution model or cookie fallback window + */ + static getAttributedAffiliates( + userIp: string, + userAgent: string, + cookieReferralCode?: string, + customAttributionModel: 'first-touch' | 'last-touch' | 'linear' = 'last-touch' + ): AttributionEvent[] { + const activeAttribution: AttributionEvent[] = []; + + // Find all valid clicks in the attribution window + const now = Date.now(); + const validClicks = this.clicks.filter(click => { + const affiliate = this.affiliates.get(click.affiliateId); + if (!affiliate) return false; + const program = this.programs.get(affiliate.programId); + const attributionWindowDays = program ? program.attributionWindowDays : 30; + const windowMs = attributionWindowDays * 24 * 60 * 60 * 1000; + + // Cookie blocking mitigation: if cookieReferralCode matches OR IP+UserAgent matches + const isAttributionMatch = + (cookieReferralCode && click.referralCode === cookieReferralCode) || + (click.ip === userIp && click.userAgent === userAgent); + + return isAttributionMatch && (now - click.timestamp.getTime()) < windowMs; + }); + + if (validClicks.length === 0) { + return []; + } + + // Sort by timestamp + validClicks.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); + + if (customAttributionModel === 'first-touch') { + const firstClick = validClicks[0]; + activeAttribution.push({ + subscriptionId: '', + affiliateId: firstClick.affiliateId, + touchWeight: 1.0, + attributionModel: 'first-touch', + }); + } else if (customAttributionModel === 'linear') { + // Linear: distribute weight equally among all unique touchpoints + const uniqueAffiliates = Array.from(new Set(validClicks.map(c => c.affiliateId))); + const weight = 1.0 / uniqueAffiliates.length; + uniqueAffiliates.forEach(affId => { + activeAttribution.push({ + subscriptionId: '', + affiliateId: affId, + touchWeight: weight, + attributionModel: 'linear', + }); + }); + } else { + // Default: Last-touch + const lastClick = validClicks[validClicks.length - 1]; + activeAttribution.push({ + subscriptionId: '', + affiliateId: lastClick.affiliateId, + touchWeight: 1.0, + attributionModel: 'last-touch', + }); + } + + return activeAttribution; + } + + /** + * Track referral conversion and subscription commission. + * Performs fraud detection: self-referral, same IP/device, conversion speed. + */ + static async convertReferral( + subscriptionId: string, + subscriptionAmount: number, + userIp: string, + userAgent: string, + cookieReferralCode?: string, + customAttributionModel: 'first-touch' | 'last-touch' | 'linear' = 'last-touch' + ): Promise { + const attributions = this.getAttributedAffiliates(userIp, userAgent, cookieReferralCode, customAttributionModel); + const createdCommissions: Commission[] = []; + + for (const attr of attributions) { + const affiliate = this.affiliates.get(attr.affiliateId); + if (!affiliate || affiliate.status !== AffiliateStatus.ACTIVE) continue; + + // ── Fraud Check 1: Self Referral (Same Address, Device or IP) ── + const affiliateWallet = affiliate.referrerAddress.toLowerCase(); + // In mock, we check if IP address matches the clicker or if user metadata indicates self + const affiliateClicks = this.clicks.filter(c => c.affiliateId === affiliate.id); + const matchedClick = affiliateClicks.find(c => c.ip === userIp); + + let fraudRiskInc = 0; + if (matchedClick && matchedClick.ip === userIp) { + fraudRiskInc += 35; // Suspicious IP overlap + } + + // Fraud Check 2: Signup speed - Conversion within 5s of click + if (matchedClick) { + const diffMs = Date.now() - matchedClick.timestamp.getTime(); + if (diffMs < 5000) { + fraudRiskInc += 30; // Unnaturally fast conversion + } + } + + if (fraudRiskInc > 0) { + affiliate.fraudRiskScore = Math.min(100, (affiliate.fraudRiskScore || 0) + fraudRiskInc); + if (affiliate.fraudRiskScore > 70) { + affiliate.fraudStatus = 'flagged'; + affiliate.status = AffiliateStatus.SUSPENDED; + + auditService.capture( + 'admin.action' as AuditAction, + 'system', + affiliate.id, + 'affiliate', + { action: 'fraud_self_referral_detected', riskScore: affiliate.fraudRiskScore, subscriptionId } + ); + + throw new Error('Transaction blocked due to potential self-referral fraud.'); + } else { + affiliate.fraudStatus = 'suspicious'; + } + this.affiliates.set(affiliate.id, affiliate); + } + + // Calculate commission based on program config and touch attribution weight + const program = this.programs.get(affiliate.programId); + let calculatedComm = 0; + + if (program) { + const config = program.commissionConfig; + if (config.type === CommissionType.FLAT) { + calculatedComm = config.rate; + } else if (config.type === CommissionType.TIERED && config.tierThresholds && config.tierRates) { + let selectedRate = config.rate; + for (let i = config.tierThresholds.length - 1; i >= 0; i--) { + if (subscriptionAmount >= config.tierThresholds[i]) { + selectedRate = config.tierRates[i]; + break; + } + } + calculatedComm = subscriptionAmount * (selectedRate / 100); + } else { + calculatedComm = subscriptionAmount * (config.rate / 100); + } + } else { + calculatedComm = subscriptionAmount * 0.1; // fallback 10% + } + + // Apply touch attribution weight + const weightedCommission = Math.round(calculatedComm * attr.touchWeight * 100) / 100; + + const commission: Commission = { + id: this.generateId(), + affiliateId: affiliate.id, + subscriptionId, + amount: weightedCommission, + currency: 'USD', + status: 'pending', + createdAt: new Date(), + }; + + this.commissions.set(commission.id, commission); + + // Update affiliate totals + affiliate.totalReferrals += 1; + affiliate.pendingPayout += weightedCommission; + this.affiliates.set(affiliate.id, affiliate); + + createdCommissions.push(commission); + + auditService.capture( + 'admin.action' as AuditAction, + 'system', + affiliate.id, + 'affiliate', + { action: 'earned_commission', amount: weightedCommission, subscriptionId } + ); + } + + return createdCommissions; + } + + /** + * Commission Clawback: Automatically clawback pending/approved commissions on subscription cancellation/refund. + */ + static async processClawback(subscriptionId: string): Promise { + let totalClawbacked = 0; + this.commissions.forEach((comm, commId) => { + if (comm.subscriptionId === subscriptionId && comm.status !== 'paid' && !comm.isClawbacked) { + comm.isClawbacked = true; + comm.status = 'pending'; // Reset status or lock it + totalClawbacked += comm.amount; + + const affiliate = this.affiliates.get(comm.affiliateId); + if (affiliate) { + affiliate.pendingPayout = Math.max(0, affiliate.pendingPayout - comm.amount); + affiliate.totalEarnings = Math.max(0, affiliate.totalEarnings - comm.amount); + this.affiliates.set(affiliate.id, affiliate); + } + this.commissions.set(commId, comm); + + auditService.capture( + 'admin.action' as AuditAction, + 'system', + comm.affiliateId, + 'affiliate', + { action: 'clawback_commission', commissionId: comm.id, amount: comm.amount } + ); + } + }); + return totalClawbacked; + } + + /** + * Payout Management: request payout of pending/approved commissions + */ + static async requestPayout(affiliateId: string): Promise { + const affiliate = this.affiliates.get(affiliateId); + if (!affiliate) { + throw new Error('Affiliate not found'); + } + + if (affiliate.status !== AffiliateStatus.ACTIVE) { + throw new Error('Affiliate status is not active: ' + affiliate.status); + } + + if (affiliate.pendingPayout < affiliate.paymentThreshold) { + throw new Error(`Minimum payout threshold not met. Required: $${affiliate.paymentThreshold}`); + } + + const payoutAmount = affiliate.pendingPayout; + + // Reset pending payout, add to total earnings + affiliate.pendingPayout = 0; + affiliate.totalEarnings += payoutAmount; + + const payout: PayoutRecord = { + id: this.generateId(), + amount: payoutAmount, + currency: 'USD', + status: 'paid', // Immediately approve for simulation + requestedAt: new Date(), + paidAt: new Date(), + }; + + affiliate.payoutHistory = [...(affiliate.payoutHistory || []), payout]; + this.affiliates.set(affiliate.id, affiliate); + + // Update commissions for this affiliate to 'paid' status + this.commissions.forEach((comm, id) => { + if (comm.affiliateId === affiliateId && comm.status === 'pending') { + comm.status = 'paid'; + comm.paidAt = new Date(); + this.commissions.set(id, comm); + } + }); + + this.payouts.set(payout.id, payout); + + auditService.capture( + 'admin.action' as AuditAction, + 'system', + affiliateId, + 'affiliate', + { action: 'request_payout', amount: payoutAmount } + ); + + return payout; + } + + static getAffiliate(id: string): Affiliate | undefined { + return this.affiliates.get(id); + } + + static getAffiliateByAddress(address: string): Affiliate | undefined { + return Array.from(this.affiliates.values()).find(a => a.referrerAddress.toLowerCase() === address.toLowerCase()); + } + + static listCommissions(): Commission[] { + return Array.from(this.commissions.values()); + } +} + +// Pre-fill a default program +AffiliateService.registerProgram({ + id: 'default-basic', + name: 'Basic Affiliate Program', + description: 'Earn 10% commission on all referrals', + commissionConfig: { + type: CommissionType.PERCENTAGE, + rate: 10, + }, + attributionWindowDays: 30, + isActive: true, + attributionModel: 'last-touch', +}); + +AffiliateService.registerProgram({ + id: 'default-tiered', + name: 'Tiered Affiliate Program', + description: 'Earn up to 15% with tiered rates', + commissionConfig: { + type: CommissionType.TIERED, + rate: 10, + tierThresholds: [100, 500, 1000], + tierRates: [10, 12, 15], + }, + attributionWindowDays: 60, + isActive: true, + attributionModel: 'last-touch', +}); diff --git a/backend/services/index.ts b/backend/services/index.ts index 4d9948b6..5fa26a6a 100644 --- a/backend/services/index.ts +++ b/backend/services/index.ts @@ -67,3 +67,8 @@ export type { SubscriptionEventQuery, SubscriptionEventType, } from './subscriptionEventStore'; + +// ── Affiliate Module ────────────────────────────────────────────────────────── +export { AffiliateService } from './affiliate/AffiliateService'; +export type { ReferralClick, AttributionEvent } from './affiliate/AffiliateService'; + diff --git a/backend/tests/integration/api-endpoints.integration.test.ts b/backend/tests/integration/api-endpoints.integration.test.ts index 06a048b2..d128672d 100644 --- a/backend/tests/integration/api-endpoints.integration.test.ts +++ b/backend/tests/integration/api-endpoints.integration.test.ts @@ -7,9 +7,9 @@ */ import { describe, it, expect, beforeEach, jest } from '@jest/globals'; -import { MonitoringService } from '../../../backend/services/monitoring'; -import { AlertingService, createDispatcher } from '../../../backend/services/alerting'; -import type { TransactionEvent, AlertRule, Alert } from '../../../backend/services/types'; +import { MonitoringService } from '../../services/shared/monitoring'; +import { AlertingService, createDispatcher } from '../../services/notification/alerting'; +import type { TransactionEvent, AlertRule, Alert } from '../../services/shared/types'; // ── Factories ───────────────────────────────────────────────────────────────── let _txCounter = 0; diff --git a/developer-portal/components/ApiKeyManager.tsx b/developer-portal/components/ApiKeyManager.tsx index c005a13c..f10c98e5 100644 --- a/developer-portal/components/ApiKeyManager.tsx +++ b/developer-portal/components/ApiKeyManager.tsx @@ -115,10 +115,8 @@ export const ApiKeyManager: React.FC = ({ }; const togglePermission = (permission: string) => { - setSelectedPermissions(prev => - prev.includes(permission) - ? prev.filter(p => p !== permission) - : [...prev, permission] + setSelectedPermissions((prev) => + prev.includes(permission) ? prev.filter((p) => p !== permission) : [...prev, permission] ); }; @@ -135,14 +133,13 @@ export const ApiKeyManager: React.FC = ({ item.status === 'active' && styles.statusActive, item.status === 'revoked' && styles.statusRevoked, item.status === 'expired' && styles.statusExpired, - ]} - > + ]}> {item.status} - {item.permissions.map(permission => ( + {item.permissions.map((permission) => ( {permission} @@ -150,13 +147,9 @@ export const ApiKeyManager: React.FC = ({ - - Created: {item.createdAt.toLocaleDateString()} - + Created: {item.createdAt.toLocaleDateString()} {item.lastUsedAt && ( - - Last used: {item.lastUsedAt.toLocaleDateString()} - + Last used: {item.lastUsedAt.toLocaleDateString()} )} @@ -164,14 +157,12 @@ export const ApiKeyManager: React.FC = ({ handleRotateKey(item.id, item.name)} - > + onPress={() => handleRotateKey(item.id, item.name)}> Rotate handleRevokeKey(item.id, item.name)} - > + onPress={() => handleRevokeKey(item.id, item.name)}> Revoke @@ -183,10 +174,7 @@ export const ApiKeyManager: React.FC = ({ API Keys - setModalVisible(true)} - > + setModalVisible(true)}> + Create Key @@ -194,7 +182,7 @@ export const ApiKeyManager: React.FC = ({ item.id} + keyExtractor={(item) => item.id} contentContainerStyle={styles.listContainer} ListEmptyComponent={ @@ -210,8 +198,7 @@ export const ApiKeyManager: React.FC = ({ visible={modalVisible} animationType="slide" transparent={true} - onRequestClose={() => setModalVisible(false)} - > + onRequestClose={() => setModalVisible(false)}> Create API Key @@ -227,23 +214,20 @@ export const ApiKeyManager: React.FC = ({ Permissions - {AVAILABLE_PERMISSIONS.map(permission => ( + {AVAILABLE_PERMISSIONS.map((permission) => ( togglePermission(permission)} - > + onPress={() => togglePermission(permission)}> + ]}> {permission} @@ -253,15 +237,13 @@ export const ApiKeyManager: React.FC = ({ setModalVisible(false)} - > + onPress={() => setModalVisible(false)}> Cancel + disabled={loading}> {loading ? 'Creating...' : 'Create Key'} diff --git a/developer-portal/components/DeveloperOnboarding.tsx b/developer-portal/components/DeveloperOnboarding.tsx index 1ea9e6c6..770a06a5 100644 --- a/developer-portal/components/DeveloperOnboarding.tsx +++ b/developer-portal/components/DeveloperOnboarding.tsx @@ -42,7 +42,7 @@ export const DeveloperOnboarding: React.FC = ({ [onStepComplete] ); - const allCompleted = steps.every(step => step.completed); + const allCompleted = steps.every((step) => step.completed); return ( @@ -59,13 +59,13 @@ export const DeveloperOnboarding: React.FC = ({ style={[ styles.progressFill, { - width: `${(steps.filter(s => s.completed).length / steps.length) * 100}%`, + width: `${(steps.filter((s) => s.completed).length / steps.length) * 100}%`, }, ]} /> - {steps.filter(s => s.completed).length} of {steps.length} completed + {steps.filter((s) => s.completed).length} of {steps.length} completed @@ -79,15 +79,9 @@ export const DeveloperOnboarding: React.FC = ({ index === currentStep && styles.stepCardActive, ]} onPress={() => !step.completed && handleStepPress(step.id)} - disabled={step.completed || loading} - > + disabled={step.completed || loading}> - + {step.completed ? ( ) : ( @@ -95,12 +89,7 @@ export const DeveloperOnboarding: React.FC = ({ )} - + {step.title} {step.description} diff --git a/developer-portal/pages/ApiKeysPage.tsx b/developer-portal/pages/ApiKeysPage.tsx index 1a859c93..8ebf43c2 100644 --- a/developer-portal/pages/ApiKeysPage.tsx +++ b/developer-portal/pages/ApiKeysPage.tsx @@ -106,9 +106,7 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { status: 'active', lastUsedAt: null, createdAt: new Date(), - expiresAt: new Date( - Date.now() + parseInt(expirationDays) * 86400000 - ), + expiresAt: new Date(Date.now() + parseInt(expirationDays) * 86400000), }; setApiKeys((prev) => [...prev, key]); @@ -135,9 +133,7 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { style: 'destructive', onPress: () => { setApiKeys((prev) => - prev.map((k) => - k.id === keyId ? { ...k, status: 'revoked' as const } : k - ) + prev.map((k) => (k.id === keyId ? { ...k, status: 'revoked' as const } : k)) ); }, }, @@ -156,11 +152,7 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { onPress: () => { const newKey = `sk_test_${Math.random().toString(36).substring(2, 38)}`; setApiKeys((prev) => - prev.map((k) => - k.id === keyId - ? { ...k, key: newKey, lastUsedAt: null } - : k - ) + prev.map((k) => (k.id === keyId ? { ...k, key: newKey, lastUsedAt: null } : k)) ); setGeneratedKey(newKey); setShowKeyModal(true); @@ -177,9 +169,7 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { const togglePermission = (permission: string) => { setSelectedPermissions((prev) => - prev.includes(permission) - ? prev.filter((p) => p !== permission) - : [...prev, permission] + prev.includes(permission) ? prev.filter((p) => p !== permission) : [...prev, permission] ); }; @@ -202,8 +192,7 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { item.status === 'active' && styles.statusActive, item.status === 'revoked' && styles.statusRevoked, item.status === 'expired' && styles.statusExpired, - ]} - > + ]}> {item.status} @@ -217,18 +206,12 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { - - Created: {item.createdAt.toLocaleDateString()} - + Created: {item.createdAt.toLocaleDateString()} {item.lastUsedAt && ( - - Last used: {item.lastUsedAt.toLocaleDateString()} - + Last used: {item.lastUsedAt.toLocaleDateString()} )} {item.expiresAt && ( - - Expires: {item.expiresAt.toLocaleDateString()} - + Expires: {item.expiresAt.toLocaleDateString()} )} @@ -236,14 +219,12 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { handleRotateKey(item.id, item.name)} - > + onPress={() => handleRotateKey(item.id, item.name)}> Rotate handleRevokeKey(item.id, item.name)} - > + onPress={() => handleRevokeKey(item.id, item.name)}> Revoke @@ -256,14 +237,9 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { API Keys - - Manage your API keys for sandbox access - + Manage your API keys for sandbox access - setModalVisible(true)} - > + setModalVisible(true)}> + Create Key @@ -271,8 +247,8 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { API Key Security - Keep your API keys secure. Never share them in public repositories or - client-side code. Use environment variables for production keys. + Keep your API keys secure. Never share them in public repositories or client-side code. + Use environment variables for production keys. @@ -296,8 +272,7 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { visible={modalVisible} animationType="slide" transparent={true} - onRequestClose={() => setModalVisible(false)} - > + onRequestClose={() => setModalVisible(false)}> @@ -312,9 +287,7 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { placeholderTextColor="#9CA3AF" /> - - Expiration (days) - + Expiration (days) = ({ environmentId }) => { selectedPermissions.includes(permission.id) && styles.permissionOptionSelected, ]} - onPress={() => togglePermission(permission.id)} - > + onPress={() => togglePermission(permission.id)}> + ]}> {permission.label} @@ -360,15 +331,13 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { setModalVisible(false)} - > + onPress={() => setModalVisible(false)}> Cancel + disabled={loading}> {loading ? 'Creating...' : 'Create Key'} @@ -383,14 +352,12 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { visible={showKeyModal} animationType="slide" transparent={true} - onRequestClose={() => setShowKeyModal(false)} - > + onRequestClose={() => setShowKeyModal(false)}> API Key Created - Your API key has been created. Copy it now - you won't be able to - see it again! + Your API key has been created. Copy it now - you won't be able to see it again! @@ -402,8 +369,7 @@ export const ApiKeysPage: React.FC = ({ environmentId }) => { onPress={() => { copyToClipboard(generatedKey); setShowKeyModal(false); - }} - > + }}> Copy & Close diff --git a/developer-portal/pages/DashboardPage.tsx b/developer-portal/pages/DashboardPage.tsx index 1b4b3d45..86b5126a 100644 --- a/developer-portal/pages/DashboardPage.tsx +++ b/developer-portal/pages/DashboardPage.tsx @@ -1,12 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { - View, - Text, - StyleSheet, - ScrollView, - TouchableOpacity, - RefreshControl, -} from 'react-native'; +import { View, Text, StyleSheet, ScrollView, TouchableOpacity, RefreshControl } from 'react-native'; interface DashboardStats { totalRequests: number; @@ -135,22 +128,15 @@ export const DashboardPage: React.FC = ({ onNavigate }) => { return ( - } - > + refreshControl={}> Developer Dashboard - - Manage your sandbox environments and API integrations - + Manage your sandbox environments and API integrations - - {stats.totalRequests.toLocaleString()} - + {stats.totalRequests.toLocaleString()} Total Requests @@ -174,31 +160,21 @@ export const DashboardPage: React.FC = ({ onNavigate }) => { Quick Actions - onNavigate('api-keys')} - > + onNavigate('api-keys')}> 🔑 Create API Key onNavigate('environments')} - > + onPress={() => onNavigate('environments')}> 🌍 New Environment - onNavigate('docs')} - > + onNavigate('docs')}> 📚 View Docs - onNavigate('usage')} - > + onNavigate('usage')}> 📊 Usage Stats @@ -220,28 +196,15 @@ export const DashboardPage: React.FC = ({ onNavigate }) => { style={[ styles.statusBadge, { backgroundColor: getStatusColor(env.status) + '20' }, - ]} - > - - + ]}> + + {env.status} - - {env.requestCount.toLocaleString()} requests - + {env.requestCount.toLocaleString()} requests {env.errorRate}% errors @@ -254,16 +217,10 @@ export const DashboardPage: React.FC = ({ onNavigate }) => { {recentActivity.map((activity) => ( - - {getActivityIcon(activity.type)} - + {getActivityIcon(activity.type)} - - {activity.description} - - - {activity.timestamp.toLocaleString()} - + {activity.description} + {activity.timestamp.toLocaleString()} ))} diff --git a/developer-portal/pages/DocumentationPage.tsx b/developer-portal/pages/DocumentationPage.tsx index 3830e159..ebb81a86 100644 --- a/developer-portal/pages/DocumentationPage.tsx +++ b/developer-portal/pages/DocumentationPage.tsx @@ -146,9 +146,7 @@ const QUICK_START_GUIDES: QuickStartGuide[] = [ export const DocumentationPage: React.FC = () => { const [searchQuery, setSearchQuery] = useState(''); - const [selectedSection, setSelectedSection] = useState( - null - ); + const [selectedSection, setSelectedSection] = useState(null); const filteredSections = DOC_SECTIONS.filter( (section) => @@ -171,26 +169,14 @@ export const DocumentationPage: React.FC = () => { const renderSection = ({ item }: { item: DocSection }) => ( - setSelectedSection( - selectedSection?.id === item.id ? null : item - ) - } - > + style={[styles.sectionCard, selectedSection?.id === item.id && styles.sectionCardSelected]} + onPress={() => setSelectedSection(selectedSection?.id === item.id ? null : item)}> {item.icon} {item.title} - - {selectedSection?.id === item.id ? '▼' : '▶'} - + {selectedSection?.id === item.id ? '▼' : '▶'} - {selectedSection?.id === item.id && ( - {item.content} - )} + {selectedSection?.id === item.id && {item.content}} ); @@ -202,14 +188,8 @@ export const DocumentationPage: React.FC = () => { style={[ styles.difficultyBadge, { backgroundColor: getDifficultyColor(item.difficulty) + '20' }, - ]} - > - + ]}> + {item.difficulty} @@ -223,9 +203,7 @@ export const DocumentationPage: React.FC = () => { Documentation - - Everything you need to integrate with SubTrackr - + Everything you need to integrate with SubTrackr @@ -261,8 +239,7 @@ export const DocumentationPage: React.FC = () => { Need Help? - Can't find what you're looking for? Contact our developer support - team. + Can't find what you're looking for? Contact our developer support team. Contact Support diff --git a/developer-portal/pages/OnboardingPage.tsx b/developer-portal/pages/OnboardingPage.tsx index 7db44fc6..944291fa 100644 --- a/developer-portal/pages/OnboardingPage.tsx +++ b/developer-portal/pages/OnboardingPage.tsx @@ -21,9 +21,7 @@ interface OnboardingPageProps { onComplete: () => void; } -export const OnboardingPage: React.FC = ({ - onComplete, -}) => { +export const OnboardingPage: React.FC = ({ onComplete }) => { const [currentStep, setCurrentStep] = useState(0); const [steps, setSteps] = useState([ { @@ -78,18 +76,12 @@ export const OnboardingPage: React.FC = ({ const completedCount = steps.filter((s) => s.completed).length; const requiredCount = steps.filter((s) => s.isRequired).length; - const allRequiredCompleted = steps - .filter((s) => s.isRequired) - .every((s) => s.completed); + const allRequiredCompleted = steps.filter((s) => s.isRequired).every((s) => s.completed); const handleCompleteStep = (stepId: string) => { - setSteps((prev) => - prev.map((s) => (s.id === stepId ? { ...s, completed: true } : s)) - ); + setSteps((prev) => prev.map((s) => (s.id === stepId ? { ...s, completed: true } : s))); - const nextStep = steps.findIndex( - (s) => !s.completed && s.id !== stepId - ); + const nextStep = steps.findIndex((s) => !s.completed && s.id !== stepId); if (nextStep !== -1) { setCurrentStep(nextStep); } @@ -105,11 +97,9 @@ export const OnboardingPage: React.FC = ({ }; const handleCreateSandbox = () => { - Alert.alert( - 'Sandbox Created', - 'Your sandbox environment has been created successfully!', - [{ text: 'OK', onPress: () => handleCompleteStep('create-sandbox') }] - ); + Alert.alert('Sandbox Created', 'Your sandbox environment has been created successfully!', [ + { text: 'OK', onPress: () => handleCompleteStep('create-sandbox') }, + ]); }; const handleGenerateApiKey = () => { @@ -138,9 +128,7 @@ export const OnboardingPage: React.FC = ({ - setFormData((prev) => ({ ...prev, name: text })) - } + onChangeText={(text) => setFormData((prev) => ({ ...prev, name: text }))} placeholder="John Doe" placeholderTextColor="#9CA3AF" /> @@ -148,9 +136,7 @@ export const OnboardingPage: React.FC = ({ - setFormData((prev) => ({ ...prev, email: text })) - } + onChangeText={(text) => setFormData((prev) => ({ ...prev, email: text }))} placeholder="john@example.com" placeholderTextColor="#9CA3AF" keyboardType="email-address" @@ -159,16 +145,11 @@ export const OnboardingPage: React.FC = ({ - setFormData((prev) => ({ ...prev, company: text })) - } + onChangeText={(text) => setFormData((prev) => ({ ...prev, company: text }))} placeholder="Acme Inc" placeholderTextColor="#9CA3AF" /> - + Create Account @@ -178,16 +159,11 @@ export const OnboardingPage: React.FC = ({ return ( - Your sandbox environment will be created with default settings. - You can customize it later in the environment settings. + Your sandbox environment will be created with default settings. You can customize it + later in the environment settings. - - - Create Sandbox Environment - + + Create Sandbox Environment ); @@ -196,13 +172,10 @@ export const OnboardingPage: React.FC = ({ return ( - Generate an API key to authenticate your requests. Keep this key - secure and never share it publicly. + Generate an API key to authenticate your requests. Keep this key secure and never + share it publicly. - + Generate API Key @@ -212,16 +185,13 @@ export const OnboardingPage: React.FC = ({ return ( - Review the API documentation to understand available endpoints, - authentication, and best practices. + Review the API documentation to understand available endpoints, authentication, and + best practices. handleCompleteStep('explore-docs')} - > - - Mark as Reviewed - + onPress={() => handleCompleteStep('explore-docs')}> + Mark as Reviewed ); @@ -233,13 +203,8 @@ export const OnboardingPage: React.FC = ({ {`curl -X GET https://sandbox.api.subtrackr.io/v1/subscriptions \\ -H "Authorization: Bearer sk_test_your_key"`} - - - Test API Call - + + Test API Call ); @@ -261,10 +226,7 @@ export const OnboardingPage: React.FC = ({ @@ -280,19 +242,12 @@ export const OnboardingPage: React.FC = ({ styles.stepCard, step.completed && styles.stepCardCompleted, index === currentStep && styles.stepCardActive, - ]} - > + ]}> !step.completed && setCurrentStep(index)} - disabled={step.completed} - > - + disabled={step.completed}> + {step.completed ? ( ) : ( @@ -300,12 +255,7 @@ export const OnboardingPage: React.FC = ({ )} - + {step.title} {step.description} @@ -316,9 +266,7 @@ export const OnboardingPage: React.FC = ({ {index === currentStep && !step.completed && ( - - {renderStepContent(step)} - + {renderStepContent(step)} )} ))} diff --git a/developer-portal/pages/UsagePage.tsx b/developer-portal/pages/UsagePage.tsx index 96b73e2e..584b8994 100644 --- a/developer-portal/pages/UsagePage.tsx +++ b/developer-portal/pages/UsagePage.tsx @@ -1,12 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { - View, - Text, - StyleSheet, - ScrollView, - TouchableOpacity, - RefreshControl, -} from 'react-native'; +import { View, Text, StyleSheet, ScrollView, TouchableOpacity, RefreshControl } from 'react-native'; interface UsageStats { totalRequests: number; @@ -37,9 +30,7 @@ export const UsagePage: React.FC = ({ environmentId }) => { const [stats, setStats] = useState(null); const [topEndpoints, setTopEndpoints] = useState([]); const [hourlyData, setHourlyData] = useState([]); - const [selectedPeriod, setSelectedPeriod] = useState<'24h' | '7d' | '30d'>( - '24h' - ); + const [selectedPeriod, setSelectedPeriod] = useState<'24h' | '7d' | '30d'>('24h'); const [refreshing, setRefreshing] = useState(false); useEffect(() => { @@ -95,33 +86,23 @@ export const UsagePage: React.FC = ({ environmentId }) => { return ( - } - > + refreshControl={}> Usage Analytics - - Monitor your API usage and performance - + Monitor your API usage and performance {(['24h', '7d', '30d'] as const).map((period) => ( setSelectedPeriod(period)} - > + style={[styles.periodButton, selectedPeriod === period && styles.periodButtonActive]} + onPress={() => setSelectedPeriod(period)}> + ]}> {period} @@ -132,9 +113,7 @@ export const UsagePage: React.FC = ({ environmentId }) => { <> - - {stats.totalRequests.toLocaleString()} - + {stats.totalRequests.toLocaleString()} Total Requests @@ -166,15 +145,12 @@ export const UsagePage: React.FC = ({ environmentId }) => { styles.bar, { height: getBarHeight(data.requests, maxRequests), - backgroundColor: - data.errors > 5 ? '#EF4444' : '#3B82F6', + backgroundColor: data.errors > 5 ? '#EF4444' : '#3B82F6', }, ]} /> - {data.hour % 4 === 0 && ( - {data.hour}h - )} + {data.hour % 4 === 0 && {data.hour}h} ))} @@ -187,21 +163,12 @@ export const UsagePage: React.FC = ({ environmentId }) => { #{index + 1} {endpoint.endpoint} - - {endpoint.count.toLocaleString()} - + {endpoint.count.toLocaleString()} - + - - {endpoint.percentage}% of total - + {endpoint.percentage}% of total ))} @@ -223,9 +190,7 @@ export const UsagePage: React.FC = ({ environmentId }) => { Uptime - - 99.95% - + 99.95% diff --git a/developer-portal/services/developerPortalService.ts b/developer-portal/services/developerPortalService.ts index a320b926..e68821f1 100644 --- a/developer-portal/services/developerPortalService.ts +++ b/developer-portal/services/developerPortalService.ts @@ -53,7 +53,12 @@ export class DeveloperPortalService { this.alerts.set(developerId, []); await this.logActivity(developerId, 'developer.registered', 'developer', developerId); - await this.createAlert(developerId, 'info', 'Welcome!', 'Your developer account has been created. Please verify your email to continue.'); + await this.createAlert( + developerId, + 'info', + 'Welcome!', + 'Your developer account has been created. Please verify your email to continue.' + ); return developer; } @@ -66,7 +71,10 @@ export class DeveloperPortalService { return false; } - if (developer.onboardingStatus.verificationExpiresAt && developer.onboardingStatus.verificationExpiresAt < new Date()) { + if ( + developer.onboardingStatus.verificationExpiresAt && + developer.onboardingStatus.verificationExpiresAt < new Date() + ) { return false; } @@ -88,17 +96,30 @@ export class DeveloperPortalService { const sandboxEnv = await sandboxService.createEnvironment(developerId, 'Default Sandbox'); developer.onboardingStatus.step = 'completed'; - developer.onboardingStatus.completedSteps.push('profile_completion', 'sandbox_setup', 'completed'); + developer.onboardingStatus.completedSteps.push( + 'profile_completion', + 'sandbox_setup', + 'completed' + ); developer.onboardingStatus.completedAt = new Date(); developer.sandboxEnvironments.push(sandboxEnv.id); developer.updatedAt = new Date(); this.developers.set(developerId, developer); - const apiKey = await this.createApiKey(developerId, 'Default API Key', 'test', ['subscriptions:read', 'subscriptions:write', 'payments:read']); + const apiKey = await this.createApiKey(developerId, 'Default API Key', 'test', [ + 'subscriptions:read', + 'subscriptions:write', + 'payments:read', + ]); await this.logActivity(developerId, 'onboarding.completed', 'developer', developerId); - await this.createAlert(developerId, 'success', 'Onboarding Complete', 'Your developer account is now fully set up. You can start using the API!'); + await this.createAlert( + developerId, + 'success', + 'Onboarding Complete', + 'Your developer account is now fully set up. You can start using the API!' + ); return developer; } @@ -107,7 +128,10 @@ export class DeveloperPortalService { return this.developers.get(developerId) || null; } - async updateDeveloper(developerId: string, updates: Partial): Promise { + async updateDeveloper( + developerId: string, + updates: Partial + ): Promise { const developer = this.developers.get(developerId); if (!developer) return null; @@ -150,7 +174,7 @@ export class DeveloperPortalService { const developer = this.developers.get(developerId); if (!developer) return false; - const apiKey = developer.apiKeys.find(key => key.id === apiKeyId); + const apiKey = developer.apiKeys.find((key) => key.id === apiKeyId); if (!apiKey) return false; apiKey.status = 'revoked'; @@ -168,7 +192,13 @@ export class DeveloperPortalService { return developer.apiKeys; } - async trackUsage(developerId: string, endpoint: string, method: string, responseTime: number, success: boolean): Promise { + async trackUsage( + developerId: string, + endpoint: string, + method: string, + responseTime: number, + success: boolean + ): Promise { const developer = this.developers.get(developerId); if (!developer) return; @@ -180,7 +210,8 @@ export class DeveloperPortalService { } developer.usage.avgResponseTime = - (developer.usage.avgResponseTime * (developer.usage.totalRequests - 1) + responseTime) / developer.usage.totalRequests; + (developer.usage.avgResponseTime * (developer.usage.totalRequests - 1) + responseTime) / + developer.usage.totalRequests; developer.updatedAt = new Date(); this.developers.set(developerId, developer); @@ -197,7 +228,7 @@ export class DeveloperPortalService { if (!developer) return null; const sandboxEnvironments = await Promise.all( - developer.sandboxEnvironments.map(async envId => { + developer.sandboxEnvironments.map(async (envId) => { const env = await sandboxService.getEnvironment(envId); const metrics = await sandboxService.getMetrics(envId); return { @@ -217,7 +248,7 @@ export class DeveloperPortalService { developer, sandboxEnvironments, recentActivity: recentActivity.slice(-10), - alerts: alerts.filter(a => !a.isRead).slice(-5), + alerts: alerts.filter((a) => !a.isRead).slice(-5), quickLinks: this.getQuickLinks(developer.tier), }; } @@ -225,20 +256,22 @@ export class DeveloperPortalService { async getDocumentation(category?: string): Promise { const docs = Array.from(this.documentation.values()); if (category) { - return docs.filter(doc => doc.category === category && doc.isPublished); + return docs.filter((doc) => doc.category === category && doc.isPublished); } - return docs.filter(doc => doc.isPublished); + return docs.filter((doc) => doc.isPublished); } async getIntegrationGuides(platform?: string): Promise { const guides = Array.from(this.integrationGuides.values()); if (platform) { - return guides.filter(guide => guide.platform.toLowerCase() === platform.toLowerCase()); + return guides.filter((guide) => guide.platform.toLowerCase() === platform.toLowerCase()); } return guides; } - async addDocumentation(doc: Omit): Promise { + async addDocumentation( + doc: Omit + ): Promise { const documentation: Documentation = { ...doc, id: this.generateDocId(), @@ -261,7 +294,7 @@ export class DeveloperPortalService { } private findDeveloperByEmail(email: string): Developer | undefined { - return Array.from(this.developers.values()).find(dev => dev.email === email); + return Array.from(this.developers.values()).find((dev) => dev.email === email); } private generateDeveloperId(): string { @@ -285,14 +318,34 @@ export class DeveloperPortalService { return `guide_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } - private getDefaultRateLimit(tier: string): { requestsPerMinute: number; requestsPerHour: number; requestsPerDay: number; burstLimit: number } { + private getDefaultRateLimit(tier: string): { + requestsPerMinute: number; + requestsPerHour: number; + requestsPerDay: number; + burstLimit: number; + } { switch (tier) { case 'enterprise': - return { requestsPerMinute: 1000, requestsPerHour: 50000, requestsPerDay: 500000, burstLimit: 2000 }; + return { + requestsPerMinute: 1000, + requestsPerHour: 50000, + requestsPerDay: 500000, + burstLimit: 2000, + }; case 'pro': - return { requestsPerMinute: 100, requestsPerHour: 5000, requestsPerDay: 50000, burstLimit: 200 }; + return { + requestsPerMinute: 100, + requestsPerHour: 5000, + requestsPerDay: 50000, + burstLimit: 200, + }; default: - return { requestsPerMinute: 20, requestsPerHour: 1000, requestsPerDay: 10000, burstLimit: 50 }; + return { + requestsPerMinute: 20, + requestsPerHour: 1000, + requestsPerDay: 10000, + burstLimit: 50, + }; } } @@ -321,7 +374,13 @@ export class DeveloperPortalService { }; } - private async logActivity(developerId: string, action: string, resource: string, resourceId: string, details?: Record): Promise { + private async logActivity( + developerId: string, + action: string, + resource: string, + resourceId: string, + details?: Record + ): Promise { const logs = this.activityLogs.get(developerId) || []; logs.push({ id: `log_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, @@ -334,7 +393,13 @@ export class DeveloperPortalService { this.activityLogs.set(developerId, logs); } - private async createAlert(developerId: string, type: 'info' | 'warning' | 'error' | 'success', title: string, message: string, actionUrl?: string): Promise { + private async createAlert( + developerId: string, + type: 'info' | 'warning' | 'error' | 'success', + title: string, + message: string, + actionUrl?: string + ): Promise { const alerts = this.alerts.get(developerId) || []; alerts.push({ id: `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, @@ -348,20 +413,52 @@ export class DeveloperPortalService { this.alerts.set(developerId, alerts); } - private getQuickLinks(tier: string): Array<{ title: string; description: string; url: string; icon: string }> { + private getQuickLinks( + tier: string + ): Array<{ title: string; description: string; url: string; icon: string }> { const links = [ - { title: 'API Documentation', description: 'View API reference documentation', url: '/docs/api', icon: 'book' }, - { title: 'Sandbox', description: 'Access your sandbox environment', url: '/sandbox', icon: 'code' }, - { title: 'API Keys', description: 'Manage your API keys', url: '/settings/api-keys', icon: 'key' }, - { title: 'Usage', description: 'View your API usage', url: '/analytics/usage', icon: 'chart' }, + { + title: 'API Documentation', + description: 'View API reference documentation', + url: '/docs/api', + icon: 'book', + }, + { + title: 'Sandbox', + description: 'Access your sandbox environment', + url: '/sandbox', + icon: 'code', + }, + { + title: 'API Keys', + description: 'Manage your API keys', + url: '/settings/api-keys', + icon: 'key', + }, + { + title: 'Usage', + description: 'View your API usage', + url: '/analytics/usage', + icon: 'chart', + }, ]; if (tier === 'pro' || tier === 'enterprise') { - links.push({ title: 'Webhooks', description: 'Configure webhooks', url: '/settings/webhooks', icon: 'webhook' }); + links.push({ + title: 'Webhooks', + description: 'Configure webhooks', + url: '/settings/webhooks', + icon: 'webhook', + }); } if (tier === 'enterprise') { - links.push({ title: 'Team', description: 'Manage team members', url: '/settings/team', icon: 'users' }); + links.push({ + title: 'Team', + description: 'Manage team members', + url: '/settings/team', + icon: 'users', + }); } return links; diff --git a/developer-portal/services/documentationService.ts b/developer-portal/services/documentationService.ts index 122555fd..60cb95f4 100644 --- a/developer-portal/services/documentationService.ts +++ b/developer-portal/services/documentationService.ts @@ -278,8 +278,8 @@ app.post('/webhooks', (req, res) => { }, ]; - this.sections.forEach(section => { - section.articles.forEach(article => { + this.sections.forEach((section) => { + section.articles.forEach((article) => { this.articles.set(article.slug, article); }); }); @@ -310,7 +310,7 @@ app.post('/webhooks', (req, res) => { } async getSection(sectionId: string): Promise { - return this.sections.find(s => s.id === sectionId) || null; + return this.sections.find((s) => s.id === sectionId) || null; } async getArticle(slug: string): Promise { @@ -321,12 +321,10 @@ app.post('/webhooks', (req, res) => { const lowerQuery = query.toLowerCase(); const results: DocumentationArticle[] = []; - this.articles.forEach(article => { + this.articles.forEach((article) => { const matchesTitle = article.title.toLowerCase().includes(lowerQuery); const matchesContent = article.content.toLowerCase().includes(lowerQuery); - const matchesTags = article.tags.some(tag => - tag.toLowerCase().includes(lowerQuery) - ); + const matchesTags = article.tags.some((tag) => tag.toLowerCase().includes(lowerQuery)); if (matchesTitle || matchesContent || matchesTags) { results.push(article); @@ -347,7 +345,7 @@ app.post('/webhooks', (req, res) => { if (!article) return []; return Array.from(this.articles.values()) - .filter(a => a.slug !== slug && a.category === article.category) + .filter((a) => a.slug !== slug && a.category === article.category) .slice(0, 3); } } diff --git a/developer-portal/services/integrationGuidesService.ts b/developer-portal/services/integrationGuidesService.ts index 52d2049c..452d9488 100644 --- a/developer-portal/services/integrationGuidesService.ts +++ b/developer-portal/services/integrationGuidesService.ts @@ -298,19 +298,19 @@ const subtrackr = new SubTrackr({ } async getGuide(guideId: string): Promise { - return this.guides.find(g => g.id === guideId) || null; + return this.guides.find((g) => g.id === guideId) || null; } async getGuidesByDifficulty( difficulty: IntegrationGuide['difficulty'] ): Promise { - return this.guides.filter(g => g.difficulty === difficulty); + return this.guides.filter((g) => g.difficulty === difficulty); } async searchGuides(query: string): Promise { const lowerQuery = query.toLowerCase(); return this.guides.filter( - guide => + (guide) => guide.title.toLowerCase().includes(lowerQuery) || guide.description.toLowerCase().includes(lowerQuery) ); diff --git a/developer-portal/services/portalService.ts b/developer-portal/services/portalService.ts index 7ccd4c0d..fc6715ac 100644 --- a/developer-portal/services/portalService.ts +++ b/developer-portal/services/portalService.ts @@ -31,9 +31,7 @@ export class DeveloperPortalService { company: string, role: PortalUser['role'] = 'developer' ): Promise { - const existingUser = Array.from(this.users.values()).find( - u => u.email === email - ); + const existingUser = Array.from(this.users.values()).find((u) => u.email === email); if (existingUser) { throw new Error('User already exists'); @@ -90,9 +88,7 @@ export class DeveloperPortalService { this.activities.set(userId, activities.slice(0, 100)); } - private async getEnvironmentSummaries( - userId: string - ): Promise { + private async getEnvironmentSummaries(userId: string): Promise { return [ { id: '1', diff --git a/developer-portal/src/components/DashboardCard.tsx b/developer-portal/src/components/DashboardCard.tsx index b94f23f6..a3704584 100644 --- a/developer-portal/src/components/DashboardCard.tsx +++ b/developer-portal/src/components/DashboardCard.tsx @@ -46,9 +46,7 @@ export const DashboardCard: React.FC = ({ {title} {value} - {trend && ( - {getTrendIcon()} - )} + {trend && {getTrendIcon()}} {subtitle && {subtitle}} diff --git a/developer-portal/src/components/PermissionSelector.tsx b/developer-portal/src/components/PermissionSelector.tsx index 90d83336..5c4bf482 100644 --- a/developer-portal/src/components/PermissionSelector.tsx +++ b/developer-portal/src/components/PermissionSelector.tsx @@ -58,7 +58,8 @@ export const PermissionSelector: React.FC = ({ {permission.icon} - + {permission.label} {permission.description} diff --git a/developer-portal/src/screens/ApiKeyManagementScreen.tsx b/developer-portal/src/screens/ApiKeyManagementScreen.tsx index b7870c25..015272cd 100644 --- a/developer-portal/src/screens/ApiKeyManagementScreen.tsx +++ b/developer-portal/src/screens/ApiKeyManagementScreen.tsx @@ -152,13 +152,9 @@ const ApiKeyManagementScreen: React.FC = () => { API Key Management - - Manage your API keys and configure permissions - + Manage your API keys and configure permissions - setShowCreateModal(true)}> + setShowCreateModal(true)}> + New Key @@ -245,12 +241,9 @@ const ApiKeyManagementScreen: React.FC = () => { 🔑 No API Keys Yet - Create your first API key to start making authenticated requests to the SubTrackr - API + Create your first API key to start making authenticated requests to the SubTrackr API - setShowCreateModal(true)}> + setShowCreateModal(true)}> Create API Key @@ -311,11 +304,7 @@ const ApiKeyManagementScreen: React.FC = () => { Create API Key - + Create @@ -405,9 +394,7 @@ const ApiKeyManagementScreen: React.FC = () => { }}> Copy to Clipboard - setCreatedKey(null)}> + setCreatedKey(null)}> I've Saved My Key diff --git a/developer-portal/src/screens/ApiTesterScreen.tsx b/developer-portal/src/screens/ApiTesterScreen.tsx index c69ee6c4..fb19a9c0 100644 --- a/developer-portal/src/screens/ApiTesterScreen.tsx +++ b/developer-portal/src/screens/ApiTesterScreen.tsx @@ -90,7 +90,7 @@ const ApiTesterScreen: React.FC = () => { } }; - const loadExample = (example: typeof EXAMPLE_ENDPOINTS[0]) => { + const loadExample = (example: (typeof EXAMPLE_ENDPOINTS)[0]) => { setSelectedMethod(example.method); setEndpoint(example.path); if (example.method === 'POST' || example.method === 'PUT') { @@ -133,10 +133,7 @@ const ApiTesterScreen: React.FC = () => { {activeKeys.map((key) => ( setSelectedApiKey(key.id)}> { Request - + {HTTP_METHODS.map((method) => ( { Response - {responseTime && ( - {responseTime}ms - )} + {responseTime && {responseTime}ms} + style={[styles.statusBadge, { backgroundColor: getStatusColor(response.status) }]}> {response.status} {response.statusText} diff --git a/developer-portal/src/screens/DeveloperPortalScreen.tsx b/developer-portal/src/screens/DeveloperPortalScreen.tsx index f8975505..909b40df 100644 --- a/developer-portal/src/screens/DeveloperPortalScreen.tsx +++ b/developer-portal/src/screens/DeveloperPortalScreen.tsx @@ -70,34 +70,29 @@ const DeveloperPortalScreen: React.FC<{ navigation: any }> = ({ navigation }) => const handleQuickCreateApiKey = async () => { if (!developer) return; - Alert.prompt( - 'Create API Key', - 'Enter a name for your new API key', - async (name) => { - if (name && name.trim()) { - try { - const newKey = await createApiKey( - developer.id, - name.trim(), - [ApiKeyPermission.READ, ApiKeyPermission.WRITE] - ); - Alert.alert( - 'API Key Created', - `Your new API key has been created:\n\n${newKey.key}\n\nCopy it now - it won't be shown again.`, - [ - { - text: 'View All Keys', - onPress: () => navigation.navigate('ApiKeyManagement'), - }, - { text: 'OK' }, - ] - ); - } catch (err) { - Alert.alert('Error', 'Failed to create API key'); - } + Alert.prompt('Create API Key', 'Enter a name for your new API key', async (name) => { + if (name && name.trim()) { + try { + const newKey = await createApiKey(developer.id, name.trim(), [ + ApiKeyPermission.READ, + ApiKeyPermission.WRITE, + ]); + Alert.alert( + 'API Key Created', + `Your new API key has been created:\n\n${newKey.key}\n\nCopy it now - it won't be shown again.`, + [ + { + text: 'View All Keys', + onPress: () => navigation.navigate('ApiKeyManagement'), + }, + { text: 'OK' }, + ] + ); + } catch (err) { + Alert.alert('Error', 'Failed to create API key'); } } - ); + }); }; if (!developer) { @@ -105,9 +100,7 @@ const DeveloperPortalScreen: React.FC<{ navigation: any }> = ({ navigation }) => Welcome to Developer Portal - - Please register to access the developer portal - + Please register to access the developer portal navigation.navigate('DeveloperRegistration')}> @@ -303,9 +296,7 @@ const DeveloperPortalScreen: React.FC<{ navigation: any }> = ({ navigation }) => 💬 Developer Support - - Get help from our developer community - + Get help from our developer community diff --git a/developer-portal/src/screens/UsageAnalyticsScreen.tsx b/developer-portal/src/screens/UsageAnalyticsScreen.tsx index 5f373387..7ec8f107 100644 --- a/developer-portal/src/screens/UsageAnalyticsScreen.tsx +++ b/developer-portal/src/screens/UsageAnalyticsScreen.tsx @@ -183,8 +183,8 @@ const UsageAnalyticsScreen: React.FC = () => { request.statusCode >= 200 && request.statusCode < 300 ? '#4CAF50' : request.statusCode >= 400 && request.statusCode < 500 - ? '#FF9800' - : '#F44336', + ? '#FF9800' + : '#F44336', }, ]}> {request.statusCode} @@ -207,9 +207,7 @@ const UsageAnalyticsScreen: React.FC = () => { Current Usage - - {usageStats?.totalCalls || 0} / 10,000 - + {usageStats?.totalCalls || 0} / 10,000 { autoCapitalize="none" keyboardType="url" /> - - Enter the URL where you want to receive webhook events - + Enter the URL where you want to receive webhook events {/* Event Types */} @@ -147,9 +145,7 @@ const WebhookTesterScreen: React.FC = () => { styles.checkbox, selectedEvents.includes(event.value) && styles.checkboxSelected, ]}> - {selectedEvents.includes(event.value) && ( - - )} + {selectedEvents.includes(event.value) && } ))} @@ -171,9 +167,7 @@ const WebhookTesterScreen: React.FC = () => { Generate - - Use this secret to verify webhook signatures - + Use this secret to verify webhook signatures {/* Signature Option */} @@ -235,9 +229,7 @@ const WebhookTesterScreen: React.FC = () => { {new Date(testResults.timestamp).toLocaleString()} )} - {testResults.error && ( - {testResults.error} - )} + {testResults.error && {testResults.error}} )} diff --git a/developer-portal/types/developer.ts b/developer-portal/types/developer.ts index 538128c5..2e2b9339 100644 --- a/developer-portal/types/developer.ts +++ b/developer-portal/types/developer.ts @@ -16,7 +16,12 @@ export interface Developer { } export interface OnboardingStatus { - step: 'registration' | 'email_verification' | 'profile_completion' | 'sandbox_setup' | 'completed'; + step: + | 'registration' + | 'email_verification' + | 'profile_completion' + | 'sandbox_setup' + | 'completed'; completedSteps: string[]; startedAt: Date; completedAt?: Date; diff --git a/developer-portal/types/portal.ts b/developer-portal/types/portal.ts index 3a5396f1..32687b89 100644 --- a/developer-portal/types/portal.ts +++ b/developer-portal/types/portal.ts @@ -26,7 +26,12 @@ export interface EnvironmentSummary { export interface ActivityEntry { id: string; - type: 'api_key_created' | 'environment_created' | 'request_made' | 'error_occurred' | 'webhook_triggered'; + type: + | 'api_key_created' + | 'environment_created' + | 'request_made' + | 'error_occurred' + | 'webhook_triggered'; description: string; timestamp: Date; metadata?: Record; diff --git a/developer-portal/utils/developerPortalUtils.ts b/developer-portal/utils/developerPortalUtils.ts index 563d256e..9ae89da0 100644 --- a/developer-portal/utils/developerPortalUtils.ts +++ b/developer-portal/utils/developerPortalUtils.ts @@ -6,7 +6,11 @@ export class DeveloperPortalUtils { return emailRegex.test(email); } - static validateApiKey(key: string): { valid: boolean; type?: 'test' | 'production'; error?: string } { + static validateApiKey(key: string): { + valid: boolean; + type?: 'test' | 'production'; + error?: string; + } { if (!key) { return { valid: false, error: 'API key is required' }; } @@ -31,7 +35,10 @@ export class DeveloperPortalUtils { return apiKey.permissions.includes(permission); } - static checkRateLimit(apiKey: ApiKey, currentUsage: { requestsPerMinute: number; requestsPerHour: number; requestsPerDay: number }): { allowed: boolean; retryAfter?: number } { + static checkRateLimit( + apiKey: ApiKey, + currentUsage: { requestsPerMinute: number; requestsPerHour: number; requestsPerDay: number } + ): { allowed: boolean; retryAfter?: number } { if (currentUsage.requestsPerMinute >= apiKey.rateLimit.requestsPerMinute) { return { allowed: false, retryAfter: 60 }; } @@ -62,13 +69,15 @@ export class DeveloperPortalUtils { avgResponseTime: string; errorRate: string; } { - const successRate = metrics.totalRequests > 0 - ? ((metrics.successfulRequests / metrics.totalRequests) * 100).toFixed(2) - : '0.00'; + const successRate = + metrics.totalRequests > 0 + ? ((metrics.successfulRequests / metrics.totalRequests) * 100).toFixed(2) + : '0.00'; - const errorRate = metrics.totalRequests > 0 - ? ((metrics.failedRequests / metrics.totalRequests) * 100).toFixed(2) - : '0.00'; + const errorRate = + metrics.totalRequests > 0 + ? ((metrics.failedRequests / metrics.totalRequests) * 100).toFixed(2) + : '0.00'; return { totalRequests: metrics.totalRequests.toLocaleString(), @@ -78,7 +87,9 @@ export class DeveloperPortalUtils { }; } - static generateOnboardingChecklist(developer: Developer): Array<{ step: string; completed: boolean; description: string }> { + static generateOnboardingChecklist( + developer: Developer + ): Array<{ step: string; completed: boolean; description: string }> { return [ { step: 'register', @@ -142,7 +153,14 @@ export class DeveloperPortalUtils { maxApiKeys: 50, maxSandboxEnvironments: 10, maxRequestsPerMonth: 1000000, - features: ['priority_support', 'custom_sla', 'dedicated_account_manager', 'advanced_analytics', 'webhooks', 'team_management'], + features: [ + 'priority_support', + 'custom_sla', + 'dedicated_account_manager', + 'advanced_analytics', + 'webhooks', + 'team_management', + ], }; case 'pro': return { @@ -180,9 +198,9 @@ export class DeveloperPortalUtils { } static generateWebhookSecret(): string { - return 'whsec_' + Array.from({ length: 32 }, () => - Math.random().toString(36).charAt(2) - ).join(''); + return ( + 'whsec_' + Array.from({ length: 32 }, () => Math.random().toString(36).charAt(2)).join('') + ); } static formatCurrency(amount: number, currency: string = 'USD'): string { diff --git a/sandbox/__tests__/developerPortal.test.ts b/sandbox/__tests__/developerPortal.test.ts index 5aca0a27..0ce5972d 100644 --- a/sandbox/__tests__/developerPortal.test.ts +++ b/sandbox/__tests__/developerPortal.test.ts @@ -10,11 +10,7 @@ describe('DeveloperPortalService', () => { describe('createUser', () => { it('should create a new user', async () => { - const user = await service.createUser( - 'test@example.com', - 'Test User', - 'Test Company' - ); + const user = await service.createUser('test@example.com', 'Test User', 'Test Company'); expect(user).toBeDefined(); expect(user.email).toBe('test@example.com'); @@ -26,9 +22,9 @@ describe('DeveloperPortalService', () => { it('should not allow duplicate email', async () => { await service.createUser('test@example.com', 'User 1', 'Company 1'); - await expect( - service.createUser('test@example.com', 'User 2', 'Company 2') - ).rejects.toThrow('User already exists'); + await expect(service.createUser('test@example.com', 'User 2', 'Company 2')).rejects.toThrow( + 'User already exists' + ); }); it('should create user with custom role', async () => { @@ -50,11 +46,7 @@ describe('DeveloperPortalService', () => { }); it('should return existing user', async () => { - const created = await service.createUser( - 'test@example.com', - 'Test User', - 'Test Company' - ); + const created = await service.createUser('test@example.com', 'Test User', 'Test Company'); const retrieved = await service.getUser(created.id); expect(retrieved).toBeDefined(); @@ -64,11 +56,7 @@ describe('DeveloperPortalService', () => { describe('updateUser', () => { it('should update user details', async () => { - const user = await service.createUser( - 'test@example.com', - 'Test User', - 'Test Company' - ); + const user = await service.createUser('test@example.com', 'Test User', 'Test Company'); const updated = await service.updateUser(user.id, { name: 'Updated Name', @@ -91,11 +79,7 @@ describe('DeveloperPortalService', () => { describe('getDashboard', () => { it('should return dashboard data', async () => { - const user = await service.createUser( - 'test@example.com', - 'Test User', - 'Test Company' - ); + const user = await service.createUser('test@example.com', 'Test User', 'Test Company'); const dashboard = await service.getDashboard(user.id); @@ -111,25 +95,15 @@ describe('DeveloperPortalService', () => { }); it('should throw for non-existent user', async () => { - await expect(service.getDashboard('non-existent')).rejects.toThrow( - 'User not found' - ); + await expect(service.getDashboard('non-existent')).rejects.toThrow('User not found'); }); }); describe('logActivity', () => { it('should log user activity', async () => { - const user = await service.createUser( - 'test@example.com', - 'Test User', - 'Test Company' - ); + const user = await service.createUser('test@example.com', 'Test User', 'Test Company'); - await service.logActivity( - user.id, - 'api_key_created', - 'New API key created' - ); + await service.logActivity(user.id, 'api_key_created', 'New API key created'); const dashboard = await service.getDashboard(user.id); expect(dashboard.recentActivity.length).toBeGreaterThan(0); diff --git a/sandbox/__tests__/sandbox.test.ts b/sandbox/__tests__/sandbox.test.ts index edb3315b..1b627b17 100644 --- a/sandbox/__tests__/sandbox.test.ts +++ b/sandbox/__tests__/sandbox.test.ts @@ -135,22 +135,12 @@ describe('SandboxIsolationService', () => { describe('updateOnboardingStep', () => { it('should update onboarding step', async () => { - const developer = await service.registerDeveloper( - 'test@example.com', - 'Test Dev', - 'Test Co' - ); + const developer = await service.registerDeveloper('test@example.com', 'Test Dev', 'Test Co'); - const updated = await service.updateOnboardingStep( - developer.id, - 'create-sandbox', - true - ); + const updated = await service.updateOnboardingStep(developer.id, 'create-sandbox', true); expect(updated).toBeDefined(); - const step = updated?.onboardingStatus.steps.find( - (s) => s.id === 'create-sandbox' - ); + const step = updated?.onboardingStatus.steps.find((s) => s.id === 'create-sandbox'); expect(step?.completed).toBe(true); }); }); @@ -179,9 +169,9 @@ describe('ApiKeyService', () => { await service.generateApiKey('env-1', `Key ${i}`, ['read']); } - await expect( - service.generateApiKey('env-1', 'Extra Key', ['read']) - ).rejects.toThrow('Maximum API keys limit reached'); + await expect(service.generateApiKey('env-1', 'Extra Key', ['read'])).rejects.toThrow( + 'Maximum API keys limit reached' + ); }); }); diff --git a/sandbox/api/sandboxApi.ts b/sandbox/api/sandboxApi.ts index d40486c0..5dc2ca66 100644 --- a/sandbox/api/sandboxApi.ts +++ b/sandbox/api/sandboxApi.ts @@ -18,11 +18,7 @@ export class SandboxApi { config?: Partial ): Promise { try { - const environment = await this.sandboxService.createEnvironment( - developerId, - name, - config - ); + const environment = await this.sandboxService.createEnvironment(developerId, name, config); return { success: true, @@ -76,10 +72,7 @@ export class SandboxApi { updates: Partial ): Promise { try { - const environment = await this.sandboxService.updateConfig( - environmentId, - updates - ); + const environment = await this.sandboxService.updateConfig(environmentId, updates); if (!environment) { return { success: false, error: 'Environment not found' }; diff --git a/sandbox/config/sandboxConfig.ts b/sandbox/config/sandboxConfig.ts index 25b8ceae..d9c94816 100644 --- a/sandbox/config/sandboxConfig.ts +++ b/sandbox/config/sandboxConfig.ts @@ -118,9 +118,12 @@ export const SANDBOX_CONSTANTS = { DEFAULT_ENVIRONMENT_TTL_DAYS: 90, }; -export function getSandboxConfig( - tier: 'free' | 'pro' | 'enterprise' = 'free' -): { resourceLimits: SandboxResourceLimits; rateLimits: RateLimit; features: SandboxFeatures; dataRetentionDays: number } { +export function getSandboxConfig(tier: 'free' | 'pro' | 'enterprise' = 'free'): { + resourceLimits: SandboxResourceLimits; + rateLimits: RateLimit; + features: SandboxFeatures; + dataRetentionDays: number; +} { return SANDBOX_TIERS[tier] || SANDBOX_TIERS.free; } diff --git a/sandbox/middleware/sandboxMiddleware.ts b/sandbox/middleware/sandboxMiddleware.ts index 330eb6b5..c41cf3bd 100644 --- a/sandbox/middleware/sandboxMiddleware.ts +++ b/sandbox/middleware/sandboxMiddleware.ts @@ -24,18 +24,14 @@ export interface SandboxResponse { } export class SandboxMiddleware { - private rateLimitStore: Map = - new Map(); + private rateLimitStore: Map = new Map(); async processRequest(request: SandboxRequest): Promise { const startTime = Date.now(); const requestId = this.generateRequestId(); try { - const isValid = await sandboxService.validateAccess( - request.environmentId, - request.apiKey - ); + const isValid = await sandboxService.validateAccess(request.environmentId, request.apiKey); if (!isValid) { return this.createErrorResponse( request, @@ -46,9 +42,7 @@ export class SandboxMiddleware { ); } - const context = await sandboxService.getIsolationContext( - request.environmentId - ); + const context = await sandboxService.getIsolationContext(request.environmentId); if (!context) { return this.createErrorResponse( request, @@ -74,20 +68,10 @@ export class SandboxMiddleware { context.resourceQuota ); if (!rateLimitResult.allowed) { - return this.createErrorResponse( - request, - requestId, - startTime, - 'Rate limit exceeded', - 429 - ); + return this.createErrorResponse(request, requestId, startTime, 'Rate limit exceeded', 429); } - await sandboxService.recordRequest( - request.environmentId, - Date.now() - startTime, - false - ); + await sandboxService.recordRequest(request.environmentId, Date.now() - startTime, false); return { success: true, @@ -101,18 +85,8 @@ export class SandboxMiddleware { }, }; } catch (error) { - await sandboxService.recordRequest( - request.environmentId, - Date.now() - startTime, - true - ); - return this.createErrorResponse( - request, - requestId, - startTime, - 'Internal sandbox error', - 500 - ); + await sandboxService.recordRequest(request.environmentId, Date.now() - startTime, true); + return this.createErrorResponse(request, requestId, startTime, 'Internal sandbox error', 500); } } @@ -144,9 +118,7 @@ export class SandboxMiddleware { }; } - async enforceResourceLimits( - envId: string - ): Promise<{ withinLimits: boolean; usage: unknown }> { + async enforceResourceLimits(envId: string): Promise<{ withinLimits: boolean; usage: unknown }> { const context = await sandboxService.getIsolationContext(envId); if (!context) throw new Error('Environment not found'); diff --git a/sandbox/services/apiKeyService.ts b/sandbox/services/apiKeyService.ts index b04f67ad..83b94a0c 100644 --- a/sandbox/services/apiKeyService.ts +++ b/sandbox/services/apiKeyService.ts @@ -75,8 +75,10 @@ export class ApiKeyService { return false; } - return validation.apiKey.permissions.includes(permission) || - validation.apiKey.permissions.includes('admin'); + return ( + validation.apiKey.permissions.includes(permission) || + validation.apiKey.permissions.includes('admin') + ); } async revokeApiKey(keyId: string): Promise { diff --git a/sandbox/services/sandboxIsolationService.ts b/sandbox/services/sandboxIsolationService.ts index 5b54fe1f..21e1e7d3 100644 --- a/sandbox/services/sandboxIsolationService.ts +++ b/sandbox/services/sandboxIsolationService.ts @@ -65,9 +65,7 @@ export class SandboxIsolationService { } async getEnvironmentsByDeveloper(developerId: string): Promise { - return Array.from(this.environments.values()).filter( - (env) => env.developerId === developerId - ); + return Array.from(this.environments.values()).filter((env) => env.developerId === developerId); } async updateEnvironment( @@ -164,11 +162,7 @@ export class SandboxIsolationService { } } - private validateRateLimits( - rateLimits: RateLimit, - errors: string[], - _warnings: string[] - ): void { + private validateRateLimits(rateLimits: RateLimit, errors: string[], _warnings: string[]): void { if (rateLimits.requestsPerMinute <= 0) { errors.push('Invalid requestsPerMinute rate limit'); } @@ -180,14 +174,8 @@ export class SandboxIsolationService { } } - async registerDeveloper( - email: string, - name: string, - company: string - ): Promise { - const existingDeveloper = Array.from(this.developers.values()).find( - (d) => d.email === email - ); + async registerDeveloper(email: string, name: string, company: string): Promise { + const existingDeveloper = Array.from(this.developers.values()).find((d) => d.email === email); if (existingDeveloper) { throw new Error('Developer already registered'); @@ -268,8 +256,9 @@ export class SandboxIsolationService { developer.onboardingStatus.step = developer.onboardingStatus.steps.filter( (s) => s.completed ).length; - developer.onboardingStatus.completed = - developer.onboardingStatus.steps.every((s) => s.completed); + developer.onboardingStatus.completed = developer.onboardingStatus.steps.every( + (s) => s.completed + ); this.developers.set(developerId, developer); return developer; @@ -300,7 +289,16 @@ export class SandboxIsolationService { private generateTestSubscriptions(): SandboxTestData['subscriptions'] { const categories = ['streaming', 'software', 'gaming', 'productivity', 'fitness']; - const names = ['Netflix', 'Spotify', 'Adobe CC', 'Slack', 'Gym Membership', 'GitHub Pro', 'Figma', 'Notion']; + const names = [ + 'Netflix', + 'Spotify', + 'Adobe CC', + 'Slack', + 'Gym Membership', + 'GitHub Pro', + 'Figma', + 'Notion', + ]; return names.map((name, index) => ({ id: `sub_test_${index + 1}`, @@ -315,7 +313,9 @@ export class SandboxIsolationService { })); } - private generateTestPayments(subscriptions: SandboxTestData['subscriptions']): SandboxTestData['payments'] { + private generateTestPayments( + subscriptions: SandboxTestData['subscriptions'] + ): SandboxTestData['payments'] { const payments: SandboxTestData['payments'] = []; const methods: Array<'card' | 'crypto' | 'bank'> = ['card', 'crypto', 'bank']; diff --git a/sandbox/services/sandboxService.ts b/sandbox/services/sandboxService.ts index 9ff0936d..30f5a2b5 100644 --- a/sandbox/services/sandboxService.ts +++ b/sandbox/services/sandboxService.ts @@ -91,9 +91,7 @@ export class SandboxService { } async getEnvironmentsByDeveloper(developerId: string): Promise { - return Array.from(this.environments.values()).filter( - (env) => env.developerId === developerId - ); + return Array.from(this.environments.values()).filter((env) => env.developerId === developerId); } async updateEnvironment( @@ -130,10 +128,7 @@ export class SandboxService { return this.configs.get(envId) || null; } - async updateConfig( - envId: string, - config: Partial - ): Promise { + async updateConfig(envId: string, config: Partial): Promise { const existingConfig = this.configs.get(envId); if (!existingConfig) return null; @@ -160,19 +155,14 @@ export class SandboxService { return this.metrics.get(envId) || null; } - async recordRequest( - envId: string, - responseTime: number, - isError: boolean - ): Promise { + async recordRequest(envId: string, responseTime: number, isError: boolean): Promise { const metrics = this.metrics.get(envId); if (!metrics) return; metrics.requestCount++; if (isError) metrics.errorCount++; metrics.avgResponseTime = - (metrics.avgResponseTime * (metrics.requestCount - 1) + responseTime) / - metrics.requestCount; + (metrics.avgResponseTime * (metrics.requestCount - 1) + responseTime) / metrics.requestCount; metrics.lastActivity = new Date(); this.metrics.set(envId, metrics); @@ -184,14 +174,16 @@ export class SandboxService { if (!env || !metrics) return null; const isWithinLimits = this.checkResourceLimits( - env.config.features ? { - maxRequestsPerMinute: env.config.rateLimits.requestsPerMinute, - maxRequestsPerDay: env.config.rateLimits.requestsPerDay, - maxStorageMB: 100, - maxConcurrentConnections: env.config.rateLimits.maxConcurrentRequests, - maxSubscriptions: 50, - maxWebhooks: 5, - } : this.getDefaultResourceLimits(), + env.config.features + ? { + maxRequestsPerMinute: env.config.rateLimits.requestsPerMinute, + maxRequestsPerDay: env.config.rateLimits.requestsPerDay, + maxStorageMB: 100, + maxConcurrentConnections: env.config.rateLimits.maxConcurrentRequests, + maxSubscriptions: 50, + maxWebhooks: 5, + } + : this.getDefaultResourceLimits(), metrics ); @@ -199,14 +191,16 @@ export class SandboxService { environmentId: envId, developerId: env.developerId, dataNamespace: `sandbox_${envId}`, - resourceQuota: env.config.features ? { - maxRequestsPerMinute: env.config.rateLimits.requestsPerMinute, - maxRequestsPerDay: env.config.rateLimits.requestsPerDay, - maxStorageMB: 100, - maxConcurrentConnections: env.config.rateLimits.maxConcurrentRequests, - maxSubscriptions: 50, - maxWebhooks: 5, - } : this.getDefaultResourceLimits(), + resourceQuota: env.config.features + ? { + maxRequestsPerMinute: env.config.rateLimits.requestsPerMinute, + maxRequestsPerDay: env.config.rateLimits.requestsPerDay, + maxStorageMB: 100, + maxConcurrentConnections: env.config.rateLimits.maxConcurrentRequests, + maxSubscriptions: 50, + maxWebhooks: 5, + } + : this.getDefaultResourceLimits(), currentUsage: metrics, isWithinLimits, }; @@ -266,16 +260,10 @@ export class SandboxService { category: categories[index % categories.length], price: Math.floor(Math.random() * 50) + 5, currency: 'USD', - billingCycle: (['monthly', 'yearly', 'weekly'] as const)[ - Math.floor(Math.random() * 3) - ], + billingCycle: (['monthly', 'yearly', 'weekly'] as const)[Math.floor(Math.random() * 3)], status: 'active' as const, - startDate: new Date( - Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000 - ), - nextBillingDate: new Date( - Date.now() + Math.random() * 30 * 24 * 60 * 60 * 1000 - ), + startDate: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000), + nextBillingDate: new Date(Date.now() + Math.random() * 30 * 24 * 60 * 60 * 1000), })); } @@ -290,13 +278,9 @@ export class SandboxService { subscriptionId: sub.id, amount: sub.price, currency: sub.currency, - status: (['pending', 'completed', 'failed'] as const)[ - Math.floor(Math.random() * 3) - ], + status: (['pending', 'completed', 'failed'] as const)[Math.floor(Math.random() * 3)], method: methods[Math.floor(Math.random() * methods.length)], - timestamp: new Date( - Date.now() - Math.random() * 90 * 24 * 60 * 60 * 1000 - ), + timestamp: new Date(Date.now() - Math.random() * 90 * 24 * 60 * 60 * 1000), }); } }); @@ -337,10 +321,7 @@ export class SandboxService { ]; } - private checkResourceLimits( - limits: SandboxResourceLimits, - metrics: SandboxMetrics - ): boolean { + private checkResourceLimits(limits: SandboxResourceLimits, metrics: SandboxMetrics): boolean { return ( metrics.requestCount < limits.maxRequestsPerDay && metrics.storageUsedMB < limits.maxStorageMB && diff --git a/sandbox/services/usageTrackingService.ts b/sandbox/services/usageTrackingService.ts index 1bf1f9c0..ee16da62 100644 --- a/sandbox/services/usageTrackingService.ts +++ b/sandbox/services/usageTrackingService.ts @@ -1,8 +1,4 @@ -import { - UsageMetrics, - HourlyUsage, - DailyUsage, -} from '../types/sandbox'; +import { UsageMetrics, HourlyUsage, DailyUsage } from '../types/sandbox'; export class UsageTrackingService { private usageData: Map = new Map(); @@ -60,26 +56,18 @@ export class UsageTrackingService { statusCode?: number; } ): Promise { - let filtered = this.requestLog.filter( - (entry) => entry.environmentId === environmentId - ); + let filtered = this.requestLog.filter((entry) => entry.environmentId === environmentId); if (options?.startDate) { - filtered = filtered.filter( - (entry) => entry.timestamp >= options.startDate! - ); + filtered = filtered.filter((entry) => entry.timestamp >= options.startDate!); } if (options?.endDate) { - filtered = filtered.filter( - (entry) => entry.timestamp <= options.endDate! - ); + filtered = filtered.filter((entry) => entry.timestamp <= options.endDate!); } if (options?.statusCode) { - filtered = filtered.filter( - (entry) => entry.statusCode === options.statusCode - ); + filtered = filtered.filter((entry) => entry.statusCode === options.statusCode); } filtered.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); @@ -111,9 +99,7 @@ export class UsageTrackingService { successfulRequests: metrics.successfulRequests, failedRequests: metrics.failedRequests, successRate: - metrics.totalRequests > 0 - ? (metrics.successfulRequests / metrics.totalRequests) * 100 - : 0, + metrics.totalRequests > 0 ? (metrics.successfulRequests / metrics.totalRequests) * 100 : 0, averageResponseTime: metrics.averageResponseTime, requestsLast24Hours: last24Hours.length, requestsLast7Days: last7Days.length, @@ -124,9 +110,7 @@ export class UsageTrackingService { async resetUsage(environmentId: string): Promise { this.usageData.delete(environmentId); - this.requestLog = this.requestLog.filter( - (entry) => entry.environmentId !== environmentId - ); + this.requestLog = this.requestLog.filter((entry) => entry.environmentId !== environmentId); return true; } @@ -159,9 +143,7 @@ export class UsageTrackingService { } private calculateAverageResponseTime(environmentId: string): number { - const entries = this.requestLog.filter( - (entry) => entry.environmentId === environmentId - ); + const entries = this.requestLog.filter((entry) => entry.environmentId === environmentId); if (entries.length === 0) return 0; @@ -169,11 +151,7 @@ export class UsageTrackingService { return Math.round(total / entries.length); } - private updateHourlyUsage( - metrics: UsageMetrics, - statusCode: number, - responseTime: number - ): void { + private updateHourlyUsage(metrics: UsageMetrics, statusCode: number, responseTime: number): void { const currentHour = new Date().getHours(); const hourlyData = metrics.last24Hours[currentHour]; @@ -189,11 +167,7 @@ export class UsageTrackingService { } } - private updateDailyUsage( - metrics: UsageMetrics, - statusCode: number, - responseTime: number - ): void { + private updateDailyUsage(metrics: UsageMetrics, statusCode: number, responseTime: number): void { const today = new Date().toISOString().split('T')[0]; const dailyData = metrics.last7Days.find((d) => d.date === today); @@ -203,16 +177,12 @@ export class UsageTrackingService { dailyData.errors++; } dailyData.avgResponseTime = Math.round( - (dailyData.avgResponseTime * (dailyData.requests - 1) + responseTime) / - dailyData.requests + (dailyData.avgResponseTime * (dailyData.requests - 1) + responseTime) / dailyData.requests ); } } - private getTopEndpoints( - environmentId: string, - limit: number - ): EndpointUsage[] { + private getTopEndpoints(environmentId: string, limit: number): EndpointUsage[] { const endpointMap = new Map(); this.requestLog @@ -232,10 +202,7 @@ export class UsageTrackingService { const errorMap = new Map(); this.requestLog - .filter( - (entry) => - entry.environmentId === environmentId && entry.statusCode >= 400 - ) + .filter((entry) => entry.environmentId === environmentId && entry.statusCode >= 400) .forEach((entry) => { errorMap.set(entry.statusCode, (errorMap.get(entry.statusCode) || 0) + 1); }); diff --git a/sandbox/utils/sandboxUtils.ts b/sandbox/utils/sandboxUtils.ts index 3f0692ba..0094ab68 100644 --- a/sandbox/utils/sandboxUtils.ts +++ b/sandbox/utils/sandboxUtils.ts @@ -25,7 +25,10 @@ export class SandboxUtils { return { valid: true }; } - static calculateResourceUsage(testData: SandboxTestData): { storageMB: number; itemCount: number } { + static calculateResourceUsage(testData: SandboxTestData): { + storageMB: number; + itemCount: number; + } { const jsonString = JSON.stringify(testData); const bytes = new TextEncoder().encode(jsonString).length; const storageMB = bytes / (1024 * 1024); @@ -41,24 +44,37 @@ export class SandboxUtils { static checkResourceLimits( limits: SandboxResourceLimits, - currentUsage: { requestsPerMinute: number; requestsPerDay: number; storageMB: number; connections: number } + currentUsage: { + requestsPerMinute: number; + requestsPerDay: number; + storageMB: number; + connections: number; + } ): { withinLimits: boolean; violations: string[] } { const violations: string[] = []; if (currentUsage.requestsPerMinute > limits.maxRequestsPerMinute) { - violations.push(`Requests per minute (${currentUsage.requestsPerMinute}) exceeds limit (${limits.maxRequestsPerMinute})`); + violations.push( + `Requests per minute (${currentUsage.requestsPerMinute}) exceeds limit (${limits.maxRequestsPerMinute})` + ); } if (currentUsage.requestsPerDay > limits.maxRequestsPerDay) { - violations.push(`Requests per day (${currentUsage.requestsPerDay}) exceeds limit (${limits.maxRequestsPerDay})`); + violations.push( + `Requests per day (${currentUsage.requestsPerDay}) exceeds limit (${limits.maxRequestsPerDay})` + ); } if (currentUsage.storageMB > limits.maxStorageMB) { - violations.push(`Storage (${currentUsage.storageMB}MB) exceeds limit (${limits.maxStorageMB}MB)`); + violations.push( + `Storage (${currentUsage.storageMB}MB) exceeds limit (${limits.maxStorageMB}MB)` + ); } if (currentUsage.connections > limits.maxConcurrentConnections) { - violations.push(`Connections (${currentUsage.connections}) exceeds limit (${limits.maxConcurrentConnections})`); + violations.push( + `Connections (${currentUsage.connections}) exceeds limit (${limits.maxConcurrentConnections})` + ); } return { @@ -73,7 +89,7 @@ export class SandboxUtils { } if (Array.isArray(data)) { - return data.map(item => this.sanitizeForSandbox(item)); + return data.map((item) => this.sanitizeForSandbox(item)); } if (typeof data === 'object' && data !== null) { @@ -116,7 +132,10 @@ export class SandboxUtils { }; } - static createSandboxHeaders(envId: string, additionalHeaders: Record = {}): Record { + static createSandboxHeaders( + envId: string, + additionalHeaders: Record = {} + ): Record { return { 'X-Sandbox-Environment': envId, 'X-Sandbox-Mode': 'true', @@ -125,7 +144,11 @@ export class SandboxUtils { }; } - static parseSandboxHeaders(headers: Record): { envId?: string; isSandbox: boolean; requestId?: string } { + static parseSandboxHeaders(headers: Record): { + envId?: string; + isSandbox: boolean; + requestId?: string; + } { return { envId: headers['X-Sandbox-Environment'], isSandbox: headers['X-Sandbox-Mode'] === 'true', diff --git a/sandbox/utils/testDataGenerator.ts b/sandbox/utils/testDataGenerator.ts index 09d1eff9..9a93cb3a 100644 --- a/sandbox/utils/testDataGenerator.ts +++ b/sandbox/utils/testDataGenerator.ts @@ -42,8 +42,28 @@ export class TestDataGenerator { const domains = ['gmail.com', 'yahoo.com', 'outlook.com', 'company.io']; for (let i = 0; i < count; i++) { - const firstName = this.randomFrom(['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace', 'Henry', 'Ivy', 'Jack']); - const lastName = this.randomFrom(['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis']); + const firstName = this.randomFrom([ + 'Alice', + 'Bob', + 'Charlie', + 'Diana', + 'Eve', + 'Frank', + 'Grace', + 'Henry', + 'Ivy', + 'Jack', + ]); + const lastName = this.randomFrom([ + 'Smith', + 'Johnson', + 'Williams', + 'Brown', + 'Jones', + 'Garcia', + 'Miller', + 'Davis', + ]); const domain = this.randomFrom(domains); users.push({ @@ -143,12 +163,15 @@ export class TestDataGenerator { return subscriptions; } - static generatePayments( - subscriptions: TestSubscription[], - count: number - ): TestPayment[] { + static generatePayments(subscriptions: TestSubscription[], count: number): TestPayment[] { const payments: TestPayment[] = []; - const statuses: TestPayment['status'][] = ['completed', 'completed', 'completed', 'pending', 'failed']; + const statuses: TestPayment['status'][] = [ + 'completed', + 'completed', + 'completed', + 'pending', + 'failed', + ]; for (let i = 0; i < count; i++) { const subscription = this.randomFrom(subscriptions); diff --git a/sdks/javascript/src/errors.ts b/sdks/javascript/src/errors.ts index 7a956dbc..bbbe4bf0 100644 --- a/sdks/javascript/src/errors.ts +++ b/sdks/javascript/src/errors.ts @@ -1,5 +1,9 @@ export class SubTrackrError extends Error { - constructor(public message: string, public statusCode?: number, public code?: string) { + constructor( + public message: string, + public statusCode?: number, + public code?: string + ) { super(message); this.name = 'SubTrackrError'; } diff --git a/src/components/UsageDashboard.tsx b/src/components/UsageDashboard.tsx index 16ef639b..dc72715a 100644 --- a/src/components/UsageDashboard.tsx +++ b/src/components/UsageDashboard.tsx @@ -5,7 +5,7 @@ export const UsageDashboard = () => { return ( Usage & Billing - + API Calls 85,000 / 100,000 diff --git a/src/components/common/ScreenTemplates.tsx b/src/components/common/ScreenTemplates.tsx index 7ac1a171..4420aa3f 100644 --- a/src/components/common/ScreenTemplates.tsx +++ b/src/components/common/ScreenTemplates.tsx @@ -111,7 +111,11 @@ export function ListScreen({ style={styles.scroll} refreshControl={ onRefresh ? ( - + ) : undefined }> {data.length === 0 ? ( @@ -123,7 +127,9 @@ export function ListScreen({ onAction={onEmptyAction} /> ) : ( - data.map((item, index) => {renderItem(item, index)}) + data.map((item, index) => ( + {renderItem(item, index)} + )) )} diff --git a/src/components/developer/DeveloperComponents.tsx b/src/components/developer/DeveloperComponents.tsx index b6a77561..c051b327 100644 --- a/src/components/developer/DeveloperComponents.tsx +++ b/src/components/developer/DeveloperComponents.tsx @@ -46,8 +46,8 @@ export const StatCard: React.FC = ({ label, value, trend, trendDi trendDirection === 'up' ? colors.success : trendDirection === 'down' - ? colors.error - : colors.textSecondary; + ? colors.error + : colors.textSecondary; return ( diff --git a/src/components/home/StatsCard.tsx b/src/components/home/StatsCard.tsx index 867f2901..2e82f616 100644 --- a/src/components/home/StatsCard.tsx +++ b/src/components/home/StatsCard.tsx @@ -10,14 +10,12 @@ interface StatsCardProps { currency?: string; } - export const StatsCard: React.FC = ({ totalMonthlySpend, totalActive, onWalletPress, currency = 'USD', }) => { - return ( {/* Monthly Spend Card - Primary Focus */} @@ -42,7 +40,6 @@ export const StatsCard: React.FC = ({ importantForAccessibility="no"> {formatCurrencyCompact(totalMonthlySpend, currency)} - {/* Active Count Card */} diff --git a/src/components/subscription/SubscriptionCard.tsx b/src/components/subscription/SubscriptionCard.tsx index accba0c4..b167bd7c 100644 --- a/src/components/subscription/SubscriptionCard.tsx +++ b/src/components/subscription/SubscriptionCard.tsx @@ -17,7 +17,6 @@ import { import { useSettingsStore } from '../../store/settingsStore'; import { currencyService } from '../../services/currencyService'; - export interface SubscriptionCardProps { subscription: Subscription; onPress: (subscription: Subscription) => void; @@ -50,7 +49,6 @@ export const SubscriptionCard: React.FC = React.memo( rates ); - return ( = React.memo( ]}> /{formatBillingCycle(subscription.billingCycle)} - diff --git a/src/config/features.ts b/src/config/features.ts index fcae8511..1bacff7d 100644 --- a/src/config/features.ts +++ b/src/config/features.ts @@ -198,7 +198,8 @@ export const FEATURE_CONFIG: FeatureConfig = { [FeatureId.DEVELOPER_PORTAL]: { id: FeatureId.DEVELOPER_PORTAL, name: 'Developer Portal', - description: 'Access the developer portal with API documentation, integration guides, and sandbox environment', + description: + 'Access the developer portal with API documentation, integration guides, and sandbox environment', enabled: true, tierAccess: [SubscriptionTier.ENTERPRISE], rolloutPercentage: 100, diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 6ef92076..27fa8aa9 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -50,7 +50,60 @@ import IntegrationGuidesScreen from '../screens/IntegrationGuidesScreen'; import PerformanceDashboardScreen from '../screens/PerformanceDashboardScreen'; import { colors } from '../utils/constants'; -import { RootStackParamList, TabParamList } from './types'; +// Lazy loaded auxiliary and heavy screens with suspense/retry support +const AddSubscriptionScreen = lazyScreen(() => import('../screens/AddSubscriptionScreen')); +const CancellationFlowScreen = lazyScreen(() => import('../screens/CancellationFlowScreen')); +const WalletConnectScreen = lazyScreen(() => import('../screens/WalletConnectV2Screen')); +const CryptoPaymentScreen = lazyScreen(() => import('../screens/CryptoPaymentScreen')); +const CommunityScreen = lazyScreen(() => import('../screens/CommunityScreen')); +const ProfileScreen = lazyScreen(() => import('../screens/ProfileScreen')); +const SubscriptionDetailScreen = lazyScreen(() => import('../screens/SubscriptionDetailScreen')); +const InvoiceListScreen = lazyScreen(() => import('../screens/InvoiceListScreen')); +const InvoiceDetailScreen = lazyScreen(() => import('../screens/InvoiceDetailScreen')); +const AnalyticsScreen = lazyScreen(() => import('../screens/AnalyticsScreen')); +const SlaDashboard = lazyScreen(() => import('../screens/SlaDashboard')); +const GDPRSettingsScreen = lazyScreen(() => import('../screens/GDPRSettingsScreen')); +const LanguageSettingsScreen = lazyScreen(() => import('../screens/LanguageSettingsScreen')); +const SessionManagementScreen = lazyScreen(() => import('../screens/SessionManagementScreen')); +const CalendarIntegrationScreen = lazyScreen(() => import('../screens/CalendarIntegrationScreen')); +const AccountingExportScreen = lazyScreen(() => import('../screens/AccountingExportScreen')); +const WebhookSettingsScreen = lazyScreen(() => import('../screens/WebhookSettingsScreen')); +const ErrorDashboardScreen = lazyScreen(() => import('../screens/ErrorDashboardScreen')); +const ImportScreen = lazyScreen(() => import('../screens/ImportScreen')); +const ExportScreen = lazyScreen(() => import('../screens/ExportScreen')); +const BatchOperationsScreen = lazyScreen(() => + import('../../app/screens/BatchOperationsScreen').then((m) => ({ + default: m.BatchOperationsScreen, + })) +); +const AdminDashboardScreen = lazyScreen(() => import('../screens/AdminDashboardScreen')); +const FraudDashboard = lazyScreen(() => import('../screens/FraudDashboard')); +const GroupManagementScreen = lazyScreen(() => import('../screens/GroupManagementScreen')); +const TaxSettingsScreen = lazyScreen(() => import('../screens/TaxSettingsScreen')); +const SupportDashboardScreen = lazyScreen(() => import('../screens/SupportDashboardScreen')); +const SegmentManagementScreen = lazyScreen(() => + import('../screens/SegmentManagementScreen').then((m) => ({ default: m.SegmentManagementScreen })) +); +const SegmentDetailScreen = lazyScreen(() => + import('../screens/SegmentDetailScreen').then((m) => ({ default: m.SegmentDetailScreen })) +); +const GamificationScreen = lazyScreen(() => + import('../screens/GamificationScreen').then((m) => ({ default: m.GamificationScreen })) +); +const RevenueReportScreen = lazyScreen(() => import('../screens/RevenueReportScreen')); +const UsageDashboardScreen = lazyScreen(() => import('../screens/UsageDashboard')); +const MerchantOnboardingScreen = lazyScreen(() => import('../screens/MerchantOnboardingScreen')); +const AffiliateDashboardScreen = lazyScreen(() => import('../screens/AffiliateDashboardScreen')); +const LoyaltyDashboardScreen = lazyScreen(() => import('../screens/LoyaltyDashboardScreen')); +const CampaignManagementScreen = lazyScreen(() => import('../screens/CampaignManagementScreen')); +const DeveloperPortalScreen = lazyScreen(() => import('../screens/DeveloperPortalScreen')); +const SandboxDashboardScreen = lazyScreen(() => import('../screens/SandboxDashboardScreen')); +const ApiKeyManagementScreen = lazyScreen(() => import('../screens/ApiKeyManagementScreen')); +const DocumentationPortalScreen = lazyScreen(() => import('../screens/DocumentationPortalScreen')); +const IntegrationGuidesScreen = lazyScreen(() => import('../screens/IntegrationGuidesScreen')); +const PerformanceDashboardScreen = lazyScreen( + () => import('../screens/PerformanceDashboardScreen') +); const Tab = createBottomTabNavigator(); const Stack = createNativeStackNavigator(); diff --git a/src/screens/AffiliateDashboardScreen.tsx b/src/screens/AffiliateDashboardScreen.tsx index c7095678..f797f261 100644 --- a/src/screens/AffiliateDashboardScreen.tsx +++ b/src/screens/AffiliateDashboardScreen.tsx @@ -7,39 +7,43 @@ import { SafeAreaView, TouchableOpacity, Alert, - ActivityIndicator, Modal, - FlatList, + TextInput, } from 'react-native'; import { colors, spacing, typography, borderRadius } from '../utils/constants'; import { useAffiliateStore } from '../store/affiliateStore'; import { useWalletStore } from '../store/walletStore'; import { Card } from '../components/common/Card'; -import { useNavigation } from '@react-navigation/native'; -import { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import { RootStackParamList } from '../navigation/types'; -import { AffiliateStatus, AffiliateProgram, Commission } from '../types/affiliate'; +import { AffiliateStatus } from '../types/affiliate'; const AffiliateDashboardScreen: React.FC = () => { - const navigation = useNavigation>(); const { affiliates, programs, commissions, metrics, - isLoading, - error, registerAffiliate, + trackClick, trackReferral, - calculateCommission, payoutCommission, updateAffiliateStatus, + triggerClawback, getMetrics, } = useAffiliateStore(); const { address } = useWalletStore(); const [programModalVisible, setProgramModalVisible] = useState(false); const [selectedProgram, setSelectedProgram] = useState(''); + const [selectedAttributionModel, setSelectedAttributionModel] = useState< + 'first-touch' | 'last-touch' | 'linear' + >('last-touch'); + const [customCookieWindow, setCustomCookieWindow] = useState('30'); + + // Simulation Inputs + const [simSubscriptionId, setSimSubscriptionId] = useState('sub_premium_99'); + const [simAmount, setSimAmount] = useState('49.99'); + const [simIp, setSimIp] = useState('192.168.1.105'); + const [simUserAgent, setSimUserAgent] = useState('Mozilla/5.0 Chrome/120.0.0'); useEffect(() => { const currentMetrics = getMetrics(); @@ -47,35 +51,110 @@ const AffiliateDashboardScreen: React.FC = () => { }, [affiliates, commissions, getMetrics]); const handleRegister = useCallback(async () => { - if (!address) { - Alert.alert('Error', 'Please connect your wallet first'); - return; - } + // Fallback/Mock address if no wallet is connected + const activeAddress = address || '0x71C7656EC7ab88b098defB751B7401B5f6d8976F'; + if (!selectedProgram) { Alert.alert('Error', 'Please select a program'); return; } - await registerAffiliate(address, selectedProgram); + + await registerAffiliate(activeAddress, selectedProgram); + + // Sync newly chosen program parameters to default programs + const updatedPrograms = programs.map((p) => + p.id === selectedProgram + ? { + ...p, + attributionWindowDays: parseInt(customCookieWindow, 10) || 30, + attributionModel: selectedAttributionModel, + } + : p + ); + useAffiliateStore.setState({ programs: updatedPrograms }); + setProgramModalVisible(false); - Alert.alert('Success', 'You are now an affiliate!'); - }, [address, selectedProgram, registerAffiliate]); + Alert.alert('Success', 'Registered as an affiliate successfully!'); + }, [ + address, + selectedProgram, + registerAffiliate, + customCookieWindow, + selectedAttributionModel, + programs, + ]); const handlePayout = useCallback( async (affiliateId: string) => { const affiliate = affiliates.find((a) => a.id === affiliateId); - if (!affiliate || affiliate.pendingPayout < affiliate.paymentThreshold) { + if (!affiliate) return; + + if (affiliate.pendingPayout < affiliate.paymentThreshold) { Alert.alert( - 'Minimum Threshold', - `You need at least $${affiliate?.paymentThreshold} to request a payout` + 'Payout Threshold', + `Required: $${affiliate.paymentThreshold.toFixed(2)}. Current pending balance: $${affiliate.pendingPayout.toFixed(2)}` ); return; } - await payoutCommission(affiliateId); - Alert.alert('Success', 'Payout requested!'); + + try { + await payoutCommission(affiliateId); + Alert.alert('Success', 'Your payout request was processed instantly!'); + } catch (err) { + Alert.alert('Error', err instanceof Error ? err.message : 'Payout failed'); + } }, [affiliates, payoutCommission] ); + const handleCopyLink = useCallback((link: string) => { + Alert.alert('Copied to Clipboard!', link); + }, []); + + const handleSimulationClick = useCallback( + async (referralCode: string) => { + try { + await trackClick(referralCode, simIp, simUserAgent); + Alert.alert('Success', `Simulated referral link click! Click logged for ${referralCode}`); + } catch (err) { + Alert.alert('Blocked', err instanceof Error ? err.message : 'Click limit reached'); + } + }, + [trackClick, simIp, simUserAgent] + ); + + const handleSimulationConversion = useCallback( + async (affiliateId: string) => { + try { + const amt = parseFloat(simAmount) || 29.99; + await trackReferral( + affiliateId, + simSubscriptionId, + amt, + simIp, + simUserAgent, + undefined, + selectedAttributionModel + ); + Alert.alert('Success', 'Simulated recurring subscription sign up! Commission tracked.'); + } catch (err) { + Alert.alert( + 'Blocked by Fraud Engine', + err instanceof Error ? err.message : 'Conversion rejected' + ); + } + }, + [trackReferral, simSubscriptionId, simAmount, simIp, simUserAgent, selectedAttributionModel] + ); + + const handleSimulationClawback = useCallback(async () => { + await triggerClawback(simSubscriptionId); + Alert.alert( + 'Success', + `Subscription ${simSubscriptionId} cancelled. Commissions within period clawed back successfully.` + ); + }, [triggerClawback, simSubscriptionId]); + const handleToggleStatus = useCallback( async (affiliateId: string, newStatus: AffiliateStatus) => { await updateAffiliateStatus(affiliateId, newStatus); @@ -85,96 +164,204 @@ const AffiliateDashboardScreen: React.FC = () => { const renderMetricsCard = () => ( - Performance Overview + Real-time Performance Metrics - {metrics.totalReferrals} - Total Referrals + {metrics.totalClicks || 0} + Total Clicks - {metrics.activeReferrals} - Active + {metrics.totalReferrals} + Conversions - ${metrics.totalEarnings.toFixed(2)} + + ${metrics.totalEarnings.toFixed(2)} + Total Earnings - ${metrics.pendingPayout.toFixed(2)} - Pending + + ${metrics.pendingPayout.toFixed(2)} + + Pending Payout Conversion Rate - {metrics.conversionRate.toFixed(1)}% + {(metrics.conversionRate || 0).toFixed(1)}% ); const renderAffiliateList = () => ( - Your Affiliates + Referral Configuration & Operations {affiliates.length === 0 ? ( - No affiliates yet + + No registered programs yet. Join a program below to start earning. + ) : ( - affiliates.map((affiliate) => ( - - - - {affiliate.referrerAddress.slice(0, 6)}... - {affiliate.referrerAddress.slice(-4)} - - - - {affiliate.totalReferrals} referrals - - - ${affiliate.totalEarnings.toFixed(2)} earned - - - - - - {affiliate.status} + affiliates.map((affiliate) => { + const currentProg = programs.find((p) => p.id === affiliate.programId); + return ( + + + + + {currentProg?.name || 'Basic Program'} + + Code: {affiliate.referralCode} + + + + {affiliate.fraudStatus === 'flagged' + ? '⚠️ Suspended' + : affiliate.fraudStatus === 'suspicious' + ? '⚠️ Suspicious' + : '🛡️ Secure'} + + - {affiliate.status === AffiliateStatus.ACTIVE ? ( + + {/* Referral Link & Custom Payout Controls */} + + + {affiliate.referralLink} + - handleToggleStatus(affiliate.id, AffiliateStatus.PAUSED) - }> - Pause + style={styles.copyButton} + onPress={() => handleCopyLink(affiliate.referralLink || '')}> + Copy Link - ) : ( + + + + Clicks: {affiliate.clicksCount || 0} + Threshold: ${affiliate.paymentThreshold} + Risk: {affiliate.fraudRiskScore || 0}% + + + - handleToggleStatus(affiliate.id, AffiliateStatus.ACTIVE) - }> - Resume + style={[ + styles.payoutActionButton, + affiliate.pendingPayout < affiliate.paymentThreshold && styles.disabledButton, + ]} + onPress={() => handlePayout(affiliate.id)}> + Request Payout - )} + + {affiliate.status === AffiliateStatus.ACTIVE ? ( + handleToggleStatus(affiliate.id, AffiliateStatus.PAUSED)}> + Pause + + ) : ( + handleToggleStatus(affiliate.id, AffiliateStatus.ACTIVE)}> + Resume + + )} + + + {/* SIMULATION BENCH */} + + Sandbox Simulator Bench + + Simulate visitor flow to test multi-touch attribution, fraud mitigation, and churn + clawbacks. + + + + + Sub ID + + + + Amount ($) + + + + + + + Visitor IP + + + + User Agent + + + + + + handleSimulationClick(affiliate.referralCode || '')}> + 1. Sim Click + + + handleSimulationConversion(affiliate.id)}> + 2. Sim Convert + + + + 3. Sim Churn + + + - - )) + ); + }) )} ); const renderProgramsCard = () => ( - Available Programs + Available Affiliate Programs {programs.map((program) => ( { {program.name} {program.description} + + Attribution: {program.attributionModel || 'last-touch'} •{' '} + {program.attributionWindowDays} days cookie + {program.commissionConfig.type === 'percentage' ? `${program.commissionConfig.rate}%` : program.commissionConfig.type === 'flat' - ? `$${program.commissionConfig.rate}` - : 'Tiered'} + ? `$${program.commissionConfig.rate}` + : 'Tiered'} - commission + Commission ))} @@ -204,67 +395,79 @@ const AffiliateDashboardScreen: React.FC = () => { const renderCommissionsList = () => ( - Recent Commissions + Recent Commission Ledger & Clawbacks {commissions.length === 0 ? ( - No commissions yet + No commissions tracked yet. ) : ( - commissions.slice(0, 5).map((commission) => ( - - - - Sub: {commission.subscriptionId.slice(0, 8)}... - - - {new Date(commission.createdAt).toLocaleDateString()} - - - - ${commission.amount.toFixed(2)} - - {commission.status} + commissions + .slice() + .reverse() + .slice(0, 5) + .map((commission) => ( + + + Sub: {commission.subscriptionId} + + {new Date(commission.createdAt).toLocaleDateString()} + + + + + ${commission.amount.toFixed(2)} + + + + {commission.isClawbacked ? 'Clawed back' : commission.status} + + - - )) + )) )} ); - if (isLoading) { - return ( - - - - Loading... - - - ); - } - return ( - Affiliate Dashboard + Affiliate & Referral Engine - Track referrals and earn commissions + Create marketing campaigns, track recurring conversions, mitigate fraud, and handle + payouts. {renderMetricsCard()} - {renderProgramsCard()} {renderAffiliateList()} + {renderProgramsCard()} {renderCommissionsList()} { onPress={() => setProgramModalVisible(true)} accessibilityRole="button" accessibilityLabel="Register as affiliate"> - Become an Affiliate + Launch New Affiliate Campaign + {/* Program Config Modal */} setProgramModalVisible(false)}> - - Select Program - - Choose an affiliate program to join - + + + Configure Affiliate Program + + Join a program and configure tracking constraints. + - {programs.map((program) => ( - setSelectedProgram(program.id)}> - - {program.name} - - {program.description} - - - ( + - {selectedProgram === program.id && ( - - )} - - - ))} - - - setProgramModalVisible(false)}> - Cancel - - - Join Program - + styles.programOption, + selectedProgram === program.id && styles.programOptionSelected, + ]} + onPress={() => setSelectedProgram(program.id)}> + + {program.name} + {program.description} + + + {selectedProgram === program.id && } + + + ))} + + Attribution & Window Parameters + + Cookie Validity Window (Days) + + + Attribution Model + + {(['first-touch', 'last-touch', 'linear'] as const).map((model) => ( + setSelectedAttributionModel(model)}> + + {model} + + + ))} + + + + setProgramModalVisible(false)}> + Cancel + + + Join Program + + - + ); }; +// Extracted styles from theme safely +const programOptionInfo = { + flex: 1, +}; + const styles = StyleSheet.create({ container: { flex: 1, @@ -341,38 +578,30 @@ const styles = StyleSheet.create({ scrollView: { flex: 1, }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - loadingText: { - marginTop: spacing.sm, - color: colors.textSecondary, - fontSize: typography.fontSizeMd, - }, header: { padding: spacing.md, paddingTop: spacing.lg, }, title: { - fontSize: typography.fontSizeXl, - fontWeight: typography.fontWeightBold, + fontSize: typography.h2.fontSize, + fontWeight: typography.h2.fontWeight, color: colors.text, }, subtitle: { - fontSize: typography.fontSizeMd, + fontSize: typography.caption.fontSize, color: colors.textSecondary, marginTop: spacing.xs, + lineHeight: 20, }, metricsCard: { padding: spacing.md, margin: spacing.md, marginTop: 0, + backgroundColor: colors.surface, }, metricsTitle: { - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body.fontSize, + fontWeight: 'bold', color: colors.text, marginBottom: spacing.md, }, @@ -386,16 +615,16 @@ const styles = StyleSheet.create({ alignItems: 'center', padding: spacing.md, marginBottom: spacing.sm, - backgroundColor: colors.surface, + backgroundColor: 'rgba(255, 255, 255, 0.03)', borderRadius: borderRadius.md, }, metricValue: { - fontSize: typography.fontSizeLg, - fontWeight: typography.fontWeightBold, + fontSize: typography.h3.fontSize, + fontWeight: 'bold', color: colors.text, }, metricLabel: { - fontSize: typography.fontSizeSm, + fontSize: typography.small.fontSize, color: colors.textSecondary, marginTop: spacing.xs, }, @@ -406,93 +635,205 @@ const styles = StyleSheet.create({ paddingTop: spacing.md, borderTopWidth: 1, borderTopColor: colors.border, + marginTop: spacing.xs, }, conversionLabel: { - fontSize: typography.fontSizeMd, + fontSize: typography.body2.fontSize, color: colors.textSecondary, }, conversionValue: { - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body.fontSize, + fontWeight: 'bold', color: colors.primary, }, listCard: { padding: spacing.md, margin: spacing.md, marginTop: 0, + backgroundColor: colors.surface, }, listTitle: { - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body.fontSize, + fontWeight: 'bold', color: colors.text, marginBottom: spacing.md, }, emptyText: { - fontSize: typography.fontSizeMd, + fontSize: typography.body2.fontSize, color: colors.textSecondary, textAlign: 'center', paddingVertical: spacing.lg, + lineHeight: 20, }, - affiliateItem: { - flexDirection: 'row', - justifyContent: 'space-between', - paddingVertical: spacing.sm, + affiliateContainer: { + paddingVertical: spacing.md, borderBottomWidth: 1, borderBottomColor: colors.border, }, - affiliateInfo: { - flex: 1, + affiliateHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', }, - affiliateAddress: { - fontSize: typography.fontSizeMd, + affiliateProgramName: { + fontSize: typography.body.fontSize, + fontWeight: 'bold', color: colors.text, - fontFamily: typography.fontFamilyMono, }, - affiliateStats: { + affiliateCode: { + fontSize: typography.body2.fontSize, + color: colors.textSecondary, + marginTop: 2, + }, + fraudBadge: { + paddingHorizontal: spacing.sm, + paddingVertical: 4, + borderRadius: borderRadius.sm, + }, + fraudBadgeText: { + fontSize: typography.small.fontSize, + fontWeight: 'bold', + }, + referralLinkSection: { flexDirection: 'row', - marginTop: spacing.xs, - gap: spacing.md, + alignItems: 'center', + backgroundColor: 'rgba(255, 255, 255, 0.02)', + borderRadius: borderRadius.md, + padding: spacing.sm, + marginTop: spacing.md, + borderWidth: 1, + borderColor: colors.border, }, - affiliateStat: { - fontSize: typography.fontSizeSm, + referralLinkLabel: { + flex: 1, + fontSize: typography.small.fontSize, color: colors.textSecondary, }, - affiliateActions: { - alignItems: 'flex-end', - }, - statusBadge: { + copyButton: { + backgroundColor: colors.primary, paddingHorizontal: spacing.sm, - paddingVertical: spacing.xs, + paddingVertical: 6, borderRadius: borderRadius.sm, - marginBottom: spacing.xs, + marginLeft: spacing.sm, }, - statusBadgeText: { + copyButtonText: { + color: '#ffffff', + fontSize: typography.small.fontSize, + fontWeight: 'bold', + }, + affiliateStatsRow: { + flexDirection: 'row', + marginTop: spacing.md, + gap: spacing.lg, + }, + statMini: { + fontSize: typography.small.fontSize, + color: colors.textSecondary, + }, + actionButtonsRow: { + flexDirection: 'row', + marginTop: spacing.md, + gap: spacing.md, + }, + payoutActionButton: { + backgroundColor: colors.success, + borderRadius: borderRadius.sm, + paddingVertical: 8, + paddingHorizontal: spacing.md, + alignItems: 'center', + }, + disabledButton: { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + }, + payoutActionText: { color: colors.text, - fontSize: typography.fontSizeXs, - fontWeight: typography.fontWeightMedium, - textTransform: 'capitalize', + fontSize: typography.body2.fontSize, + fontWeight: 'bold', }, pauseButton: { - paddingHorizontal: spacing.sm, - paddingVertical: spacing.xs, + paddingHorizontal: spacing.md, + paddingVertical: 8, borderRadius: borderRadius.sm, borderWidth: 1, borderColor: colors.warning, + justifyContent: 'center', }, pauseButtonText: { color: colors.warning, - fontSize: typography.fontSizeSm, + fontSize: typography.body2.fontSize, + fontWeight: 'bold', }, resumeButton: { - paddingHorizontal: spacing.sm, - paddingVertical: spacing.xs, + paddingHorizontal: spacing.md, + paddingVertical: 8, borderRadius: borderRadius.sm, borderWidth: 1, borderColor: colors.success, + justifyContent: 'center', }, resumeButtonText: { color: colors.success, - fontSize: typography.fontSizeSm, + fontSize: typography.body2.fontSize, + fontWeight: 'bold', + }, + simulatorBench: { + marginTop: spacing.lg, + padding: spacing.md, + backgroundColor: 'rgba(255, 255, 255, 0.02)', + borderRadius: borderRadius.md, + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.05)', + }, + simulatorTitle: { + fontSize: typography.body2.fontSize, + fontWeight: 'bold', + color: colors.text, + }, + simulatorSubtitle: { + fontSize: typography.small.fontSize, + color: colors.textSecondary, + marginTop: 2, + lineHeight: 16, + }, + simInputsRow: { + flexDirection: 'row', + gap: spacing.md, + marginTop: spacing.sm, + }, + simInputBox: { + flex: 1, + }, + inputLabel: { + fontSize: typography.small.fontSize, + color: colors.textSecondary, + marginBottom: 4, + }, + simInput: { + backgroundColor: colors.background, + borderColor: colors.border, + borderWidth: 1, + borderRadius: borderRadius.sm, + padding: 6, + color: colors.text, + fontSize: typography.body2.fontSize, + }, + simButtonsContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: spacing.md, + gap: spacing.xs, + }, + simBtn: { + flex: 1, + backgroundColor: 'rgba(255,255,255,0.08)', + paddingVertical: 8, + borderRadius: borderRadius.sm, + alignItems: 'center', + }, + simBtnText: { + fontSize: 11, + fontWeight: 'bold', + color: colors.text, }, programItem: { flexDirection: 'row', @@ -505,25 +846,30 @@ const styles = StyleSheet.create({ flex: 1, }, programName: { - fontSize: typography.fontSizeMd, + fontSize: typography.body.fontSize, color: colors.text, - fontWeight: typography.fontWeightMedium, + fontWeight: 'bold', }, programDescription: { - fontSize: typography.fontSizeSm, + fontSize: typography.body2.fontSize, color: colors.textSecondary, - marginTop: spacing.xs, + marginTop: 4, + }, + programSubMeta: { + fontSize: typography.small.fontSize, + color: colors.primary, + marginTop: 6, }, programRate: { alignItems: 'flex-end', }, rateValue: { - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body.fontSize, + fontWeight: 'bold', color: colors.primary, }, rateLabel: { - fontSize: typography.fontSizeXs, + fontSize: typography.small.fontSize, color: colors.textSecondary, }, commissionItem: { @@ -537,33 +883,32 @@ const styles = StyleSheet.create({ flex: 1, }, commissionId: { - fontSize: typography.fontSizeSm, + fontSize: typography.body2.fontSize, color: colors.text, - fontFamily: typography.fontFamilyMono, + fontWeight: 'bold', }, commissionDate: { - fontSize: typography.fontSizeXs, + fontSize: typography.small.fontSize, color: colors.textSecondary, - marginTop: spacing.xs, + marginTop: 4, }, commissionAmount: { alignItems: 'flex-end', }, amountValue: { - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + fontSize: typography.body.fontSize, + fontWeight: 'bold', color: colors.text, }, commissionStatus: { paddingHorizontal: spacing.sm, paddingVertical: 2, borderRadius: borderRadius.sm, - marginTop: spacing.xs, + marginTop: 4, }, commissionStatusText: { - color: colors.text, - fontSize: typography.fontSizeXs, - fontWeight: typography.fontWeightMedium, + fontSize: typography.small.fontSize, + fontWeight: 'bold', textTransform: 'capitalize', }, registerButton: { @@ -574,13 +919,17 @@ const styles = StyleSheet.create({ alignItems: 'center', }, registerButtonText: { - color: colors.text, - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + color: '#ffffff', + fontSize: typography.body.fontSize, + fontWeight: 'bold', }, modalOverlay: { flex: 1, - backgroundColor: 'rgba(0, 0, 0, 0.5)', + backgroundColor: 'rgba(0, 0, 0, 0.7)', + justifyContent: 'flex-end', + }, + modalScroll: { + flexGrow: 1, justifyContent: 'flex-end', }, modalContent: { @@ -588,16 +937,15 @@ const styles = StyleSheet.create({ borderTopLeftRadius: borderRadius.lg, borderTopRightRadius: borderRadius.lg, padding: spacing.lg, - maxHeight: '70%', }, modalTitle: { - fontSize: typography.fontSizeLg, - fontWeight: typography.fontWeightBold, + fontSize: typography.h3.fontSize, + fontWeight: 'bold', color: colors.text, marginBottom: spacing.xs, }, modalSubtitle: { - fontSize: typography.fontSizeMd, + fontSize: typography.body2.fontSize, color: colors.textSecondary, marginBottom: spacing.lg, }, @@ -608,23 +956,22 @@ const styles = StyleSheet.create({ borderRadius: borderRadius.md, marginBottom: spacing.sm, backgroundColor: colors.background, + borderWidth: 1, + borderColor: colors.border, }, programOptionSelected: { borderWidth: 2, borderColor: colors.primary, }, - programOptionInfo: { - flex: 1, - }, programOptionName: { - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightMedium, + fontSize: typography.body.fontSize, + fontWeight: 'bold', color: colors.text, }, programOptionDesc: { - fontSize: typography.fontSizeSm, + fontSize: typography.body2.fontSize, color: colors.textSecondary, - marginTop: spacing.xs, + marginTop: 4, }, radioCircle: { width: 24, @@ -644,6 +991,55 @@ const styles = StyleSheet.create({ borderRadius: 6, backgroundColor: colors.primary, }, + sectionLabelHeader: { + fontSize: typography.body.fontSize, + fontWeight: 'bold', + color: colors.text, + marginTop: spacing.md, + marginBottom: spacing.xs, + }, + inputTitleLabel: { + fontSize: typography.small.fontSize, + color: colors.textSecondary, + marginTop: spacing.sm, + marginBottom: 4, + }, + modalTextInput: { + backgroundColor: colors.background, + borderColor: colors.border, + borderWidth: 1, + borderRadius: borderRadius.sm, + padding: 10, + color: colors.text, + fontSize: typography.body2.fontSize, + marginBottom: spacing.sm, + }, + optionsSelectorRow: { + flexDirection: 'row', + gap: spacing.sm, + marginBottom: spacing.md, + }, + optionSelectorItem: { + flex: 1, + backgroundColor: colors.background, + borderWidth: 1, + borderColor: colors.border, + borderRadius: borderRadius.sm, + paddingVertical: 10, + alignItems: 'center', + }, + optionSelectorItemSelected: { + borderColor: colors.primary, + backgroundColor: 'rgba(255,255,255,0.03)', + }, + optionSelectorText: { + fontSize: 12, + color: colors.textSecondary, + }, + optionSelectorTextActive: { + color: colors.primary, + fontWeight: 'bold', + }, modalButtons: { flexDirection: 'row', justifyContent: 'space-between', @@ -656,11 +1052,13 @@ const styles = StyleSheet.create({ borderRadius: borderRadius.md, padding: spacing.md, alignItems: 'center', + borderWidth: 1, + borderColor: colors.border, }, cancelButtonText: { color: colors.text, - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightMedium, + fontSize: typography.body.fontSize, + fontWeight: 'bold', }, confirmButton: { flex: 1, @@ -670,10 +1068,10 @@ const styles = StyleSheet.create({ alignItems: 'center', }, confirmButtonText: { - color: colors.text, - fontSize: typography.fontSizeMd, - fontWeight: typography.fontWeightBold, + color: colors.background, + fontSize: typography.body.fontSize, + fontWeight: 'bold', }, }); -export default AffiliateDashboardScreen; \ No newline at end of file +export default AffiliateDashboardScreen; diff --git a/src/screens/AnalyticsScreen.tsx b/src/screens/AnalyticsScreen.tsx index 25bd9c55..5ba4d7e9 100644 --- a/src/screens/AnalyticsScreen.tsx +++ b/src/screens/AnalyticsScreen.tsx @@ -18,7 +18,6 @@ import { currencyService } from '../services/currencyService'; import { calculateSubscriptionAnalytics } from '../services/analyticsService'; import { formatCurrency } from '../utils/formatting'; - const { width: screenWidth } = Dimensions.get('window'); const CHART_WIDTH = screenWidth - spacing.xl * 2; const CHART_HEIGHT = 200; @@ -34,7 +33,6 @@ const AnalyticsScreen: React.FC = () => { calculateStats(); }, [subscriptions, calculateStats, preferredCurrency, exchangeRates]); - const categoryData = useMemo(() => { const categories = Object.values(SubscriptionCategory); return categories @@ -103,7 +101,6 @@ const AnalyticsScreen: React.FC = () => { else if (sub.billingCycle === BillingCycle.YEARLY) total += priceInPreferred / 12; else if (sub.billingCycle === BillingCycle.WEEKLY) total += priceInPreferred * 4; } - } }); return { month, amount: total }; @@ -195,7 +192,6 @@ const AnalyticsScreen: React.FC = () => { importantForAccessibility="no"> {formatCurrency(stats.totalMonthlySpend, preferredCurrency)} - { importantForAccessibility="no"> {formatCurrency(stats.totalYearlySpend, preferredCurrency)} - @@ -301,7 +296,6 @@ const AnalyticsScreen: React.FC = () => { textAnchor="middle"> {formatCurrency(data.amount, preferredCurrency)} - )} ); @@ -389,7 +383,6 @@ const AnalyticsScreen: React.FC = () => { {formatCurrency(stats.totalYearlySpend, preferredCurrency)} - diff --git a/src/screens/ApiKeyManagementScreen.tsx b/src/screens/ApiKeyManagementScreen.tsx index 7be0fac3..4d198836 100644 --- a/src/screens/ApiKeyManagementScreen.tsx +++ b/src/screens/ApiKeyManagementScreen.tsx @@ -121,9 +121,7 @@ const ApiKeyManagementScreen: React.FC = () => { - - Environment: Sandbox (Development) - + Environment: Sandbox (Development) Keys are created for the current sandbox environment @@ -142,14 +140,10 @@ const ApiKeyManagementScreen: React.FC = () => { - handleCopyKey(showNewKey)}> + handleCopyKey(showNewKey)}> Copy Key - setShowNewKey(null)}> + setShowNewKey(null)}> Dismiss @@ -180,8 +174,8 @@ const ApiKeyManagementScreen: React.FC = () => { key.status === ApiKeyStatus.ACTIVE ? colors.success : key.status === ApiKeyStatus.REVOKED - ? colors.error - : colors.warning, + ? colors.error + : colors.warning, }, ]}> {key.status} @@ -192,16 +186,15 @@ const ApiKeyManagementScreen: React.FC = () => { - - {apiKeyService.maskApiKey(key.key)} - + {apiKeyService.maskApiKey(key.key)} Permissions: {(key.permissions ?? key.scopes ?? ['read']).join(', ')} - Rate: {key.rateLimit?.requestsPerMinute ?? 60}/min · {key.rateLimit?.requestsPerDay ?? 10000}/day + Rate: {key.rateLimit?.requestsPerMinute ?? 60}/min ·{' '} + {key.rateLimit?.requestsPerDay ?? 10000}/day {key.lastUsedAt && ( diff --git a/src/screens/CalendarIntegrationScreen.tsx b/src/screens/CalendarIntegrationScreen.tsx index 4db03dcd..3da35840 100644 --- a/src/screens/CalendarIntegrationScreen.tsx +++ b/src/screens/CalendarIntegrationScreen.tsx @@ -132,9 +132,15 @@ const CalendarIntegrationScreen: React.FC = () => { message: payload.ical, title: payload.filename, }); - Alert.alert('Calendar exported', `Exported ${payload.events.length} events to ${payload.filename}`); + Alert.alert( + 'Calendar exported', + `Exported ${payload.events.length} events to ${payload.filename}` + ); } catch (exportError) { - Alert.alert('Export failed', exportError instanceof Error ? exportError.message : 'Could not export calendar.'); + Alert.alert( + 'Export failed', + exportError instanceof Error ? exportError.message : 'Could not export calendar.' + ); } }, [subscriptions, timezone, exportCalendar]); @@ -424,13 +430,17 @@ const CalendarIntegrationScreen: React.FC = () => { Set your preferred timezone for calendar events. Current: {timezone}. - + {SUBSCRIPTION_TIMEZONES.map((tz) => ( setTimezone(tz)}> - + {tz} @@ -446,23 +456,26 @@ const CalendarIntegrationScreen: React.FC = () => { Check for conflicts - {scheduleConflicts.length > 0 ? ( - scheduleConflicts.slice(0, 5).map((conflict) => ( - - {conflict.date} - - {conflict.conflictingSubscriptions.length} subscriptions — {conflict.totalAmount.toFixed(2)} USD total - - {conflict.conflictingSubscriptions.map((sub) => ( - - {sub.name}: {sub.currency} {sub.amount.toFixed(2)} + {scheduleConflicts.length > 0 + ? scheduleConflicts.slice(0, 5).map((conflict) => ( + + {conflict.date} + + {conflict.conflictingSubscriptions.length} subscriptions —{' '} + {conflict.totalAmount.toFixed(2)} USD total - ))} - - )) - ) : scheduleConflicts.length === 0 && ( - No conflicts detected. Tap "Check for conflicts" to scan. - )} + {conflict.conflictingSubscriptions.map((sub) => ( + + {sub.name}: {sub.currency} {sub.amount.toFixed(2)} + + ))} + + )) + : scheduleConflicts.length === 0 && ( + + No conflicts detected. Tap "Check for conflicts" to scan. + + )} @@ -480,7 +493,9 @@ const CalendarIntegrationScreen: React.FC = () => { {payment.currency} {payment.amount.toFixed(2)} — {payment.status} - {new Date(payment.scheduledDate).toLocaleDateString()} + + {new Date(payment.scheduledDate).toLocaleDateString()} + {payment.status === 'pending' && ( { {campaign.name} + style={[styles.statusBadge, { backgroundColor: getStatusColor(campaign.status) }]}> {campaign.status} {getTypeLabel(campaign.type)} @@ -148,9 +145,7 @@ const CampaignManagementScreen: React.FC = () => { {analytics && ( - - {analytics.totalRecipients} - + {analytics.totalRecipients} Recipients @@ -281,8 +276,7 @@ const CampaignManagementScreen: React.FC = () => { key={channel} style={[ styles.channelOption, - newCampaign.channels.includes(channel) && - styles.channelOptionSelected, + newCampaign.channels.includes(channel) && styles.channelOptionSelected, ]} onPress={() => { const channels = newCampaign.channels.includes(channel) @@ -293,8 +287,7 @@ const CampaignManagementScreen: React.FC = () => { {channel} @@ -303,9 +296,7 @@ const CampaignManagementScreen: React.FC = () => { - + Create Campaign @@ -329,9 +320,7 @@ const CampaignManagementScreen: React.FC = () => { Campaign Management - - Create and manage marketing campaigns - + Create and manage marketing campaigns { {campaigns.length === 0 ? ( No campaigns yet - - Create your first campaign to get started - + Create your first campaign to get started ) : ( campaigns.map(renderCampaignItem) @@ -635,4 +622,4 @@ const styles = StyleSheet.create({ }, }); -export default CampaignManagementScreen; \ No newline at end of file +export default CampaignManagementScreen; diff --git a/src/screens/CancellationFlowScreen.tsx b/src/screens/CancellationFlowScreen.tsx index 96ad8112..371cc9cb 100644 --- a/src/screens/CancellationFlowScreen.tsx +++ b/src/screens/CancellationFlowScreen.tsx @@ -81,7 +81,9 @@ const CancellationFlowScreen: React.FC = ({ route, navigation }) => { {OFFER_TYPE_ICONS[offer.type] ?? '🎁'} {offer.title} - {offer.abVariant === 'A' ? 'Popular' : 'Best Value'} + + {offer.abVariant === 'A' ? 'Popular' : 'Best Value'} + {offer.description} diff --git a/src/screens/DeveloperPortalScreen.tsx b/src/screens/DeveloperPortalScreen.tsx index 87017952..3b41c791 100644 --- a/src/screens/DeveloperPortalScreen.tsx +++ b/src/screens/DeveloperPortalScreen.tsx @@ -54,7 +54,11 @@ const DeveloperPortalScreen: React.FC = () => { Alert.alert('Required fields', 'Name and email are required.'); return; } - await createDeveloperProfile(profileForm.name, profileForm.email, profileForm.company || undefined); + await createDeveloperProfile( + profileForm.name, + profileForm.email, + profileForm.company || undefined + ); await completeOnboardingStep(DeveloperOnboardingStep.CREATE_ACCOUNT); setShowOnboarding(false); }; @@ -63,7 +67,10 @@ const DeveloperPortalScreen: React.FC = () => { const name = newKeyName || 'Default Key'; try { const key = await generateApiKey(name); - Alert.alert('API Key Generated', `Your new API key:\n\n${key}\n\nCopy it now, it won't be shown again.`); + Alert.alert( + 'API Key Generated', + `Your new API key:\n\n${key}\n\nCopy it now, it won't be shown again.` + ); setNewKeyName(''); } catch { Alert.alert('Error', 'Failed to generate API key.'); @@ -161,31 +168,28 @@ const DeveloperPortalScreen: React.FC = () => { Developer Portal - - Welcome back, {developerProfile?.name || 'Developer'} - + Welcome back, {developerProfile?.name || 'Developer'} - {[SandboxEnvironment.DEVELOPMENT, SandboxEnvironment.STAGING, SandboxEnvironment.PRODUCTION].map( - (env) => ( - switchEnvironment(env)} - /> - ) - )} + {[ + SandboxEnvironment.DEVELOPMENT, + SandboxEnvironment.STAGING, + SandboxEnvironment.PRODUCTION, + ].map((env) => ( + switchEnvironment(env)} + /> + ))} - + { {guide.difficulty} · {guide.estimatedTime} - {guide.isCompleted && ( - - )} + {guide.isCompleted && } ))} @@ -330,8 +332,8 @@ const DeveloperPortalScreen: React.FC = () => { sub.status === 'active' ? colors.success : sub.status === 'paused' - ? colors.warning - : colors.error, + ? colors.warning + : colors.error, }, ]}> {sub.status} diff --git a/src/screens/DocumentationPortalScreen.tsx b/src/screens/DocumentationPortalScreen.tsx index de1ff3c0..0449256c 100644 --- a/src/screens/DocumentationPortalScreen.tsx +++ b/src/screens/DocumentationPortalScreen.tsx @@ -1,12 +1,5 @@ import React, { useState } from 'react'; -import { - View, - Text, - StyleSheet, - ScrollView, - SafeAreaView, - TouchableOpacity, -} from 'react-native'; +import { View, Text, StyleSheet, ScrollView, SafeAreaView, TouchableOpacity } from 'react-native'; import { Card } from '../components/common/Card'; import { colors, spacing, typography, borderRadius } from '../utils/constants'; @@ -28,7 +21,8 @@ const DOC_SECTIONS: DocSection[] = [ subsections: [ { title: 'Base URL', - content: 'Sandbox: https://api.sandbox.subtrackr.dev/v1\nProduction: https://api.subtrackr.dev/v1', + content: + 'Sandbox: https://api.sandbox.subtrackr.dev/v1\nProduction: https://api.subtrackr.dev/v1', }, { title: 'Authentication', @@ -37,8 +31,7 @@ const DOC_SECTIONS: DocSection[] = [ }, { title: 'Rate Limits', - content: - 'Sandbox: 60 requests/minute, 10,000 requests/day\nProduction: Varies by plan', + content: 'Sandbox: 60 requests/minute, 10,000 requests/day\nProduction: Varies by plan', }, ], }, @@ -50,7 +43,8 @@ const DOC_SECTIONS: DocSection[] = [ subsections: [ { title: 'GET /subscriptions', - content: 'List all subscriptions with optional filtering and pagination.\n\nQuery params: status, category, page, limit', + content: + 'List all subscriptions with optional filtering and pagination.\n\nQuery params: status, category, page, limit', }, { title: 'POST /subscriptions', @@ -146,12 +140,13 @@ const DOC_SECTIONS: DocSection[] = [ subsections: [ { title: 'Error Format', - content: 'All errors return: { "error": { "code": "string", "message": "string", "details": {} } }', + content: + 'All errors return: { "error": { "code": "string", "message": "string", "details": {} } }', }, { title: 'Common Error Codes', content: - '400 - Bad Request: Invalid parameters\n401 - Unauthorized: Invalid or missing API key\n403 - Forbidden: Insufficient permissions\n404 - Not Found: Resource doesn\'t exist\n429 - Rate Limited: Too many requests\n500 - Server Error: Internal error', + "400 - Bad Request: Invalid parameters\n401 - Unauthorized: Invalid or missing API key\n403 - Forbidden: Insufficient permissions\n404 - Not Found: Resource doesn't exist\n429 - Rate Limited: Too many requests\n500 - Server Error: Internal error", }, ], }, @@ -166,9 +161,7 @@ const DocumentationPortalScreen: React.FC = () => { API Documentation - - Complete reference for the SubTrackr API - + Complete reference for the SubTrackr API @@ -181,13 +174,12 @@ const DocumentationPortalScreen: React.FC = () => { setExpandedSection(expandedSection === section.id ? null : section.id)}> + onPress={() => + setExpandedSection(expandedSection === section.id ? null : section.id) + }> {section.icon} + style={[styles.tocText, expandedSection === section.id && styles.tocTextActive]}> {section.title} @@ -205,9 +197,7 @@ const DocumentationPortalScreen: React.FC = () => { {section.icon} {section.title} - - {expandedSection === section.id ? '▼' : '▶'} - + {expandedSection === section.id ? '▼' : '▶'} {expandedSection === section.id && ( diff --git a/src/screens/FraudDashboard.tsx b/src/screens/FraudDashboard.tsx index f26dd4b0..bb261eb6 100644 --- a/src/screens/FraudDashboard.tsx +++ b/src/screens/FraudDashboard.tsx @@ -73,15 +73,40 @@ const FraudDashboard: React.FC = () => { - {renderMetric('Total checks', analytics.totalChecks.toString(), 'Subscriptions reviewed', colors.accent)} - {renderMetric('Blocked', analytics.blocked.toString(), 'Automated hard stops', colors.error)} - {renderMetric('Flagged', analytics.flagged.toString(), 'Queued for review', colors.warning)} + {renderMetric( + 'Total checks', + analytics.totalChecks.toString(), + 'Subscriptions reviewed', + colors.accent + )} + {renderMetric( + 'Blocked', + analytics.blocked.toString(), + 'Automated hard stops', + colors.error + )} + {renderMetric( + 'Flagged', + analytics.flagged.toString(), + 'Queued for review', + colors.warning + )} {renderMetric('Avg risk', `${analytics.avgRisk}`, 'Aggregate risk score', colors.primary)} - {renderMetric('Velocity alerts', analytics.velocityAlerts.toString(), 'Rapid creation detected', colors.secondary)} - {renderMetric('Anomaly alerts', analytics.anomalyAlerts.toString(), 'Usage deviates from baseline', colors.accent)} + {renderMetric( + 'Velocity alerts', + analytics.velocityAlerts.toString(), + 'Rapid creation detected', + colors.secondary + )} + {renderMetric( + 'Anomaly alerts', + analytics.anomalyAlerts.toString(), + 'Usage deviates from baseline', + colors.accent + )} {renderMetric( 'Chargeback predictions', analytics.chargebackPredictions.toString(), @@ -121,19 +146,27 @@ const FraudDashboard: React.FC = () => { - approveSubscription(item.subscriptionId)}> + approveSubscription(item.subscriptionId)}> Approve - resolveCase(item.subscriptionId, 'flag')}> + resolveCase(item.subscriptionId, 'flag')}> Flag - blockSubscription(item.subscriptionId)}> + blockSubscription(item.subscriptionId)}> Block ))} - {reviewQueue.length === 0 ? No cases awaiting manual review. : null} + {reviewQueue.length === 0 ? ( + No cases awaiting manual review. + ) : null} @@ -181,7 +214,8 @@ const FraudDashboard: React.FC = () => { {report.averageRisk} - {report.totalSubscriptions} subs · {report.flaggedSubscriptions} flagged · {report.blockedSubscriptions} blocked + {report.totalSubscriptions} subs · {report.flaggedSubscriptions} flagged ·{' '} + {report.blockedSubscriptions} blocked Manual review {report.manualReviewCount} @@ -201,21 +235,45 @@ const FraudDashboard: React.FC = () => { Approved - + {analytics.approved} Flagged - + {analytics.flagged} Blocked - + {analytics.blocked} diff --git a/src/screens/GroupManagementScreen.tsx b/src/screens/GroupManagementScreen.tsx index 4b094d05..a40cf754 100644 --- a/src/screens/GroupManagementScreen.tsx +++ b/src/screens/GroupManagementScreen.tsx @@ -39,7 +39,9 @@ const GroupManagementScreen: React.FC = () => { Seats: {analytics?.activeSeats ?? group.members.length}/{group.planSharingRules.seatLimit} - Outstanding: ${analytics?.outstandingBalance.toFixed(2) ?? '0.00'} + + Outstanding: ${analytics?.outstandingBalance.toFixed(2) ?? '0.00'} +