From 404168d6f1113f1469e86baa77043b8fd1a77595 Mon Sep 17 00:00:00 2001 From: Amarjeet Patel Date: Sun, 29 Mar 2026 15:46:03 +0530 Subject: [PATCH] feat(mobile): add interactive portfolio performance charts --- apps/backend/src/app.module.ts | 2 +- .../portfolio/dto/portfolio-snapshot.dto.ts | 18 +- .../src/portfolio/portfolio.controller.ts | 1 + .../src/portfolio/portfolio.service.ts | 22 +- apps/mobile/app/(tabs)/portfolio.tsx | 2 + apps/mobile/app/_layout.tsx | 23 +- apps/mobile/components/PortfolioChart.tsx | 194 +++++ apps/mobile/lib/api.ts | 21 + apps/mobile/package-lock.json | 353 +++++++++ apps/mobile/package.json | 5 + docker-compose.yml | 2 +- git_diff.txt | Bin 0 -> 56966 bytes git_diff2.txt | 723 ++++++++++++++++++ 13 files changed, 1350 insertions(+), 16 deletions(-) create mode 100644 apps/mobile/components/PortfolioChart.tsx create mode 100644 git_diff.txt create mode 100644 git_diff2.txt diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index aa992021..633bf764 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -40,7 +40,7 @@ const appLogger = new Logger('TypeORM'); password: configService.get('DB_PASSWORD'), database: configService.get('DB_DATABASE'), entities: [__dirname + '/**/*.entity{.ts,.js}'], - synchronize: false, + synchronize: true, migrations: [__dirname + '/migrations/*{.ts,.js}'], logging: true, }), diff --git a/apps/backend/src/portfolio/dto/portfolio-snapshot.dto.ts b/apps/backend/src/portfolio/dto/portfolio-snapshot.dto.ts index 50112c7b..dd82cbb9 100644 --- a/apps/backend/src/portfolio/dto/portfolio-snapshot.dto.ts +++ b/apps/backend/src/portfolio/dto/portfolio-snapshot.dto.ts @@ -1,7 +1,14 @@ -import { IsNumber, IsOptional, Min } from 'class-validator'; +import { IsEnum, IsNumber, IsOptional, Min } from 'class-validator'; import { Type } from 'class-transformer'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +export enum PortfolioHistoryRange { + '1D' = '1D', + '1W' = '1W', + '1M' = '1M', + 'ALL' = 'ALL', +} + export class AssetBalanceDto { @ApiProperty({ description: 'Asset code', example: 'XLM' }) assetCode: string; @@ -94,6 +101,15 @@ export class GetPortfolioHistoryDto { @IsNumber() @Min(1) limit?: number = 10; + + @ApiPropertyOptional({ + description: 'Time range for history', + enum: PortfolioHistoryRange, + example: PortfolioHistoryRange['1D'], + }) + @IsOptional() + @IsEnum(PortfolioHistoryRange) + range?: PortfolioHistoryRange; } export class PortfolioHistoryResponseDto { diff --git a/apps/backend/src/portfolio/portfolio.controller.ts b/apps/backend/src/portfolio/portfolio.controller.ts index fd987dad..fac13fb3 100644 --- a/apps/backend/src/portfolio/portfolio.controller.ts +++ b/apps/backend/src/portfolio/portfolio.controller.ts @@ -75,6 +75,7 @@ export class PortfolioController { userId, query.page, query.limit, + query.range, ); } diff --git a/apps/backend/src/portfolio/portfolio.service.ts b/apps/backend/src/portfolio/portfolio.service.ts index 74eb0cb7..7dc4dbd8 100644 --- a/apps/backend/src/portfolio/portfolio.service.ts +++ b/apps/backend/src/portfolio/portfolio.service.ts @@ -1,12 +1,13 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Between, MoreThanOrEqual, Repository } from 'typeorm'; import { Cron, CronExpression } from '@nestjs/schedule'; import { PortfolioSnapshot } from './entities/portfolio-snapshot.entity'; import { PortfolioAsset } from './portfolio-asset.entity'; import { User } from '../users/entities/user.entity'; import { StellarBalanceService } from './stellar-balance.service'; import { + PortfolioHistoryRange, PortfolioHistoryResponseDto, PortfolioSnapshotDto, PortfolioSummaryResponseDto, @@ -109,17 +110,32 @@ export class PortfolioService { } /** - * Get portfolio history for a user with pagination + * Get portfolio history for a user with pagination and optional range */ async getPortfolioHistory( userId: string, page: number = 1, limit: number = 10, + range?: PortfolioHistoryRange, ): Promise { const skip = (page - 1) * limit; + const now = new Date(); + const where: any = { userId }; + + if (range && range !== PortfolioHistoryRange.ALL) { + const startDate = new Date(); + if (range === PortfolioHistoryRange['1D']) { + startDate.setHours(now.getHours() - 24); + } else if (range === PortfolioHistoryRange['1W']) { + startDate.setDate(now.getDate() - 7); + } else if (range === PortfolioHistoryRange['1M']) { + startDate.setMonth(now.getMonth() - 1); + } + where.createdAt = MoreThanOrEqual(startDate); + } const [snapshots, total] = await this.snapshotRepository.findAndCount({ - where: { userId }, + where, order: { createdAt: 'DESC' }, skip, take: limit, diff --git a/apps/mobile/app/(tabs)/portfolio.tsx b/apps/mobile/app/(tabs)/portfolio.tsx index 98158025..b82b322a 100644 --- a/apps/mobile/app/(tabs)/portfolio.tsx +++ b/apps/mobile/app/(tabs)/portfolio.tsx @@ -14,6 +14,7 @@ import { useRouter } from 'expo-router'; import { useAuth } from '../../contexts/AuthContext'; import { useTheme } from '../../contexts/ThemeContext'; import { portfolioApi, AssetBalance, PortfolioSummary } from '../../lib/api'; +import PortfolioChart from '../../components/PortfolioChart'; // ─── Helpers ────────────────────────────────────────────────────────────────── @@ -277,6 +278,7 @@ export default function PortfolioScreen() { ListHeaderComponent={ <> Portfolio + {summary && } {summary && } {summary && summary.assets.length > 0 && ( diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index 1a0a6619..95aa1566 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -1,3 +1,4 @@ +import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { Stack } from 'expo-router'; import { AuthProvider } from '../contexts/AuthContext'; import { ThemeProvider } from '../contexts/ThemeContext'; @@ -5,15 +6,17 @@ import { NotificationsProvider } from '../contexts/NotificationsContext'; export default function RootLayout() { return ( - - - - - - - - - - + + + + + + + + + + + + ); } \ No newline at end of file diff --git a/apps/mobile/components/PortfolioChart.tsx b/apps/mobile/components/PortfolioChart.tsx new file mode 100644 index 00000000..a27c2e38 --- /dev/null +++ b/apps/mobile/components/PortfolioChart.tsx @@ -0,0 +1,194 @@ +import React, { useEffect, useState } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + ActivityIndicator, + Dimensions, +} from 'react-native'; +import { LineChart } from 'react-native-wagmi-charts'; +import * as Haptics from 'expo-haptics'; +import { portfolioApi, PortfolioHistorySnapshot } from '../lib/api'; +import { useTheme } from '../contexts/ThemeContext'; + +const { width: SCREEN_WIDTH } = Dimensions.get('window'); + +type Range = '1D' | '1W' | '1M' | 'ALL'; + +interface ChartPoint { + timestamp: number; + value: number; +} + +export default function PortfolioChart() { + const { colors } = useTheme(); + const [range, setRange] = useState('1M'); + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchHistory = async (selectedRange: Range) => { + setIsLoading(true); + setError(null); + try { + const response = await portfolioApi.getHistory(selectedRange); + if (response.success && response.data) { + const snapshots = response.data.snapshots; + if (snapshots.length > 0) { + // Wagmi-charts expects timestamp as number and value as number + // Snapshots are returned DESC, we need ASC for the chart + const formattedData = [...snapshots] + .reverse() + .map((s: PortfolioHistorySnapshot) => ({ + timestamp: new Date(s.createdAt).getTime(), + value: parseFloat(s.totalValueUsd), + })); + setData(formattedData); + } else { + setData([]); + } + } else { + setError('Failed to load chart data'); + } + } catch (err) { + setError('An error occurred while fetching history'); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchHistory(range); + }, [range]); + + const handleRangeChange = (newRange: Range) => { + if (newRange !== range) { + setRange(newRange); + void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + } + }; + + const renderRangeSelector = () => ( + + {(['1D', '1W', '1M', 'ALL'] as Range[]).map((r) => ( + handleRangeChange(r)} + style={[ + styles.rangeButton, + range === r && { backgroundColor: `${colors.accent}22` }, + ]} + > + + {r} + + + ))} + + ); + + if (isLoading && data.length === 0) { + return ( + + + + ); + } + + if (error && data.length === 0) { + return ( + + {error} + fetchHistory(range)} style={styles.retryButton}> + Retry + + + ); + } + + return ( + + + {data.length > 0 ? ( + + + + + + + + + + + ) : ( + + No historical data available + + )} + + + {isLoading && data.length > 0 && ( + + + Loading chart... + + )} + + {renderRangeSelector()} + + ); +} + +const styles = StyleSheet.create({ + container: { + marginHorizontal: 16, + marginBottom: 24, + minHeight: 260, + position: 'relative', + }, + center: { + justifyContent: 'center', + alignItems: 'center', + }, + emptyContainer: { + height: 200, + justifyContent: 'center', + alignItems: 'center', + }, + rangeSelector: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 16, + backgroundColor: 'rgba(0,0,0,0.03)', + borderRadius: 12, + padding: 4, + }, + rangeButton: { + flex: 1, + paddingVertical: 8, + alignItems: 'center', + borderRadius: 8, + }, + rangeText: { + fontSize: 12, + fontWeight: '600', + }, + retryButton: { + marginTop: 8, + padding: 8, + }, + loadingOverlay: { + ...StyleSheet.absoluteFillObject, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0,0,0,0.12)', + borderRadius: 12, + }, +}); diff --git a/apps/mobile/lib/api.ts b/apps/mobile/lib/api.ts index c2c3c1eb..11066218 100644 --- a/apps/mobile/lib/api.ts +++ b/apps/mobile/lib/api.ts @@ -46,6 +46,20 @@ export interface PortfolioSummary { hasLinkedAccount: boolean; } +export interface PortfolioHistorySnapshot { + id: string; + createdAt: string; + totalValueUsd: string; +} + +export interface PortfolioHistoryResponse { + snapshots: PortfolioHistorySnapshot[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + export interface SnapshotResponse { success: boolean; snapshot: { @@ -152,6 +166,13 @@ export const portfolioApi = { async createSnapshot(): Promise> { return apiClient.post('/portfolio/snapshot'); }, + + /** + * Get portfolio history for charting + */ + async getHistory(range: string = '1D'): Promise> { + return apiClient.get(`/portfolio/history?range=${range}&limit=100`); + }, }; /** diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json index 1940308b..81a8484e 100644 --- a/apps/mobile/package-lock.json +++ b/apps/mobile/package-lock.json @@ -16,6 +16,7 @@ "expo-barcode-scanner": "^13.0.1", "expo-constants": "~18.0.13", "expo-dev-client": "^6.0.20", + "expo-haptics": "~15.0.8", "expo-linking": "~8.0.11", "expo-router": "~6.0.22", "expo-secure-store": "^15.0.8", @@ -23,9 +24,13 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.5", + "react-native-gesture-handler": "^2.30.0", + "react-native-haptic-feedback": "^2.3.4", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", + "react-native-svg": "^15.12.1", + "react-native-wagmi-charts": "^2.9.1", "react-native-web": "^0.21.2", "react-native-worklets": "0.5.1", "react-native-worklets-core": "^1.6.2" @@ -1577,6 +1582,18 @@ "node": ">=6.9.0" } }, + "node_modules/@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "license": "MIT", + "dependencies": { + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@emnapi/core": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", @@ -4172,6 +4189,12 @@ "@types/node": "*" } }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "license": "MIT" + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -5145,6 +5168,12 @@ "node": ">=6.5" } }, + "node_modules/abs-svg-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", + "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", + "license": "MIT" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -7164,6 +7193,101 @@ "dev": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-color": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", + "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-color": "1" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "node_modules/d3-scale/node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -8629,6 +8753,15 @@ "react-native": "*" } }, + "node_modules/expo-haptics": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-15.0.8.tgz", + "integrity": "sha512-lftutojy8Qs8zaDzzjwM3gKHFZ8bOOEZDCkmh2Ddpe95Ra6kt2izeOfOfKuP/QEh0MZ1j9TfqippyHdRd1ZM9g==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-image-loader": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-4.7.0.tgz", @@ -9940,6 +10073,21 @@ "hermes-estree": "0.29.1" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -10373,6 +10521,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -12784,6 +12941,15 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-svg-path": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", + "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", + "license": "MIT", + "dependencies": { + "svg-arc-to-cubic-bezier": "^3.0.0" + } + }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -13258,6 +13424,12 @@ "node": ">=10" } }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -14457,6 +14629,24 @@ "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", "license": "MIT" }, + "node_modules/react-keyed-flatten-children": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/react-keyed-flatten-children/-/react-keyed-flatten-children-1.3.0.tgz", + "integrity": "sha512-qB7A6n+NHU0x88qTZGAJw6dsqwI941jcRPBB640c/CyWqjPQQ+YUmXOuzPziuHb7iqplM3xksWAbGYwkQT0tXA==", + "license": "MIT", + "dependencies": { + "react-is": "^16.8.6" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, + "node_modules/react-keyed-flatten-children/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-native": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", @@ -14514,6 +14704,33 @@ } } }, + "node_modules/react-native-gesture-handler": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.30.0.tgz", + "integrity": "sha512-5YsnKHGa0X9C8lb5oCnKm0fLUPM6CRduvUUw2Bav4RIj/C3HcFh4RIUnF8wgG6JQWCL1//gRx4v+LVWgcIQdGA==", + "license": "MIT", + "dependencies": { + "@egjs/hammerjs": "^2.0.17", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-haptic-feedback": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/react-native-haptic-feedback/-/react-native-haptic-feedback-2.3.4.tgz", + "integrity": "sha512-3nHECsi+pL1c2TKNpOD5aPtAfFzrkjpABjioi6OUBb6OVeuUObQlx9idoXSbj1h/L7TCaC13LqKT5ILl0xxHjQ==", + "license": "MIT", + "workspaces": [ + "example" + ], + "peerDependencies": { + "react-native": ">=0.60.0" + } + }, "node_modules/react-native-is-edge-to-edge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz", @@ -14552,6 +14769,23 @@ "node": ">=10" } }, + "node_modules/react-native-redash": { + "version": "18.1.5", + "resolved": "https://registry.npmjs.org/react-native-redash/-/react-native-redash-18.1.5.tgz", + "integrity": "sha512-0Dy04RcN8tqHv9p3osr3WtsPxWwiXUwt+soFUYsKQEbn+LodSnB8LHnmd0eWemCYXzBPJRNt7YgPA9XTBYoGWQ==", + "license": "MIT", + "dependencies": { + "abs-svg-path": "^0.1.1", + "normalize-svg-path": "^1.0.1", + "parse-svg-path": "^0.1.2" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-gesture-handler": "*", + "react-native-reanimated": ">=2.0.0" + } + }, "node_modules/react-native-safe-area-context": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", @@ -14577,6 +14811,119 @@ "react-native": "*" } }, + "node_modules/react-native-svg": { + "version": "15.12.1", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz", + "integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "css-tree": "^1.1.3", + "warn-once": "0.1.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-svg/node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/react-native-svg/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/react-native-svg/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/react-native-svg/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/react-native-svg/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/react-native-wagmi-charts": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/react-native-wagmi-charts/-/react-native-wagmi-charts-2.9.1.tgz", + "integrity": "sha512-1VFO4qGK8JY0o7dxcKSP5anVme1V+AfvV+oqN5vY6ah4bR2zGVvDwHwXboez+zGgz7UI5F3VgXbCyBrSiXDorw==", + "workspaces": [ + "example" + ], + "dependencies": { + "d3-array": "^3.1.6", + "d3-scale": "^2", + "d3-shape": "^3.0.1", + "fbjs": "^3.0.5", + "react-keyed-flatten-children": "^1.3.0", + "react-native-redash": "^18.1.3" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-gesture-handler": "*", + "react-native-reanimated": "*", + "react-native-redash": "*", + "react-native-svg": "*" + } + }, "node_modules/react-native-web": { "version": "0.21.2", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", @@ -16216,6 +16563,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-arc-to-cubic-bezier": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", + "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", + "license": "ISC" + }, "node_modules/svgo": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.2.tgz", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 317b835a..60f8fa3d 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -20,6 +20,7 @@ "expo-barcode-scanner": "^13.0.1", "expo-constants": "~18.0.13", "expo-dev-client": "^6.0.20", + "expo-haptics": "~15.0.8", "expo-linking": "~8.0.11", "expo-router": "~6.0.22", "expo-secure-store": "^15.0.8", @@ -27,9 +28,13 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.5", + "react-native-gesture-handler": "^2.30.0", + "react-native-haptic-feedback": "^2.3.4", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", + "react-native-svg": "^15.12.1", + "react-native-wagmi-charts": "^2.9.1", "react-native-web": "^0.21.2", "react-native-worklets": "0.5.1", "react-native-worklets-core": "^1.6.2" diff --git a/docker-compose.yml b/docker-compose.yml index 214d6f79..556f5fba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: container_name: lumenpulse-postgres restart: unless-stopped ports: - - "5433:5432" + - "5434:5432" environment: POSTGRES_USER: lumenpulse POSTGRES_PASSWORD: lumenpulse diff --git a/git_diff.txt b/git_diff.txt new file mode 100644 index 0000000000000000000000000000000000000000..78396d90bc2111198b941c2cd3a4947a63f72497 GIT binary patch literal 56966 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 literal 0 HcmV?d00001 diff --git a/git_diff2.txt b/git_diff2.txt new file mode 100644 index 00000000..e8c07b8c --- /dev/null +++ b/git_diff2.txt @@ -0,0 +1,723 @@ +diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts +index aa99202..633bf76 100644 +--- a/apps/backend/src/app.module.ts ++++ b/apps/backend/src/app.module.ts +@@ -40,7 +40,7 @@ const appLogger = new Logger('TypeORM'); + password: configService.get('DB_PASSWORD'), + database: configService.get('DB_DATABASE'), + entities: [__dirname + '/**/*.entity{.ts,.js}'], +- synchronize: false, ++ synchronize: true, + migrations: [__dirname + '/migrations/*{.ts,.js}'], + logging: true, + }), +diff --git a/apps/backend/src/portfolio/dto/portfolio-snapshot.dto.ts b/apps/backend/src/portfolio/dto/portfolio-snapshot.dto.ts +index 50112c7..dd82cbb 100644 +--- a/apps/backend/src/portfolio/dto/portfolio-snapshot.dto.ts ++++ b/apps/backend/src/portfolio/dto/portfolio-snapshot.dto.ts +@@ -1,7 +1,14 @@ +-import { IsNumber, IsOptional, Min } from 'class-validator'; ++import { IsEnum, IsNumber, IsOptional, Min } from 'class-validator'; + import { Type } from 'class-transformer'; + import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + ++export enum PortfolioHistoryRange { ++ '1D' = '1D', ++ '1W' = '1W', ++ '1M' = '1M', ++ 'ALL' = 'ALL', ++} ++ + export class AssetBalanceDto { + @ApiProperty({ description: 'Asset code', example: 'XLM' }) + assetCode: string; +@@ -94,6 +101,15 @@ export class GetPortfolioHistoryDto { + @IsNumber() + @Min(1) + limit?: number = 10; ++ ++ @ApiPropertyOptional({ ++ description: 'Time range for history', ++ enum: PortfolioHistoryRange, ++ example: PortfolioHistoryRange['1D'], ++ }) ++ @IsOptional() ++ @IsEnum(PortfolioHistoryRange) ++ range?: PortfolioHistoryRange; + } + + export class PortfolioHistoryResponseDto { +diff --git a/apps/backend/src/portfolio/portfolio.controller.ts b/apps/backend/src/portfolio/portfolio.controller.ts +index fd987da..fac13fb 100644 +--- a/apps/backend/src/portfolio/portfolio.controller.ts ++++ b/apps/backend/src/portfolio/portfolio.controller.ts +@@ -75,6 +75,7 @@ export class PortfolioController { + userId, + query.page, + query.limit, ++ query.range, + ); + } + +diff --git a/apps/backend/src/portfolio/portfolio.service.ts b/apps/backend/src/portfolio/portfolio.service.ts +index 74eb0cb..7dc4dbd 100644 +--- a/apps/backend/src/portfolio/portfolio.service.ts ++++ b/apps/backend/src/portfolio/portfolio.service.ts +@@ -1,12 +1,13 @@ + import { Injectable, Logger, NotFoundException } from '@nestjs/common'; + import { InjectRepository } from '@nestjs/typeorm'; +-import { Repository } from 'typeorm'; ++import { Between, MoreThanOrEqual, Repository } from 'typeorm'; + import { Cron, CronExpression } from '@nestjs/schedule'; + import { PortfolioSnapshot } from './entities/portfolio-snapshot.entity'; + import { PortfolioAsset } from './portfolio-asset.entity'; + import { User } from '../users/entities/user.entity'; + import { StellarBalanceService } from './stellar-balance.service'; + import { ++ PortfolioHistoryRange, + PortfolioHistoryResponseDto, + PortfolioSnapshotDto, + PortfolioSummaryResponseDto, +@@ -109,17 +110,32 @@ export class PortfolioService { + } + + /** +- * Get portfolio history for a user with pagination ++ * Get portfolio history for a user with pagination and optional range + */ + async getPortfolioHistory( + userId: string, + page: number = 1, + limit: number = 10, ++ range?: PortfolioHistoryRange, + ): Promise { + const skip = (page - 1) * limit; ++ const now = new Date(); ++ const where: any = { userId }; ++ ++ if (range && range !== PortfolioHistoryRange.ALL) { ++ const startDate = new Date(); ++ if (range === PortfolioHistoryRange['1D']) { ++ startDate.setHours(now.getHours() - 24); ++ } else if (range === PortfolioHistoryRange['1W']) { ++ startDate.setDate(now.getDate() - 7); ++ } else if (range === PortfolioHistoryRange['1M']) { ++ startDate.setMonth(now.getMonth() - 1); ++ } ++ where.createdAt = MoreThanOrEqual(startDate); ++ } + + const [snapshots, total] = await this.snapshotRepository.findAndCount({ +- where: { userId }, ++ where, + order: { createdAt: 'DESC' }, + skip, + take: limit, +diff --git a/apps/mobile/app/(tabs)/portfolio.tsx b/apps/mobile/app/(tabs)/portfolio.tsx +index 9815802..b82b322 100644 +--- a/apps/mobile/app/(tabs)/portfolio.tsx ++++ b/apps/mobile/app/(tabs)/portfolio.tsx +@@ -14,6 +14,7 @@ import { useRouter } from 'expo-router'; + import { useAuth } from '../../contexts/AuthContext'; + import { useTheme } from '../../contexts/ThemeContext'; + import { portfolioApi, AssetBalance, PortfolioSummary } from '../../lib/api'; ++import PortfolioChart from '../../components/PortfolioChart'; + + // ΓöÇΓöÇΓöÇ Helpers ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ + +@@ -277,6 +278,7 @@ export default function PortfolioScreen() { + ListHeaderComponent={ + <> + Portfolio ++ {summary && } + {summary && } + {summary && summary.assets.length > 0 && ( + +diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx +index 1a0a661..95aa156 100644 +--- a/apps/mobile/app/_layout.tsx ++++ b/apps/mobile/app/_layout.tsx +@@ -1,3 +1,4 @@ ++import { GestureHandlerRootView } from 'react-native-gesture-handler'; + import { Stack } from 'expo-router'; + import { AuthProvider } from '../contexts/AuthContext'; + import { ThemeProvider } from '../contexts/ThemeContext'; +@@ -5,15 +6,17 @@ import { NotificationsProvider } from '../contexts/NotificationsContext'; + + export default function RootLayout() { + return ( +- +- +- +- +- +- +- +- +- +- ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ + ); + } +\ No newline at end of file +diff --git a/apps/mobile/lib/api.ts b/apps/mobile/lib/api.ts +index c2c3c1e..1106621 100644 +--- a/apps/mobile/lib/api.ts ++++ b/apps/mobile/lib/api.ts +@@ -46,6 +46,20 @@ export interface PortfolioSummary { + hasLinkedAccount: boolean; + } + ++export interface PortfolioHistorySnapshot { ++ id: string; ++ createdAt: string; ++ totalValueUsd: string; ++} ++ ++export interface PortfolioHistoryResponse { ++ snapshots: PortfolioHistorySnapshot[]; ++ total: number; ++ page: number; ++ limit: number; ++ totalPages: number; ++} ++ + export interface SnapshotResponse { + success: boolean; + snapshot: { +@@ -152,6 +166,13 @@ export const portfolioApi = { + async createSnapshot(): Promise> { + return apiClient.post('/portfolio/snapshot'); + }, ++ ++ /** ++ * Get portfolio history for charting ++ */ ++ async getHistory(range: string = '1D'): Promise> { ++ return apiClient.get(`/portfolio/history?range=${range}&limit=100`); ++ }, + }; + + /** +diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json +index 1940308..81a8484 100644 +--- a/apps/mobile/package-lock.json ++++ b/apps/mobile/package-lock.json +@@ -16,6 +16,7 @@ + "expo-barcode-scanner": "^13.0.1", + "expo-constants": "~18.0.13", + "expo-dev-client": "^6.0.20", ++ "expo-haptics": "~15.0.8", + "expo-linking": "~8.0.11", + "expo-router": "~6.0.22", + "expo-secure-store": "^15.0.8", +@@ -23,9 +24,13 @@ + "react": "19.1.0", + "react-dom": "19.1.0", + "react-native": "0.81.5", ++ "react-native-gesture-handler": "^2.30.0", ++ "react-native-haptic-feedback": "^2.3.4", + "react-native-reanimated": "~4.1.1", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0", ++ "react-native-svg": "^15.12.1", ++ "react-native-wagmi-charts": "^2.9.1", + "react-native-web": "^0.21.2", + "react-native-worklets": "0.5.1", + "react-native-worklets-core": "^1.6.2" +@@ -1577,6 +1582,18 @@ + "node": ">=6.9.0" + } + }, ++ "node_modules/@egjs/hammerjs": { ++ "version": "2.0.17", ++ "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", ++ "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", ++ "license": "MIT", ++ "dependencies": { ++ "@types/hammerjs": "^2.0.36" ++ }, ++ "engines": { ++ "node": ">=0.8.0" ++ } ++ }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", +@@ -4172,6 +4189,12 @@ + "@types/node": "*" + } + }, ++ "node_modules/@types/hammerjs": { ++ "version": "2.0.46", ++ "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", ++ "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", ++ "license": "MIT" ++ }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", +@@ -5145,6 +5168,12 @@ + "node": ">=6.5" + } + }, ++ "node_modules/abs-svg-path": { ++ "version": "0.1.1", ++ "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", ++ "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", ++ "license": "MIT" ++ }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", +@@ -7164,6 +7193,101 @@ + "dev": true, + "license": "MIT" + }, ++ "node_modules/d3-array": { ++ "version": "3.2.4", ++ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", ++ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", ++ "license": "ISC", ++ "dependencies": { ++ "internmap": "1 - 2" ++ }, ++ "engines": { ++ "node": ">=12" ++ } ++ }, ++ "node_modules/d3-collection": { ++ "version": "1.0.7", ++ "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", ++ "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", ++ "license": "BSD-3-Clause" ++ }, ++ "node_modules/d3-color": { ++ "version": "1.4.1", ++ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", ++ "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==", ++ "license": "BSD-3-Clause" ++ }, ++ "node_modules/d3-format": { ++ "version": "1.4.5", ++ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", ++ "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", ++ "license": "BSD-3-Clause" ++ }, ++ "node_modules/d3-interpolate": { ++ "version": "1.4.0", ++ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", ++ "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", ++ "license": "BSD-3-Clause", ++ "dependencies": { ++ "d3-color": "1" ++ } ++ }, ++ "node_modules/d3-path": { ++ "version": "3.1.0", ++ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", ++ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", ++ "license": "ISC", ++ "engines": { ++ "node": ">=12" ++ } ++ }, ++ "node_modules/d3-scale": { ++ "version": "2.2.2", ++ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", ++ "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", ++ "license": "BSD-3-Clause", ++ "dependencies": { ++ "d3-array": "^1.2.0", ++ "d3-collection": "1", ++ "d3-format": "1", ++ "d3-interpolate": "1", ++ "d3-time": "1", ++ "d3-time-format": "2" ++ } ++ }, ++ "node_modules/d3-scale/node_modules/d3-array": { ++ "version": "1.2.4", ++ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", ++ "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", ++ "license": "BSD-3-Clause" ++ }, ++ "node_modules/d3-shape": { ++ "version": "3.2.0", ++ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", ++ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", ++ "license": "ISC", ++ "dependencies": { ++ "d3-path": "^3.1.0" ++ }, ++ "engines": { ++ "node": ">=12" ++ } ++ }, ++ "node_modules/d3-time": { ++ "version": "1.1.0", ++ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", ++ "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", ++ "license": "BSD-3-Clause" ++ }, ++ "node_modules/d3-time-format": { ++ "version": "2.3.0", ++ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", ++ "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", ++ "license": "BSD-3-Clause", ++ "dependencies": { ++ "d3-time": "1" ++ } ++ }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", +@@ -8629,6 +8753,15 @@ + "react-native": "*" + } + }, ++ "node_modules/expo-haptics": { ++ "version": "15.0.8", ++ "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-15.0.8.tgz", ++ "integrity": "sha512-lftutojy8Qs8zaDzzjwM3gKHFZ8bOOEZDCkmh2Ddpe95Ra6kt2izeOfOfKuP/QEh0MZ1j9TfqippyHdRd1ZM9g==", ++ "license": "MIT", ++ "peerDependencies": { ++ "expo": "*" ++ } ++ }, + "node_modules/expo-image-loader": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-4.7.0.tgz", +@@ -9940,6 +10073,21 @@ + "hermes-estree": "0.29.1" + } + }, ++ "node_modules/hoist-non-react-statics": { ++ "version": "3.3.2", ++ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", ++ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", ++ "license": "BSD-3-Clause", ++ "dependencies": { ++ "react-is": "^16.7.0" ++ } ++ }, ++ "node_modules/hoist-non-react-statics/node_modules/react-is": { ++ "version": "16.13.1", ++ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", ++ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", ++ "license": "MIT" ++ }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", +@@ -10373,6 +10521,15 @@ + "node": ">= 0.4" + } + }, ++ "node_modules/internmap": { ++ "version": "2.0.3", ++ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", ++ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", ++ "license": "ISC", ++ "engines": { ++ "node": ">=12" ++ } ++ }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", +@@ -12784,6 +12941,15 @@ + "node": ">=0.10.0" + } + }, ++ "node_modules/normalize-svg-path": { ++ "version": "1.1.0", ++ "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", ++ "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", ++ "license": "MIT", ++ "dependencies": { ++ "svg-arc-to-cubic-bezier": "^3.0.0" ++ } ++ }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", +@@ -13258,6 +13424,12 @@ + "node": ">=10" + } + }, ++ "node_modules/parse-svg-path": { ++ "version": "0.1.2", ++ "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", ++ "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", ++ "license": "MIT" ++ }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", +@@ -14457,6 +14629,24 @@ + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT" + }, ++ "node_modules/react-keyed-flatten-children": { ++ "version": "1.3.0", ++ "resolved": "https://registry.npmjs.org/react-keyed-flatten-children/-/react-keyed-flatten-children-1.3.0.tgz", ++ "integrity": "sha512-qB7A6n+NHU0x88qTZGAJw6dsqwI941jcRPBB640c/CyWqjPQQ+YUmXOuzPziuHb7iqplM3xksWAbGYwkQT0tXA==", ++ "license": "MIT", ++ "dependencies": { ++ "react-is": "^16.8.6" ++ }, ++ "peerDependencies": { ++ "react": ">=15.0.0" ++ } ++ }, ++ "node_modules/react-keyed-flatten-children/node_modules/react-is": { ++ "version": "16.13.1", ++ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", ++ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", ++ "license": "MIT" ++ }, + "node_modules/react-native": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", +@@ -14514,6 +14704,33 @@ + } + } + }, ++ "node_modules/react-native-gesture-handler": { ++ "version": "2.30.0", ++ "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.30.0.tgz", ++ "integrity": "sha512-5YsnKHGa0X9C8lb5oCnKm0fLUPM6CRduvUUw2Bav4RIj/C3HcFh4RIUnF8wgG6JQWCL1//gRx4v+LVWgcIQdGA==", ++ "license": "MIT", ++ "dependencies": { ++ "@egjs/hammerjs": "^2.0.17", ++ "hoist-non-react-statics": "^3.3.0", ++ "invariant": "^2.2.4" ++ }, ++ "peerDependencies": { ++ "react": "*", ++ "react-native": "*" ++ } ++ }, ++ "node_modules/react-native-haptic-feedback": { ++ "version": "2.3.4", ++ "resolved": "https://registry.npmjs.org/react-native-haptic-feedback/-/react-native-haptic-feedback-2.3.4.tgz", ++ "integrity": "sha512-3nHECsi+pL1c2TKNpOD5aPtAfFzrkjpABjioi6OUBb6OVeuUObQlx9idoXSbj1h/L7TCaC13LqKT5ILl0xxHjQ==", ++ "license": "MIT", ++ "workspaces": [ ++ "example" ++ ], ++ "peerDependencies": { ++ "react-native": ">=0.60.0" ++ } ++ }, + "node_modules/react-native-is-edge-to-edge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz", +@@ -14552,6 +14769,23 @@ + "node": ">=10" + } + }, ++ "node_modules/react-native-redash": { ++ "version": "18.1.5", ++ "resolved": "https://registry.npmjs.org/react-native-redash/-/react-native-redash-18.1.5.tgz", ++ "integrity": "sha512-0Dy04RcN8tqHv9p3osr3WtsPxWwiXUwt+soFUYsKQEbn+LodSnB8LHnmd0eWemCYXzBPJRNt7YgPA9XTBYoGWQ==", ++ "license": "MIT", ++ "dependencies": { ++ "abs-svg-path": "^0.1.1", ++ "normalize-svg-path": "^1.0.1", ++ "parse-svg-path": "^0.1.2" ++ }, ++ "peerDependencies": { ++ "react": "*", ++ "react-native": "*", ++ "react-native-gesture-handler": "*", ++ "react-native-reanimated": ">=2.0.0" ++ } ++ }, + "node_modules/react-native-safe-area-context": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", +@@ -14577,6 +14811,119 @@ + "react-native": "*" + } + }, ++ "node_modules/react-native-svg": { ++ "version": "15.12.1", ++ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz", ++ "integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==", ++ "license": "MIT", ++ "dependencies": { ++ "css-select": "^5.1.0", ++ "css-tree": "^1.1.3", ++ "warn-once": "0.1.1" ++ }, ++ "peerDependencies": { ++ "react": "*", ++ "react-native": "*" ++ } ++ }, ++ "node_modules/react-native-svg/node_modules/css-select": { ++ "version": "5.2.2", ++ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", ++ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", ++ "license": "BSD-2-Clause", ++ "dependencies": { ++ "boolbase": "^1.0.0", ++ "css-what": "^6.1.0", ++ "domhandler": "^5.0.2", ++ "domutils": "^3.0.1", ++ "nth-check": "^2.0.1" ++ }, ++ "funding": { ++ "url": "https://github.com/sponsors/fb55" ++ } ++ }, ++ "node_modules/react-native-svg/node_modules/dom-serializer": { ++ "version": "2.0.0", ++ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", ++ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", ++ "license": "MIT", ++ "dependencies": { ++ "domelementtype": "^2.3.0", ++ "domhandler": "^5.0.2", ++ "entities": "^4.2.0" ++ }, ++ "funding": { ++ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" ++ } ++ }, ++ "node_modules/react-native-svg/node_modules/domhandler": { ++ "version": "5.0.3", ++ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", ++ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", ++ "license": "BSD-2-Clause", ++ "dependencies": { ++ "domelementtype": "^2.3.0" ++ }, ++ "engines": { ++ "node": ">= 4" ++ }, ++ "funding": { ++ "url": "https://github.com/fb55/domhandler?sponsor=1" ++ } ++ }, ++ "node_modules/react-native-svg/node_modules/domutils": { ++ "version": "3.2.2", ++ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", ++ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", ++ "license": "BSD-2-Clause", ++ "dependencies": { ++ "dom-serializer": "^2.0.0", ++ "domelementtype": "^2.3.0", ++ "domhandler": "^5.0.3" ++ }, ++ "funding": { ++ "url": "https://github.com/fb55/domutils?sponsor=1" ++ } ++ }, ++ "node_modules/react-native-svg/node_modules/entities": { ++ "version": "4.5.0", ++ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", ++ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", ++ "license": "BSD-2-Clause", ++ "engines": { ++ "node": ">=0.12" ++ }, ++ "funding": { ++ "url": "https://github.com/fb55/entities?sponsor=1" ++ } ++ }, ++ "node_modules/react-native-wagmi-charts": { ++ "version": "2.9.1", ++ "resolved": "https://registry.npmjs.org/react-native-wagmi-charts/-/react-native-wagmi-charts-2.9.1.tgz", ++ "integrity": "sha512-1VFO4qGK8JY0o7dxcKSP5anVme1V+AfvV+oqN5vY6ah4bR2zGVvDwHwXboez+zGgz7UI5F3VgXbCyBrSiXDorw==", ++ "workspaces": [ ++ "example" ++ ], ++ "dependencies": { ++ "d3-array": "^3.1.6", ++ "d3-scale": "^2", ++ "d3-shape": "^3.0.1", ++ "fbjs": "^3.0.5", ++ "react-keyed-flatten-children": "^1.3.0", ++ "react-native-redash": "^18.1.3" ++ }, ++ "engines": { ++ "node": ">= 18.0.0" ++ }, ++ "peerDependencies": { ++ "react": "*", ++ "react-native": "*", ++ "react-native-gesture-handler": "*", ++ "react-native-reanimated": "*", ++ "react-native-redash": "*", ++ "react-native-svg": "*" ++ } ++ }, + "node_modules/react-native-web": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", +@@ -16216,6 +16563,12 @@ + "url": "https://github.com/sponsors/ljharb" + } + }, ++ "node_modules/svg-arc-to-cubic-bezier": { ++ "version": "3.2.0", ++ "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", ++ "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", ++ "license": "ISC" ++ }, + "node_modules/svgo": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.2.tgz", +diff --git a/apps/mobile/package.json b/apps/mobile/package.json +index 317b835..60f8fa3 100644 +--- a/apps/mobile/package.json ++++ b/apps/mobile/package.json +@@ -20,6 +20,7 @@ + "expo-barcode-scanner": "^13.0.1", + "expo-constants": "~18.0.13", + "expo-dev-client": "^6.0.20", ++ "expo-haptics": "~15.0.8", + "expo-linking": "~8.0.11", + "expo-router": "~6.0.22", + "expo-secure-store": "^15.0.8", +@@ -27,9 +28,13 @@ + "react": "19.1.0", + "react-dom": "19.1.0", + "react-native": "0.81.5", ++ "react-native-gesture-handler": "^2.30.0", ++ "react-native-haptic-feedback": "^2.3.4", + "react-native-reanimated": "~4.1.1", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0", ++ "react-native-svg": "^15.12.1", ++ "react-native-wagmi-charts": "^2.9.1", + "react-native-web": "^0.21.2", + "react-native-worklets": "0.5.1", + "react-native-worklets-core": "^1.6.2" +diff --git a/docker-compose.yml b/docker-compose.yml +index 214d6f7..556f5fb 100644 +--- a/docker-compose.yml ++++ b/docker-compose.yml +@@ -4,7 +4,7 @@ services: + container_name: lumenpulse-postgres + restart: unless-stopped + ports: +- - "5433:5432" ++ - "5434:5432" + environment: + POSTGRES_USER: lumenpulse + POSTGRES_PASSWORD: lumenpulse