Compare commits
5 Commits
Steven
...
22dc3abf65
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22dc3abf65 | ||
|
|
ed1f6fc49d | ||
|
|
218b2e8b29 | ||
|
|
fb1377eb4b | ||
|
|
c07f1f20d5 |
5
App.tsx
5
App.tsx
@@ -6,7 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
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';
|
||||||
@@ -16,10 +15,6 @@ 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
97
package-lock.json
generated
@@ -14,8 +14,6 @@
|
|||||||
"@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",
|
||||||
@@ -82,7 +80,6 @@
|
|||||||
"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",
|
||||||
@@ -486,6 +483,7 @@
|
|||||||
"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"
|
||||||
@@ -502,6 +500,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -517,6 +516,7 @@
|
|||||||
"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,6 +532,7 @@
|
|||||||
"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",
|
||||||
@@ -549,6 +550,7 @@
|
|||||||
"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"
|
||||||
@@ -649,6 +651,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -769,6 +772,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -955,6 +959,7 @@
|
|||||||
"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"
|
||||||
@@ -1020,6 +1025,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1066,6 +1072,7 @@
|
|||||||
"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"
|
||||||
@@ -1134,6 +1141,7 @@
|
|||||||
"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"
|
||||||
@@ -1150,6 +1158,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1165,6 +1174,7 @@
|
|||||||
"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"
|
||||||
@@ -1181,6 +1191,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1196,6 +1207,7 @@
|
|||||||
"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"
|
||||||
@@ -1212,6 +1224,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1291,6 +1304,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1336,6 +1350,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1351,6 +1366,7 @@
|
|||||||
"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"
|
||||||
@@ -1383,6 +1399,7 @@
|
|||||||
"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",
|
||||||
@@ -1401,6 +1418,7 @@
|
|||||||
"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"
|
||||||
@@ -1433,6 +1451,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1497,6 +1516,7 @@
|
|||||||
"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"
|
||||||
@@ -1592,6 +1612,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1717,6 +1738,7 @@
|
|||||||
"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"
|
||||||
@@ -1733,6 +1755,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1829,6 +1852,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1863,6 +1887,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -1878,6 +1903,7 @@
|
|||||||
"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"
|
||||||
@@ -1910,6 +1936,7 @@
|
|||||||
"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"
|
||||||
@@ -2028,6 +2055,7 @@
|
|||||||
"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",
|
||||||
@@ -2645,7 +2673,6 @@
|
|||||||
"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": "*"
|
||||||
}
|
}
|
||||||
@@ -3182,18 +3209,6 @@
|
|||||||
"@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",
|
||||||
@@ -3702,7 +3717,6 @@
|
|||||||
"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",
|
||||||
@@ -3884,7 +3898,6 @@
|
|||||||
"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"
|
||||||
@@ -4434,15 +4447,6 @@
|
|||||||
"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",
|
||||||
@@ -4505,7 +4509,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"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",
|
||||||
@@ -4530,9 +4533,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
"version": "6.0.3",
|
"version": "5.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4550,7 +4553,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base64-js": "^1.3.1",
|
"base64-js": "^1.3.1",
|
||||||
"ieee754": "^1.2.1"
|
"ieee754": "^1.1.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/buffer-alloc": {
|
"node_modules/buffer-alloc": {
|
||||||
@@ -5530,6 +5533,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -5654,7 +5658,6 @@
|
|||||||
"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",
|
||||||
@@ -9182,7 +9185,6 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -9266,7 +9268,6 @@
|
|||||||
"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",
|
||||||
@@ -9368,7 +9369,6 @@
|
|||||||
"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,7 +9379,6 @@
|
|||||||
"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"
|
||||||
@@ -11058,30 +11057,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -15,8 +15,6 @@
|
|||||||
"@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",
|
||||||
|
|||||||
@@ -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://localhost:8000';
|
export const API_BASE_URL = 'http://192.168.56.103:8000';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API request timeout in milliseconds
|
* API request timeout in milliseconds
|
||||||
@@ -112,7 +112,6 @@ 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,
|
||||||
@@ -138,6 +137,44 @@ 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;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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
|
||||||
@@ -137,24 +138,25 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign out and clear stored auth
|
* Sign out and clear stored auth and session data
|
||||||
*/
|
*/
|
||||||
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}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ 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
|
||||||
@@ -57,16 +59,21 @@ interface ChatSession {
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export default function FlowScreen() {
|
export default function FlowScreen() {
|
||||||
const { token, signOut } = useAuth();
|
const { token, user, 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;
|
||||||
@@ -95,23 +102,87 @@ 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'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-scroll to bottom when new messages arrive
|
// Load history on mount
|
||||||
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]);
|
}, [messages, selectedRole.id, user]);
|
||||||
|
|
||||||
|
// 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 = () => {
|
||||||
@@ -143,7 +214,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(
|
||||||
@@ -153,11 +224,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(),
|
||||||
@@ -166,11 +237,11 @@ export default function FlowScreen() {
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
};
|
};
|
||||||
setMessages(prev => [...prev, userMsg]);
|
setMessages(prev => [...prev, userMsg]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Call AI proxy
|
// Call AI proxy with selected role's system prompt
|
||||||
const aiResponse = await aiService.sendMessage(userMessage, token);
|
const aiResponse = await aiService.sendMessage(userMessage, token, selectedRole.systemPrompt);
|
||||||
|
|
||||||
// Add AI response
|
// Add AI response
|
||||||
const aiMsg: ChatMessage = {
|
const aiMsg: ChatMessage = {
|
||||||
id: (Date.now() + 1).toString(),
|
id: (Date.now() + 1).toString(),
|
||||||
@@ -179,20 +250,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(
|
||||||
@@ -202,7 +273,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(),
|
||||||
@@ -246,7 +317,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(
|
||||||
@@ -289,9 +360,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');
|
||||||
|
|
||||||
@@ -334,9 +405,12 @@ export default function FlowScreen() {
|
|||||||
};
|
};
|
||||||
setChatHistory(prev => [newSession, ...prev]);
|
setChatHistory(prev => [newSession, ...prev]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear current messages
|
// Clear current messages and storage for this role
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
|
if (user) {
|
||||||
|
storageService.saveCurrentChat(selectedRole.id, [], user.id);
|
||||||
|
}
|
||||||
closeHistoryModal();
|
closeHistoryModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -355,7 +429,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();
|
||||||
@@ -370,8 +444,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))
|
||||||
},
|
},
|
||||||
@@ -400,10 +474,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
|
||||||
@@ -420,8 +494,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"
|
||||||
/>
|
/>
|
||||||
@@ -451,9 +525,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}>Start a conversation</Text>
|
<Text style={styles.emptyTitle}>Chatting with {selectedRole.name}</Text>
|
||||||
<Text style={styles.emptySubtitle}>
|
<Text style={styles.emptySubtitle}>
|
||||||
Ask me anything or share your thoughts
|
{selectedRole.description}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -462,7 +536,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)}
|
||||||
@@ -504,9 +578,26 @@ 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}
|
||||||
>
|
>
|
||||||
@@ -515,7 +606,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}
|
||||||
@@ -529,7 +620,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]}>
|
||||||
@@ -541,7 +632,7 @@ export default function FlowScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={{ height: 20 }} />
|
<View style={{ height: 20 }} />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
@@ -549,7 +640,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}
|
||||||
@@ -572,7 +663,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}
|
||||||
@@ -590,15 +681,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>
|
||||||
)}
|
)}
|
||||||
@@ -607,17 +698,16 @@ export default function FlowScreen() {
|
|||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
|
||||||
{/* History Modal - Background appears instantly, content slides up */}
|
<Modal
|
||||||
<Modal
|
visible={showHistoryModal}
|
||||||
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,
|
||||||
{
|
{
|
||||||
@@ -630,46 +720,129 @@ 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
|
||||||
{/* Close Button */}
|
data={chatHistory}
|
||||||
<TouchableOpacity
|
renderItem={renderHistoryItem}
|
||||||
style={styles.closeButton}
|
keyExtractor={item => item.id}
|
||||||
onPress={closeHistoryModal}
|
style={styles.historyList}
|
||||||
>
|
ListEmptyComponent={
|
||||||
<Text style={styles.closeButtonText}>Close</Text>
|
<View style={styles.historyEmpty}>
|
||||||
</TouchableOpacity>
|
<Ionicons name="chatbubbles-outline" size={48} color={colors.flow.textSecondary} />
|
||||||
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -680,16 +853,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',
|
||||||
@@ -697,12 +870,33 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: spacing.base,
|
paddingHorizontal: spacing.base,
|
||||||
paddingTop: spacing.sm,
|
paddingTop: spacing.sm,
|
||||||
paddingBottom: spacing.md,
|
paddingBottom: spacing.sm,
|
||||||
|
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,
|
||||||
@@ -731,20 +925,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',
|
||||||
@@ -772,7 +966,97 @@ 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',
|
||||||
@@ -837,7 +1121,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,
|
||||||
@@ -893,7 +1177,7 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Modal styles
|
// Modal styles
|
||||||
modalOverlay: {
|
modalOverlay: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -941,7 +1225,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,
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ 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[] = [
|
||||||
@@ -220,7 +222,8 @@ 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);
|
||||||
@@ -294,18 +297,26 @@ export default function MeScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAbandonIsland = () => {
|
const handleAbandonIsland = () => {
|
||||||
Alert.alert(
|
console.log('[MeScreen] Sign out button clicked');
|
||||||
'Sign Out',
|
setShowSignOutModal(true);
|
||||||
'Are you sure you want to sign out?',
|
};
|
||||||
[
|
|
||||||
{ text: 'Cancel', style: 'cancel' },
|
const handleConfirmSignOut = () => {
|
||||||
{
|
console.log('[MeScreen] User confirmed sign out');
|
||||||
text: 'Sign Out',
|
setShowSignOutModal(false);
|
||||||
style: 'destructive',
|
signOut();
|
||||||
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 (
|
||||||
@@ -618,10 +629,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>
|
||||||
@@ -885,6 +896,21 @@ 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
|
||||||
@@ -1412,6 +1438,46 @@ 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1814,6 +1880,23 @@ 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',
|
||||||
@@ -2333,4 +2416,71 @@ 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',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -8,18 +8,107 @@ 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: string;
|
icon: StatusIconName;
|
||||||
description: string;
|
description: string;
|
||||||
gradientColors: [string, string];
|
gradientColors: [string, string];
|
||||||
}> = {
|
}> = {
|
||||||
@@ -79,76 +168,200 @@ 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
|
||||||
Animated.loop(
|
const pulseAnimation = Animated.loop(
|
||||||
Animated.sequence([
|
Animated.sequence([
|
||||||
Animated.timing(pulseAnim, {
|
Animated.timing(pulseAnim, {
|
||||||
toValue: 1.06,
|
toValue: 1.06,
|
||||||
duration: 1200,
|
duration: ANIMATION_DURATION.pulse,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(pulseAnim, {
|
Animated.timing(pulseAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 1200,
|
duration: ANIMATION_DURATION.pulse,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
).start();
|
);
|
||||||
|
pulseAnimation.start();
|
||||||
|
|
||||||
// Glow animation
|
// Glow animation
|
||||||
Animated.loop(
|
const glowAnimation = Animated.loop(
|
||||||
Animated.sequence([
|
Animated.sequence([
|
||||||
Animated.timing(glowAnim, {
|
Animated.timing(glowAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 1500,
|
duration: ANIMATION_DURATION.glow,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(glowAnim, {
|
Animated.timing(glowAnim, {
|
||||||
toValue: 0.5,
|
toValue: 0.5,
|
||||||
duration: 1500,
|
duration: ANIMATION_DURATION.glow,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
).start();
|
);
|
||||||
|
glowAnimation.start();
|
||||||
|
|
||||||
// Slow rotate for ship wheel
|
// Slow rotate for ship wheel
|
||||||
Animated.loop(
|
const rotateAnimation = Animated.loop(
|
||||||
Animated.timing(rotateAnim, {
|
Animated.timing(rotateAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 30000,
|
duration: ANIMATION_DURATION.rotate,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
})
|
})
|
||||||
).start();
|
);
|
||||||
}, []);
|
rotateAnimation.start();
|
||||||
|
|
||||||
const openVault = () => {
|
// Cleanup animations on unmount to prevent memory leaks
|
||||||
|
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: 150,
|
duration: ANIMATION_DURATION.heartbeatPress,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(pulseAnim, {
|
Animated.timing(pulseAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 150,
|
duration: ANIMATION_DURATION.heartbeatPress,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
]).start();
|
]).start();
|
||||||
|
|
||||||
// Add new log
|
// Add new log using functional update to avoid stale closure
|
||||||
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([newLog, ...logs]);
|
setLogs((prevLogs) => [newLog, ...prevLogs]);
|
||||||
|
|
||||||
// Reset status if warning
|
// Reset status if warning
|
||||||
if (status === 'warning') {
|
if (status === 'warning') {
|
||||||
@@ -226,7 +439,7 @@ export default function SentinelScreen() {
|
|||||||
colors={currentStatus.gradientColors}
|
colors={currentStatus.gradientColors}
|
||||||
style={styles.statusCircle}
|
style={styles.statusCircle}
|
||||||
>
|
>
|
||||||
<Ionicons name={currentStatus.icon as any} size={56} color="#fff" />
|
<Ionicons name={currentStatus.icon} size={56} color="#fff" />
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
<Text style={[styles.statusLabel, { color: currentStatus.color }]}>
|
<Text style={[styles.statusLabel, { color: currentStatus.color }]}>
|
||||||
@@ -290,8 +503,10 @@ export default function SentinelScreen() {
|
|||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.vaultAccessButton}
|
style={styles.vaultAccessButton}
|
||||||
onPress={openVault}
|
onPress={handleOpenVault}
|
||||||
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>
|
||||||
@@ -302,6 +517,8 @@ 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]}
|
||||||
@@ -348,17 +565,172 @@ export default function SentinelScreen() {
|
|||||||
onRequestClose={() => setShowVault(false)}
|
onRequestClose={() => setShowVault(false)}
|
||||||
>
|
>
|
||||||
<View style={styles.vaultModalContainer}>
|
<View style={styles.vaultModalContainer}>
|
||||||
{showVault ? <VaultScreen /> : null}
|
<VaultScreen />
|
||||||
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -619,4 +991,196 @@ 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
@@ -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,13 +143,14 @@ 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): Promise<string> {
|
async sendMessage(content: string, token?: string, systemPrompt?: string): Promise<string> {
|
||||||
const messages: AIMessage[] = [
|
const messages: AIMessage[] = [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: AI_CONFIG.DEFAULT_SYSTEM_PROMPT,
|
content: systemPrompt || AI_CONFIG.DEFAULT_SYSTEM_PROMPT,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
@@ -179,7 +180,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,
|
||||||
@@ -217,7 +218,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);
|
||||||
@@ -233,7 +234,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);
|
||||||
|
|||||||
120
src/services/storage.service.ts
Normal file
120
src/services/storage.service.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -77,7 +77,6 @@ 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;
|
||||||
|
|||||||
5
src/utils/index.ts
Normal file
5
src/utils/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* Utility functions for Sentinel
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './sss';
|
||||||
268
src/utils/sss.ts
Normal file
268
src/utils/sss.ts
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user