added login and register

This commit is contained in:
lusixing
2026-01-27 19:52:36 -08:00
parent b8c241c1a0
commit 4d94888bb8
10 changed files with 960 additions and 151 deletions

35
App.tsx
View File

@@ -2,16 +2,33 @@ import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { StyleSheet } from 'react-native';
import { StyleSheet, View, ActivityIndicator } from 'react-native';
import TabNavigator from './src/navigation/TabNavigator';
import AuthNavigator from './src/navigation/AuthNavigator';
import { AuthProvider, useAuth } from './src/context/AuthContext';
import { colors } from './src/theme/colors';
function AppContent() {
const { user } = useAuth();
return (
<NavigationContainer>
<StatusBar style="auto" />
{user ? (
<TabNavigator />
) : (
<AuthNavigator />
)}
</NavigationContainer>
);
}
export default function App() {
return (
<GestureHandlerRootView style={styles.container}>
<NavigationContainer>
<StatusBar style="auto" />
<TabNavigator />
</NavigationContainer>
<AuthProvider>
<AppContent />
</AuthProvider>
</GestureHandlerRootView>
);
}
@@ -19,5 +36,13 @@ export default function App() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.sentinel.background,
},
});

60
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@expo/vector-icons": "~14.0.4",
"@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0",
"expo": "~52.0.0",
"expo-asset": "~11.0.5",
"expo-constants": "~17.0.8",
@@ -76,7 +77,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
@@ -480,6 +480,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",
"integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/traverse": "^7.28.5"
@@ -496,6 +497,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",
"integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -511,6 +513,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",
"integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -526,6 +529,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",
"integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
@@ -543,6 +547,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",
"integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6",
"@babel/traverse": "^7.28.6"
@@ -643,6 +648,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",
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.9.0"
},
@@ -763,6 +769,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz",
"integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6"
},
@@ -949,6 +956,7 @@
"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==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
"@babel/helper-plugin-utils": "^7.18.6"
@@ -1014,6 +1022,7 @@
"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==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1060,6 +1069,7 @@
"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==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.28.6",
"@babel/helper-plugin-utils": "^7.28.6"
@@ -1128,6 +1138,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz",
"integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6"
@@ -1144,6 +1155,7 @@
"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==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1159,6 +1171,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",
"integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6"
@@ -1175,6 +1188,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz",
"integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1190,6 +1204,7 @@
"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==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6",
"@babel/plugin-transform-destructuring": "^7.28.5"
@@ -1206,6 +1221,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz",
"integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6"
},
@@ -1285,6 +1301,7 @@
"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==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6"
},
@@ -1330,6 +1347,7 @@
"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==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1345,6 +1363,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz",
"integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-module-transforms": "^7.27.1",
"@babel/helper-plugin-utils": "^7.27.1"
@@ -1377,6 +1396,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz",
"integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helper-plugin-utils": "^7.27.1",
@@ -1395,6 +1415,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz",
"integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-module-transforms": "^7.27.1",
"@babel/helper-plugin-utils": "^7.27.1"
@@ -1427,6 +1448,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz",
"integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1491,6 +1513,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz",
"integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-replace-supers": "^7.27.1"
@@ -1586,6 +1609,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz",
"integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1711,6 +1735,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz",
"integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6"
@@ -1727,6 +1752,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz",
"integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1823,6 +1849,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz",
"integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1857,6 +1884,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz",
"integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1872,6 +1900,7 @@
"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==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6"
@@ -1904,6 +1933,7 @@
"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==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6"
@@ -2022,6 +2052,7 @@
"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==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
"@babel/types": "^7.4.4",
@@ -2639,7 +2670,6 @@
"resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-4.0.1.tgz",
"integrity": "sha512-CRpbLvdJ1T42S+lrYa1iZp1KfDeBp4oeZOK3hdpiS5n0vR0nhD6sC1gGF0sTboCTp64tLteikz5Y3j53dvgOIw==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react-native": "*"
}
@@ -3672,7 +3702,6 @@
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.18.tgz",
"integrity": "sha512-mIT9MiL/vMm4eirLcmw2h6h/Nm5FICtnYSdohq4vTLA2FF/6PNhByM7s8ffqoVfE5L0uAa6Xda1B7oddolUiGg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@react-navigation/core": "^6.4.17",
"escape-string-regexp": "^4.0.0",
@@ -3684,6 +3713,23 @@
"react-native": "*"
}
},
"node_modules/@react-navigation/native-stack": {
"version": "6.11.0",
"resolved": "https://registry.npmmirror.com/@react-navigation/native-stack/-/native-stack-6.11.0.tgz",
"integrity": "sha512-U5EcUB9Q2NQspCFwYGGNJm0h6wBCOv7T30QjndmvlawLkNt7S7KWbpWyxS9XBHSIKF57RgWjfxuJNTgTstpXxw==",
"license": "MIT",
"dependencies": {
"@react-navigation/elements": "^1.3.31",
"warn-once": "^0.1.0"
},
"peerDependencies": {
"@react-navigation/native": "^6.0.0",
"react": "*",
"react-native": "*",
"react-native-safe-area-context": ">= 3.0.0",
"react-native-screens": ">= 3.0.0"
}
},
"node_modules/@react-navigation/routers": {
"version": "6.1.9",
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.9.tgz",
@@ -3837,7 +3883,6 @@
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -4440,7 +4485,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -5456,6 +5500,7 @@
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"license": "BSD-2-Clause",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5580,7 +5625,6 @@
"resolved": "https://registry.npmjs.org/expo/-/expo-52.0.48.tgz",
"integrity": "sha512-/HR/vuo57KGEWlvF3GWaquwEAjXuA5hrOCsaLcZ3pMSA8mQ27qKd1jva4GWzpxXYedlzs/7LLP1XpZo6hXTsog==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "0.22.27",
@@ -9053,7 +9097,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -9137,7 +9180,6 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.76.9.tgz",
"integrity": "sha512-+LRwecWmTDco7OweGsrECIqJu0iyrREd6CTCgC/uLLYipiHvk+MH9nd6drFtCw/6Blz6eoKTcH9YTTJusNtrWg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.6.3",
"@react-native/assets-registry": "0.76.9",
@@ -9239,7 +9281,6 @@
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.12.0.tgz",
"integrity": "sha512-ukk5PxcF4p3yu6qMZcmeiZgowhb5AsKRnil54YFUUAXVIS7PJcMHGGC+q44fCiBg44/1AJk5njGMez1m9H0BVQ==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
@@ -9250,7 +9291,6 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.4.0.tgz",
"integrity": "sha512-c7zc7Zwjty6/pGyuuvh9gK3YBYqHPOxrhXfG1lF4gHlojQSmIx2piNbNaV+Uykj+RDTmFXK0e/hA+fucw/Qozg==",
"license": "MIT",
"peer": true,
"dependencies": {
"react-freeze": "^1.0.0",
"warn-once": "^0.1.0"

View File

@@ -13,6 +13,7 @@
"@expo/vector-icons": "~14.0.4",
"@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0",
"expo": "~52.0.0",
"expo-asset": "~11.0.5",
"expo-constants": "~17.0.8",

View File

@@ -0,0 +1,66 @@
import React, { createContext, useContext, useState, ReactNode } from 'react';
import { User, LoginRequest, RegisterRequest } from '../types';
import { authService } from '../services/auth.service';
import { Alert } from 'react-native';
interface AuthContextType {
user: User | null;
token: string | null;
isLoading: boolean;
signIn: (credentials: LoginRequest) => Promise<void>;
signUp: (data: RegisterRequest) => Promise<void>;
signOut: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const signIn = async (credentials: LoginRequest) => {
setIsLoading(true);
try {
const response = await authService.login(credentials);
setToken(response.access_token);
setUser(response.user);
} catch (error) {
throw error;
} finally {
setIsLoading(false);
}
};
const signUp = async (data: RegisterRequest) => {
setIsLoading(true);
try {
await authService.register(data);
// After successful registration, sign in automatically
await signIn({ username: data.username, password: data.password });
} catch (error) {
throw error;
} finally {
setIsLoading(false);
}
};
const signOut = () => {
setUser(null);
setToken(null);
};
return (
<AuthContext.Provider value={{ user, token, isLoading, signIn, signUp, signOut }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import LoginScreen from '../screens/LoginScreen';
import RegisterScreen from '../screens/RegisterScreen';
const Stack = createNativeStackNavigator();
export default function AuthNavigator() {
return (
<Stack.Navigator
screenOptions={{
headerShown: false,
animation: 'slide_from_right',
}}
>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
</Stack.Navigator>
);
}

256
src/screens/LoginScreen.tsx Normal file
View File

@@ -0,0 +1,256 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
TextInput,
TouchableOpacity,
KeyboardAvoidingView,
Platform,
SafeAreaView,
Alert,
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Feather, MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, borderRadius, typography, shadows } from '../theme/colors';
import { useAuth } from '../context/AuthContext';
export default function LoginScreen({ navigation }: any) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const { signIn, isLoading } = useAuth();
const handleLogin = async () => {
setError(null);
if (!email || !password) {
setError('Please enter both username and password.');
return;
}
try {
await signIn({ username: email, password });
} catch (err: any) {
setError('Login failed. Please check your credentials.');
}
};
return (
<View style={styles.container}>
<LinearGradient
colors={[colors.sentinel.backgroundGradientStart, colors.sentinel.backgroundGradientEnd]}
style={styles.gradient}
>
<SafeAreaView style={styles.safeArea}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.content}
>
<View style={styles.header}>
<View style={styles.logoContainer}>
<MaterialCommunityIcons name="anchor" size={48} color={colors.sentinel.primary} />
</View>
<Text style={styles.title}>Welcome Aboard!</Text>
<Text style={styles.subtitle}>Sign in to access your sanctuaryy</Text>
{error && (
<View style={styles.errorContainer}>
<MaterialCommunityIcons name="alert-circle-outline" size={20} color={colors.sentinel.statusCritical} />
<Text style={styles.errorText}>{error}</Text>
</View>
)}
</View>
<View style={styles.form}>
<View style={styles.inputContainer}>
<Feather name="mail" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="Username"
placeholderTextColor={colors.sentinel.textSecondary}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
/>
</View>
<View style={styles.inputContainer}>
<Feather name="lock" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="Password"
placeholderTextColor={colors.sentinel.textSecondary}
value={password}
onChangeText={setPassword}
secureTextEntry
/>
</View>
<TouchableOpacity style={styles.forgotButton}>
<Text style={styles.forgotText}>Forgot Password?</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.loginButton}
activeOpacity={0.8}
onPress={handleLogin}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color={colors.white} />
) : (
<Text style={styles.loginButtonText}>Login</Text>
)}
</TouchableOpacity>
<View style={styles.divider}>
<View style={styles.dividerLine} />
<Text style={styles.dividerText}>OR</Text>
<View style={styles.dividerLine} />
</View>
<TouchableOpacity
style={styles.registerLink}
onPress={() => navigation.navigate('Register')}
>
<Text style={styles.registerText}>
Don't have an account? <Text style={styles.registerHighlight}>Join the Fleet</Text>
</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
</LinearGradient>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
gradient: {
flex: 1,
},
safeArea: {
flex: 1,
},
content: {
flex: 1,
paddingHorizontal: spacing.lg,
justifyContent: 'center',
},
header: {
alignItems: 'center',
marginBottom: spacing.xxl,
},
logoContainer: {
width: 80,
height: 80,
borderRadius: borderRadius.full,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.lg,
borderWidth: 1,
borderColor: 'rgba(184, 224, 229, 0.2)',
},
title: {
fontSize: typography.fontSize.xxl,
fontWeight: 'bold',
color: colors.sentinel.text,
marginBottom: spacing.xs,
letterSpacing: 0.5,
},
subtitle: {
fontSize: typography.fontSize.base,
color: colors.sentinel.textSecondary,
marginBottom: spacing.md,
},
errorContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 68, 68, 0.1)',
padding: spacing.sm,
borderRadius: borderRadius.md,
marginTop: spacing.sm,
borderWidth: 1,
borderColor: 'rgba(255, 68, 68, 0.2)',
},
errorText: {
color: colors.sentinel.statusCritical,
fontSize: typography.fontSize.sm,
marginLeft: spacing.xs,
fontWeight: '500',
},
form: {
width: '100%',
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: 'rgba(184, 224, 229, 0.1)',
marginBottom: spacing.base,
height: 56,
paddingHorizontal: spacing.base,
},
inputIcon: {
marginRight: spacing.md,
},
input: {
flex: 1,
color: colors.sentinel.text,
fontSize: typography.fontSize.base,
},
forgotButton: {
alignSelf: 'flex-end',
marginBottom: spacing.lg,
},
forgotText: {
color: colors.sentinel.primary,
fontSize: typography.fontSize.sm,
},
loginButton: {
backgroundColor: colors.nautical.teal,
height: 56,
borderRadius: borderRadius.lg,
alignItems: 'center',
justifyContent: 'center',
...shadows.glow,
},
loginButtonText: {
color: colors.white,
fontSize: typography.fontSize.md,
fontWeight: 'bold',
letterSpacing: 0.5,
},
divider: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: spacing.xl,
},
dividerLine: {
flex: 1,
height: 1,
backgroundColor: 'rgba(184, 224, 229, 0.1)',
},
dividerText: {
color: colors.sentinel.textSecondary,
paddingHorizontal: spacing.md,
fontSize: typography.fontSize.sm,
},
registerLink: {
alignItems: 'center',
},
registerText: {
color: colors.sentinel.textSecondary,
fontSize: typography.fontSize.base,
},
registerHighlight: {
color: colors.sentinel.primary,
fontWeight: 'bold',
},
});

