From 6501df309e1e2f9ae49c7038fa7eb1b57dda0959 Mon Sep 17 00:00:00 2001 From: Francis6-git Date: Wed, 17 Jun 2026 13:24:21 +0100 Subject: [PATCH 1/7] feat: implement app-wide error tracking with Sentry (#20) --- .env.example | 7 + .gitignore | 4 + app.json | 10 +- app/_layout.tsx | 73 ++++-- components/SentryErrorBoundary.tsx | 76 ++++++ package-lock.json | 379 +++++++++++++++++++++++++++++ package.json | 1 + services/api.ts | 17 ++ services/auth.service.ts | 34 ++- services/loans.service.ts | 61 ++++- services/reputation.service.ts | 14 +- services/sentry.ts | 162 ++++++++++++ 12 files changed, 799 insertions(+), 39 deletions(-) create mode 100644 .env.example create mode 100644 components/SentryErrorBoundary.tsx create mode 100644 services/sentry.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..481a886 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Sentry Configuration +# EXPO_PUBLIC_SENTRY_DSN= + +# Sentry Source Map Upload (set these as EAS Secrets for production builds) +# SENTRY_AUTH_TOKEN= +# SENTRY_ORG=stepfi-org +# SENTRY_PROJECT=stepfi-app \ No newline at end of file diff --git a/.gitignore b/.gitignore index a23cacd..3fe909d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Environment secrets +.env +.env.local + node_modules/ .expo/ dist/ diff --git a/app.json b/app.json index 26c77e8..154bee3 100644 --- a/app.json +++ b/app.json @@ -17,7 +17,15 @@ "expo-router", "expo-secure-store", "expo-font", - "expo-local-authentication" + "expo-local-authentication", + [ + "@sentry/react-native", + { + "organization": "stepfi-org", + "project": "stepfi-app", + "url": "https://sentry.io/" + } + ] ], "orientation": "portrait", "icon": "./assets/icon.png", diff --git a/app/_layout.tsx b/app/_layout.tsx index 1fbbef3..6fd98eb 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -6,8 +6,19 @@ import { useAuthStore } from '../stores/auth.store'; import { useSecurityStore } from '../src/security/security.store'; import { biometricService } from '../src/security/biometric.service'; import { BiometricGate } from '../src/components/BiometricGate'; +import { SentryErrorBoundary } from '../components/SentryErrorBoundary'; +import { + initSentry, + setSentryUser, + clearSentryUser, + addBreadcrumb, + Sentry, +} from '../services/sentry'; import '../global.css'; +// Initialise Sentry as early as possible (module‑level, before any component) +initSentry(); + const IDLE_TIMEOUT_MS = 5 * 60 * 1000; function useAuthGuard() { @@ -29,7 +40,22 @@ function useAuthGuard() { }, [isAuthenticated, isLoading, segments, router]); } -export default function RootLayout() { +// Sentry user‑context sync — attach / clear wallet identity on auth changes +function useSentryUserContext() { + const walletAddress = useAuthStore((s) => s.walletAddress); + const isAuthenticated = useAuthStore((s) => s.isAuthenticated); + + useEffect(() => { + if (isAuthenticated && walletAddress) { + setSentryUser(walletAddress); + addBreadcrumb('auth', 'User context set', { hasWallet: true }); + } else { + clearSentryUser(); + } + }, [isAuthenticated, walletAddress]); +} + +function RootLayout() { const hydrate = useAuthStore((s) => s.hydrate); const isAuthenticated = useAuthStore((s) => s.isAuthenticated); const isLoading = useAuthStore((s) => s.isLoading); @@ -60,6 +86,7 @@ export default function RootLayout() { }, [hydrate]); useAuthGuard(); + useSentryUserContext(); useEffect(() => { if (!isLoading && isAuthenticated && !biometricCheckDone) { @@ -78,6 +105,8 @@ export default function RootLayout() { useEffect(() => { const subscription = AppState.addEventListener('change', (state) => { + addBreadcrumb('app.lifecycle', `AppState → ${state}`); + if (state === 'active') { const elapsed = Date.now() - lastActiveRef.current; if ( @@ -109,24 +138,28 @@ export default function RootLayout() { } return ( - - {isLocked && isAuthenticated ? ( - - ) : ( - { - if (!useSecurityStore.getState().isLocked) { - startIdleTimer(); - } - }} - > - - - - - - )} - + + + {isLocked && isAuthenticated ? ( + + ) : ( + { + if (!useSecurityStore.getState().isLocked) { + startIdleTimer(); + } + }} + > + + + + + + )} + + ); } + +export default Sentry.wrap(RootLayout); diff --git a/components/SentryErrorBoundary.tsx b/components/SentryErrorBoundary.tsx new file mode 100644 index 0000000..9239001 --- /dev/null +++ b/components/SentryErrorBoundary.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { View, Text, Pressable, StyleSheet } from 'react-native'; +import * as Sentry from '@sentry/react-native'; + +interface FallbackProps { + resetError: () => void; +} + +function ErrorFallback({ resetError }: FallbackProps) { + return ( + + ⚠️ + Something went wrong + + An unexpected error occurred. Our team has been notified. + + + Reload App + + + ); +} + +interface Props { + children: React.ReactNode; +} + +export function SentryErrorBoundary({ children }: Props) { + return ( + } + showDialog={false} + > + {children} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#080F1A', + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 32, + }, + emoji: { + fontSize: 48, + marginBottom: 16, + }, + title: { + fontSize: 22, + fontWeight: '700', + color: '#FFFFFF', + marginBottom: 8, + textAlign: 'center', + }, + subtitle: { + fontSize: 15, + color: '#94A3B8', + textAlign: 'center', + lineHeight: 22, + marginBottom: 32, + }, + button: { + backgroundColor: '#6366F1', + paddingHorizontal: 32, + paddingVertical: 14, + borderRadius: 12, + }, + buttonText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '600', + }, +}); diff --git a/package-lock.json b/package-lock.json index d6dee36..fc2a6a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@expo/vector-icons": "^15.0.3", "@react-navigation/native": "^7.1.8", + "@sentry/react-native": "~7.2.0", "axios": "^1.16.0", "expo": "^54.0.0", "expo-blur": "~15.0.8", @@ -103,6 +104,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -3372,6 +3374,7 @@ "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.28.tgz", "integrity": "sha512-d1QDn+KNHfHGt3UIwOZvupvdsDdiHYZBEj7+wL2yDVo3tMezamYy60H9s3EnNVE1Ae1ty0trc7F2OKqo/RmsdQ==", "license": "MIT", + "peer": true, "dependencies": { "@react-navigation/core": "^7.14.0", "escape-string-regexp": "^4.0.0", @@ -3419,6 +3422,342 @@ "dev": true, "license": "MIT" }, + "node_modules/@sentry-internal/browser-utils": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.12.0.tgz", + "integrity": "sha512-dozbx389jhKynj0d657FsgbBVOar7pX3mb6GjqCxslXF0VKpZH2Xks0U32RgDY/nK27O+o095IWz7YvjVmPkDw==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.12.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.12.0.tgz", + "integrity": "sha512-0+7ceO6yQPPqfxRc9ue/xoPHKcnB917ezPaehGQNfAFNQB9PNTG1y55+8mRu0Fw+ANbZeCt/HyoCmXuRdxmkpg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.12.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.12.0.tgz", + "integrity": "sha512-/1093gSNGN5KlOBsuyAl33JkzGiG38kCnxswQLZWpPpR6LBbR1Ddb18HjhDpoQNNEZybJBgJC3a5NKl43C2TSQ==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.12.0", + "@sentry/core": "10.12.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.12.0.tgz", + "integrity": "sha512-W/z1/+69i3INNfPjD1KuinSNaRQaApjzwb37IFmiyF440F93hxmEYgXHk3poOlYYaigl2JMYbysGPWOiXnqUXA==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "10.12.0", + "@sentry/core": "10.12.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/babel-plugin-component-annotate": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.3.0.tgz", + "integrity": "sha512-OuxqBprXRyhe8Pkfyz/4yHQJc5c3lm+TmYWSSx8u48g5yKewSQDOxkiLU5pAk3WnbLPy8XwU/PN+2BG0YFU9Nw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@sentry/browser": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.12.0.tgz", + "integrity": "sha512-lKyaB2NFmr7SxPjmMTLLhQ7xfxaY3kdkMhpzuRI5qwOngtKt4+FtvNYHRuz+PTtEFv4OaHhNNbRn6r91gWguQg==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.12.0", + "@sentry-internal/feedback": "10.12.0", + "@sentry-internal/replay": "10.12.0", + "@sentry-internal/replay-canvas": "10.12.0", + "@sentry/core": "10.12.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/cli": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.55.0.tgz", + "integrity": "sha512-cynvcIM2xL8ddwELyFRSpZQw4UtFZzoM2rId2l9vg7+wDREPDocMJB9lEQpBIo3eqhp9JswqUT037yjO6iJ5Sw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.7", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "which": "^2.0.2" + }, + "bin": { + "sentry-cli": "bin/sentry-cli" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.55.0", + "@sentry/cli-linux-arm": "2.55.0", + "@sentry/cli-linux-arm64": "2.55.0", + "@sentry/cli-linux-i686": "2.55.0", + "@sentry/cli-linux-x64": "2.55.0", + "@sentry/cli-win32-arm64": "2.55.0", + "@sentry/cli-win32-i686": "2.55.0", + "@sentry/cli-win32-x64": "2.55.0" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.55.0.tgz", + "integrity": "sha512-jGHE7SHHzqXUmnsmRLgorVH6nmMmTjQQXdPZbSL5tRtH8d3OIYrVNr5D72DSgD26XAPBDMV0ibqOQ9NKoiSpfA==", + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.55.0.tgz", + "integrity": "sha512-ATjU0PsiWADSPLF/kZroLZ7FPKd5W9TDWHVkKNwIUNTei702LFgTjNeRwOIzTgSvG3yTmVEqtwFQfFN/7hnVXQ==", + "cpu": [ + "arm" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.55.0.tgz", + "integrity": "sha512-jNB/0/gFcOuDCaY/TqeuEpsy/k52dwyk1SOV3s1ku4DUsln6govTppeAGRewY3T1Rj9B2vgIWTrnB8KVh9+Rgg==", + "cpu": [ + "arm64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.55.0.tgz", + "integrity": "sha512-8LZjo6PncTM6bWdaggscNOi5r7F/fqRREsCwvd51dcjGj7Kp1plqo9feEzYQ+jq+KUzVCiWfHrUjddFmYyZJrg==", + "cpu": [ + "x86", + "ia32" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-x64": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.55.0.tgz", + "integrity": "sha512-5LUVvq74Yj2cZZy5g5o/54dcWEaX4rf3myTHy73AKhRj1PABtOkfexOLbF9xSrZy95WXWaXyeH+k5n5z/vtHfA==", + "cpu": [ + "x64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-arm64": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.55.0.tgz", + "integrity": "sha512-cWIQdzm1pfLwPARsV6dUb8TVd6Y3V1A2VWxjTons3Ift6GvtVmiAe0OWL8t2Yt95i8v61kTD/6Tq21OAaogqzA==", + "cpu": [ + "arm64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.55.0.tgz", + "integrity": "sha512-ldepCn2t9r4I0wvgk7NRaA7coJyy4rTQAzM66u9j5nTEsUldf66xym6esd5ZZRAaJUjffqvHqUIr/lrieTIrVg==", + "cpu": [ + "x86", + "ia32" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.55.0.tgz", + "integrity": "sha512-4hPc/I/9tXx+HLTdTGwlagtAfDSIa2AoTUP30tl32NAYQhx9a6niUbPAemK2qfxesiufJ7D2djX83rCw6WnJVA==", + "cpu": [ + "x64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@sentry/cli/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@sentry/cli/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/@sentry/core": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.12.0.tgz", + "integrity": "sha512-Jrf0Yo7DvmI/ZQcvBnA0xKNAFkJlVC/fMlvcin+5IrFNRcqOToZ2vtF+XqTgjRZymXQNE8s1QTD7IomPHk0TAw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/react": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.12.0.tgz", + "integrity": "sha512-TpqgdoYbkf5JynmmW2oQhHQ/h5w+XPYk0cEb/UrsGlvJvnBSR+5tgh0AqxCSi3gvtp82rAXI5w1TyRPBbhLDBw==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "10.12.0", + "@sentry/core": "10.12.0", + "hoist-non-react-statics": "^3.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@sentry/react-native": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sentry/react-native/-/react-native-7.2.0.tgz", + "integrity": "sha512-rjqYgEjntPz1sPysud78wi4B9ui7LBVPsG6qr8s/htLMYho9GPGFA5dF+eqsQWqMX8NDReAxNkLTC4+gCNklLQ==", + "license": "MIT", + "dependencies": { + "@sentry/babel-plugin-component-annotate": "4.3.0", + "@sentry/browser": "10.12.0", + "@sentry/cli": "2.55.0", + "@sentry/core": "10.12.0", + "@sentry/react": "10.12.0", + "@sentry/types": "10.12.0" + }, + "bin": { + "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" + }, + "peerDependencies": { + "expo": ">=49.0.0", + "react": ">=17.0.0", + "react-native": ">=0.65.0" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@sentry/types": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-10.12.0.tgz", + "integrity": "sha512-sKGj3l3V8ZKISh2Tu88bHfnm5ztkRtSLdmpZ6TmCeJdSM9pV+RRd6CMJ0RnSEXmYHselPNUod521t2NQFd4W1w==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.12.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3564,6 +3903,7 @@ "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3634,6 +3974,7 @@ "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", @@ -4195,6 +4536,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4936,6 +5278,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6134,6 +6477,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6346,6 +6690,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6578,6 +6923,7 @@ "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.34.tgz", "integrity": "sha512-XkVHguZZDC8BcTQxHAd14/TQFbDp1Wt0Z/KApO9t68Ll5A127hLCPzU+a9gytfCIiyL/V1IpF1vIcOLKEVAoNQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.24", @@ -6656,6 +7002,7 @@ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz", "integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==", "license": "MIT", + "peer": true, "dependencies": { "@expo/config": "~12.0.13", "@expo/env": "~2.0.8" @@ -6692,6 +7039,7 @@ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz", "integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==", "license": "MIT", + "peer": true, "dependencies": { "fontfaceobserver": "^2.1.0" }, @@ -6727,6 +7075,7 @@ "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.12.tgz", "integrity": "sha512-FpXeIpFgZuxihwT9lBo86YD3y6LphBuAhN680MMxm/Y7fmsc57vimn2d3vFu68VI0+Z9w457t494mu2wvlgWTQ==", "license": "MIT", + "peer": true, "dependencies": { "expo-constants": "~18.0.13", "invariant": "^2.2.4" @@ -7954,6 +8303,21 @@ "hermes-estree": "0.32.0" } }, + "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", @@ -8861,6 +9225,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -10622,6 +10987,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10775,6 +11141,7 @@ "integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11072,6 +11439,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11091,6 +11459,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -11127,6 +11496,7 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", "license": "MIT", + "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", @@ -11259,6 +11629,7 @@ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz", "integrity": "sha512-F+ZJBYiok/6Jzp1re75F/9aLzkgoQCOh4yxrnwATa8392RvM3kx+fiXXFvwcgE59v48lMwd9q0nzF1oJLXpfxQ==", "license": "MIT", + "peer": true, "dependencies": { "react-native-is-edge-to-edge": "^1.2.1", "semver": "7.7.2" @@ -11287,6 +11658,7 @@ "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -11297,6 +11669,7 @@ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", "license": "MIT", + "peer": true, "dependencies": { "react-freeze": "^1.0.0", "react-native-is-edge-to-edge": "^1.2.1", @@ -11312,6 +11685,7 @@ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz", "integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==", "license": "MIT", + "peer": true, "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", @@ -11469,6 +11843,7 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12656,6 +13031,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -12853,6 +13229,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13059,6 +13436,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13308,6 +13686,7 @@ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } diff --git a/package.json b/package.json index 3f28d91..e825541 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@expo/vector-icons": "^15.0.3", "@react-navigation/native": "^7.1.8", + "@sentry/react-native": "~7.2.0", "axios": "^1.16.0", "expo": "^54.0.0", "expo-blur": "~15.0.8", diff --git a/services/api.ts b/services/api.ts index 039d64a..109cd05 100644 --- a/services/api.ts +++ b/services/api.ts @@ -2,6 +2,7 @@ import axios, { AxiosError, AxiosRequestConfig } from 'axios'; import { router } from 'expo-router'; import { config } from '../constants/config'; import { useAuthStore } from '../stores/auth.store'; +import { addBreadcrumb, captureServiceError } from './sentry'; const api = axios.create({ baseURL: config.API_BASE_URL, @@ -14,6 +15,13 @@ api.interceptors.request.use((req) => { req.headers = req.headers ?? {}; (req.headers as Record).Authorization = `Bearer ${accessToken}`; } + + // Breadcrumb for every outgoing request (URL only, no auth headers) + addBreadcrumb('http.request', `${req.method?.toUpperCase()} ${req.url}`, { + baseURL: req.baseURL ?? '', + timeout: req.timeout ?? 0, + }); + return req; }); @@ -50,6 +58,15 @@ api.interceptors.response.use( const original = error.config as RetriableRequest | undefined; const status = error.response?.status; + // Capture non-401 errors to Sentry (401s are expected during token refresh) + if (status !== 401) { + captureServiceError('api', 'response', error); + addBreadcrumb('http.error', `HTTP ${status ?? 'network'} error`, { + url: original?.url ?? 'unknown', + status: status ?? 0, + }, 'error'); + } + if (status !== 401 || !original || original._retry) { return Promise.reject(error); } diff --git a/services/auth.service.ts b/services/auth.service.ts index 317a6ac..c249e5b 100644 --- a/services/auth.service.ts +++ b/services/auth.service.ts @@ -1,4 +1,5 @@ import api from './api'; +import { addBreadcrumb, captureServiceError } from './sentry'; export interface NonceResponse { nonce: string; @@ -13,17 +14,38 @@ export interface AuthTokens { export const authService = { async getNonce(wallet: string): Promise { - const res = await api.post('/auth/nonce', { wallet }); - return res.data; + addBreadcrumb('auth.service', 'Requesting nonce'); + try { + const res = await api.post('/auth/nonce', { wallet }); + addBreadcrumb('auth.service', 'Nonce received'); + return res.data; + } catch (error) { + captureServiceError('auth', 'getNonce', error); + throw error; + } }, async verify(wallet: string, nonce: string, signature: string): Promise { - const res = await api.post('/auth/verify', { wallet, nonce, signature }); - return res.data; + addBreadcrumb('auth.service', 'Verifying wallet signature'); + try { + const res = await api.post('/auth/verify', { wallet, nonce, signature }); + addBreadcrumb('auth.service', 'Wallet verified successfully'); + return res.data; + } catch (error) { + captureServiceError('auth', 'verify', error); + throw error; + } }, async refresh(refreshToken: string): Promise { - const res = await api.post('/auth/refresh', { refreshToken }); - return res.data; + addBreadcrumb('auth.service', 'Refreshing auth tokens'); + try { + const res = await api.post('/auth/refresh', { refreshToken }); + addBreadcrumb('auth.service', 'Tokens refreshed'); + return res.data; + } catch (error) { + captureServiceError('auth', 'refresh', error); + throw error; + } }, }; diff --git a/services/loans.service.ts b/services/loans.service.ts index fe54229..ca73d3e 100644 --- a/services/loans.service.ts +++ b/services/loans.service.ts @@ -1,4 +1,5 @@ import api from './api'; +import { addBreadcrumb, captureServiceError } from './sentry'; import type { Installment, Loan } from '../types/loan.types'; export interface AvailableCredit { @@ -20,23 +21,52 @@ export interface UnsignedXdrResponse { export const loansService = { async getMyLoans(): Promise { - const res = await api.get<{ data: Loan[] }>('/loans/my-loans'); - return res.data.data; + addBreadcrumb('loans.service', 'Fetching user loans'); + try { + const res = await api.get<{ data: Loan[] }>('/loans/my-loans'); + addBreadcrumb('loans.service', 'Loans fetched', { count: res.data.data.length }); + return res.data.data; + } catch (error) { + captureServiceError('loans', 'getMyLoans', error); + throw error; + } }, async getLoanById(id: string): Promise { - const res = await api.get(`/loans/${id}`); - return res.data; + addBreadcrumb('loans.service', 'Fetching loan by ID', { loanId: id }); + try { + const res = await api.get(`/loans/${id}`); + return res.data; + } catch (error) { + captureServiceError('loans', 'getLoanById', error); + throw error; + } }, async getAvailableCredit(): Promise { - const res = await api.get('/loans/available-credit'); - return res.data; + addBreadcrumb('loans.service', 'Fetching available credit'); + try { + const res = await api.get('/loans/available-credit'); + return res.data; + } catch (error) { + captureServiceError('loans', 'getAvailableCredit', error); + throw error; + } }, async createLoan(dto: CreateLoanDto): Promise { - const res = await api.post('/loans/create', dto); - return res.data; + addBreadcrumb('loans.service', 'Creating loan', { + vendorId: dto.vendorId, + totalAmount: dto.totalAmount, + }); + try { + const res = await api.post('/loans/create', dto); + addBreadcrumb('loans.service', 'Loan created'); + return res.data; + } catch (error) { + captureServiceError('loans', 'createLoan', error); + throw error; + } }, async repayInstallment( @@ -44,10 +74,21 @@ export const loansService = { installmentIndex: number, amount: number, ): Promise { - const res = await api.post(`/loans/${loanId}/repay-installment`, { + addBreadcrumb('loans.service', 'Repaying installment', { + loanId, installmentIndex, amount, }); - return res.data; + try { + const res = await api.post(`/loans/${loanId}/repay-installment`, { + installmentIndex, + amount, + }); + addBreadcrumb('loans.service', 'Installment repaid'); + return res.data; + } catch (error) { + captureServiceError('loans', 'repayInstallment', error); + throw error; + } }, }; diff --git a/services/reputation.service.ts b/services/reputation.service.ts index c8b8f46..9bb8acf 100644 --- a/services/reputation.service.ts +++ b/services/reputation.service.ts @@ -1,4 +1,5 @@ import api from './api'; +import { addBreadcrumb, captureServiceError } from './sentry'; export interface ReputationScore { score: number; @@ -9,7 +10,16 @@ export interface ReputationScore { export const reputationService = { async getScore(wallet: string): Promise { - const res = await api.get(`/reputation/${wallet}`); - return res.data; + addBreadcrumb('reputation.service', 'Fetching reputation score'); + try { + const res = await api.get(`/reputation/${wallet}`); + addBreadcrumb('reputation.service', 'Reputation score received', { + tier: res.data.tier, + }); + return res.data; + } catch (error) { + captureServiceError('reputation', 'getScore', error); + throw error; + } }, }; diff --git a/services/sentry.ts b/services/sentry.ts new file mode 100644 index 0000000..b5db767 --- /dev/null +++ b/services/sentry.ts @@ -0,0 +1,162 @@ +import * as Sentry from '@sentry/react-native'; +import Constants from 'expo-constants'; + +// Sensitive field names that must never be sent to Sentry +const SENSITIVE_KEYS = new Set([ + 'accessToken', + 'refreshToken', + 'signature', + 'nonce', + 'walletAddress', + 'publicKey', + 'authorization', + 'Authorization', +]); + +// Helpers + +/** + * Recursively strip sensitive keys from a plain object. + * Returns a shallow‑cloned object — the original is never mutated. + */ +function scrub(obj: T): T { + if (obj === null || obj === undefined || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map(scrub) as unknown as T; + } + + const cleaned: Record = {}; + for (const [key, value] of Object.entries(obj as Record)) { + if (SENSITIVE_KEYS.has(key)) { + cleaned[key] = '[REDACTED]'; + } else { + cleaned[key] = scrub(value); + } + } + return cleaned as T; +} + +// Initialisation + +let _initialised = false; + +export function initSentry(): void { + if (_initialised) return; + + const dsn = process.env.EXPO_PUBLIC_SENTRY_DSN; + if (!dsn) { + if (__DEV__) { + // eslint-disable-next-line no-console + console.warn('[Sentry] EXPO_PUBLIC_SENTRY_DSN is not set — skipping init'); + } + return; + } + + const release = Constants.expoConfig?.version + ? `${Constants.expoConfig.slug ?? 'stepfi-app'}@${Constants.expoConfig.version}` + : undefined; + + Sentry.init({ + dsn, + release, + environment: __DEV__ ? 'development' : 'production', + + // Sessions + enableAutoSessionTracking: true, + + // Performance + tracesSampleRate: __DEV__ ? 1.0 : 0.2, + + // Data scrubbing — strip tokens / secrets before they leave the device + beforeSend(event) { + // Scrub extra data + if (event.extra) { + event.extra = scrub(event.extra); + } + + // Scrub breadcrumb data + if (event.breadcrumbs) { + event.breadcrumbs = event.breadcrumbs.map((bc) => ({ + ...bc, + data: bc.data ? scrub(bc.data) : bc.data, + })); + } + + // Scrub context values + if (event.contexts) { + event.contexts = scrub(event.contexts); + } + + return event; + }, + + // Only send events in production unless DSN is explicitly provided in dev + enabled: !__DEV__ || Boolean(dsn), + }); + + _initialised = true; +} + +// User context + +/** + * Set the Sentry user context when a wallet connects. + * We only set the wallet address as `id` — no PII like email. + */ +export function setSentryUser(walletAddress: string): void { + Sentry.setUser({ id: walletAddress }); +} + +/** Clear user context on logout / wallet disconnect. */ +export function clearSentryUser(): void { + Sentry.setUser(null); +} + +// Breadcrumbs + +export function addBreadcrumb( + category: string, + message: string, + data?: Record, + level: Sentry.SeverityLevel = 'info', +): void { + Sentry.addBreadcrumb({ + category, + message, + data: data ? scrub(data) : undefined, + level, + }); +} + +// Error capture + +/** + * Capture a service‑layer error with useful tags so errors can be filtered + * by service name and operation in the Sentry dashboard. + */ +export function captureServiceError( + service: string, + operation: string, + error: unknown, +): void { + Sentry.withScope((scope) => { + scope.setTag('service', service); + scope.setTag('operation', operation); + + if (error instanceof Error) { + scope.setExtra('errorMessage', error.message); + Sentry.captureException(error); + } else { + Sentry.captureMessage(`[${service}.${operation}] Non-Error thrown`, { + level: 'error', + extra: { rawError: scrub(error as Record) }, + }); + } + }); +} + +// Re-export Sentry's wrap helper for the root component +export { Sentry }; From 12e5b4ce048c202fff9cf5638598a0a8b0f25ecb Mon Sep 17 00:00:00 2001 From: Francis6-git Date: Wed, 17 Jun 2026 13:34:31 +0100 Subject: [PATCH 2/7] no changes --- services/sentry.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/services/sentry.ts b/services/sentry.ts index b5db767..a7a1907 100644 --- a/services/sentry.ts +++ b/services/sentry.ts @@ -104,7 +104,6 @@ export function initSentry(): void { /** * Set the Sentry user context when a wallet connects. - * We only set the wallet address as `id` — no PII like email. */ export function setSentryUser(walletAddress: string): void { Sentry.setUser({ id: walletAddress }); From 612e8e5017be30ced5a11c806633f582c123130d Mon Sep 17 00:00:00 2001 From: Francis6-git Date: Wed, 17 Jun 2026 17:47:55 +0100 Subject: [PATCH 3/7] Resolve merge conflict --- app/_layout.tsx | 37 +++++++++++++++-- package.json | 16 +++++++- services/api.ts | 105 +++++++++++++++++++++++++++++++++++++----------- 3 files changed, 131 insertions(+), 27 deletions(-) diff --git a/app/_layout.tsx b/app/_layout.tsx index 6fd98eb..f253af0 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,11 +1,15 @@ import { Stack, useRouter, useSegments } from 'expo-router'; -import { useEffect, useRef, useCallback } from 'react'; +import { useEffect, useRef, useCallback, useState } from 'react'; import { AppState, View } from 'react-native'; import { SafeAreaProvider } from 'react-native-safe-area-context'; +import NetInfo from '@react-native-community/netinfo'; import { useAuthStore } from '../stores/auth.store'; import { useSecurityStore } from '../src/security/security.store'; import { biometricService } from '../src/security/biometric.service'; import { BiometricGate } from '../src/components/BiometricGate'; +import { useConnectivityStore } from '../src/offline/connectivity.store'; +import { processQueue } from '../src/offline/offline-sync'; +import { initPromise } from '../src/locales/i18n'; import { SentryErrorBoundary } from '../components/SentryErrorBoundary'; import { initSentry, @@ -56,6 +60,7 @@ function useSentryUserContext() { } function RootLayout() { + const [i18nReady, setI18nReady] = useState(false); const hydrate = useAuthStore((s) => s.hydrate); const isAuthenticated = useAuthStore((s) => s.isAuthenticated); const isLoading = useAuthStore((s) => s.isLoading); @@ -83,6 +88,7 @@ function RootLayout() { useEffect(() => { void hydrate(); + initPromise.then(() => setI18nReady(true)); }, [hydrate]); useAuthGuard(); @@ -133,7 +139,32 @@ function RootLayout() { } }, [isLocked, isAuthenticated, startIdleTimer]); - if (isLoading) { + const prevConnectedRef = useRef(true); + + useEffect(() => { + NetInfo.fetch().then((state) => { + useConnectivityStore.getState().setConnected(state.isConnected ?? false); + useConnectivityStore.getState().setConnectionType(state.type); + useConnectivityStore.getState().setInternetReachable(state.isInternetReachable); + prevConnectedRef.current = state.isConnected ?? false; + }); + + const unsubscribe = NetInfo.addEventListener((state) => { + const connected = state.isConnected ?? false; + useConnectivityStore.getState().setConnected(connected); + useConnectivityStore.getState().setConnectionType(state.type); + useConnectivityStore.getState().setInternetReachable(state.isInternetReachable); + + if (!prevConnectedRef.current && connected) { + processQueue().catch(() => {}); + } + prevConnectedRef.current = connected; + }); + + return () => unsubscribe(); + }, []); + + if (isLoading || !i18nReady) { return null; } @@ -162,4 +193,4 @@ function RootLayout() { ); } -export default Sentry.wrap(RootLayout); +export default Sentry.wrap(RootLayout); \ No newline at end of file diff --git a/package.json b/package.json index e825541..cc77450 100644 --- a/package.json +++ b/package.json @@ -8,28 +8,38 @@ "prebuild": "expo prebuild", "lint": "eslint \"**/*.{js,jsx,ts,tsx}\" && prettier -c \"**/*.{js,jsx,ts,tsx,json}\"", "format": "eslint \"**/*.{js,jsx,ts,tsx}\" --fix && prettier \"**/*.{js,jsx,ts,tsx,json}\" --write", - "web": "expo start --web" + "web": "expo start --web", + "test": "jest" }, "dependencies": { "@expo/vector-icons": "^15.0.3", + "@react-native-async-storage/async-storage": "^2.2.0", + "@react-native-community/netinfo": "^11.4.1", "@react-navigation/native": "^7.1.8", "@sentry/react-native": "~7.2.0", + "@walletconnect/sign-client": "^2.23.9", + "@walletconnect/types": "^2.23.9", "axios": "^1.16.0", "expo": "^54.0.0", "expo-blur": "~15.0.8", + "expo-calendar": "~15.0.8", "expo-constants": "~18.0.13", "expo-crypto": "~15.0.9", "expo-font": "~14.0.11", "expo-image-picker": "~17.0.11", "expo-linking": "~8.0.12", "expo-local-authentication": "~17.0.8", + "expo-localization": "^56.0.6", + "expo-notifications": "~0.32.17", "expo-router": "~6.0.23", "expo-secure-store": "~15.0.8", "expo-status-bar": "~3.0.8", + "i18next": "^26.3.1", "lucide-react-native": "^0.562.0", "nativewind": "latest", "react": "19.1.0", "react-dom": "19.1.0", + "react-i18next": "^17.0.8", "react-native": "0.81.5", "react-native-keyboard-aware-scroll-view": "^0.9.5", "react-native-reanimated": "~4.1.1", @@ -42,14 +52,18 @@ }, "devDependencies": { "@babel/core": "^7.20.0", + "@types/jest": "^29.5.14", "@types/react": "~19.1.10", "babel-preset-expo": "^54.0.10", "eslint": "^9.25.1", "eslint-config-expo": "~10.0.0", "eslint-config-prettier": "^10.1.2", + "jest": "^29.7.0", + "jest-expo": "^56.0.5", "prettier": "^3.2.5", "prettier-plugin-tailwindcss": "^0.5.11", "tailwindcss": "^3.4.0", + "ts-jest": "^29.4.11", "typescript": "~5.9.2" }, "main": "expo-router/entry", diff --git a/services/api.ts b/services/api.ts index 109cd05..d44ff3e 100644 --- a/services/api.ts +++ b/services/api.ts @@ -2,6 +2,10 @@ import axios, { AxiosError, AxiosRequestConfig } from 'axios'; import { router } from 'expo-router'; import { config } from '../constants/config'; import { useAuthStore } from '../stores/auth.store'; +import { getFromCache, setToCache } from '../src/offline/cache'; +import { useConnectivityStore } from '../src/offline/connectivity.store'; +import { enqueueAction } from '../src/offline/offline-queue'; +import type { QueueAction, QueueActionType } from '../src/offline/offline-queue'; import { addBreadcrumb, captureServiceError } from './sentry'; const api = axios.create({ @@ -9,7 +13,7 @@ const api = axios.create({ timeout: 10000, }); -api.interceptors.request.use((req) => { +api.interceptors.request.use(async (req) => { const { accessToken } = useAuthStore.getState(); if (accessToken) { req.headers = req.headers ?? {}; @@ -22,7 +26,27 @@ api.interceptors.request.use((req) => { timeout: req.timeout ?? 0, }); - return req; + const method = req.method?.toLowerCase(); + if (!method || !['post', 'put', 'patch', 'delete'].includes(method)) { + return req; + } + + const { isConnected } = useConnectivityStore.getState(); + if (isConnected) return req; + + // Offline mitigation queue + const action = await enqueueAction({ + type: getActionType(req.url ?? '', req.method ?? 'POST'), + endpoint: req.url ?? '', + method: req.method?.toUpperCase() as QueueAction['method'], + data: (req.data as Record) ?? {}, + }); + + return Promise.reject({ + __offline_queued: true, + __action: action, + config: req, + }); }); interface RetriableRequest extends AxiosRequestConfig { @@ -53,43 +77,78 @@ async function performRefresh(): Promise { } api.interceptors.response.use( - (res) => res, - async (error: AxiosError) => { + (res) => { + const method = res.config?.method?.toLowerCase(); + if (method === 'get' && res.config?.url) { + setToCache(`GET:${res.config.url}`, res.data).catch(() => {}); + } + return res; + }, + async (error: any) => { + // Intercept offline-queued mock items immediately + if (error?.__offline_queued) { + return { + data: { queued: true, actionId: error.__action.id, unsignedXdr: '' }, + status: 202, + statusText: 'Accepted (queued offline)', + headers: {}, + config: error.config, + }; + } + const original = error.config as RetriableRequest | undefined; const status = error.response?.status; - // Capture non-401 errors to Sentry (401s are expected during token refresh) + // Capture non-401 production exceptions to Sentry if (status !== 401) { - captureServiceError('api', 'response', error); + captureServiceError('api', 'response', error as AxiosError); addBreadcrumb('http.error', `HTTP ${status ?? 'network'} error`, { url: original?.url ?? 'unknown', status: status ?? 0, }, 'error'); } - if (status !== 401 || !original || original._retry) { - return Promise.reject(error); - } + // Handle Token Expiration Refresh Sequence + if (status === 401 && original && !original._retry) { + original._retry = true; - original._retry = true; + if (!refreshInFlight) { + refreshInFlight = performRefresh().finally(() => { + refreshInFlight = null; + }); + } - if (!refreshInFlight) { - refreshInFlight = performRefresh().finally(() => { - refreshInFlight = null; - }); - } + const newToken = await refreshInFlight; - const newToken = await refreshInFlight; + if (!newToken) { + router.replace('/(auth)/sign-in'); + return Promise.reject(error); + } - if (!newToken) { - router.replace('/(auth)/sign-in'); - return Promise.reject(error); + original.headers = original.headers ?? {}; + (original.headers as Record).Authorization = `Bearer ${newToken}`; + return api.request(original); } - original.headers = original.headers ?? {}; - (original.headers as Record).Authorization = `Bearer ${newToken}`; - return api.request(original); + // Offline read strategy fallback for standard broken GET failures + if (original?.method?.toLowerCase() === 'get' && original?.url) { + const cached = await getFromCache(`GET:${original.url}`); + if (cached !== null) { + return { data: cached, status: 200, statusText: 'OK (cached)', headers: {}, config: original }; + } + } + + return Promise.reject(error); }, ); -export default api; +function getActionType(url: string, method: string): QueueActionType { + if (url.includes('/repay-installment')) return 'REPAY_INSTALLMENT'; + if (url.includes('/loans/create')) return 'CREATE_LOAN'; + if (url.includes('/vouches/submit')) return 'SUBMIT_VOUCH'; + if (url.includes('/liquidity/deposit')) return 'DEPOSIT'; + if (url.includes('/transactions/submit')) return 'SUBMIT_SIGNED_XDR'; + return 'SUBMIT_SIGNED_XDR'; +} + +export default api; \ No newline at end of file From a614d6d3f1034c6620544fa96215fde4e5361c48 Mon Sep 17 00:00:00 2001 From: Francis6-git Date: Thu, 18 Jun 2026 19:24:53 +0100 Subject: [PATCH 4/7] chore: trigger CI From 37cda24ea44ecd59ffcb3e568f928c0cbbe5fa63 Mon Sep 17 00:00:00 2001 From: Francis6-git Date: Thu, 18 Jun 2026 19:30:12 +0100 Subject: [PATCH 5/7] chore: trigger CI From 485ff0267f2b13a2eeb27c4e2fc5645d2e46c89d Mon Sep 17 00:00:00 2001 From: Francis6-git Date: Thu, 18 Jun 2026 19:38:24 +0100 Subject: [PATCH 6/7] fix: sync package-lock.json with merged dependencies --- package-lock.json | 176 ++++++++++++++++++++-------------------------- 1 file changed, 77 insertions(+), 99 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a0d473..90b47bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "@react-native-community/netinfo": "^11.4.1", "@react-navigation/native": "^7.1.8", "@sentry/react-native": "~7.2.0", + "@walletconnect/sign-client": "^2.23.9", + "@walletconnect/types": "^2.23.9", "axios": "^1.16.0", "expo": "^54.0.0", "expo-blur": "~15.0.8", @@ -122,7 +124,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -3742,7 +3743,6 @@ "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.28.tgz", "integrity": "sha512-d1QDn+KNHfHGt3UIwOZvupvdsDdiHYZBEj7+wL2yDVo3tMezamYy60H9s3EnNVE1Ae1ty0trc7F2OKqo/RmsdQ==", "license": "MIT", - "peer": true, "dependencies": { "@react-navigation/core": "^7.14.0", "escape-string-regexp": "^4.0.0", @@ -3790,6 +3790,81 @@ "dev": true, "license": "MIT" }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sentry-internal/browser-utils": { "version": "10.12.0", "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.12.0.tgz", @@ -4132,78 +4207,6 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "license": "MIT" }, - "node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "license": "MIT" - }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -4376,7 +4379,6 @@ "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4454,7 +4456,6 @@ "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", @@ -5362,7 +5363,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6162,7 +6162,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7684,7 +7683,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7897,7 +7895,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -8221,7 +8218,6 @@ "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.34.tgz", "integrity": "sha512-XkVHguZZDC8BcTQxHAd14/TQFbDp1Wt0Z/KApO9t68Ll5A127hLCPzU+a9gytfCIiyL/V1IpF1vIcOLKEVAoNQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.24", @@ -8319,7 +8315,6 @@ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz", "integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==", "license": "MIT", - "peer": true, "dependencies": { "@expo/config": "~12.0.13", "@expo/env": "~2.0.8" @@ -8356,7 +8351,6 @@ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz", "integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==", "license": "MIT", - "peer": true, "dependencies": { "fontfaceobserver": "^2.1.0" }, @@ -8392,7 +8386,6 @@ "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.12.tgz", "integrity": "sha512-FpXeIpFgZuxihwT9lBo86YD3y6LphBuAhN680MMxm/Y7fmsc57vimn2d3vFu68VI0+Z9w457t494mu2wvlgWTQ==", "license": "MIT", - "peer": true, "dependencies": { "expo-constants": "~18.0.13", "invariant": "^2.2.4" @@ -11604,7 +11597,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -13863,7 +13855,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -14022,7 +14013,6 @@ "integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -14386,7 +14376,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -14406,7 +14395,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -14469,7 +14457,6 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", @@ -14602,7 +14589,6 @@ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz", "integrity": "sha512-F+ZJBYiok/6Jzp1re75F/9aLzkgoQCOh4yxrnwATa8392RvM3kx+fiXXFvwcgE59v48lMwd9q0nzF1oJLXpfxQ==", "license": "MIT", - "peer": true, "dependencies": { "react-native-is-edge-to-edge": "^1.2.1", "semver": "7.7.2" @@ -14631,7 +14617,6 @@ "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -14642,7 +14627,6 @@ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", "license": "MIT", - "peer": true, "dependencies": { "react-freeze": "^1.0.0", "react-native-is-edge-to-edge": "^1.2.1", @@ -14658,7 +14642,6 @@ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz", "integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==", "license": "MIT", - "peer": true, "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", @@ -14816,7 +14799,6 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -16189,7 +16171,6 @@ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -16396,7 +16377,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -16698,7 +16678,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17137,7 +17116,6 @@ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } From 25ca498ae6b896daeb18bd3f56b0c4f1e9ce6ddd Mon Sep 17 00:00:00 2001 From: Francis6-git Date: Thu, 18 Jun 2026 19:42:55 +0100 Subject: [PATCH 7/7] chore: trigger CI