From fe301edb16234d93e38739dd6fee5e70e79a6d79 Mon Sep 17 00:00:00 2001 From: sweetesty Date: Fri, 29 May 2026 22:23:55 +0100 Subject: [PATCH 1/3] feat(perf): implement lazy loading and code splitting for faster startup (#410) --- metro.config.js | 10 + package-lock.json | 201 +++++++++++++++ package.json | 2 + src/navigation/AppNavigator.tsx | 104 ++++---- .../__tests__/AppNavigator.lazy.test.tsx | 174 +++++++++++++ src/utils/lazyLoading.tsx | 230 ++++++++++++++++++ 6 files changed, 679 insertions(+), 42 deletions(-) create mode 100644 src/navigation/__tests__/AppNavigator.lazy.test.tsx create mode 100644 src/utils/lazyLoading.tsx diff --git a/metro.config.js b/metro.config.js index 32938e8..d57b5a3 100644 --- a/metro.config.js +++ b/metro.config.js @@ -2,4 +2,14 @@ const { getDefaultConfig } = require('expo/metro-config'); const config = getDefaultConfig(__dirname); +config.transformer = { + ...config.transformer, + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: true, + inlineRequires: true, + }, + }), +}; + module.exports = config; diff --git a/package-lock.json b/package-lock.json index 3e952a6..c873ce5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "@semantic-release/npm": "^12.0.2", "@semantic-release/release-notes-generator": "^14.1.0", "@size-limit/file": "^11.1.4", + "@testing-library/react-native": "^13.3.3", "@typechain/ethers-v5": "^11.1.2", "@types/detox": "^17.14.3", "@types/jest": "^29.5.14", @@ -75,6 +76,7 @@ "jest-expo": "~53.0.5", "lint-staged": "^16.4.0", "prettier": "^3.8.3", + "react-test-renderer": "^19.2.5", "semantic-release": "^24.2.9", "size-limit": "^11.1.4", "ts-jest": "^29.4.11", @@ -8437,6 +8439,124 @@ } } }, + "node_modules/@testing-library/react-native": { + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-13.3.3.tgz", + "integrity": "sha512-k6Mjsd9dbZgvY4Bl7P1NIpePQNi+dfYtlJ5voi9KQlynxSyQkfOgJmYGCYmw/aSgH/rUcFvG8u5gd4npzgRDyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-matcher-utils": "^30.0.5", + "picocolors": "^1.1.1", + "pretty-format": "^30.0.5", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "jest": ">=29.0.0", + "react": ">=18.2.0", + "react-native": ">=0.71", + "react-test-renderer": ">=18.2.0" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + } + } + }, + "node_modules/@testing-library/react-native/node_modules/@jest/diff-sequences": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@testing-library/react-native/node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@testing-library/react-native/node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react-native/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/react-native/node_modules/jest-diff": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.4.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@testing-library/react-native/node_modules/jest-matcher-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@testing-library/react-native/node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -24554,6 +24674,16 @@ "dom-walk": "^0.1.0" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -29984,6 +30114,22 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "dev": true, + "license": "MIT" + }, "node_modules/react-native": { "version": "0.85.2", "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.85.2.tgz", @@ -30435,6 +30581,34 @@ "node": ">=0.10.0" } }, + "node_modules/react-test-renderer": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-19.2.5.tgz", + "integrity": "sha512-kwViRpdISMTpcpy5B6TSewfJzRjnajihRaj57ZmOWKD+SPN6k9LUM13O0pfOuW8ir6B6OOiAXwCRqOoVxRNykA==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-is": "^19.2.5", + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-test-renderer/node_modules/react-is": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-test-renderer/node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/read-package-up": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", @@ -30643,6 +30817,33 @@ "node": ">= 12.13.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redent/node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reduce-flatten": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", diff --git a/package.json b/package.json index fb96d7a..1ac22cd 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "@semantic-release/npm": "^12.0.2", "@semantic-release/release-notes-generator": "^14.1.0", "@size-limit/file": "^11.1.4", + "@testing-library/react-native": "^13.3.3", "@typechain/ethers-v5": "^11.1.2", "@types/detox": "^17.14.3", "@types/jest": "^29.5.14", @@ -113,6 +114,7 @@ "jest-expo": "~53.0.5", "lint-staged": "^16.4.0", "prettier": "^3.8.3", + "react-test-renderer": "^19.2.5", "semantic-release": "^24.2.9", "size-limit": "^11.1.4", "ts-jest": "^29.4.11", diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 1f3bbeb..9533328 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -5,51 +5,63 @@ import { navigationRef } from './navigationRef'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { useTranslation } from 'react-i18next'; +import { colors } from '../utils/constants'; +import { lazyScreen, prefetchModule } from '../utils/lazyLoading'; +import { RootStackParamList, TabParamList } from './types'; + +// Eagerly loaded primary entrypoints for instant rendering import HomeScreen from '../screens/HomeScreen'; -import AddSubscriptionScreen from '../screens/AddSubscriptionScreen'; -import CancellationFlowScreen from '../screens/CancellationFlowScreen'; -import WalletConnectScreen from '../screens/WalletConnectV2Screen'; -import CryptoPaymentScreen from '../screens/CryptoPaymentScreen'; -import CommunityScreen from '../screens/CommunityScreen'; -import ProfileScreen from '../screens/ProfileScreen'; -import SubscriptionDetailScreen from '../screens/SubscriptionDetailScreen'; -import InvoiceListScreen from '../screens/InvoiceListScreen'; -import InvoiceDetailScreen from '../screens/InvoiceDetailScreen'; -import AnalyticsScreen from '../screens/AnalyticsScreen'; -import SlaDashboard from '../screens/SlaDashboard'; -import GDPRSettingsScreen from '../screens/GDPRSettingsScreen'; -import LanguageSettingsScreen from '../screens/LanguageSettingsScreen'; -import SessionManagementScreen from '../screens/SessionManagementScreen'; import SettingsScreen from '../screens/SettingsScreen'; -import CalendarIntegrationScreen from '../screens/CalendarIntegrationScreen'; -import AccountingExportScreen from '../screens/AccountingExportScreen'; -import WebhookSettingsScreen from '../screens/WebhookSettingsScreen'; -import ErrorDashboardScreen from '../screens/ErrorDashboardScreen'; -import ImportScreen from '../screens/ImportScreen'; -import ExportScreen from '../screens/ExportScreen'; -import { BatchOperationsScreen } from '../../app/screens/BatchOperationsScreen'; -import AdminDashboardScreen from '../screens/AdminDashboardScreen'; -import FraudDashboard from '../screens/FraudDashboard'; -import GroupManagementScreen from '../screens/GroupManagementScreen'; -import TaxSettingsScreen from '../screens/TaxSettingsScreen'; -import SupportDashboardScreen from '../screens/SupportDashboardScreen'; -import { SegmentManagementScreen } from '../screens/SegmentManagementScreen'; -import { SegmentDetailScreen } from '../screens/SegmentDetailScreen'; -import { GamificationScreen } from '../screens/GamificationScreen'; -import RevenueReportScreen from '../screens/RevenueReportScreen'; -import UsageDashboardScreen from '../screens/UsageDashboard'; -import MerchantOnboardingScreen from '../screens/MerchantOnboardingScreen'; -import AffiliateDashboardScreen from '../screens/AffiliateDashboardScreen'; -import LoyaltyDashboardScreen from '../screens/LoyaltyDashboardScreen'; -import CampaignManagementScreen from '../screens/CampaignManagementScreen'; -import DeveloperPortalScreen from '../screens/DeveloperPortalScreen'; -import SandboxDashboardScreen from '../screens/SandboxDashboardScreen'; -import ApiKeyManagementScreen from '../screens/ApiKeyManagementScreen'; -import DocumentationPortalScreen from '../screens/DocumentationPortalScreen'; -import IntegrationGuidesScreen from '../screens/IntegrationGuidesScreen'; -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 CalendarIntegrationScreen = lazyScreen(() => import('../screens/CalendarIntegrationScreen')); +const AccountingExportScreen = lazyScreen(() => import('../screens/AccountingExportScreen')); +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 Tab = createBottomTabNavigator(); const Stack = createNativeStackNavigator(); @@ -371,6 +383,14 @@ const TabNavigator = () => { }; export const AppNavigator = () => { + React.useEffect(() => { + // Prefetch critical modules on app idle to optimize startup transition + prefetchModule('AddSubscription', () => import('../screens/AddSubscriptionScreen')); + prefetchModule('WalletConnect', () => import('../screens/WalletConnectV2Screen')); + prefetchModule('Analytics', () => import('../screens/AnalyticsScreen')); + prefetchModule('SubscriptionDetail', () => import('../screens/SubscriptionDetailScreen')); + }, []); + return ( diff --git a/src/navigation/__tests__/AppNavigator.lazy.test.tsx b/src/navigation/__tests__/AppNavigator.lazy.test.tsx new file mode 100644 index 0000000..294fd2e --- /dev/null +++ b/src/navigation/__tests__/AppNavigator.lazy.test.tsx @@ -0,0 +1,174 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react-native'; +import { Text } from 'react-native'; +import { lazyWithRetry, LazyErrorBoundary, SuspenseLoadingFallback } from '../../utils/lazyLoading'; + +// Mock react-native completely with pass-through elements so testID is fully discoverable by testing-library +jest.mock('react-native', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const mockReact = require('react') as typeof import('react'); + return { + View: ({ + children, + testID, + style, + }: { + children?: React.ReactNode; + testID?: string; + style?: object; + }) => mockReact.createElement('View', { testID, style }, children), + Text: ({ + children, + testID, + style, + }: { + children?: React.ReactNode; + testID?: string; + style?: object; + }) => mockReact.createElement('Text', { testID, style }, children), + ActivityIndicator: ({ color }: { color?: string }) => + mockReact.createElement('ActivityIndicator', { color }), + TouchableOpacity: ({ + children, + onPress, + style, + testID, + }: { + children?: React.ReactNode; + onPress?: () => void; + style?: object; + testID?: string; + }) => mockReact.createElement('TouchableOpacity', { onPress, style, testID }, children), + InteractionManager: { + runAfterInteractions: (cb: () => void) => cb(), + }, + StyleSheet: { + create: (styles: object) => styles, + flatten: (styles: object) => styles, + }, + Platform: { + OS: 'ios', + }, + }; +}); + +// Mocking design system constants +jest.mock('../../utils/constants', () => ({ + colors: { + primary: '#6366f1', + secondary: '#8b5cf6', + accent: '#06b6d4', + success: '#10b981', + warning: '#f59e0b', + error: '#ef4444', + background: '#0f172a', + surface: '#1e293b', + text: '#f8fafc', + textSecondary: '#cbd5e1', + onPrimary: '#ffffff', + border: '#334155', + }, + spacing: { + xs: 4, + sm: 8, + md: 16, + lg: 24, + xl: 32, + xxl: 48, + }, + borderRadius: { + sm: 4, + md: 8, + lg: 12, + xl: 16, + }, + typography: { + h3: { fontSize: 20 }, + body: { fontSize: 16 }, + body2: { fontSize: 14 }, + button: { fontSize: 16 }, + }, + shadows: { + sm: {}, + md: {}, + lg: {}, + }, +})); + +const DummyComponent = () => Loaded Content Successfully; + +describe('Lazy Loading Utilities & Error Boundaries', () => { + it('renders SuspenseLoadingFallback correctly', () => { + const { getByTestId, getByText } = render(); + expect(getByTestId('lazy-loading-fallback')).toBeTruthy(); + expect(getByText('Preparing premium modules...')).toBeTruthy(); + }); + + it('lazyWithRetry resolves successfully on initial attempt', async () => { + const importFn = jest.fn().mockResolvedValue({ default: DummyComponent }); + const lazyComponent = lazyWithRetry(importFn) as unknown as { + _payload: { _result: () => Promise<{ default: typeof DummyComponent }> }; + }; + + const loadFn = lazyComponent._payload._result; + const result = await loadFn(); + + expect(result.default).toBe(DummyComponent); + expect(importFn).toHaveBeenCalledTimes(1); + }); + + it('lazyWithRetry retries on failure and resolves on second attempt', async () => { + let called = 0; + const importFn = jest.fn().mockImplementation(() => { + called++; + if (called === 1) { + return Promise.reject(new Error('Transient connection drop')); + } + return Promise.resolve({ default: DummyComponent }); + }); + + const lazyComponent = lazyWithRetry(importFn, 2, 5) as unknown as { + _payload: { _result: () => Promise<{ default: typeof DummyComponent }> }; + }; + const loadFn = lazyComponent._payload._result; + const result = await loadFn(); + + expect(result.default).toBe(DummyComponent); + expect(importFn).toHaveBeenCalledTimes(2); + }); + + it('LazyErrorBoundary catches loading failures and renders interactive retry screen', async () => { + let shouldThrow = true; + const FailingComponent = () => { + if (shouldThrow) { + throw new Error('All retries failed'); + } + return Recovered!; + }; + + // Suppress console.error to keep the logs clean during expected test failure + const originalConsoleError = console.error; + console.error = jest.fn(); + + try { + const { getByTestId, getByText } = render( + + + + ); + + expect(getByTestId('lazy-error-fallback')).toBeTruthy(); + expect(getByText('Connection Interrupted')).toBeTruthy(); + + // Disable throwing state before retry click + shouldThrow = false; + + const retryButton = getByText('Try Again'); + fireEvent.press(retryButton); + + expect(getByTestId('recovered-component')).toBeTruthy(); + } finally { + console.error = originalConsoleError; + } + }); +}); diff --git a/src/utils/lazyLoading.tsx b/src/utils/lazyLoading.tsx new file mode 100644 index 0000000..3de9d78 --- /dev/null +++ b/src/utils/lazyLoading.tsx @@ -0,0 +1,230 @@ +import React from 'react'; +import { + ActivityIndicator, + View, + StyleSheet, + Text, + TouchableOpacity, + InteractionManager, +} from 'react-native'; +import { colors, spacing, borderRadius, typography, shadows } from './constants'; + +/** + * Wraps a dynamic component import with automated retry logic to handle transient bundle loading failures. + */ +export function lazyWithRetry>>( + componentImport: () => Promise<{ default: T }>, + retries = 3, + delay = 1500 +): React.LazyExoticComponent { + return React.lazy(() => + componentImport().catch((_error) => { + return new Promise<{ default: T }>((resolve, reject) => { + let attempts = 0; + const executeAttempt = () => { + attempts++; + componentImport() + .then(resolve) + .catch((err) => { + if (attempts >= retries) { + reject(err); + } else { + setTimeout(executeAttempt, delay); + } + }); + }; + setTimeout(executeAttempt, delay); + }); + }) + ); +} + +/** + * A visually stunning loading fallback screen styled with the SubTrackr HSL/Slate design tokens. + */ +export function SuspenseLoadingFallback() { + return ( + + + + Preparing premium modules... + + + ); +} + +interface ErrorBoundaryProps { + children: React.ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +/** + * A customized Error Boundary specifically designed to handle chunk loading failures gracefully. + */ +export class LazyErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error) { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + // eslint-disable-next-line no-console + console.error('[LazyErrorBoundary] Caught dynamic import failure:', error, errorInfo); + } + + handleRetry = () => { + this.setState({ hasError: false, error: null }); + }; + + render() { + if (this.state.hasError) { + return ( + + + 📡 + Connection Interrupted + + We couldn't load this section of the app. Check your connection and try again. + + + Try Again + + + + ); + } + + return this.props.children; + } +} + +/** + * Factory function that wraps a dynamic import into a lazy-loaded screen protected by Suspense and an Error Boundary. + */ +export function lazyScreen>>( + importFn: () => Promise<{ default: T }> +) { + const LazyComponent = lazyWithRetry(importFn); + + const WrappedScreen = (props: React.ComponentPropsWithRef) => ( + + }> + + + + ); + + WrappedScreen.displayName = `lazyScreen(${importFn.toString().replace(/\s+/g, ' ')})`; + return WrappedScreen; +} + +const prefetchedModules = new Set(); + +/** + * Prefetches dynamic imports when the app enters an idle state or is otherwise not processing heavy interactions. + */ +export function prefetchModule(name: string, importFn: () => Promise) { + if (prefetchedModules.has(name)) return; + + const idleRunner = + (global as { requestIdleCallback?: (cb: () => void) => void }).requestIdleCallback ?? + ((cb: () => void) => setTimeout(cb, 1200)); + + idleRunner(() => { + InteractionManager.runAfterInteractions(() => { + importFn() + .then(() => { + prefetchedModules.add(name); + // eslint-disable-next-line no-console + console.log(`[Prefetch] Successfully cached chunk: ${name}`); + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.warn(`[Prefetch] Failed to cache chunk ${name}:`, err); + }); + }); + }); +} +export { prefetchedModules }; + +const styles = StyleSheet.create({ + loadingContainer: { + flex: 1, + backgroundColor: colors.background, + justifyContent: 'center', + alignItems: 'center', + padding: spacing.xl, + }, + card: { + backgroundColor: colors.surface, + paddingVertical: spacing.xl, + paddingHorizontal: spacing.xxl, + borderRadius: borderRadius.lg, + alignItems: 'center', + borderWidth: 1, + borderColor: colors.border, + ...shadows.md, + }, + loadingText: { + marginTop: spacing.md, + color: colors.textSecondary, + fontSize: typography.body.fontSize, + fontWeight: '500', + textAlign: 'center', + }, + errorContainer: { + flex: 1, + backgroundColor: colors.background, + justifyContent: 'center', + alignItems: 'center', + padding: spacing.xl, + }, + errorCard: { + backgroundColor: colors.surface, + padding: spacing.xl, + borderRadius: borderRadius.lg, + alignItems: 'center', + borderWidth: 1, + borderColor: colors.error + '40', // 25% opacity error border + ...shadows.lg, + maxWidth: 320, + }, + errorEmoji: { + fontSize: 48, + marginBottom: spacing.sm, + }, + errorTitle: { + fontSize: typography.h3.fontSize, + fontWeight: 'bold', + color: colors.text, + marginBottom: spacing.xs, + textAlign: 'center', + }, + errorSubtitle: { + fontSize: typography.body2.fontSize, + color: colors.textSecondary, + textAlign: 'center', + marginBottom: spacing.lg, + lineHeight: 20, + }, + retryButton: { + backgroundColor: colors.primary, + paddingVertical: spacing.sm + 2, + paddingHorizontal: spacing.lg, + borderRadius: borderRadius.md, + ...shadows.sm, + }, + retryButtonText: { + color: colors.onPrimary, + fontWeight: 'bold', + fontSize: typography.button.fontSize, + }, +}); From d329adfd27afdc618989c299aefde72be3e1f384 Mon Sep 17 00:00:00 2001 From: sweetesty Date: Sun, 31 May 2026 02:01:27 +0100 Subject: [PATCH 2/3] fix: Fix subscriptionStore syntax errors, broken imports, formatting, and audit allowlist - Fix stats object not closed in subscriptionStore (root cause of 21 TS errors) - Fix previewPlanChange missing if-block closing brace - Restore persist() config argument - Fix broken imports from backend refactoring - Add 6 new GHSA advisories to audit-ci allowlist - Run Prettier across all files --- App.tsx | 2 - audit-ci.json | 8 +- backend/services/__tests__/webhook.test.ts | 2 +- .../api-endpoints.integration.test.ts | 6 +- developer-portal/components/ApiKeyManager.tsx | 52 ++--- .../components/DeveloperOnboarding.tsx | 23 +- developer-portal/pages/ApiKeysPage.tsx | 80 ++----- developer-portal/pages/DashboardPage.tsx | 73 ++---- developer-portal/pages/DocumentationPage.tsx | 41 +--- developer-portal/pages/OnboardingPage.tsx | 112 +++------ developer-portal/pages/UsagePage.tsx | 63 ++---- .../services/developerPortalService.ts | 155 ++++++++++--- .../services/documentationService.ts | 14 +- .../services/integrationGuidesService.ts | 6 +- developer-portal/services/portalService.ts | 8 +- .../src/components/DashboardCard.tsx | 4 +- .../src/components/PermissionSelector.tsx | 3 +- .../src/screens/ApiKeyManagementScreen.tsx | 25 +- .../src/screens/ApiTesterScreen.tsx | 21 +- .../src/screens/DeveloperPortalScreen.tsx | 55 ++--- .../src/screens/UsageAnalyticsScreen.tsx | 8 +- .../src/screens/WebhookTesterScreen.tsx | 16 +- developer-portal/types/developer.ts | 7 +- developer-portal/types/portal.ts | 7 +- .../utils/developerPortalUtils.ts | 44 ++-- sandbox/__tests__/developerPortal.test.ts | 46 +--- sandbox/__tests__/sandbox.test.ts | 22 +- sandbox/api/sandboxApi.ts | 11 +- sandbox/config/sandboxConfig.ts | 9 +- sandbox/middleware/sandboxMiddleware.ts | 44 +--- sandbox/services/apiKeyService.ts | 6 +- sandbox/services/sandboxIsolationService.ts | 40 ++-- sandbox/services/sandboxService.ts | 79 +++---- sandbox/services/usageTrackingService.ts | 59 ++--- sandbox/utils/sandboxUtils.ts | 41 +++- sandbox/utils/testDataGenerator.ts | 37 ++- sdks/javascript/src/errors.ts | 6 +- src/components/UsageDashboard.tsx | 2 +- src/components/common/ScreenTemplates.tsx | 10 +- .../developer/DeveloperComponents.tsx | 4 +- src/components/home/StatsCard.tsx | 3 - .../subscription/SubscriptionCard.tsx | 3 - src/config/features.ts | 3 +- src/navigation/AppNavigator.tsx | 4 +- src/screens/AffiliateDashboardScreen.tsx | 46 ++-- src/screens/AnalyticsScreen.tsx | 7 - src/screens/ApiKeyManagementScreen.tsx | 23 +- src/screens/CalendarIntegrationScreen.tsx | 57 +++-- src/screens/CampaignManagementScreen.tsx | 29 +-- src/screens/CancellationFlowScreen.tsx | 4 +- src/screens/DeveloperPortalScreen.tsx | 50 ++-- src/screens/DocumentationPortalScreen.tsx | 40 ++-- src/screens/FraudDashboard.tsx | 84 +++++-- src/screens/GroupManagementScreen.tsx | 4 +- src/screens/HomeScreen.tsx | 3 - src/screens/IntegrationGuideDetailScreen.tsx | 19 +- src/screens/IntegrationGuidesScreen.tsx | 29 +-- src/screens/LoyaltyDashboardScreen.tsx | 56 ++--- src/screens/MerchantOnboardingScreen.tsx | 31 +-- src/screens/RoleManagementScreen.tsx | 115 ++++++---- src/screens/SandboxDashboardScreen.tsx | 73 +++--- src/screens/SandboxDetailScreen.tsx | 46 ++-- src/screens/SandboxScreen.tsx | 102 +++------ src/screens/SupportDashboardScreen.tsx | 17 +- src/screens/TaxSettingsScreen.tsx | 6 +- src/services/FEATURE_GATING_README.md | 63 +++++- src/services/__tests__/slaService.test.ts | 23 +- src/services/__tests__/walletService.test.ts | 16 +- src/services/accountingExport.ts | 8 +- src/services/adminDashboardService.ts | 6 +- src/services/analyticsService.ts | 20 +- src/services/calendarService.ts | 14 +- src/services/groupService.ts | 11 +- src/services/notificationService.ts | 12 +- src/services/oraclePriceService.ts | 22 +- .../sandbox/developerOnboardingService.ts | 6 +- .../sandbox/developerPortalService.ts | 8 +- src/services/sandbox/documentationService.ts | 6 +- src/services/sandbox/sandboxService.ts | 67 ++++-- src/services/sandbox/testDataGenerator.ts | 72 ++++-- src/services/taxService.ts | 9 +- src/services/walletService.ts | 49 ++-- src/store/__tests__/slaStore.test.ts | 5 +- src/store/affiliateStore.ts | 15 +- src/store/calendarStore.ts | 8 +- src/store/developerPortalStore.ts | 25 +- src/store/fraudStore.ts | 135 +++++++++-- src/store/groupStore.ts | 7 +- src/store/invoiceStore.ts | 5 +- src/store/loyaltyStore.ts | 2 +- src/store/merchantStore.ts | 2 +- src/store/sandboxStore.ts | 214 ++++++++++++++---- src/store/settingsStore.ts | 4 +- src/store/subscriptionStore.ts | 100 ++++---- src/store/supportStore.ts | 8 +- src/store/taxStore.ts | 8 +- src/types/affiliate.ts | 2 +- src/types/calendar.ts | 2 +- src/types/developerPortal.ts | 24 +- src/types/fraud.ts | 7 +- src/types/loyalty.ts | 2 +- src/types/merchant.ts | 2 +- src/types/rateLimiting.ts | 5 +- src/utils/formatting.ts | 1 - src/utils/invoice.ts | 10 +- src/utils/proration.ts | 45 ++-- 106 files changed, 1632 insertions(+), 1523 deletions(-) diff --git a/App.tsx b/App.tsx index f08cd6b..6ff13d2 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 4333052..6fa3d5e 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 fc91bd8..ee4222e 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/tests/integration/api-endpoints.integration.test.ts b/backend/tests/integration/api-endpoints.integration.test.ts index 06a048b..d128672 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 c005a13..f10c98e 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 1ea9e6c..770a06a 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 1a859c9..8ebf43c 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 1b4b3d4..86b5126 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 3830e15..ebb81a8 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 7db44fc..944291f 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 96b73e2..584b899 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 a320b92..e68821f 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 122555f..60cb95f 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 52d2049..452d948 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 7ccd4c0..fc6715a 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 b94f23f..a370458 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 90d8333..5c4bf48 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 b7870c2..015272c 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 c69ee6c..fb19a9c 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 f897550..909b40d 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 5f37338..7ec8f10 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 538128c..2e2b933 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 3a5396f..32687b8 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 563d256..9ae89da 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 5aca0a2..0ce5972 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 edb3315..1b627b1 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 d40486c..5dc2ca6 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 25b8cea..d9c9481 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 330eb6b..c41cf3b 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 b04f67a..83b94a0 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 5b54fe1..21e1e7d 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 9ff0936..30f5a2b 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 1bf1f9c..ee16da6 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 3f0692b..0094ab6 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 09d1eff..9a93cb3 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 7a956db..bbbe4bf 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 16ef639..dc72715 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 7ac1a17..4420aa3 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 b6a7756..c051b32 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 867f290..2e82f61 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 accba0c..b167bd7 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 fcae851..1bacff7 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 0d461f0..efb9880 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -64,7 +64,9 @@ const SandboxDashboardScreen = lazyScreen(() => import('../screens/SandboxDashbo 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 PerformanceDashboardScreen = lazyScreen( + () => import('../screens/PerformanceDashboardScreen') +); const Tab = createBottomTabNavigator(); const Stack = createNativeStackNavigator(); diff --git a/src/screens/AffiliateDashboardScreen.tsx b/src/screens/AffiliateDashboardScreen.tsx index c709567..fbf6860 100644 --- a/src/screens/AffiliateDashboardScreen.tsx +++ b/src/screens/AffiliateDashboardScreen.tsx @@ -125,9 +125,7 @@ const AffiliateDashboardScreen: React.FC = () => { {affiliate.referrerAddress.slice(-4)} - - {affiliate.totalReferrals} referrals - + {affiliate.totalReferrals} referrals ${affiliate.totalEarnings.toFixed(2)} earned @@ -142,8 +140,8 @@ const AffiliateDashboardScreen: React.FC = () => { affiliate.status === AffiliateStatus.ACTIVE ? colors.success : affiliate.status === AffiliateStatus.PAUSED - ? colors.warning - : colors.danger, + ? colors.warning + : colors.danger, }, ]}> {affiliate.status} @@ -151,17 +149,13 @@ const AffiliateDashboardScreen: React.FC = () => { {affiliate.status === AffiliateStatus.ACTIVE ? ( - handleToggleStatus(affiliate.id, AffiliateStatus.PAUSED) - }> + onPress={() => handleToggleStatus(affiliate.id, AffiliateStatus.PAUSED)}> Pause ) : ( - handleToggleStatus(affiliate.id, AffiliateStatus.ACTIVE) - }> + onPress={() => handleToggleStatus(affiliate.id, AffiliateStatus.ACTIVE)}> Resume )} @@ -192,8 +186,8 @@ const AffiliateDashboardScreen: React.FC = () => { {program.commissionConfig.type === 'percentage' ? `${program.commissionConfig.rate}%` : program.commissionConfig.type === 'flat' - ? `$${program.commissionConfig.rate}` - : 'Tiered'} + ? `$${program.commissionConfig.rate}` + : 'Tiered'} commission @@ -228,8 +222,8 @@ const AffiliateDashboardScreen: React.FC = () => { commission.status === 'paid' ? colors.success : commission.status === 'approved' - ? colors.warning - : colors.textSecondary, + ? colors.warning + : colors.textSecondary, }, ]}> {commission.status} @@ -257,9 +251,7 @@ const AffiliateDashboardScreen: React.FC = () => { Affiliate Dashboard - - Track referrals and earn commissions - + Track referrals and earn commissions {renderMetricsCard()} @@ -284,9 +276,7 @@ const AffiliateDashboardScreen: React.FC = () => { Select Program - - Choose an affiliate program to join - + Choose an affiliate program to join {programs.map((program) => ( { onPress={() => setSelectedProgram(program.id)}> {program.name} - - {program.description} - + {program.description} - {selectedProgram === program.id && ( - - )} + {selectedProgram === program.id && } ))} @@ -320,9 +306,7 @@ const AffiliateDashboardScreen: React.FC = () => { onPress={() => setProgramModalVisible(false)}> Cancel - + Join Program @@ -676,4 +660,4 @@ const styles = StyleSheet.create({ }, }); -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 25bd9c5..5ba4d7e 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 7be0fac..4d19883 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 4db03dc..3da3584 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 96ad811..371cc9c 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 8701795..3b41c79 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 de1ff3c..0449256 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 f26dd4b..bb261eb 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 4b094d0..a40cf75 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'} +