View File

@@ -13,6 +13,7 @@ import {
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
import { useAuth } from '../context/AuthContext';
// Sentinel Protocol Status
const protocolStatus = {
@@ -179,21 +180,22 @@ export default function MeScreen() {
const [triggerGraceDays, setTriggerGraceDays] = useState(15);
const [triggerSource, setTriggerSource] = useState<'dual' | 'subscription' | 'activity'>('dual');
const [triggerKillSwitch, setTriggerKillSwitch] = useState(true);
const { user, signOut } = useAuth();
const handleOpenLink = (url: string) => {
Linking.openURL(url).catch(() => {});
Linking.openURL(url).catch(() => { });
};
const handleAbandonIsland = () => {
Alert.alert(
'Abandon Island',
'Are you sure you want to delete your account? This action is irreversible. All your data, including vault contents, will be permanently destroyed.',
'Sign Out',
'Are you sure you want to sign out?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete Account',
text: 'Sign Out',
style: 'destructive',
onPress: () => Alert.alert('Account Deletion', 'Please contact support to proceed with account deletion.')
onPress: signOut
},
]
);
@@ -235,11 +237,11 @@ export default function MeScreen() {
</View>
</View>
<View style={styles.profileInfo}>
<Text style={styles.profileName}>Captain</Text>
<Text style={styles.profileName}>{user?.username || 'Captain'}</Text>
<Text style={styles.profileTitle}>MASTER OF THE SANCTUM</Text>
<View style={styles.profileBadge}>
<MaterialCommunityIcons name="crown" size={12} color={colors.nautical.gold} />
<Text style={styles.profileBadgeText}>Pro Member</Text>
<Text style={styles.profileBadgeText}>{user?.tier || 'Pro Member'}</Text>
</View>
</View>
</View>
@@ -309,14 +311,14 @@ export default function MeScreen() {
))}
</View>
{/* Abandon Island Button */}
{/* Abandon Island Button (Logout for now) */}
<TouchableOpacity
style={styles.abandonButton}
onPress={handleAbandonIsland}
activeOpacity={0.8}
>
<Feather name="log-out" size={18} color={colors.nautical.coral} />
<Text style={styles.abandonButtonText}>ABANDON ISLAND</Text>
<Text style={styles.abandonButtonText}>SIGN OUT</Text>
</TouchableOpacity>
{/* Settings Menu */}
@@ -661,7 +663,7 @@ export default function MeScreen() {
activeOpacity={1}
onPress={() => setShowTideModal(false)}
>
<TouchableOpacity activeOpacity={1} onPress={() => {}}>
<TouchableOpacity activeOpacity={1} onPress={() => { }}>
<View style={styles.spiritModal}>
<View style={styles.spiritHeader}>
<View style={styles.spiritIcon}>

View File

@@ -0,0 +1,290 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
TextInput,
TouchableOpacity,
KeyboardAvoidingView,
Platform,
SafeAreaView,
ScrollView,
Alert,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Feather, MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, borderRadius, typography, shadows } from '../theme/colors';
import { useAuth } from '../context/AuthContext';
import { ActivityIndicator } from 'react-native';
export default function RegisterScreen({ navigation }: any) {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const { signUp, isLoading } = useAuth();
const handleRegister = async () => {
setError(null);
if (!name || !email || !password || !confirmPassword) {
setError('Please fill in all fields.');
return;
}
if (password !== confirmPassword) {
setError('Passwords do not match.');
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
setError('Please enter a valid email address.');
return;
}
try {
await signUp({ username: name, email, password });
} catch (err: any) {
setError(err.message || 'Registration failed. Please try again.');
}
};
return (
<View style={styles.container}>
<LinearGradient
colors={[colors.sentinel.backgroundGradientStart, colors.sentinel.backgroundGradientEnd]}
style={styles.gradient}
>
<SafeAreaView style={styles.safeArea}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.content}
>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<Feather name="arrow-left" size={24} color={colors.sentinel.text} />
</TouchableOpacity>
<View style={styles.header}>
<View style={styles.logoContainer}>
<MaterialCommunityIcons name="compass-outline" size={48} color={colors.sentinel.primary} />
</View>
<Text style={styles.title}>Join the Fleet</Text>
<Text style={styles.subtitle}>Begin your journey with Sentinel</Text>
{error && (
<View style={styles.errorContainer}>
<MaterialCommunityIcons name="alert-circle-outline" size={20} color={colors.sentinel.statusCritical} />
<Text style={styles.errorText}>{error}</Text>
</View>
)}
</View>
<View style={styles.form}>
<View style={styles.inputContainer}>
<Feather name="user" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="Captain's Name"
placeholderTextColor={colors.sentinel.textSecondary}
value={name}
onChangeText={setName}
/>
</View>
<View style={styles.inputContainer}>
<Feather name="mail" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="Email"
placeholderTextColor={colors.sentinel.textSecondary}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
/>
</View>
<View style={styles.inputContainer}>
<Feather name="lock" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="Password"
placeholderTextColor={colors.sentinel.textSecondary}
value={password}
onChangeText={setPassword}
secureTextEntry
/>
</View>
<View style={styles.inputContainer}>
<Feather name="check-circle" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="Confirm Password"
placeholderTextColor={colors.sentinel.textSecondary}
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry
/>
</View>
<TouchableOpacity
style={[styles.registerButton, isLoading && styles.disabledButton]}
activeOpacity={0.8}
onPress={handleRegister}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color={colors.white} />
) : (
<Text style={styles.registerButtonText}>Create Account</Text>
)}
</TouchableOpacity>
<TouchableOpacity
style={styles.loginLink}
onPress={() => navigation.navigate('Login')}
>
<Text style={styles.loginLinkText}>
Already have an account? <Text style={styles.highlight}>Login</Text>
</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
</LinearGradient>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
gradient: {
flex: 1,
},
safeArea: {
flex: 1,
},
content: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
paddingHorizontal: spacing.lg,
paddingBottom: spacing.xl,
},
backButton: {
marginTop: spacing.md,
marginBottom: spacing.lg,
width: 40,
height: 40,
borderRadius: borderRadius.full,
backgroundColor: 'rgba(255, 255, 255, 0.05)',
alignItems: 'center',
justifyContent: 'center',
},
header: {
alignItems: 'center',
marginBottom: spacing.xl,
},
logoContainer: {
width: 80,
height: 80,
borderRadius: borderRadius.full,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.lg,
borderWidth: 1,
borderColor: 'rgba(184, 224, 229, 0.2)',
},
title: {
fontSize: typography.fontSize.xxl,
fontWeight: 'bold',
color: colors.sentinel.text,
marginBottom: spacing.xs,
letterSpacing: 0.5,
},
subtitle: {
fontSize: typography.fontSize.base,
color: colors.sentinel.textSecondary,
},
form: {
width: '100%',
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: 'rgba(184, 224, 229, 0.1)',
marginBottom: spacing.base,
height: 56,
paddingHorizontal: spacing.base,
},
inputIcon: {
marginRight: spacing.md,
},
input: {
flex: 1,
color: colors.sentinel.text,
fontSize: typography.fontSize.base,
},
registerButton: {
backgroundColor: colors.nautical.seafoam,
height: 56,
borderRadius: borderRadius.lg,
alignItems: 'center',
justifyContent: 'center',
marginTop: spacing.md,
...shadows.glow,
},
registerButtonText: {
color: colors.white,
fontSize: typography.fontSize.md,
fontWeight: 'bold',
letterSpacing: 0.5,
},
loginLink: {
alignItems: 'center',
marginTop: spacing.xl,
},
loginLinkText: {
color: colors.sentinel.textSecondary,
fontSize: typography.fontSize.base,
},
highlight: {
color: colors.nautical.seafoam,
fontWeight: 'bold',
},
disabledButton: {
opacity: 0.7,
},
errorContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 68, 68, 0.1)',
padding: spacing.sm,
borderRadius: borderRadius.md,
marginTop: spacing.md,
borderWidth: 1,
borderColor: 'rgba(255, 68, 68, 0.2)',
},
errorText: {
color: colors.sentinel.statusCritical,
fontSize: typography.fontSize.sm,
marginLeft: spacing.xs,
fontWeight: '500',
},
});

