2 Commits

Author SHA1 Message Date
d64a6557a8 The vault notification mode1 2026-01-31 22:58:51 -04:00
56bb72aab8 The vault notification mode1 2026-01-30 22:42:20 -04:00
14 changed files with 1164 additions and 1642 deletions

View File

@@ -6,6 +6,7 @@
*/ */
import React from 'react'; import React from 'react';
import { Buffer } from 'buffer';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
@@ -15,6 +16,10 @@ import AuthNavigator from './src/navigation/AuthNavigator';
import { AuthProvider, useAuth } from './src/context/AuthContext'; import { AuthProvider, useAuth } from './src/context/AuthContext';
import { colors } from './src/theme/colors'; import { colors } from './src/theme/colors';
if (typeof globalThis !== 'undefined' && !globalThis.Buffer) {
globalThis.Buffer = Buffer;
}
/** /**
* Loading screen shown while restoring auth state * Loading screen shown while restoring auth state
*/ */

97
package-lock.json generated
View File

@@ -14,6 +14,8 @@
"@react-navigation/bottom-tabs": "^6.6.1", "@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18", "@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0", "@react-navigation/native-stack": "^6.11.0",
"bip39": "^3.1.0",
"buffer": "^6.0.3",
"expo": "~52.0.0", "expo": "~52.0.0",
"expo-asset": "~11.0.5", "expo-asset": "~11.0.5",
"expo-constants": "~17.0.8", "expo-constants": "~17.0.8",
@@ -80,6 +82,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.28.6", "@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6", "@babel/generator": "^7.28.6",
@@ -483,7 +486,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz",
"integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1",
"@babel/traverse": "^7.28.5" "@babel/traverse": "^7.28.5"
@@ -500,7 +502,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz",
"integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1" "@babel/helper-plugin-utils": "^7.27.1"
}, },
@@ -516,7 +517,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz",
"integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1" "@babel/helper-plugin-utils": "^7.27.1"
}, },
@@ -532,7 +532,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz",
"integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
@@ -550,7 +549,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz",
"integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6",
"@babel/traverse": "^7.28.6" "@babel/traverse": "^7.28.6"
@@ -651,7 +649,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
}, },
@@ -772,7 +769,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz",
"integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.28.6" "@babel/helper-plugin-utils": "^7.28.6"
}, },
@@ -959,7 +955,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
"integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-create-regexp-features-plugin": "^7.18.6",
"@babel/helper-plugin-utils": "^7.18.6" "@babel/helper-plugin-utils": "^7.18.6"
@@ -1025,7 +1020,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz",
"integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1" "@babel/helper-plugin-utils": "^7.27.1"
}, },
@@ -1072,7 +1066,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz",
"integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-create-class-features-plugin": "^7.28.6",
"@babel/helper-plugin-utils": "^7.28.6" "@babel/helper-plugin-utils": "^7.28.6"
@@ -1141,7 +1134,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz",
"integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6" "@babel/helper-plugin-utils": "^7.28.6"
@@ -1158,7 +1150,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz",
"integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1" "@babel/helper-plugin-utils": "^7.27.1"
}, },
@@ -1174,7 +1165,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz",
"integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==", "integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6" "@babel/helper-plugin-utils": "^7.28.6"
@@ -1191,7 +1181,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz",
"integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1" "@babel/helper-plugin-utils": "^7.27.1"
}, },
@@ -1207,7 +1196,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz",
"integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6",
"@babel/plugin-transform-destructuring": "^7.28.5" "@babel/plugin-transform-destructuring": "^7.28.5"
@@ -1224,7 +1212,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz",
"integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.28.6" "@babel/helper-plugin-utils": "^7.28.6"
}, },
@@ -1304,7 +1291,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz",
"integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.28.6" "@babel/helper-plugin-utils": "^7.28.6"
}, },
@@ -1350,7 +1336,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz",
"integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1" "@babel/helper-plugin-utils": "^7.27.1"
}, },
@@ -1366,7 +1351,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz",
"integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-module-transforms": "^7.27.1", "@babel/helper-module-transforms": "^7.27.1",
"@babel/helper-plugin-utils": "^7.27.1" "@babel/helper-plugin-utils": "^7.27.1"
@@ -1399,7 +1383,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz",
"integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-module-transforms": "^7.28.3", "@babel/helper-module-transforms": "^7.28.3",
"@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1",
@@ -1418,7 +1401,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz",
"integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-module-transforms": "^7.27.1", "@babel/helper-module-transforms": "^7.27.1",
"@babel/helper-plugin-utils": "^7.27.1" "@babel/helper-plugin-utils": "^7.27.1"
@@ -1451,7 +1433,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz",
"integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1" "@babel/helper-plugin-utils": "^7.27.1"
}, },
@@ -1516,7 +1497,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz",
"integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-replace-supers": "^7.27.1" "@babel/helper-replace-supers": "^7.27.1"
@@ -1612,7 +1592,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz",
"integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1" "@babel/helper-plugin-utils": "^7.27.1"
}, },
@@ -1738,7 +1717,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz",
"integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6" "@babel/helper-plugin-utils": "^7.28.6"
@@ -1755,7 +1733,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz",
"integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1" "@babel/helper-plugin-utils": "^7.27.1"
}, },
@@ -1852,7 +1829,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz",
"integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1" "@babel/helper-plugin-utils": "^7.27.1"
}, },
@@ -1887,7 +1863,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz",
"integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1" "@babel/helper-plugin-utils": "^7.27.1"
}, },
@@ -1903,7 +1878,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz",
"integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6" "@babel/helper-plugin-utils": "^7.28.6"
@@ -1936,7 +1910,6 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz",
"integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5", "@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6" "@babel/helper-plugin-utils": "^7.28.6"
@@ -2055,7 +2028,6 @@
"resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
"integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0",
"@babel/types": "^7.4.4", "@babel/types": "^7.4.4",
@@ -2673,6 +2645,7 @@
"resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-4.0.1.tgz", "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-4.0.1.tgz",
"integrity": "sha512-CRpbLvdJ1T42S+lrYa1iZp1KfDeBp4oeZOK3hdpiS5n0vR0nhD6sC1gGF0sTboCTp64tLteikz5Y3j53dvgOIw==", "integrity": "sha512-CRpbLvdJ1T42S+lrYa1iZp1KfDeBp4oeZOK3hdpiS5n0vR0nhD6sC1gGF0sTboCTp64tLteikz5Y3j53dvgOIw==",
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"react-native": "*" "react-native": "*"
} }
@@ -3209,6 +3182,18 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"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/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3717,6 +3702,7 @@
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.18.tgz", "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.18.tgz",
"integrity": "sha512-mIT9MiL/vMm4eirLcmw2h6h/Nm5FICtnYSdohq4vTLA2FF/6PNhByM7s8ffqoVfE5L0uAa6Xda1B7oddolUiGg==", "integrity": "sha512-mIT9MiL/vMm4eirLcmw2h6h/Nm5FICtnYSdohq4vTLA2FF/6PNhByM7s8ffqoVfE5L0uAa6Xda1B7oddolUiGg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@react-navigation/core": "^6.4.17", "@react-navigation/core": "^6.4.17",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
@@ -3898,6 +3884,7 @@
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.2.2" "csstype": "^3.2.2"
@@ -4447,6 +4434,15 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/bip39": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz",
"integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==",
"license": "ISC",
"dependencies": {
"@noble/hashes": "^1.2.0"
}
},
"node_modules/bplist-creator": { "node_modules/bplist-creator": {
"version": "0.0.7", "version": "0.0.7",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz",
@@ -4509,6 +4505,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -4533,9 +4530,9 @@
} }
}, },
"node_modules/buffer": { "node_modules/buffer": {
"version": "5.7.1", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -4553,7 +4550,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"base64-js": "^1.3.1", "base64-js": "^1.3.1",
"ieee754": "^1.1.13" "ieee754": "^1.2.1"
} }
}, },
"node_modules/buffer-alloc": { "node_modules/buffer-alloc": {
@@ -5533,7 +5530,6 @@
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -5658,6 +5654,7 @@
"resolved": "https://registry.npmjs.org/expo/-/expo-52.0.48.tgz", "resolved": "https://registry.npmjs.org/expo/-/expo-52.0.48.tgz",
"integrity": "sha512-/HR/vuo57KGEWlvF3GWaquwEAjXuA5hrOCsaLcZ3pMSA8mQ27qKd1jva4GWzpxXYedlzs/7LLP1XpZo6hXTsog==", "integrity": "sha512-/HR/vuo57KGEWlvF3GWaquwEAjXuA5hrOCsaLcZ3pMSA8mQ27qKd1jva4GWzpxXYedlzs/7LLP1XpZo6hXTsog==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.0", "@babel/runtime": "^7.20.0",
"@expo/cli": "0.22.27", "@expo/cli": "0.22.27",
@@ -9185,6 +9182,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
}, },
@@ -9268,6 +9266,7 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.76.9.tgz", "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.76.9.tgz",
"integrity": "sha512-+LRwecWmTDco7OweGsrECIqJu0iyrREd6CTCgC/uLLYipiHvk+MH9nd6drFtCw/6Blz6eoKTcH9YTTJusNtrWg==", "integrity": "sha512-+LRwecWmTDco7OweGsrECIqJu0iyrREd6CTCgC/uLLYipiHvk+MH9nd6drFtCw/6Blz6eoKTcH9YTTJusNtrWg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@jest/create-cache-key-function": "^29.6.3", "@jest/create-cache-key-function": "^29.6.3",
"@react-native/assets-registry": "0.76.9", "@react-native/assets-registry": "0.76.9",
@@ -9369,6 +9368,7 @@
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.12.0.tgz", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.12.0.tgz",
"integrity": "sha512-ukk5PxcF4p3yu6qMZcmeiZgowhb5AsKRnil54YFUUAXVIS7PJcMHGGC+q44fCiBg44/1AJk5njGMez1m9H0BVQ==", "integrity": "sha512-ukk5PxcF4p3yu6qMZcmeiZgowhb5AsKRnil54YFUUAXVIS7PJcMHGGC+q44fCiBg44/1AJk5njGMez1m9H0BVQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"react": "*", "react": "*",
"react-native": "*" "react-native": "*"
@@ -9379,6 +9379,7 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.4.0.tgz", "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.4.0.tgz",
"integrity": "sha512-c7zc7Zwjty6/pGyuuvh9gK3YBYqHPOxrhXfG1lF4gHlojQSmIx2piNbNaV+Uykj+RDTmFXK0e/hA+fucw/Qozg==", "integrity": "sha512-c7zc7Zwjty6/pGyuuvh9gK3YBYqHPOxrhXfG1lF4gHlojQSmIx2piNbNaV+Uykj+RDTmFXK0e/hA+fucw/Qozg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"react-freeze": "^1.0.0", "react-freeze": "^1.0.0",
"warn-once": "^0.1.0" "warn-once": "^0.1.0"
@@ -11057,6 +11058,30 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/whatwg-url-without-unicode/node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": { "node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",

View File

@@ -15,6 +15,8 @@
"@react-navigation/bottom-tabs": "^6.6.1", "@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18", "@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0", "@react-navigation/native-stack": "^6.11.0",
"bip39": "^3.1.0",
"buffer": "^6.0.3",
"expo": "~52.0.0", "expo": "~52.0.0",
"expo-asset": "~11.0.5", "expo-asset": "~11.0.5",
"expo-constants": "~17.0.8", "expo-constants": "~17.0.8",

View File

@@ -27,7 +27,7 @@ export const DEBUG_MODE = true;
/** /**
* Base URL for the backend API server * Base URL for the backend API server
*/ */
export const API_BASE_URL = 'http://192.168.56.103:8000'; export const API_BASE_URL = 'http://localhost:8000';
/** /**
* API request timeout in milliseconds * API request timeout in milliseconds
@@ -112,6 +112,7 @@ export const MOCK_CONFIG = {
USER: { USER: {
id: 999, id: 999,
username: 'MockCaptain', username: 'MockCaptain',
email: 'captain@sentinel.local',
public_key: 'mock_public_key', public_key: 'mock_public_key',
is_admin: true, is_admin: true,
guale: false, guale: false,
@@ -137,44 +138,6 @@ export const AI_CONFIG = {
* Mock response delay in milliseconds (for NO_BACKEND_MODE) * Mock response delay in milliseconds (for NO_BACKEND_MODE)
*/ */
MOCK_RESPONSE_DELAY: 500, MOCK_RESPONSE_DELAY: 500,
/**
* AI Roles configuration
*/
ROLES: [
{
id: 'reflective',
name: 'Reflective Assistant',
description: 'Helps you dive deep into your thoughts and feelings through meaningful reflection.',
systemPrompt: 'You are a helpful journal assistant. Help the user reflect on their thoughts and feelings.',
icon: 'journal-outline',
iconFamily: 'Ionicons',
},
{
id: 'creative',
name: 'Creative Spark',
description: 'A partner for brainstorming, creative writing, and exploring new ideas.',
systemPrompt: 'You are a creative brainstorming partner. Help the user explore new ideas, write stories, or look at things from a fresh perspective.',
icon: 'bulb-outline',
iconFamily: 'Ionicons',
},
{
id: 'planner',
name: 'Action Planner',
description: 'Focused on turning thoughts into actionable plans and organized goals.',
systemPrompt: 'You are a productivity coach. Help the user break down their thoughts into actionable steps and clear goals.',
icon: 'list-outline',
iconFamily: 'Ionicons',
},
{
id: 'empathetic',
name: 'Empathetic Guide',
description: 'Provides a safe, non-judgmental space for emotional support and empathy.',
systemPrompt: 'You are a supportive and empathetic friend. Listen to the user\'s concerns and provide emotional support without judgment.',
icon: 'heart-outline',
iconFamily: 'Ionicons',
},
],
} as const; } as const;
// ============================================================================= // =============================================================================

View File