View File

@@ -0,0 +1,80 @@
import { LoginRequest, LoginResponse, RegisterRequest, User } from '../types';
const PLATFORM_URL = 'http://192.168.56.103:8000';
const no_backend_mode = true;
const MOCK_USER: User = {
id: 999,
username: 'MockCaptain',
public_key: 'mock_public_key',
is_admin: true,
guale: false,
tier: 'premium',
tier_expires_at: '2026-12-31T23:59:59Z',
last_active_at: new Date().toISOString(),
};
export const authService = {
async login(credentials: LoginRequest): Promise<LoginResponse> {
if (no_backend_mode) {
console.log('No-Backend Mode: Simulating login...');
return new Promise((resolve) => {
setTimeout(() => {
resolve({
access_token: 'mock_access_token',
token_type: 'bearer',
user: { ...MOCK_USER, username: credentials.username },
});
}, 200);
});
}
try {
const response = await fetch(`${PLATFORM_URL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials),
});
if (!response.ok) {
throw new Error('Login failed');
}
return await response.json();
} catch (error) {
console.error('Login error:', error);
throw error;
}
},
async register(data: RegisterRequest): Promise<User> {
if (no_backend_mode) {
console.log('No-Backend Mode: Simulating registration...');
return new Promise((resolve) => {
setTimeout(() => {
resolve({ ...MOCK_USER, username: data.username });
}, 200);
});
}
try {
const response = await fetch(`${PLATFORM_URL}/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail?.[0]?.msg || 'Registration failed');
}
return await response.json();
} catch (error) {
console.error('Registration error:', error);
throw error;
}
},
};

View File

@@ -72,3 +72,32 @@ export interface ProtocolInfo {
version: string;
lastUpdated: Date;
}
// Auth Types
export interface User {
id: number;
username: string;
public_key: string;
is_admin: boolean;
guale: boolean;
tier: string;
tier_expires_at: string;
last_active_at: string;
}
export interface LoginRequest {
username: string;
password: string;
}
export interface RegisterRequest {
username: string;
password: string;
email: string;
}
export interface LoginResponse {
access_token: string;
token_type: string;
user: User;
}