@@ -9,7 +9,6 @@ import React, { createContext, useContext, useState, useEffect, ReactNode } from
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { User, LoginRequest, RegisterRequest } from '../types'; import { User, LoginRequest, RegisterRequest } from '../types';
import { authService } from '../services/auth.service'; import { authService } from '../services/auth.service';
import { storageService } from '../services/storage.service';
// ============================================================================= // =============================================================================
// Type Definitions // Type Definitions
@@ -138,25 +137,24 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}; };
/** /**
* Sign out and clear stored auth and session data * Sign out and clear stored auth
*/ */
const signOut = () => { const signOut = () => {
setUser(null); setUser(null);
setToken(null); setToken(null);
clearAuth(); clearAuth();
//storageService.clearAllData();
}; };
return ( return (
<AuthContext.Provider <AuthContext.Provider
value={{ value={{
user, user,
token, token,
isLoading, isLoading,
isInitializing, isInitializing,
signIn, signIn,
signUp, signUp,
signOut signOut
}} }}
> >
{children} {children}

View File

@@ -31,8 +31,6 @@ import * as ImagePicker from 'expo-image-picker';
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
import { aiService } from '../services/ai.service'; import { aiService } from '../services/ai.service';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { AI_CONFIG } from '../config';
import { storageService } from '../services/storage.service';
// ============================================================================= // =============================================================================
// Type Definitions // Type Definitions
@@ -59,21 +57,16 @@ interface ChatSession {
// ============================================================================= // =============================================================================
export default function FlowScreen() { export default function FlowScreen() {
const { token, user, signOut } = useAuth(); const { token, signOut } = useAuth();
const scrollViewRef = useRef<ScrollView>(null); const scrollViewRef = useRef<ScrollView>(null);
// Current conversation state // Current conversation state
const [messages, setMessages] = useState<ChatMessage[]>([]); const [messages, setMessages] = useState<ChatMessage[]>([]);
const [newContent, setNewContent] = useState(''); const [newContent, setNewContent] = useState('');
const [isSending, setIsSending] = useState(false); const [isSending, setIsSending] = useState(false);
const [isRecording, setIsRecording] = useState(false); const [isRecording, setIsRecording] = useState(false);
const [selectedImage, setSelectedImage] = useState<string | null>(null); const [selectedImage, setSelectedImage] = useState<string | null>(null);
// AI Role state
const [selectedRole, setSelectedRole] = useState(AI_CONFIG.ROLES[0]);
const [showRoleModal, setShowRoleModal] = useState(false);
const [expandedRoleId, setExpandedRoleId] = useState<string | null>(null);
// History modal state // History modal state
const [showHistoryModal, setShowHistoryModal] = useState(false); const [showHistoryModal, setShowHistoryModal] = useState(false);
const modalSlideAnim = useRef(new Animated.Value(0)).current; const modalSlideAnim = useRef(new Animated.Value(0)).current;
@@ -102,87 +95,23 @@ export default function FlowScreen() {
updatedAt: new Date('2024-01-16T20:30:00'), updatedAt: new Date('2024-01-16T20:30:00'),
}, },
]); ]);
// Header date display // Header date display
const today = new Date(); const today = new Date();
const dateStr = today.toLocaleDateString('en-US', { const dateStr = today.toLocaleDateString('en-US', {
weekday: 'long', weekday: 'long',
month: 'long', month: 'long',
day: 'numeric' day: 'numeric'
}); });
// Load history on mount // Auto-scroll to bottom when new messages arrive
useEffect(() => { useEffect(() => {
const loadHistory = async () => {
if (!user) return;
try {
console.log('[FlowScreen] Loading chat history...');
const savedHistory = await storageService.getChatHistory(user.id);
if (savedHistory && savedHistory.length > 0) {
const formattedHistory = savedHistory.map((session: any) => ({
...session,
createdAt: new Date(session.createdAt),
updatedAt: new Date(session.updatedAt),
messages: session.messages.map((msg: any) => ({
...msg,
createdAt: new Date(msg.createdAt)
}))
}));
setChatHistory(formattedHistory);
console.log('[FlowScreen] Chat history loaded:', formattedHistory.length, 'sessions');
} else {
console.log('[FlowScreen] No chat history found');
}
} catch (error) {
console.error('Failed to load history:', error);
}
};
loadHistory();
}, [user]);
// Load messages whenever role changes
useEffect(() => {
const loadRoleMessages = async () => {
if (!user) return;
try {
const savedMessages = await storageService.getCurrentChat(selectedRole.id, user.id);
if (savedMessages) {
const formattedMessages = savedMessages.map((msg: any) => ({
...msg,
createdAt: new Date(msg.createdAt)
}));
setMessages(formattedMessages);
} else {
setMessages([]);
}
} catch (error) {
console.error(`Failed to load messages for role ${selectedRole.id}:`, error);
setMessages([]);
}
};
loadRoleMessages();
}, [selectedRole.id, user]);
// Save current messages for the active role when they change
useEffect(() => {
if (user && messages.length >= 0) { // Save even if empty to allow clearing
storageService.saveCurrentChat(selectedRole.id, messages, user.id);
}
if (messages.length > 0) { if (messages.length > 0) {
setTimeout(() => { setTimeout(() => {
scrollViewRef.current?.scrollToEnd({ animated: true }); scrollViewRef.current?.scrollToEnd({ animated: true });
}, 100); }, 100);
} }
}, [messages, selectedRole.id, user]); }, [messages]);
// Save history when it changes
useEffect(() => {
if (user) {
storageService.saveChatHistory(chatHistory, user.id);
}
}, [chatHistory, user]);
// Modal animation control // Modal animation control
const openHistoryModal = () => { const openHistoryModal = () => {
@@ -214,7 +143,7 @@ export default function FlowScreen() {
*/ */
const handleSendMessage = async () => { const handleSendMessage = async () => {
if (!newContent.trim() || isSending) return; if (!newContent.trim() || isSending) return;
// Check authentication // Check authentication
if (!token) { if (!token) {
Alert.alert( Alert.alert(
@@ -224,11 +153,11 @@ export default function FlowScreen() {
); );
return; return;
} }
const userMessage = newContent.trim(); const userMessage = newContent.trim();
setIsSending(true); setIsSending(true);
setNewContent(''); setNewContent('');
// Add user message immediately // Add user message immediately
const userMsg: ChatMessage = { const userMsg: ChatMessage = {
id: Date.now().toString(), id: Date.now().toString(),
@@ -237,11 +166,11 @@ export default function FlowScreen() {
createdAt: new Date(), createdAt: new Date(),
}; };
setMessages(prev => [...prev, userMsg]); setMessages(prev => [...prev, userMsg]);
try { try {
// Call AI proxy with selected role's system prompt // Call AI proxy
const aiResponse = await aiService.sendMessage(userMessage, token, selectedRole.systemPrompt); const aiResponse = await aiService.sendMessage(userMessage, token);
// Add AI response // Add AI response
const aiMsg: ChatMessage = { const aiMsg: ChatMessage = {
id: (Date.now() + 1).toString(), id: (Date.now() + 1).toString(),
@@ -250,20 +179,20 @@ export default function FlowScreen() {
createdAt: new Date(), createdAt: new Date(),
}; };
setMessages(prev => [...prev, aiMsg]); setMessages(prev => [...prev, aiMsg]);
} catch (error) { } catch (error) {
console.error('AI request failed:', error); console.error('AI request failed:', error);
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
// Handle authentication errors (401, credentials, unauthorized) // Handle authentication errors (401, credentials, unauthorized)
const isAuthError = const isAuthError =
errorMessage.includes('401') || errorMessage.includes('401') ||
errorMessage.includes('credentials') || errorMessage.includes('credentials') ||
errorMessage.includes('Unauthorized') || errorMessage.includes('Unauthorized') ||
errorMessage.includes('Not authenticated') || errorMessage.includes('Not authenticated') ||
errorMessage.includes('validate'); errorMessage.includes('validate');
if (isAuthError) { if (isAuthError) {
signOut(); signOut();
Alert.alert( Alert.alert(
@@ -273,7 +202,7 @@ export default function FlowScreen() {
); );
return; return;
} }
// Show error as AI message // Show error as AI message
const errorMsg: ChatMessage = { const errorMsg: ChatMessage = {
id: (Date.now() + 1).toString(), id: (Date.now() + 1).toString(),
@@ -317,7 +246,7 @@ export default function FlowScreen() {
if (!result.canceled && result.assets[0]) { if (!result.canceled && result.assets[0]) {
const imageAsset = result.assets[0]; const imageAsset = result.assets[0];
setSelectedImage(imageAsset.uri); setSelectedImage(imageAsset.uri);
// Check authentication // Check authentication
if (!token) { if (!token) {
Alert.alert( Alert.alert(
@@ -360,9 +289,9 @@ export default function FlowScreen() {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// Handle authentication errors // Handle authentication errors
const isAuthError = const isAuthError =
errorMessage.includes('401') || errorMessage.includes('401') ||
errorMessage.includes('Unauthorized') || errorMessage.includes('Unauthorized') ||
errorMessage.includes('credentials') || errorMessage.includes('credentials') ||
errorMessage.includes('validate'); errorMessage.includes('validate');
@@ -405,12 +334,9 @@ export default function FlowScreen() {
}; };
setChatHistory(prev => [newSession, ...prev]); setChatHistory(prev => [newSession, ...prev]);
} }
// Clear current messages and storage for this role // Clear current messages
setMessages([]); setMessages([]);
if (user) {
storageService.saveCurrentChat(selectedRole.id, [], user.id);
}
closeHistoryModal(); closeHistoryModal();
}; };
@@ -429,7 +355,7 @@ export default function FlowScreen() {
}; };
setChatHistory(prev => [currentSession, ...prev.filter(s => s.id !== session.id)]); setChatHistory(prev => [currentSession, ...prev.filter(s => s.id !== session.id)]);
} }
// Load selected conversation // Load selected conversation
setMessages(session.messages); setMessages(session.messages);
closeHistoryModal(); closeHistoryModal();
@@ -444,8 +370,8 @@ export default function FlowScreen() {
'Are you sure you want to delete this conversation?', 'Are you sure you want to delete this conversation?',
[ [
{ text: 'Cancel', style: 'cancel' }, { text: 'Cancel', style: 'cancel' },
{ {
text: 'Delete', text: 'Delete',
style: 'destructive', style: 'destructive',
onPress: () => setChatHistory(prev => prev.filter(s => s.id !== sessionId)) onPress: () => setChatHistory(prev => prev.filter(s => s.id !== sessionId))
}, },
@@ -474,10 +400,10 @@ export default function FlowScreen() {
*/ */
const renderMessage = (message: ChatMessage, index: number) => { const renderMessage = (message: ChatMessage, index: number) => {
const isUser = message.role === 'user'; const isUser = message.role === 'user';
return ( return (
<View <View
key={message.id} key={message.id}
style={[ style={[
styles.messageBubble, styles.messageBubble,
isUser ? styles.userBubble : styles.aiBubble isUser ? styles.userBubble : styles.aiBubble
@@ -494,8 +420,8 @@ export default function FlowScreen() {
]}> ]}>
{/* Show image if present */} {/* Show image if present */}
{message.imageUri && ( {message.imageUri && (
<Image <Image
source={{ uri: message.imageUri }} source={{ uri: message.imageUri }}
style={styles.messageImage} style={styles.messageImage}
resizeMode="cover" resizeMode="cover"
/> />
@@ -525,9 +451,9 @@ export default function FlowScreen() {
<View style={styles.emptyIcon}> <View style={styles.emptyIcon}>
<Feather name="feather" size={48} color={colors.nautical.seafoam} /> <Feather name="feather" size={48} color={colors.nautical.seafoam} />
</View> </View>
<Text style={styles.emptyTitle}>Chatting with {selectedRole.name}</Text> <Text style={styles.emptyTitle}>Start a conversation</Text>
<Text style={styles.emptySubtitle}> <Text style={styles.emptySubtitle}>
{selectedRole.description} Ask me anything or share your thoughts
</Text> </Text>
</View> </View>
); );
@@ -536,7 +462,7 @@ export default function FlowScreen() {
* Render history item in modal * Render history item in modal
*/ */
const renderHistoryItem = ({ item }: { item: ChatSession }) => ( const renderHistoryItem = ({ item }: { item: ChatSession }) => (
<TouchableOpacity <TouchableOpacity
style={styles.historyItem} style={styles.historyItem}
onPress={() => handleLoadHistory(item)} onPress={() => handleLoadHistory(item)}
onLongPress={() => handleDeleteHistory(item.id)} onLongPress={() => handleDeleteHistory(item.id)}
@@ -578,26 +504,9 @@ export default function FlowScreen() {
<Text style={styles.headerDate}>{dateStr}</Text> <Text style={styles.headerDate}>{dateStr}</Text>
</View> </View>
</View> </View>
{/* Role Header Dropdown */}
<TouchableOpacity
style={styles.headerRoleButton}
onPress={() => setShowRoleModal(true)}
activeOpacity={0.7}
>
<Ionicons
name={selectedRole.icon as any}
size={16}
color={colors.nautical.teal}
/>
<Text style={styles.headerRoleText} numberOfLines={1}>
{selectedRole.name}
</Text>
<Ionicons name="chevron-down" size={14} color={colors.flow.textSecondary} />
</TouchableOpacity>
{/* History Button */} {/* History Button */}
<TouchableOpacity <TouchableOpacity
style={styles.historyButton} style={styles.historyButton}
onPress={openHistoryModal} onPress={openHistoryModal}
> >
@@ -606,7 +515,7 @@ export default function FlowScreen() {
</View> </View>
{/* Chat Messages */} {/* Chat Messages */}
<ScrollView <ScrollView
ref={scrollViewRef} ref={scrollViewRef}
style={styles.messagesContainer} style={styles.messagesContainer}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
@@ -620,7 +529,7 @@ export default function FlowScreen() {
) : ( ) : (
messages.map((message, index) => renderMessage(message, index)) messages.map((message, index) => renderMessage(message, index))
)} )}
{/* Loading indicator when sending */} {/* Loading indicator when sending */}
{isSending && ( {isSending && (
<View style={[styles.messageBubble, styles.aiBubble]}> <View style={[styles.messageBubble, styles.aiBubble]}>
@@ -632,7 +541,7 @@ export default function FlowScreen() {
</View> </View>
</View> </View>
)} )}
<View style={{ height: 20 }} /> <View style={{ height: 20 }} />
</ScrollView> </ScrollView>
@@ -640,7 +549,7 @@ export default function FlowScreen() {
<View style={styles.inputBarContainer}> <View style={styles.inputBarContainer}>
<View style={styles.inputBar}> <View style={styles.inputBar}>
{/* Image attachment button */} {/* Image attachment button */}
<TouchableOpacity <TouchableOpacity
style={styles.inputBarButton} style={styles.inputBarButton}
onPress={handleAddImage} onPress={handleAddImage}
activeOpacity={0.7} activeOpacity={0.7}
@@ -663,7 +572,7 @@ export default function FlowScreen() {
{/* Send or Voice button */} {/* Send or Voice button */}
{newContent.trim() || isSending ? ( {newContent.trim() || isSending ? (
<TouchableOpacity <TouchableOpacity
style={[styles.sendButton, isSending && styles.sendButtonDisabled]} style={[styles.sendButton, isSending && styles.sendButtonDisabled]}
onPress={handleSendMessage} onPress={handleSendMessage}
activeOpacity={0.8} activeOpacity={0.8}
@@ -681,15 +590,15 @@ export default function FlowScreen() {
</LinearGradient> </LinearGradient>
</TouchableOpacity> </TouchableOpacity>
) : ( ) : (
<TouchableOpacity <TouchableOpacity
style={[styles.inputBarButton, isRecording && styles.recordingButton]} style={[styles.inputBarButton, isRecording && styles.recordingButton]}
onPress={handleVoiceRecord} onPress={handleVoiceRecord}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Feather <Feather
name="mic" name="mic"
size={22} size={22}
color={isRecording ? '#fff' : colors.flow.textSecondary} color={isRecording ? '#fff' : colors.flow.textSecondary}
/> />
</TouchableOpacity> </TouchableOpacity>
)} )}
@@ -698,16 +607,17 @@ export default function FlowScreen() {
</SafeAreaView> </SafeAreaView>
</LinearGradient> </LinearGradient>
<Modal {/* History Modal - Background appears instantly, content slides up */}
visible={showHistoryModal} <Modal
visible={showHistoryModal}
animationType="none" animationType="none"
transparent transparent
onRequestClose={closeHistoryModal} onRequestClose={closeHistoryModal}
> >
<TouchableWithoutFeedback onPress={closeHistoryModal}> <TouchableWithoutFeedback onPress={closeHistoryModal}>
<View style={styles.modalOverlay}> <View style={styles.modalOverlay}>
<TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}> <TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}>
<Animated.View <Animated.View
style={[ style={[
styles.modalContent, styles.modalContent,
{ {
@@ -720,129 +630,46 @@ export default function FlowScreen() {
} }
]} ]}
> >
<View style={styles.modalHandle} /> <View style={styles.modalHandle} />
{/* Modal Header */} {/* Modal Header */}
<View style={styles.modalHeader}> <View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Chat History</Text> <Text style={styles.modalTitle}>Chat History</Text>
<TouchableOpacity <TouchableOpacity
style={styles.newChatButton} style={styles.newChatButton}
onPress={handleNewChat} onPress={handleNewChat}
> >
<Ionicons name="add" size={20} color="#fff" /> <Ionicons name="add" size={20} color="#fff" />
<Text style={styles.newChatText}>New Chat</Text> <Text style={styles.newChatText}>New Chat</Text>
</TouchableOpacity> </TouchableOpacity>
</View>
{/* History List */}
<FlatList
data={chatHistory}
renderItem={renderHistoryItem}
keyExtractor={item => item.id}
style={styles.historyList}
ListEmptyComponent={
<View style={styles.historyEmpty}>
<Ionicons name="chatbubbles-outline" size={48} color={colors.flow.textSecondary} />
<Text style={styles.historyEmptyText}>No chat history yet</Text>
</View> </View>
}
{/* History List */} />
<FlatList
data={chatHistory} {/* Close Button */}
renderItem={renderHistoryItem} <TouchableOpacity
keyExtractor={item => item.id} style={styles.closeButton}
style={styles.historyList} onPress={closeHistoryModal}
ListEmptyComponent={ >
<View style={styles.historyEmpty}> <Text style={styles.closeButtonText}>Close</Text>
<Ionicons name="chatbubbles-outline" size={48} color={colors.flow.textSecondary} /> </TouchableOpacity>
<Text style={styles.historyEmptyText}>No chat history yet</Text>
</View>
}
/>
{/* Close Button */}
<TouchableOpacity
style={styles.closeButton}
onPress={closeHistoryModal}
>
<Text style={styles.closeButtonText}>Close</Text>
</TouchableOpacity>
</Animated.View> </Animated.View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
</View> </View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
</Modal> </Modal>
{/* Role Selection Modal */}
<Modal
visible={showRoleModal}
animationType="fade"
transparent
onRequestClose={() => setShowRoleModal(false)}
>
<TouchableWithoutFeedback onPress={() => setShowRoleModal(false)}>
<View style={styles.modalOverlay}>
<TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}>
<View style={[styles.modalContent, styles.roleModalContent]}>
<View style={styles.modalHandle} />
<Text style={styles.modalTitle}>Choose AI Assistant</Text>
<ScrollView style={styles.roleList} showsVerticalScrollIndicator={false}>
{AI_CONFIG.ROLES.map((role) => (
<View key={role.id} style={styles.roleItemContainer}>
<View
style={[
styles.roleItem,
selectedRole.id === role.id && styles.roleItemActive
]}
>
<TouchableOpacity
style={styles.roleSelectionArea}
onPress={() => {
setSelectedRole(role as any);
setShowRoleModal(false);
}}
>
<View style={[
styles.roleItemIcon,
selectedRole.id === role.id && styles.roleItemIconActive
]}>
<Ionicons
name={role.icon as any}
size={20}
color={selectedRole.id === role.id ? '#fff' : colors.nautical.teal}
/>
</View>
<Text style={[
styles.roleItemName,
selectedRole.id === role.id && styles.roleItemNameActive
]}>
{role.name}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.infoButton}
onPress={() => {
setExpandedRoleId(expandedRoleId === role.id ? null : role.id);
}}
>
<Ionicons
name={expandedRoleId === role.id ? "close-circle-outline" : "information-circle-outline"}
size={24}
color={expandedRoleId === role.id ? colors.nautical.coral : colors.flow.textSecondary}
/>
</TouchableOpacity>
</View>
{expandedRoleId === role.id && (
<View style={styles.roleDescription}>
<Text style={styles.roleDescriptionText}>{role.description}</Text>
</View>
)}
</View>
))}
</ScrollView>
<TouchableOpacity
style={styles.closeButton}
onPress={() => setShowRoleModal(false)}
>
<Text style={styles.closeButtonText}>Cancel</Text>
</TouchableOpacity>
</View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
</Modal>
</View> </View>
); );
} }
@@ -853,16 +680,16 @@ export default function FlowScreen() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
// Container styles // Container styles
container: { container: {
flex: 1 flex: 1
}, },
gradient: { gradient: {
flex: 1 flex: 1
}, },
safeArea: { safeArea: {
flex: 1 flex: 1
}, },
// Header styles // Header styles
header: { header: {
flexDirection: 'row', flexDirection: 'row',
@@ -870,33 +697,12 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
paddingHorizontal: spacing.base, paddingHorizontal: spacing.base,
paddingTop: spacing.sm, paddingTop: spacing.sm,
paddingBottom: spacing.sm, paddingBottom: spacing.md,
borderBottomWidth: 1,
borderBottomColor: 'rgba(0,0,0,0.05)',
}, },
headerLeft: { headerLeft: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: spacing.sm, gap: spacing.sm,
flex: 1,
},
headerRoleButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.flow.cardBackground,
paddingHorizontal: spacing.sm,
paddingVertical: 6,
borderRadius: borderRadius.full,
marginHorizontal: spacing.sm,
borderWidth: 1,
borderColor: colors.flow.cardBorder,
maxWidth: '40%',
},
headerRoleText: {
fontSize: typography.fontSize.xs,
fontWeight: '600',
color: colors.flow.text,
marginHorizontal: 4,
}, },
iconCircle: { iconCircle: {
width: 44, width: 44,
@@ -925,20 +731,20 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
...shadows.soft, ...shadows.soft,
}, },
// Messages container styles // Messages container styles
messagesContainer: { messagesContainer: {
flex: 1 flex: 1
}, },
messagesContent: { messagesContent: {
padding: spacing.base, padding: spacing.base,
paddingTop: 0 paddingTop: 0
}, },
emptyContent: { emptyContent: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
}, },
// Empty state styles // Empty state styles
emptyState: { emptyState: {
alignItems: 'center', alignItems: 'center',
@@ -966,97 +772,7 @@ const styles = StyleSheet.create({
color: colors.flow.textSecondary, color: colors.flow.textSecondary,
textAlign: 'center', textAlign: 'center',
}, },
// Role selection styles
roleDropdown: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.flow.cardBackground,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: borderRadius.lg,
marginBottom: spacing.md,
...shadows.soft,
borderWidth: 1,
borderColor: colors.flow.cardBorder,
},
roleIcon: {
marginRight: spacing.sm,
},
roleDropdownText: {
fontSize: typography.fontSize.base,
fontWeight: '600',
color: colors.flow.text,
marginRight: spacing.xs,
},
roleModalContent: {
paddingBottom: spacing.xl,
},
roleList: {
marginTop: spacing.sm,
maxHeight: 400,
},
roleItemContainer: {
marginBottom: spacing.sm,
},
roleItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderRadius: borderRadius.lg,
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: 'transparent',
overflow: 'hidden',
},
roleItemActive: {
backgroundColor: colors.nautical.paleAqua,
borderColor: colors.nautical.lightMint,
},
roleSelectionArea: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
padding: spacing.md,
},
roleItemIcon: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: colors.flow.backgroundGradientStart,
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
roleItemIconActive: {
backgroundColor: colors.nautical.teal,
},
roleItemName: {
fontSize: typography.fontSize.base,
fontWeight: '500',
color: colors.flow.text,
},
roleItemNameActive: {
fontWeight: '700',
color: colors.nautical.teal,
},
infoButton: {
padding: spacing.md,
justifyContent: 'center',
alignItems: 'center',
},
roleDescription: {
paddingHorizontal: spacing.md + 36 + spacing.md, // icon width + margins
paddingBottom: spacing.sm,
paddingTop: 0,
},
roleDescriptionText: {
fontSize: typography.fontSize.sm,
color: colors.flow.textSecondary,
fontStyle: 'italic',
lineHeight: 18,
},
// Message bubble styles // Message bubble styles
messageBubble: { messageBubble: {
flexDirection: 'row', flexDirection: 'row',
@@ -1121,7 +837,7 @@ const styles = StyleSheet.create({
aiMessageTime: { aiMessageTime: {
color: colors.flow.textSecondary, color: colors.flow.textSecondary,
}, },
// Input bar styles // Input bar styles
inputBarContainer: { inputBarContainer: {
paddingHorizontal: spacing.base, paddingHorizontal: spacing.base,
@@ -1177,7 +893,7 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
// Modal styles // Modal styles
modalOverlay: { modalOverlay: {
flex: 1, flex: 1,
@@ -1225,7 +941,7 @@ const styles = StyleSheet.create({
fontWeight: '600', fontWeight: '600',
color: '#fff', color: '#fff',
}, },
// History list styles // History list styles
historyList: { historyList: {
flex: 1, flex: 1,

View File

@@ -14,11 +14,9 @@ import {
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons'; import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { Heir, HeirStatus, PaymentStrategy } from '../types'; import { Heir, HeirStatus, PaymentStrategy } from '../types';
import HeritageScreen from './HeritageScreen'; import HeritageScreen from './HeritageScreen';
import { VAULT_STORAGE_KEYS } from './SentinelScreen';
// Mock heirs data // Mock heirs data
const initialHeirs: Heir[] = [ const initialHeirs: Heir[] = [
@@ -222,8 +220,7 @@ export default function MeScreen() {
const [showHeritageModal, setShowHeritageModal] = useState(false); const [showHeritageModal, setShowHeritageModal] = useState(false);
const [showThemeModal, setShowThemeModal] = useState(false); const [showThemeModal, setShowThemeModal] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(false); const [isDarkMode, setIsDarkMode] = useState(false);
const [showSignOutModal, setShowSignOutModal] = useState(false);
// Heritage / Fleet Legacy states // Heritage / Fleet Legacy states
const [heirs, setHeirs] = useState<Heir[]>(initialHeirs); const [heirs, setHeirs] = useState<Heir[]>(initialHeirs);
const [showAddHeirModal, setShowAddHeirModal] = useState(false); const [showAddHeirModal, setShowAddHeirModal] = useState(false);
@@ -297,26 +294,18 @@ export default function MeScreen() {
}; };
const handleAbandonIsland = () => { const handleAbandonIsland = () => {
console.log('[MeScreen] Sign out button clicked'); Alert.alert(
setShowSignOutModal(true); 'Sign Out',
}; 'Are you sure you want to sign out?',
[
const handleConfirmSignOut = () => { { text: 'Cancel', style: 'cancel' },
console.log('[MeScreen] User confirmed sign out'); {
setShowSignOutModal(false); text: 'Sign Out',
signOut(); style: 'destructive',
}; onPress: signOut
},
const handleResetVault = async () => { ]
try { );
await AsyncStorage.multiRemove([
VAULT_STORAGE_KEYS.INITIALIZED,
VAULT_STORAGE_KEYS.SHARE_DEVICE,
]);
Alert.alert('Done', 'Vault state reset. Go to Sentinel → Open Shadow Vault to see first-time flow.');
} catch (e) {
Alert.alert('Error', 'Failed to reset vault state.');
}
}; };
return ( return (
@@ -629,10 +618,10 @@ export default function MeScreen() {
activeOpacity={0.85} activeOpacity={0.85}
> >
<View style={{ flexDirection: 'row', alignItems: 'center', gap: spacing.sm }}> <View style={{ flexDirection: 'row', alignItems: 'center', gap: spacing.sm }}>
<Ionicons <Ionicons
name={isDarkMode ? 'moon' : 'sunny'} name={isDarkMode ? 'moon' : 'sunny'}
size={18} size={18}
color={colors.me.primary} color={colors.me.primary}
/> />
<Text style={styles.sanctumText}>Dark Mode</Text> <Text style={styles.sanctumText}>Dark Mode</Text>
</View> </View>
@@ -896,21 +885,6 @@ export default function MeScreen() {
<Text style={styles.sanctumValue}>View</Text> <Text style={styles.sanctumValue}>View</Text>
</View> </View>
</View> </View>
{__DEV__ && (
<View style={styles.sanctumSection}>
<Text style={styles.tideLabel}>DEV ONLY</Text>
<TouchableOpacity
style={styles.devResetButton}
onPress={handleResetVault}
activeOpacity={0.85}
>
<Ionicons name="refresh" size={16} color={colors.nautical.coral} />
<Text style={styles.devResetText}>Reset Vault State</Text>
</TouchableOpacity>
<Text style={styles.sanctumHint}>Clear hasVaultInitialized & Share A. Test first-open flow.</Text>
</View>
)}
</ScrollView> </ScrollView>
<View style={styles.tideModalButtons}> <View style={styles.tideModalButtons}>
<TouchableOpacity <TouchableOpacity
@@ -1438,46 +1412,6 @@ export default function MeScreen() {
</View> </View>
</View> </View>
</Modal> </Modal>
{/* Sign Out Confirmation Modal */}
<Modal
visible={showSignOutModal}
animationType="fade"
transparent
onRequestClose={() => setShowSignOutModal(false)}
>
<View style={styles.spiritOverlay}>
<View style={styles.signOutModal}>
<View style={styles.signOutHeader}>
<View style={styles.signOutIcon}>
<Feather name="log-out" size={32} color={colors.nautical.coral} />
</View>
<Text style={styles.signOutTitle}>Sign Out</Text>
<Text style={styles.signOutMessage}>
Are you sure you want to sign out? You'll need to log in again to access your account.
</Text>
</View>
<View style={styles.signOutButtons}>
<TouchableOpacity
style={styles.signOutCancelButton}
onPress={() => setShowSignOutModal(false)}
activeOpacity={0.85}
>
<Text style={styles.signOutCancelText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.signOutConfirmButton}
onPress={handleConfirmSignOut}
activeOpacity={0.85}
>
<Text style={styles.signOutConfirmText}>Sign Out</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</View> </View>
); );
} }
@@ -1880,23 +1814,6 @@ const styles = StyleSheet.create({
fontSize: typography.fontSize.sm, fontSize: typography.fontSize.sm,
color: colors.me.textSecondary, color: colors.me.textSecondary,
}, },
devResetButton: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
backgroundColor: `${colors.nautical.coral}15`,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: `${colors.nautical.coral}40`,
alignSelf: 'flex-start',
},
devResetText: {
fontSize: typography.fontSize.sm,
color: colors.nautical.coral,
fontWeight: '600',
},
sanctumAlert: { sanctumAlert: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@@ -2416,71 +2333,4 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
zIndex: 10, zIndex: 10,
}, },
// Sign Out Modal Styles
signOutModal: {
backgroundColor: colors.me.cardBackground,
borderRadius: borderRadius.xl,
padding: spacing.xl,
marginHorizontal: spacing.xl,
maxWidth: 400,
width: '100%',
...shadows.medium,
},
signOutHeader: {
alignItems: 'center',
marginBottom: spacing.xl,
},
signOutIcon: {
width: 72,
height: 72,
borderRadius: 36,
backgroundColor: `${colors.nautical.coral}15`,
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.base,
},
signOutTitle: {
fontSize: typography.fontSize.xl,
fontWeight: '700',
color: colors.me.text,
marginBottom: spacing.sm,
},
signOutMessage: {
fontSize: typography.fontSize.base,
color: colors.me.textSecondary,
textAlign: 'center',
lineHeight: typography.fontSize.base * 1.5,
},
signOutButtons: {
flexDirection: 'row',
gap: spacing.md,
},
signOutCancelButton: {
flex: 1,
paddingVertical: spacing.base,
paddingHorizontal: spacing.lg,
borderRadius: borderRadius.lg,
backgroundColor: colors.me.cardBorder,
alignItems: 'center',
justifyContent: 'center',
},
signOutCancelText: {
fontSize: typography.fontSize.base,
fontWeight: '600',
color: colors.me.text,
},
signOutConfirmButton: {
flex: 1,
paddingVertical: spacing.base,
paddingHorizontal: spacing.lg,
borderRadius: borderRadius.lg,
backgroundColor: colors.nautical.coral,
alignItems: 'center',
justifyContent: 'center',
},
signOutConfirmText: {
fontSize: typography.fontSize.base,
fontWeight: '600',
color: '#fff',
},
}); });

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect } from 'react';
import { import {
View, View,
Text, Text,
@@ -8,107 +8,18 @@ import {
SafeAreaView, SafeAreaView,
Animated, Animated,
Modal, Modal,
TextInput,
KeyboardAvoidingView,
Platform,
Share,
Alert,
Linking,
} from 'react-native'; } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons'; import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
import { captureRef } from 'react-native-view-shot';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
import { SystemStatus, KillSwitchLog } from '../types'; import { SystemStatus, KillSwitchLog } from '../types';
import VaultScreen from './VaultScreen'; import VaultScreen from './VaultScreen';
import BiometricModal from '../components/common/BiometricModal';
import {
SSSShare,
mnemonicToEntropy,
splitSecret,
formatShareCompact,
serializeShare,
verifyShares,
} from '../utils/sss';
// Vault storage keys (for testing: clear these to simulate first-open)
export const VAULT_STORAGE_KEYS = {
INITIALIZED: 'sentinel_vault_initialized',
SHARE_DEVICE: 'sentinel_share_device',
} as const;
// Nautical-themed mnemonic word list (unique words only)
const MNEMONIC_WORDS = [
'anchor', 'harbor', 'compass', 'lighthouse', 'current', 'ocean', 'tide', 'voyage',
'keel', 'stern', 'bow', 'mast', 'sail', 'port', 'starboard', 'reef',
'signal', 'beacon', 'chart', 'helm', 'gale', 'calm', 'cove', 'isle',
'horizon', 'sextant', 'sound', 'drift', 'wake', 'mariner', 'pilot', 'fathom',
'buoy', 'lantern', 'harpoon', 'lagoon', 'bay', 'strait', 'riptide', 'foam',
'coral', 'pearl', 'trident', 'ebb', 'flow', 'vault', 'cipher', 'shroud',
'salt', 'wave', 'grotto', 'storm', 'north', 'south', 'east', 'west',
'ember', 'cabin', 'ledger', 'torch', 'sanctum', 'oath', 'depths', 'captain',
] as const;
// Animation timing constants
const ANIMATION_DURATION = {
pulse: 1200,
glow: 1500,
rotate: 30000,
heartbeatPress: 150,
} as const;
const generateMnemonic = (wordCount = 12) => {
const words: string[] = [];
for (let i = 0; i < wordCount; i += 1) {
const index = Math.floor(Math.random() * MNEMONIC_WORDS.length);
words.push(MNEMONIC_WORDS[index]);
}
return words;
};
/**
* Generate SSS shares from mnemonic words
* Uses Shamir's Secret Sharing (3,2) threshold scheme
*/
const generateSSSShares = (words: string[]): SSSShare[] => {
try {
// Convert mnemonic to entropy (big integer)
const entropy = mnemonicToEntropy(words, MNEMONIC_WORDS);
// Split entropy into 3 shares using SSS
const shares = splitSecret(entropy);
// Verify shares can recover the original (optional, for debugging)
if (__DEV__) {
const isValid = verifyShares(shares, entropy);
if (!isValid) {
console.warn('SSS verification failed!');
} else {
console.log('SSS shares verified successfully');
}
}
return shares;
} catch (error) {
console.error('Failed to generate SSS shares:', error);
// Fallback: return empty shares (should not happen in production)
return [
{ x: 1, y: BigInt(0), label: 'device' },
{ x: 2, y: BigInt(0), label: 'cloud' },
{ x: 3, y: BigInt(0), label: 'heir' },
];
}
};
// Icon names type for type safety
type StatusIconName = 'checkmark-circle' | 'warning' | 'alert-circle';
// Status configuration with nautical theme // Status configuration with nautical theme
const statusConfig: Record<SystemStatus, { const statusConfig: Record<SystemStatus, {
color: string; color: string;
label: string; label: string;
icon: StatusIconName; icon: string;
description: string; description: string;
gradientColors: [string, string]; gradientColors: [string, string];
}> = { }> = {
@@ -168,200 +79,76 @@ export default function SentinelScreen() {
const [glowAnim] = useState(new Animated.Value(0.5)); const [glowAnim] = useState(new Animated.Value(0.5));
const [rotateAnim] = useState(new Animated.Value(0)); const [rotateAnim] = useState(new Animated.Value(0));
const [showVault, setShowVault] = useState(false); const [showVault, setShowVault] = useState(false);
const [showMnemonic, setShowMnemonic] = useState(false);
const [mnemonicWords, setMnemonicWords] = useState<string[]>([]);
const [sssShares, setSssShares] = useState<SSSShare[]>([]);
const [showEmailForm, setShowEmailForm] = useState(false);
const [emailAddress, setEmailAddress] = useState('');
const [emailRecipientType, setEmailRecipientType] = useState<'self' | 'heir'>('self');
const [isCapturing, setIsCapturing] = useState(false);
const [hasVaultInitialized, setHasVaultInitialized] = useState<boolean | null>(null);
const [showSetupBiometric, setShowSetupBiometric] = useState(false);
const mnemonicRef = useRef<View>(null);
// Load vault init status on mount (1.1)
useEffect(() => {
AsyncStorage.getItem(VAULT_STORAGE_KEYS.INITIALIZED)
.then((val) => setHasVaultInitialized(val === 'true'))
.catch(() => setHasVaultInitialized(false));
}, []);
useEffect(() => { useEffect(() => {
// Pulse animation // Pulse animation
const pulseAnimation = Animated.loop( Animated.loop(
Animated.sequence([ Animated.sequence([
Animated.timing(pulseAnim, { Animated.timing(pulseAnim, {
toValue: 1.06, toValue: 1.06,
duration: ANIMATION_DURATION.pulse, duration: 1200,
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.timing(pulseAnim, { Animated.timing(pulseAnim, {
toValue: 1, toValue: 1,
duration: ANIMATION_DURATION.pulse, duration: 1200,
useNativeDriver: true, useNativeDriver: true,
}), }),
]) ])
); ).start();
pulseAnimation.start();
// Glow animation // Glow animation
const glowAnimation = Animated.loop( Animated.loop(
Animated.sequence([ Animated.sequence([
Animated.timing(glowAnim, { Animated.timing(glowAnim, {
toValue: 1, toValue: 1,
duration: ANIMATION_DURATION.glow, duration: 1500,
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.timing(glowAnim, { Animated.timing(glowAnim, {
toValue: 0.5, toValue: 0.5,
duration: ANIMATION_DURATION.glow, duration: 1500,
useNativeDriver: true, useNativeDriver: true,
}), }),
]) ])
); ).start();
glowAnimation.start();
// Slow rotate for ship wheel // Slow rotate for ship wheel
const rotateAnimation = Animated.loop( Animated.loop(
Animated.timing(rotateAnim, { Animated.timing(rotateAnim, {
toValue: 1, toValue: 1,
duration: ANIMATION_DURATION.rotate, duration: 30000,
useNativeDriver: true, useNativeDriver: true,
}) })
); ).start();
rotateAnimation.start(); }, []);
// Cleanup animations on unmount to prevent memory leaks const openVault = () => {
return () => {
pulseAnimation.stop();
glowAnimation.stop();
rotateAnimation.stop();
};
}, [pulseAnim, glowAnim, rotateAnim]);
const startFirstTimeSetup = () => {
const words = generateMnemonic();
const shares = generateSSSShares(words);
setMnemonicWords(words);
setSssShares(shares);
setShowMnemonic(true);
setShowVault(false);
setShowEmailForm(false);
setEmailAddress('');
setEmailRecipientType('self');
if (shares[0]) {
AsyncStorage.setItem(VAULT_STORAGE_KEYS.SHARE_DEVICE, serializeShare(shares[0])).catch(() => {});
}
};
const completeSetupAndEnterVault = () => {
setShowMnemonic(false);
setShowEmailForm(false);
setEmailAddress('');
setShowSetupBiometric(true);
};
const handleSetupBiometricSuccess = () => {
setShowSetupBiometric(false);
AsyncStorage.setItem(VAULT_STORAGE_KEYS.INITIALIZED, 'true').catch(() => {});
setHasVaultInitialized(true);
setShowVault(true); setShowVault(true);
}; };
const handleSetupBiometricSkip = () => {
setShowSetupBiometric(false);
AsyncStorage.setItem(VAULT_STORAGE_KEYS.INITIALIZED, 'true').catch(() => {});
setHasVaultInitialized(true);
setShowVault(true);
};
const handleOpenVault = () => {
if (hasVaultInitialized === true) {
setShowVault(true);
setShowMnemonic(false);
} else {
startFirstTimeSetup();
}
};
const handleScreenshot = async () => {
try {
setIsCapturing(true);
const uri = await captureRef(mnemonicRef, {
format: 'png',
quality: 1,
result: 'tmpfile',
});
await Share.share({
url: uri,
message: 'Sentinel key backup',
});
completeSetupAndEnterVault();
} catch (error) {
Alert.alert('Screenshot failed', 'Please try again or use email backup.');
} finally {
setIsCapturing(false);
}
};
const handleEmailBackup = () => {
setShowEmailForm(true);
};
const handleCompleteBackupLocal = () => {
completeSetupAndEnterVault();
};
const handleSendEmail = async () => {
const trimmed = emailAddress.trim();
if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
Alert.alert('Invalid email', 'Please enter a valid email address.');
return;
}
const subject = encodeURIComponent(
emailRecipientType === 'heir'
? 'Sentinel Vault - Share C (Heir)'
: 'Sentinel Vault Recovery Key'
);
const body = encodeURIComponent(
emailRecipientType === 'heir'
? `Share C (for heir, 2-of-3 required to recover):\n${sssShares[2] ? serializeShare(sssShares[2]) : ''}\n\nKeep this secure. Combined with Share B from Sentinel cloud, this can restore the vault.`
: `Your 12-word mnemonic (backup for yourself):\n${mnemonicWords.join(' ')}`
);
const mailtoUrl = `mailto:${trimmed}?subject=${subject}&body=${body}`;
try {
await Linking.openURL(mailtoUrl);
completeSetupAndEnterVault();
} catch (error) {
Alert.alert('Email failed', 'Unable to open email client.');
}
};
const handleHeartbeat = () => { const handleHeartbeat = () => {
// Animate pulse // Animate pulse
Animated.sequence([ Animated.sequence([
Animated.timing(pulseAnim, { Animated.timing(pulseAnim, {
toValue: 1.15, toValue: 1.15,
duration: ANIMATION_DURATION.heartbeatPress, duration: 150,
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.timing(pulseAnim, { Animated.timing(pulseAnim, {
toValue: 1, toValue: 1,
duration: ANIMATION_DURATION.heartbeatPress, duration: 150,
useNativeDriver: true, useNativeDriver: true,
}), }),
]).start(); ]).start();
// Add new log using functional update to avoid stale closure // Add new log
const newLog: KillSwitchLog = { const newLog: KillSwitchLog = {
id: Date.now().toString(), id: Date.now().toString(),
action: 'HEARTBEAT_CONFIRMED', action: 'HEARTBEAT_CONFIRMED',
timestamp: new Date(), timestamp: new Date(),
}; };
setLogs((prevLogs) => [newLog, ...prevLogs]); setLogs([newLog, ...logs]);
// Reset status if warning // Reset status if warning
if (status === 'warning') { if (status === 'warning') {
@@ -439,7 +226,7 @@ export default function SentinelScreen() {
colors={currentStatus.gradientColors} colors={currentStatus.gradientColors}
style={styles.statusCircle} style={styles.statusCircle}
> >
<Ionicons name={currentStatus.icon} size={56} color="#fff" /> <Ionicons name={currentStatus.icon as any} size={56} color="#fff" />
</LinearGradient> </LinearGradient>
</Animated.View> </Animated.View>
<Text style={[styles.statusLabel, { color: currentStatus.color }]}> <Text style={[styles.statusLabel, { color: currentStatus.color }]}>
@@ -503,10 +290,8 @@ export default function SentinelScreen() {
</View> </View>
<TouchableOpacity <TouchableOpacity
style={styles.vaultAccessButton} style={styles.vaultAccessButton}
onPress={handleOpenVault} onPress={openVault}
activeOpacity={0.8} activeOpacity={0.8}
accessibilityLabel="Open Shadow Vault"
accessibilityRole="button"
> >
<Text style={styles.vaultAccessButtonText}>Open</Text> <Text style={styles.vaultAccessButtonText}>Open</Text>
</TouchableOpacity> </TouchableOpacity>
@@ -517,8 +302,6 @@ export default function SentinelScreen() {
style={styles.heartbeatButton} style={styles.heartbeatButton}
onPress={handleHeartbeat} onPress={handleHeartbeat}
activeOpacity={0.9} activeOpacity={0.9}
accessibilityLabel="Signal the watch - Confirm your presence"
accessibilityRole="button"
> >
<LinearGradient <LinearGradient
colors={[colors.nautical.teal, colors.nautical.seafoam]} colors={[colors.nautical.teal, colors.nautical.seafoam]}
@@ -565,172 +348,17 @@ export default function SentinelScreen() {
onRequestClose={() => setShowVault(false)} onRequestClose={() => setShowVault(false)}
> >
<View style={styles.vaultModalContainer}> <View style={styles.vaultModalContainer}>
<VaultScreen /> {showVault ? <VaultScreen /> : null}
<TouchableOpacity <TouchableOpacity
style={styles.vaultCloseButton} style={styles.vaultCloseButton}
onPress={() => setShowVault(false)} onPress={() => setShowVault(false)}
activeOpacity={0.85} activeOpacity={0.85}
accessibilityLabel="Close vault"
accessibilityRole="button"
> >
<Ionicons name="close" size={20} color={colors.nautical.cream} /> <Ionicons name="close" size={20} color={colors.nautical.cream} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</Modal> </Modal>
{/* Mnemonic Modal */}
<Modal
visible={showMnemonic}
animationType="fade"
transparent
onRequestClose={() => setShowMnemonic(false)}
>
<KeyboardAvoidingView
style={styles.mnemonicOverlay}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView
style={styles.mnemonicScroll}
contentContainerStyle={styles.mnemonicScrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<View ref={mnemonicRef} collapsable={false}>
<LinearGradient
colors={[colors.sentinel.cardBackground, colors.sentinel.backgroundGradientEnd]}
style={styles.mnemonicCard}
>
<TouchableOpacity
style={styles.mnemonicClose}
onPress={() => setShowMnemonic(false)}
activeOpacity={0.85}
accessibilityLabel="Close mnemonic modal"
accessibilityRole="button"
>
<Ionicons name="close" size={18} color={colors.sentinel.textSecondary} />
</TouchableOpacity>
<View style={styles.mnemonicHeader}>
<MaterialCommunityIcons name="key-variant" size={22} color={colors.sentinel.primary} />
<Text style={styles.mnemonicTitle}>12-Word Mnemonic</Text>
</View>
<Text style={styles.mnemonicSubtitle}>
Your seed is protected by SSS (3,2) threshold encryption. Any 2 shares can restore your vault.
</Text>
<View style={styles.mnemonicBlock}>
<Text style={styles.mnemonicBlockText}>
{mnemonicWords.join(' ')}
</Text>
</View>
<View style={styles.partGrid}>
<View style={[styles.partCard, styles.partCardStored]}>
<Text style={styles.partLabel}>SHARE A DEVICE</Text>
<Text style={styles.partValue}>
{sssShares[0] ? formatShareCompact(sssShares[0]) : '---'}
</Text>
<Text style={styles.partHint}>Stored on this device</Text>
</View>
<View style={styles.partCard}>
<Text style={styles.partLabel}>SHARE B CLOUD</Text>
<Text style={styles.partValue}>
{sssShares[1] ? formatShareCompact(sssShares[1]) : '---'}
</Text>
<Text style={styles.partHint}>To be synced to Sentinel</Text>
</View>
<View style={styles.partCard}>
<Text style={styles.partLabel}>SHARE C HEIR</Text>
<Text style={styles.partValue}>
{sssShares[2] ? formatShareCompact(sssShares[2]) : '---'}
</Text>
<Text style={styles.partHint}>For your heir (2-of-3 required)</Text>
</View>
</View>
<TouchableOpacity
style={[styles.mnemonicPrimaryButton, isCapturing && styles.mnemonicButtonDisabled]}
onPress={handleScreenshot}
activeOpacity={0.85}
disabled={isCapturing}
accessibilityLabel="Take screenshot backup of mnemonic"
accessibilityRole="button"
accessibilityState={{ disabled: isCapturing }}
>
<Text style={styles.mnemonicPrimaryText}>
{isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.mnemonicSecondaryButton}
onPress={handleEmailBackup}
activeOpacity={0.85}
accessibilityLabel="Send backup via email"
accessibilityRole="button"
>
<Text style={styles.mnemonicSecondaryText}>EMAIL BACKUP</Text>
</TouchableOpacity>
{showEmailForm ? (
<View style={styles.emailForm}>
<View style={styles.emailTypeRow}>
<TouchableOpacity
style={[styles.emailTypeButton, emailRecipientType === 'self' && styles.emailTypeButtonActive]}
onPress={() => setEmailRecipientType('self')}
>
<Text style={[styles.emailTypeText, emailRecipientType === 'self' && styles.emailTypeTextActive]}>
To Myself
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.emailTypeButton, emailRecipientType === 'heir' && styles.emailTypeButtonActive]}
onPress={() => setEmailRecipientType('heir')}
>
<Text style={[styles.emailTypeText, emailRecipientType === 'heir' && styles.emailTypeTextActive]}>
To Heir
</Text>
</TouchableOpacity>
</View>
<TextInput
style={styles.emailInput}
value={emailAddress}
onChangeText={setEmailAddress}
placeholder={emailRecipientType === 'heir' ? 'heir@email.com' : 'you@email.com'}
placeholderTextColor={colors.sentinel.textSecondary}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity
style={styles.emailSendButton}
onPress={handleSendEmail}
activeOpacity={0.85}
accessibilityLabel="Send backup email"
accessibilityRole="button"
>
<Text style={styles.emailSendText}>SEND EMAIL</Text>
</TouchableOpacity>
</View>
) : null}
<TouchableOpacity
style={styles.mnemonicTertiaryButton}
onPress={handleCompleteBackupLocal}
activeOpacity={0.85}
accessibilityLabel="I've completed backup locally"
accessibilityRole="button"
>
<Text style={styles.mnemonicTertiaryText}>COMPLETE BACKUP (LOCAL)</Text>
</TouchableOpacity>
</LinearGradient>
</View>
</ScrollView>
</KeyboardAvoidingView>
</Modal>
{/* Biometric setup prompt after first-time backup (1.4) */}
<BiometricModal
visible={showSetupBiometric}
onSuccess={handleSetupBiometricSuccess}
onCancel={handleSetupBiometricSkip}
title="Quick Unlock"
message="Enable Face ID / Touch ID for faster access next time?"
isDark
/>
</View> </View>
); );
} }
@@ -991,196 +619,4 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
mnemonicOverlay: {
flex: 1,
backgroundColor: 'rgba(11, 20, 24, 0.72)',
justifyContent: 'center',
padding: spacing.lg,
},
mnemonicScroll: {
flex: 1,
},
mnemonicScrollContent: {
flexGrow: 1,
justifyContent: 'center',
},
mnemonicCard: {
borderRadius: borderRadius.xl,
padding: spacing.lg,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
...shadows.glow,
},
mnemonicHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
marginBottom: spacing.sm,
},
mnemonicClose: {
position: 'absolute',
top: spacing.sm,
right: spacing.sm,
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(26, 58, 74, 0.35)',
},
mnemonicTitle: {
fontSize: typography.fontSize.lg,
fontWeight: '700',
color: colors.sentinel.text,
letterSpacing: typography.letterSpacing.wide,
},
mnemonicSubtitle: {
fontSize: typography.fontSize.sm,
color: colors.sentinel.textSecondary,
marginBottom: spacing.md,
},
mnemonicBlock: {
backgroundColor: colors.sentinel.cardBackground,
borderRadius: borderRadius.lg,
paddingVertical: spacing.md,
paddingHorizontal: spacing.md,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
marginBottom: spacing.lg,
},
partGrid: {
gap: spacing.sm,
marginBottom: spacing.lg,
},
partCard: {
backgroundColor: colors.sentinel.cardBackground,
borderRadius: borderRadius.lg,
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
},
partCardStored: {
borderColor: colors.sentinel.primary,
},
partLabel: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.textSecondary,
letterSpacing: typography.letterSpacing.wide,
marginBottom: 4,
fontWeight: '600',
},
partValue: {
fontSize: typography.fontSize.md,
color: colors.sentinel.text,
fontFamily: typography.fontFamily.mono,
fontWeight: '700',
marginBottom: 2,
},
partHint: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.textSecondary,
},
mnemonicBlockText: {
fontSize: typography.fontSize.sm,
color: colors.sentinel.text,
fontFamily: typography.fontFamily.mono,
fontWeight: '600',
lineHeight: 22,
textAlign: 'center',
},
mnemonicPrimaryButton: {
backgroundColor: colors.sentinel.primary,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
alignItems: 'center',
marginBottom: spacing.sm,
},
mnemonicButtonDisabled: {
opacity: 0.6,
},
mnemonicPrimaryText: {
color: colors.nautical.cream,
fontWeight: '700',
letterSpacing: typography.letterSpacing.wide,
},
mnemonicSecondaryButton: {
backgroundColor: 'transparent',
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
alignItems: 'center',
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
},
mnemonicSecondaryText: {
color: colors.sentinel.text,
fontWeight: '700',
letterSpacing: typography.letterSpacing.wide,
},
mnemonicTertiaryButton: {
backgroundColor: 'transparent',
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
alignItems: 'center',
marginTop: spacing.sm,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
borderStyle: 'dashed',
},
mnemonicTertiaryText: {
color: colors.sentinel.textSecondary,
fontWeight: '600',
letterSpacing: typography.letterSpacing.wide,
fontSize: typography.fontSize.sm,
},
emailTypeRow: {
flexDirection: 'row',
gap: spacing.sm,
marginBottom: spacing.sm,
},
emailTypeButton: {
flex: 1,
paddingVertical: spacing.sm,
borderRadius: borderRadius.lg,
alignItems: 'center',
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
},
emailTypeButtonActive: {
borderColor: colors.sentinel.primary,
backgroundColor: `${colors.sentinel.primary}15`,
},
emailTypeText: {
fontSize: typography.fontSize.sm,
color: colors.sentinel.textSecondary,
fontWeight: '600',
},
emailTypeTextActive: {
color: colors.sentinel.primary,
},
emailForm: {
marginTop: spacing.sm,
},
emailInput: {
height: 44,
borderRadius: borderRadius.full,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
paddingHorizontal: spacing.md,
color: colors.sentinel.text,
fontSize: typography.fontSize.sm,
backgroundColor: 'rgba(255, 255, 255, 0.02)',
marginBottom: spacing.sm,
},
emailSendButton: {
backgroundColor: colors.nautical.teal,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
alignItems: 'center',
},
emailSendText: {
color: colors.nautical.cream,
fontWeight: '700',
letterSpacing: typography.letterSpacing.wide,
},
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -95,7 +95,7 @@ export const aiService = {
} }
const url = buildApiUrl(API_ENDPOINTS.AI.PROXY); const url = buildApiUrl(API_ENDPOINTS.AI.PROXY);
logApiDebug('AI Request', { logApiDebug('AI Request', {
url, url,
hasToken: !!token, hasToken: !!token,
@@ -114,7 +114,7 @@ export const aiService = {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
logApiDebug('AI Error Response', errorText); logApiDebug('AI Error Response', errorText);
let errorDetail = 'AI request failed'; let errorDetail = 'AI request failed';
try { try {
const errorData = JSON.parse(errorText); const errorData = JSON.parse(errorText);
@@ -131,7 +131,7 @@ export const aiService = {
model: data.model, model: data.model,
choicesCount: data.choices?.length, choicesCount: data.choices?.length,
}); });
return data; return data;
} catch (error) { } catch (error) {
console.error('AI proxy error:', error); console.error('AI proxy error:', error);
@@ -143,14 +143,13 @@ export const aiService = {
* Simple helper for single message chat * Simple helper for single message chat
* @param content - User message content * @param content - User message content
* @param token - JWT token for authentication * @param token - JWT token for authentication
* @param systemPrompt - Optional custom system prompt
* @returns AI response text * @returns AI response text
*/ */
async sendMessage(content: string, token?: string, systemPrompt?: string): Promise<string> { async sendMessage(content: string, token?: string): Promise<string> {
const messages: AIMessage[] = [ const messages: AIMessage[] = [
{ {
role: 'system', role: 'system',
content: systemPrompt || AI_CONFIG.DEFAULT_SYSTEM_PROMPT, content: AI_CONFIG.DEFAULT_SYSTEM_PROMPT,
}, },
{ {
role: 'user', role: 'user',
@@ -180,7 +179,7 @@ export const aiService = {
} }
const url = buildApiUrl(API_ENDPOINTS.AI.PROXY); const url = buildApiUrl(API_ENDPOINTS.AI.PROXY);
logApiDebug('AI Image Request', { logApiDebug('AI Image Request', {
url, url,
hasToken: !!token, hasToken: !!token,
@@ -218,7 +217,7 @@ export const aiService = {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
logApiDebug('AI Image Error Response', errorText); logApiDebug('AI Image Error Response', errorText);
let errorDetail = 'AI image request failed'; let errorDetail = 'AI image request failed';
try { try {
const errorData = JSON.parse(errorText); const errorData = JSON.parse(errorText);
@@ -234,7 +233,7 @@ export const aiService = {
id: data.id, id: data.id,
model: data.model, model: data.model,
}); });
return data.choices[0]?.message?.content || 'No response'; return data.choices[0]?.message?.content || 'No response';
} catch (error) { } catch (error) {
console.error('AI image proxy error:', error); console.error('AI image proxy error:', error);

View File

@@ -1,120 +0,0 @@
/**
* Storage Service
*
* Handles local persistence of chat history and active conversations
* using AsyncStorage with user-specific isolation.
*/
import AsyncStorage from '@react-native-async-storage/async-storage';
// =============================================================================
// Constants
// =============================================================================
const STORAGE_KEYS = {
CHAT_HISTORY: '@sentinel:chat_history',
CURRENT_MESSAGES: '@sentinel:current_messages',
} as const;
// =============================================================================
// Service Implementation
// =============================================================================
export const storageService = {
/**
* Get user-specific storage key
*/
getUserKey(baseKey: string, userId: string | number): string {
return `${baseKey}:user_${userId}`;
},
/**
* Save the complete list of chat sessions to local storage for a specific user
*/
async saveChatHistory(history: any[], userId: string | number): Promise<void> {
try {
const jsonValue = JSON.stringify(history);
const key = this.getUserKey(STORAGE_KEYS.CHAT_HISTORY, userId);
await AsyncStorage.setItem(key, jsonValue);
console.log(`[Storage] Saved chat history for user ${userId}:`, history.length, 'sessions');
} catch (e) {
console.error('Error saving chat history:', e);
}
},
/**
* Load the list of chat sessions from local storage for a specific user
*/
async getChatHistory(userId: string | number): Promise<any[]> {
try {
const key = this.getUserKey(STORAGE_KEYS.CHAT_HISTORY, userId);
const jsonValue = await AsyncStorage.getItem(key);
const result = jsonValue != null ? JSON.parse(jsonValue) : [];
console.log(`[Storage] Loaded chat history for user ${userId}:`, result.length, 'sessions');
return result;
} catch (e) {
console.error('Error getting chat history:', e);
return [];
}
},
/**
* Save the current active conversation messages for a specific role and user
*/
async saveCurrentChat(roleId: string, messages: any[], userId: string | number): Promise<void> {
try {
const jsonValue = JSON.stringify(messages);
const key = `${this.getUserKey(STORAGE_KEYS.CURRENT_MESSAGES, userId)}:${roleId}`;
await AsyncStorage.setItem(key, jsonValue);
console.log(`[Storage] Saved current chat for user ${userId}, role ${roleId}:`, messages.length, 'messages');
} catch (e) {
console.error(`Error saving current chat for role ${roleId}:`, e);
}
},
/**
* Load the current active conversation messages for a specific role and user
*/
async getCurrentChat(roleId: string, userId: string | number): Promise<any[]> {
try {
const key = `${this.getUserKey(STORAGE_KEYS.CURRENT_MESSAGES, userId)}:${roleId}`;
const jsonValue = await AsyncStorage.getItem(key);
const result = jsonValue != null ? JSON.parse(jsonValue) : [];
console.log(`[Storage] Loaded current chat for user ${userId}, role ${roleId}:`, result.length, 'messages');
return result;
} catch (e) {
console.error(`Error getting current chat for role ${roleId}:`, e);
return [];
}
},
/**
* Clear all stored chat data for a specific user
*/
async clearUserData(userId: string | number): Promise<void> {
try {
const keys = await AsyncStorage.getAllKeys();
const userPrefix = `:user_${userId}`;
const userKeys = keys.filter(key => key.includes(userPrefix));
await AsyncStorage.multiRemove(userKeys);
console.log(`[Storage] Cleared all data for user ${userId}:`, userKeys.length, 'keys removed');
} catch (e) {
console.error('Error clearing user data:', e);
}
},
/**
* Clear all stored chat data (all users)
*/
async clearAllData(): Promise<void> {
try {
const keys = await AsyncStorage.getAllKeys();
const sentinelKeys = keys.filter(key => key.startsWith('@sentinel:'));
await AsyncStorage.multiRemove(sentinelKeys);
console.log('[Storage] Cleared all data:', sentinelKeys.length, 'keys removed');
} catch (e) {
console.error('Error clearing storage data:', e);
}
}
};

View File

@@ -77,6 +77,7 @@ export interface ProtocolInfo {
export interface User { export interface User {
id: number; id: number;
username: string; username: string;
email?: string;
public_key: string; public_key: string;
is_admin: boolean; is_admin: boolean;
guale: boolean; guale: boolean;

View File

@@ -1,5 +0,0 @@
/**
* Utility functions for Sentinel
*/
export * from './sss';

View File

@@ -1,268 +0,0 @@
/**
* Shamir's Secret Sharing (SSS) Implementation
*
* This implements a (3,2) threshold scheme where:
* - Secret is split into 3 shares
* - Any 2 shares can recover the original secret
*
* Correspondence with crypto_core_demo (Python):
* - sp_trust_sharding.py: split_to_shares(), recover_from_shares()
* - Same algorithm: f(x) = secret + a*x (mod P), Lagrange interpolation
* - Difference: entropy conversion. Python uses BIP-39 (mnemonic.to_entropy);
* we use custom word list index-based encoding for compatibility with
* existing MNEMONIC_WORDS. SSS split/recover logic is identical.
*/
// Use a large prime for the finite field
// We use 2^127 - 1 (a Mersenne prime) which fits well in BigInt
// This is smaller than the Python version's 2^521 - 1 but sufficient for our 128-bit entropy
const PRIME = BigInt('170141183460469231731687303715884105727'); // 2^127 - 1
/**
* Represents an SSS share as a coordinate point (x, y)
*/
export interface SSSShare {
x: number;
y: bigint;
label: string; // 'device' | 'cloud' | 'heir'
}
/**
* Generate a cryptographically secure random BigInt in range [0, max)
*/
function secureRandomBigInt(max: bigint): bigint {
// Get the number of bytes needed
const byteLength = Math.ceil(max.toString(2).length / 8);
const randomBytes = new Uint8Array(byteLength);
// Use crypto.getRandomValues for secure randomness
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
crypto.getRandomValues(randomBytes);
} else {
// Fallback for environments without crypto
for (let i = 0; i < byteLength; i++) {
randomBytes[i] = Math.floor(Math.random() * 256);
}
}
// Convert to BigInt
let result = BigInt(0);
for (let i = 0; i < randomBytes.length; i++) {
result = (result << BigInt(8)) + BigInt(randomBytes[i]);
}
// Ensure result is within range
return result % max;
}
/**
* Convert mnemonic words to entropy (as BigInt)
* Each word is mapped to its index, then combined into a single large number
*/
export function mnemonicToEntropy(words: string[], wordList: readonly string[]): bigint {
let entropy = BigInt(0);
const wordListLength = BigInt(wordList.length);
for (const word of words) {
const index = wordList.indexOf(word);
if (index === -1) {
throw new Error(`Word "${word}" not found in word list`);
}
entropy = entropy * wordListLength + BigInt(index);
}
return entropy;
}
/**
* Convert entropy back to mnemonic words
*/
export function entropyToMnemonic(entropy: bigint, wordCount: number, wordList: readonly string[]): string[] {
const words: string[] = [];
const wordListLength = BigInt(wordList.length);
let remaining = entropy;
for (let i = 0; i < wordCount; i++) {
const index = Number(remaining % wordListLength);
words.unshift(wordList[index]);
remaining = remaining / wordListLength;
}
return words;
}
/**
* Modular inverse using extended Euclidean algorithm
* Returns x such that (a * x) % p === 1
*/
function modInverse(a: bigint, p: bigint): bigint {
let [oldR, r] = [a % p, p];
let [oldS, s] = [BigInt(1), BigInt(0)];
while (r !== BigInt(0)) {
const quotient = oldR / r;
[oldR, r] = [r, oldR - quotient * r];
[oldS, s] = [s, oldS - quotient * s];
}
// Ensure positive result
return ((oldS % p) + p) % p;
}
/**
* Modular arithmetic helper to ensure positive results
*/
function mod(n: bigint, p: bigint): bigint {
return ((n % p) + p) % p;
}
/**
* Split a secret into 3 shares using SSS (3,2) threshold scheme
*
* Uses linear polynomial: f(x) = secret + a*x (mod p)
* where 'a' is a random coefficient
*
* Any 2 points on this line can recover the y-intercept (secret)
*/
export function splitSecret(secret: bigint): SSSShare[] {
// Generate random coefficient for the polynomial
const a = secureRandomBigInt(PRIME);
// Polynomial: f(x) = secret + a*x (mod PRIME)
const f = (x: number): bigint => {
return mod(secret + a * BigInt(x), PRIME);
};
// Generate 3 shares at x = 1, 2, 3
return [
{ x: 1, y: f(1), label: 'device' },
{ x: 2, y: f(2), label: 'cloud' },
{ x: 3, y: f(3), label: 'heir' },
];
}
/**
* Recover the secret from any 2 shares using Lagrange interpolation
*
* For 2 points (x1, y1) and (x2, y2), the secret (y-intercept) is:
* S = (x2*y1 - x1*y2) / (x2 - x1) (mod p)
*/
export function recoverSecret(shareA: SSSShare, shareB: SSSShare): bigint {
const { x: x1, y: y1 } = shareA;
const { x: x2, y: y2 } = shareB;
// Numerator: x2*y1 - x1*y2
const numerator = mod(
BigInt(x2) * y1 - BigInt(x1) * y2,
PRIME
);
// Denominator: x2 - x1
const denominator = mod(BigInt(x2 - x1), PRIME);
// Division in modular arithmetic = multiply by modular inverse
const invDenominator = modInverse(denominator, PRIME);
return mod(numerator * invDenominator, PRIME);
}
/**
* Format a share for display (truncated for readability)
* Shows first 8 and last 4 characters of the y-value
*/
export function formatShareForDisplay(share: SSSShare): string {
const yStr = share.y.toString();
if (yStr.length <= 16) {
return `(${share.x}, ${yStr})`;
}
return `(${share.x}, ${yStr.slice(0, 8)}...${yStr.slice(-4)})`;
}
/**
* Format a share as a compact display string (for UI cards)
* Returns a shorter format showing the share index and a hash-like preview
*/
export function formatShareCompact(share: SSSShare): string {
const yStr = share.y.toString();
// Create a "fingerprint" from the y value
const fingerprint = yStr.slice(0, 4) + '-' + yStr.slice(4, 8) + '-' + yStr.slice(-4);
return fingerprint;
}
/**
* Serialize a share to a string for storage/transmission
*/
export function serializeShare(share: SSSShare): string {
return JSON.stringify({
x: share.x,
y: share.y.toString(),
label: share.label,
});
}
/**
* Deserialize a share from a string
*/
export function deserializeShare(str: string): SSSShare {
const parsed = JSON.parse(str);
return {
x: parsed.x,
y: BigInt(parsed.y),
label: parsed.label,
};
}
/**
* Main function to generate mnemonic and SSS shares
* This is the entry point for the vault initialization flow
*/
export interface VaultKeyData {
mnemonic: string[];
shares: SSSShare[];
entropy: bigint;
}
export function generateVaultKeys(
wordList: readonly string[],
wordCount: number = 12
): VaultKeyData {
// Generate random mnemonic
const mnemonic: string[] = [];
for (let i = 0; i < wordCount; i++) {
const index = Math.floor(Math.random() * wordList.length);
mnemonic.push(wordList[index]);
}
// Convert to entropy
const entropy = mnemonicToEntropy(mnemonic, wordList);
// Split into shares
const shares = splitSecret(entropy);
return { mnemonic, shares, entropy };
}
/**
* Verify that shares can recover the original entropy
* Useful for testing and validation
*/
export function verifyShares(
shares: SSSShare[],
originalEntropy: bigint
): boolean {
// Test all 3 combinations of 2 shares
const combinations = [
[shares[0], shares[1]], // Device + Cloud
[shares[1], shares[2]], // Cloud + Heir
[shares[0], shares[2]], // Device + Heir
];
for (const [a, b] of combinations) {
const recovered = recoverSecret(a, b);
if (recovered !== originalEntropy) {
return false;
}
}
return true;
}