diff --git a/README.md b/README.md
index 0ce1149..28112a4 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
@@ -13,9 +13,11 @@
- A full-featured CRUD application built with React Native, Expo Router, NativeWind, Drizzle ORM, and SQLite. Create, read, update, and delete records with a clean, production-ready mobile interface.
+ A starter boilerplate for authentication in React Native, built with Expo Router, NativeWind, Drizzle ORM, and SQLite. Includes sign up, sign in, sign out, forgot password, and reset password flows — designed as a solid foundation to build on, not a production-ready solution out of the box.
+> ⚠️ **This is a starting point.** Before shipping to production, review the [Security Considerations](#security-considerations) section below. Several intentional simplifications were made to keep this boilerplate approachable.
+
---
@@ -30,6 +32,66 @@
---
+
+ Security Considerations
+
+
+This project is intentionally simplified. Before using it as a base for a real-world app, you should address the following:
+
+### Password Hashing
+
+Currently, passwords are hashed using `Crypto.CryptoDigestAlgorithm.SHA256` via `expo-crypto`. **SHA-256 is not suitable for password hashing in production** — it is fast by design, which makes brute-force attacks trivial.
+
+> The `argon2` npm package is **not compatible with React Native** as it relies on Node.js native bindings (C++). Use one of the alternatives below instead.
+
+**Option 1 — `react-native-argon2`** (recommended, requires `expo prebuild` — exits Expo Go):
+
+```bash
+bun add react-native-argon2
+bun run prebuild
+```
+
+```ts
+import Argon2 from "react-native-argon2";
+
+async function hashPassword(password: string, salt: string): Promise {
+ const { rawHash } = await Argon2.hash(password, salt, { mode: "argon2id" });
+ return rawHash;
+}
+```
+
+**Option 2 — `expo-crypto` with SHA-512 + salt** (no eject required, stays in Expo Go):
+
+```ts
+import * as Crypto from "expo-crypto";
+
+async function hashPassword(password: string, salt: string): Promise {
+ return Crypto.digestStringAsync(
+ Crypto.CryptoDigestAlgorithm.SHA512,
+ salt + password,
+ );
+}
+```
+
+> SHA-512 is still not ideal for password hashing, but significantly better than SHA-256 when combined with a unique per-user salt stored alongside the hash.
+
+**Option 3 — Backend authentication** (most secure for production):
+Move auth entirely to a server (Node.js, Go, etc.) where Argon2id runs natively, and have the app communicate via HTTPS. Services like [Supabase](https://supabase.com) or [Firebase Auth](https://firebase.google.com/products/auth) handle this out of the box.
+
+> Argon2id is the current recommendation from [OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) and [RFC 9106](https://www.rfc-editor.org/rfc/rfc9106).
+
+### Other Areas to Improve Before Production
+
+- **Password reset tokens** — currently passed via route params (visible in navigation). Consider storing them only server-side or using a deeper link strategy.
+- **Session management** — sessions are stored in SQLite with no expiry. Add a `expiresAt` column and invalidate stale sessions.
+- **Input validation** — add stricter password rules (min length, complexity) via Zod schemas.
+- **Rate limiting** — no protection against brute-force login attempts exists in a local-first setup; consider adding attempt counters.
+- **Token expiry** — reset tokens expire after 15 minutes by default; adjust as needed for your use case.
+- **No email verification** — there is no email confirmation step on sign up.
+- **Local-only** — this project uses SQLite with no backend. For multi-device or server-side auth, you will need to integrate a backend (e.g., Supabase, Firebase, or a custom API).
+
+---
+
Installation & Setup
@@ -37,8 +99,8 @@
### 1. Clone the Repository
```bash
-git clone https://github.com/Victor-Zarzar/react-native-crud
-cd react-native-crud
+git clone https://github.com/Victor-Zarzar/react-native-basic-auth
+cd react-native-basic-auth
```
### 2. Install Dependencies
@@ -80,6 +142,7 @@ Open the app:
| `bun run ios` | Start on iOS Simulator |
| `bun run web` | Start on Web |
| `bun run db:generate` | Generate Drizzle ORM migration files |
+| `bun run upgrade-deps` | Fix and align dependencies with Expo SDK |
| `bun run prebuild` | Rebuild native directories with Expo Prebuild |
| `bun run ios:native` | Run native iOS build |
| `bun run android:native` | Run native Android build |
@@ -100,9 +163,9 @@ Open the app:
- **Expo Router** – File-based routing
- **TypeScript** – Type-safe development
- **NativeWind** – Tailwind CSS for React Native
-- **SQLite (Expo SQLite)** – Local persistent storage for CRUD operations
+- **SQLite (Expo SQLite)** – Local persistent storage
- **Drizzle ORM** – Type-safe ORM for SQLite with migration support
-- **Expo Crypto** – Cryptographic utilities for secure ID generation and hashing
+- **Expo Crypto** – Cryptographic utilities (SHA-256 for dev; replace with Argon2id for prod)
- **React Native Reusables** – Accessible UI component system
---
@@ -111,11 +174,11 @@ Open the app:
Key Features
-- Full CRUD operations — Create, Read, Update, and Delete records
+- Complete auth flow — Sign Up, Sign In, Sign Out, Forgot Password, Reset Password
- Local data persistence with SQLite via Expo SQLite
- Type-safe database queries with Drizzle ORM
- Drizzle Studio integration via `expo-drizzle-studio-plugin` for database inspection during development
-- Secure ID generation with Expo Crypto
+- Password hashing with Expo Crypto (SHA-256 — see [Security Considerations](#security-considerations) for production upgrade path)
- Production-ready scalable structure
- File-based routing with Expo Router
- Dark mode support
@@ -146,7 +209,7 @@ Before starting, ensure you have:
```
-react-native-crud/
+react-native-basic-auth/
├── .expo/ # Expo cache and config
├── .github/ # GitHub Actions & workflows
├── assets/ # Images and fonts
@@ -251,7 +314,7 @@ With the development server running (`bun run dev`), press `Shift + M` in the te
-
+
@@ -316,6 +379,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
Victor Zarzar - [@Victor-Zarzar](https://github.com/Victor-Zarzar)
-Project Link: [https://github.com/Victor-Zarzar/react-native-crud](https://github.com/Victor-Zarzar/react-native-crud)
+Project Link: [https://github.com/Victor-Zarzar/react-native-basic-auth](https://github.com/Victor-Zarzar/react-native-basic-auth)
---
diff --git a/bun.lock b/bun.lock
index cca7f0c..30f07b8 100644
--- a/bun.lock
+++ b/bun.lock
@@ -20,13 +20,13 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.45.1",
- "expo": "~55.0.4",
+ "expo": "~55.0.5",
"expo-constants": "~55.0.7",
- "expo-crypto": "55.0.8",
+ "expo-crypto": "~55.0.9",
"expo-drizzle-studio-plugin": "^0.2.1",
"expo-linking": "~55.0.7",
"expo-localization": "~55.0.8",
- "expo-router": "~55.0.3",
+ "expo-router": "~55.0.5",
"expo-splash-screen": "~55.0.10",
"expo-sqlite": "^55.0.10",
"expo-status-bar": "~55.0.4",
@@ -328,7 +328,7 @@
"@expo-google-fonts/material-symbols": ["@expo-google-fonts/material-symbols@0.4.24", "", {}, "sha512-1bJ63Yv2Bn8SN2MjrlbwLwUhnC8COOeejd15H88WjCtw5iNErqEPaBnpvmYyqciVYwudGo5drUIdY9C/5yPGbg=="],
- "@expo/cli": ["@expo/cli@55.0.14", "", { "dependencies": { "@expo/code-signing-certificates": "^0.0.6", "@expo/config": "~55.0.8", "@expo/config-plugins": "~55.0.6", "@expo/devcert": "^1.2.1", "@expo/env": "~2.1.1", "@expo/image-utils": "^0.8.12", "@expo/json-file": "^10.0.12", "@expo/log-box": "55.0.7", "@expo/metro": "~54.2.0", "@expo/metro-config": "~55.0.9", "@expo/osascript": "^2.4.2", "@expo/package-manager": "^1.10.3", "@expo/plist": "^0.5.2", "@expo/prebuild-config": "^55.0.8", "@expo/require-utils": "^55.0.2", "@expo/router-server": "^55.0.9", "@expo/schema-utils": "^55.0.2", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.4.0", "@react-native/dev-middleware": "0.83.2", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "dnssd-advertise": "^1.1.3", "expo-server": "^55.0.6", "fetch-nodeshim": "^0.4.6", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.0", "multitars": "^0.2.3", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^4.0.3", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "resolve-from": "^5.0.0", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "terminal-link": "^2.1.1", "toqr": "^0.1.1", "wrap-ansi": "^7.0.0", "ws": "^8.12.1", "zod": "^3.25.76" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "build/bin/cli" } }, "sha512-glXPSjjLCIz+KX/ezqLTGIF9eTE1lexiCxunvB3loRZNnGeBDGW3eF++cuPKudW26jeC6bqZkcqBG7Lp0Sp9qg=="],
+ "@expo/cli": ["@expo/cli@55.0.16", "", { "dependencies": { "@expo/code-signing-certificates": "^0.0.6", "@expo/config": "~55.0.8", "@expo/config-plugins": "~55.0.6", "@expo/devcert": "^1.2.1", "@expo/env": "~2.1.1", "@expo/image-utils": "^0.8.12", "@expo/json-file": "^10.0.12", "@expo/log-box": "55.0.7", "@expo/metro": "~54.2.0", "@expo/metro-config": "~55.0.9", "@expo/osascript": "^2.4.2", "@expo/package-manager": "^1.10.3", "@expo/plist": "^0.5.2", "@expo/prebuild-config": "^55.0.8", "@expo/require-utils": "^55.0.2", "@expo/router-server": "^55.0.10", "@expo/schema-utils": "^55.0.2", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.4.0", "@react-native/dev-middleware": "0.83.2", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "dnssd-advertise": "^1.1.3", "expo-server": "^55.0.6", "fetch-nodeshim": "^0.4.6", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.0", "multitars": "^0.2.3", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^4.0.3", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "resolve-from": "^5.0.0", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "terminal-link": "^2.1.1", "toqr": "^0.1.1", "wrap-ansi": "^7.0.0", "ws": "^8.12.1", "zod": "^3.25.76" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "build/bin/cli" } }, "sha512-rp1mBnA5msGDPTfFuqVl+9RsJOtuA0cXsWSJpHdvsIxcSVg0oJyF/rgvrwsFrNQCLXzkMXm+o3CsY9iL1D/CDA=="],
"@expo/code-signing-certificates": ["@expo/code-signing-certificates@0.0.6", "", { "dependencies": { "node-forge": "^1.3.3" } }, "sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w=="],
@@ -346,7 +346,7 @@
"@expo/env": ["@expo/env@2.1.1", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "getenv": "^2.0.0" } }, "sha512-rVvHC4I6xlPcg+mAO09ydUi2Wjv1ZytpLmHOSzvXzBAz9mMrJggqCe4s4dubjJvi/Ino/xQCLhbaLCnTtLpikg=="],
- "@expo/fingerprint": ["@expo/fingerprint@0.16.5", "", { "dependencies": { "@expo/env": "^2.0.11", "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "getenv": "^2.0.0", "glob": "^13.0.0", "ignore": "^5.3.1", "minimatch": "^10.2.2", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-mLrcymtgkW9IJ/G1e8MH1Xt2VIb1MOS86ePY0ePcnV3nVyJqm7gfa/AXD1Hk+eZXvf8XhioYz6QZaamBdEzR3A=="],
+ "@expo/fingerprint": ["@expo/fingerprint@0.16.6", "", { "dependencies": { "@expo/env": "^2.0.11", "@expo/spawn-async": "^1.7.2", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "getenv": "^2.0.0", "glob": "^13.0.0", "ignore": "^5.3.1", "minimatch": "^10.2.2", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-nRITNbnu3RKSHPvKVehrSU4KG2VY9V8nvULOHBw98ukHCAU4bGrU5APvcblOkX3JAap+xEHsg/mZvqlvkLInmQ=="],
"@expo/image-utils": ["@expo/image-utils@0.8.12", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "getenv": "^2.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" } }, "sha512-3KguH7kyKqq7pNwLb9j6BBdD/bjmNwXZG/HPWT6GWIXbwrvAJt2JNyYTP5agWJ8jbbuys1yuCzmkX+TU6rmI7A=="],
@@ -372,7 +372,7 @@
"@expo/require-utils": ["@expo/require-utils@55.0.2", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0" }, "optionalPeers": ["typescript"] }, "sha512-dV5oCShQ1umKBKagMMT4B/N+SREsQe3lU4Zgmko5AO0rxKV0tynZT6xXs+e2JxuqT4Rz997atg7pki0BnZb4uw=="],
- "@expo/router-server": ["@expo/router-server@55.0.9", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@expo/metro-runtime": "^55.0.6", "expo": "*", "expo-constants": "^55.0.7", "expo-font": "^55.0.4", "expo-router": "*", "expo-server": "^55.0.6", "react": "*", "react-dom": "*", "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" }, "optionalPeers": ["@expo/metro-runtime", "expo-router", "react-dom", "react-server-dom-webpack"] }, "sha512-LcCFi+P1qfZOsw0DO4JwNKRxtWt4u2bjTYj0PUe4WVf9NVG/NfUetAXYRbBS6P+gupfM6SC+/bdzdqCWQh7j8g=="],
+ "@expo/router-server": ["@expo/router-server@55.0.10", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@expo/metro-runtime": "^55.0.6", "expo": "*", "expo-constants": "^55.0.7", "expo-font": "^55.0.4", "expo-router": "*", "expo-server": "^55.0.6", "react": "*", "react-dom": "*", "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" }, "optionalPeers": ["@expo/metro-runtime", "expo-router", "react-dom", "react-server-dom-webpack"] }, "sha512-NZQzHwkaedufNPayVfPxsZGEMngOD3gDvYx9lld4sitRexrKDx5sHmmNHi6IByGbmCb4jwLXub5sIyWh6z1xPQ=="],
"@expo/schema-utils": ["@expo/schema-utils@55.0.2", "", {}, "sha512-QZ5WKbJOWkCrMq0/kfhV9ry8te/OaS34YgLVpG8u9y2gix96TlpRTbxM/YATjNcUR2s4fiQmPCOxkGtog4i37g=="],
@@ -652,7 +652,7 @@
"babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.2.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg=="],
- "babel-preset-expo": ["babel-preset-expo@55.0.10", "", { "dependencies": { "@babel/generator": "^7.20.5", "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.83.2", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "resolve-from": "^5.0.0" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", "expo-widgets": "^55.0.2", "react-refresh": ">=0.14.0 <1.0.0" }, "optionalPeers": ["@babel/runtime", "expo", "expo-widgets"] }, "sha512-aRtW7qJKohGU2V0LUJ6IeP7py3+kVUo9zcc8+v1Kix8jGGuIvqvpo9S6W1Fmn9VFP2DBwkFDLiyzkCZS85urVA=="],
+ "babel-preset-expo": ["babel-preset-expo@55.0.11", "", { "dependencies": { "@babel/generator": "^7.20.5", "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.83.2", "babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "resolve-from": "^5.0.0" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", "expo-widgets": "^55.0.4", "react-refresh": ">=0.14.0 <1.0.0" }, "optionalPeers": ["@babel/runtime", "expo", "expo-widgets"] }, "sha512-ti8t4xufD6gUQQh+qY+b+VT/1zyA0n1PBnwOzCkPUyEDiIVBpaOixR+BzVH68hqu9mH2wDfzoFuGgv+2LfRdqw=="],
"babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="],
@@ -824,13 +824,13 @@
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
- "expo": ["expo@55.0.4", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "55.0.14", "@expo/config": "~55.0.8", "@expo/config-plugins": "~55.0.6", "@expo/devtools": "55.0.2", "@expo/fingerprint": "0.16.5", "@expo/local-build-cache-provider": "55.0.6", "@expo/log-box": "55.0.7", "@expo/metro": "~54.2.0", "@expo/metro-config": "55.0.9", "@expo/vector-icons": "^15.0.2", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "~55.0.10", "expo-asset": "~55.0.8", "expo-constants": "~55.0.7", "expo-file-system": "~55.0.10", "expo-font": "~55.0.4", "expo-keep-awake": "~55.0.4", "expo-modules-autolinking": "55.0.8", "expo-modules-core": "55.0.13", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.1" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-cbQBPYwmH6FRvh942KR8mSdEcrVdsIMkjdHthtf59zlpzgrk28FabhOdL/Pc9WuS+CsIP3EIQbZqmLkTjv6qPg=="],
+ "expo": ["expo@55.0.6", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "55.0.16", "@expo/config": "~55.0.8", "@expo/config-plugins": "~55.0.6", "@expo/devtools": "55.0.2", "@expo/fingerprint": "0.16.6", "@expo/local-build-cache-provider": "55.0.6", "@expo/log-box": "55.0.7", "@expo/metro": "~54.2.0", "@expo/metro-config": "55.0.9", "@expo/vector-icons": "^15.0.2", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "~55.0.11", "expo-asset": "~55.0.8", "expo-constants": "~55.0.7", "expo-file-system": "~55.0.10", "expo-font": "~55.0.4", "expo-keep-awake": "~55.0.4", "expo-modules-autolinking": "55.0.9", "expo-modules-core": "55.0.15", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.1" }, "peerDependencies": { "@expo/dom-webview": "*", "@expo/metro-runtime": "*", "react": "*", "react-native": "*", "react-native-webview": "*" }, "optionalPeers": ["@expo/dom-webview", "@expo/metro-runtime", "react-native-webview"], "bin": { "expo": "bin/cli", "fingerprint": "bin/fingerprint", "expo-modules-autolinking": "bin/autolinking" } }, "sha512-gaF8bh5beWmrptz3d4Gr138CiPoLJtzjNbqNSOQ8kdQm3wMW8lJGT1dsY5NPJTZ7MNJBTN+pcRwshr4BMK4OiA=="],
"expo-asset": ["expo-asset@55.0.8", "", { "dependencies": { "@expo/image-utils": "^0.8.12", "expo-constants": "~55.0.7" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-yEz2svDX67R0yiW2skx6dJmcE0q7sj9ECpGMcxBExMCbctc+nMoZCnjUuhzPl5vhClUsO5HFFXS5vIGmf1bgHQ=="],
"expo-constants": ["expo-constants@55.0.7", "", { "dependencies": { "@expo/config": "~55.0.8", "@expo/env": "~2.1.1" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-kdcO4TsQRRqt0USvjaY5vgQMO9H52K3kBZ/ejC7F6rz70mv08GoowrZ1CYOr5O4JpPDRlIpQfZJUucaS/c+KWQ=="],
- "expo-crypto": ["expo-crypto@55.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-s4G4l1gRgSv3rS4JeKbybXzobWFUcO/B1F6nffjtoGS4WWU5+DNrmbiMnF9mvW21/ozYKtUu21u3pl2+7cGWnw=="],
+ "expo-crypto": ["expo-crypto@55.0.9", "", { "peerDependencies": { "expo": "*" } }, "sha512-hYiZYRPMXGQXSgKjp/m84l/6Uq8mTeMts1C7bFZXN5M5TUOiRhrLeqMSYZFXrAlkFpXeO46V+Ts1CFauMBLuCw=="],
"expo-drizzle-studio-plugin": ["expo-drizzle-studio-plugin@0.2.1", "", { "peerDependencies": { "expo": ">=53.0.5", "expo-sqlite": ">=15.2.9" } }, "sha512-AjMC7SOutMAv/MkeJSp/26gnHLAA17SG8EtsjIKW6tDcBkq4plDVhjMLusoGe9f0vzYK8KrOvWjEWZ/R+KvIGg=="],
@@ -838,9 +838,9 @@
"expo-font": ["expo-font@55.0.4", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-ZKeGTFffPygvY5dM/9ATM2p7QDkhsaHopH7wFAWgP2lKzqUMS9B/RxCvw5CaObr9Ro7x9YptyeRKX2HmgmMfrg=="],
- "expo-glass-effect": ["expo-glass-effect@55.0.7", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-G7Q9rUaEY0YC36fGE6irDljfsfvzz/y49zagARAKvSJSyQMUSrhR25WOr5LK5Cw7gQNNBEy9U1ctlr7yCay/fQ=="],
+ "expo-glass-effect": ["expo-glass-effect@55.0.8", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-IvUjHb/4t6r2H/LXDjcQ4uDoHrmO2cLOvEb9leLavQ4HX5+P4LRtQrMDMlkWAn5Wo5DkLcG8+1CrQU2nqgogTA=="],
- "expo-image": ["expo-image@55.0.5", "", { "dependencies": { "sf-symbols-typescript": "^2.2.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-oejmMwy5O9EtC8po9NxkcurWHqND6p8xuJaj9FGNo8NXLt9e+w3cKWx7HuPzkH5y3qFXQ9Od+z+I/wxEci36fw=="],
+ "expo-image": ["expo-image@55.0.6", "", { "dependencies": { "sf-symbols-typescript": "^2.2.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-TKuu0uBmgTZlhd91Glv+V4vSBMlfl0bdQxfl97oKKZUo3OBC13l3eLik7v3VNLJN7PZbiwOAiXkZkqSOBx/Xsw=="],
"expo-keep-awake": ["expo-keep-awake@55.0.4", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-vwfdMtMS5Fxaon8gC0AiE70SpxTsHJ+rjeoVJl8kdfdbxczF7OIaVmfjFJ5Gfigd/WZiLqxhfZk34VAkXF4PNg=="],
@@ -848,11 +848,11 @@
"expo-localization": ["expo-localization@55.0.8", "", { "dependencies": { "rtl-detect": "^1.0.2" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-uFmpTsoDT7JE5Nwgt0EQ5gBvFVo7/u458SlY6V9Ep9wY/WPucL0o00VpXoFULaMtKHquKBgVUdHwk6E+JFz4dg=="],
- "expo-modules-autolinking": ["expo-modules-autolinking@55.0.8", "", { "dependencies": { "@expo/require-utils": "^55.0.2", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-nrWB1pkNp7bR8ECUTgYUiJ2Pyh6AvxCBXZ+lyPlfl1TzEIGhwU1Yqr+d78eJDueXaW+9zKeE0HqrTZoLS3ve4A=="],
+ "expo-modules-autolinking": ["expo-modules-autolinking@55.0.9", "", { "dependencies": { "@expo/require-utils": "^55.0.2", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-OXIrxSYKlT/1Av1AMyUWeSTW1GChGofWV14sB73o5eFbfuz6ocv18fnKx+Ji67ZC7a0RztDctcZTuEQK84S4iw=="],
- "expo-modules-core": ["expo-modules-core@55.0.13", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-DYLQTOJAR7jD3M9S0sH9myZaPEtShdicHrPiWcupIXMeMkQxFzErx+adUI8gZPy4AU45BgeGgtaogRfT25iLfw=="],
+ "expo-modules-core": ["expo-modules-core@55.0.15", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MAGz1SYSVgQbwVeUysWgPtLh8ozbBwORatXoA4w0NZqZBZzEyBgUQNhuwaroaIi9W8Ir3wy1McmZcDYDJNGmVw=="],
- "expo-router": ["expo-router@55.0.3", "", { "dependencies": { "@expo/metro-runtime": "^55.0.6", "@expo/schema-utils": "^55.0.2", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-navigation/bottom-tabs": "^7.10.1", "@react-navigation/native": "^7.1.28", "@react-navigation/native-stack": "^7.10.1", "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-glass-effect": "^55.0.7", "expo-image": "^55.0.5", "expo-server": "^55.0.6", "expo-symbols": "^55.0.4", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-native-is-edge-to-edge": "^1.2.1", "semver": "~7.6.3", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "shallowequal": "^1.1.0", "use-latest-callback": "^0.2.1", "vaul": "^1.1.2" }, "peerDependencies": { "@expo/log-box": "55.0.7", "@react-navigation/drawer": "^7.7.2", "@testing-library/react-native": ">= 13.2.0", "expo": "*", "expo-constants": "^55.0.7", "expo-linking": "^55.0.7", "react": "*", "react-dom": "*", "react-native": "*", "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", "react-native-screens": "*", "react-native-web": "*", "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" }, "optionalPeers": ["@react-navigation/drawer", "@testing-library/react-native", "react-dom", "react-native-gesture-handler", "react-native-reanimated", "react-native-web", "react-server-dom-webpack"] }, "sha512-B3MQAeZq9B2SS5kgEybGqXYR0AY7QYM7fQ5E4bJwtvZLJjWPmWhDALhBpD26ovK/i1k0fi9VgW47FKJODxM5Jg=="],
+ "expo-router": ["expo-router@55.0.5", "", { "dependencies": { "@expo/metro-runtime": "^55.0.6", "@expo/schema-utils": "^55.0.2", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-navigation/bottom-tabs": "^7.10.1", "@react-navigation/native": "^7.1.28", "@react-navigation/native-stack": "^7.10.1", "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-glass-effect": "^55.0.8", "expo-image": "^55.0.6", "expo-server": "^55.0.6", "expo-symbols": "^55.0.5", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-native-is-edge-to-edge": "^1.2.1", "semver": "~7.6.3", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "shallowequal": "^1.1.0", "use-latest-callback": "^0.2.1", "vaul": "^1.1.2" }, "peerDependencies": { "@expo/log-box": "55.0.7", "@react-navigation/drawer": "^7.7.2", "@testing-library/react-native": ">= 13.2.0", "expo": "*", "expo-constants": "^55.0.7", "expo-linking": "^55.0.7", "react": "*", "react-dom": "*", "react-native": "*", "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", "react-native-screens": "*", "react-native-web": "*", "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" }, "optionalPeers": ["@react-navigation/drawer", "@testing-library/react-native", "react-dom", "react-native-gesture-handler", "react-native-reanimated", "react-native-web", "react-server-dom-webpack"] }, "sha512-PzN545wLtznKuVQmJXnAKB/JFjSJJIPHatsjJe4Cl6bRADr/MbWv5d2fqOpqFD/C0ZGCRHY1uBalq7mb5IQ3ZQ=="],
"expo-server": ["expo-server@55.0.6", "", {}, "sha512-xI72FTm469FfuuBL2R5aNtthgH+GR7ygOpsx/KcPS0K8AZaZd7VjtEExbzn9/qyyYkWW3T+3dAmCDKOMX8gdmQ=="],
@@ -862,7 +862,7 @@
"expo-status-bar": ["expo-status-bar@55.0.4", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-BPDjUXKqv1F9j2YNGLRZfkBEZXIEEpqj+t81y4c+4fdSN3Pos7goIHXgcl2ozbKQLgKRZQyNZQtbUgh5UjHYUQ=="],
- "expo-symbols": ["expo-symbols@55.0.4", "", { "dependencies": { "@expo-google-fonts/material-symbols": "^0.4.1", "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "expo": "*", "expo-font": "*", "react": "*", "react-native": "*" } }, "sha512-w9rxPlpta3gks0G4Tvpq/qQdiMp4R/XOeOzyjSruYUQakmsWbQBKA+Sd/fCVXs7qFJSvVTOGXiOhZm+YJRYZVg=="],
+ "expo-symbols": ["expo-symbols@55.0.5", "", { "dependencies": { "@expo-google-fonts/material-symbols": "^0.4.1", "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "expo": "*", "expo-font": "*", "react": "*", "react-native": "*" } }, "sha512-W/QYRvnYVes947ZYOHtuKL8Gobs7BUjeu9oknzbo4jGnou7Ks6bj1CwdT0ZWNBgaTopbS4/POXumJIkW4cTPSQ=="],
"expo-system-ui": ["expo-system-ui@55.0.9", "", { "dependencies": { "@react-native/normalize-colors": "0.83.2", "debug": "^4.3.2" }, "peerDependencies": { "expo": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-8ygP1B0uFAFI8s7eHY2IcGnE83GhFeZYwHBr/fQ4dSXnc7iVT9zp2PvyTyiDiibQ69dBG+fauMQ4KlPcOO51kQ=="],
diff --git a/drizzle/0001_nifty_alice.sql b/drizzle/0001_nifty_alice.sql
new file mode 100644
index 0000000..de3f559
--- /dev/null
+++ b/drizzle/0001_nifty_alice.sql
@@ -0,0 +1,8 @@
+CREATE TABLE `password_reset_tokens` (
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ `user_id` text NOT NULL,
+ `token` text NOT NULL,
+ `created_at` integer NOT NULL
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX `password_reset_tokens_token_unique` ON `password_reset_tokens` (`token`);
diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json
new file mode 100644
index 0000000..226c9d4
--- /dev/null
+++ b/drizzle/meta/0001_snapshot.json
@@ -0,0 +1,221 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "d509af9b-df90-49d7-a4df-8e531445d73c",
+ "prevId": "a6d42745-9c63-42b7-aac0-76d74e9fff7a",
+ "tables": {
+ "items": {
+ "name": "items",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "password_reset_tokens": {
+ "name": "password_reset_tokens",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "password_reset_tokens_token_unique": {
+ "name": "password_reset_tokens_token_unique",
+ "columns": ["token"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "password_resets": {
+ "name": "password_resets",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "code": {
+ "name": "code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "sessions": {
+ "name": "sessions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "users": {
+ "name": "users",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "avatar_url": {
+ "name": "avatar_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "columns": ["email"],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 574d4a5..b1034df 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -8,6 +8,13 @@
"when": 1772545231738,
"tag": "0000_calm_captain_britain",
"breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "6",
+ "when": 1773337960538,
+ "tag": "0001_nifty_alice",
+ "breakpoints": true
}
]
}
diff --git a/drizzle/migrations.js b/drizzle/migrations.js
index ac28fda..2645e46 100644
--- a/drizzle/migrations.js
+++ b/drizzle/migrations.js
@@ -1,11 +1,13 @@
// This file is required for Expo/React Native SQLite migrations - https://orm.drizzle.team/quick-sqlite/expo
import m0000 from "./0000_calm_captain_britain.sql";
+import m0001 from "./0001_nifty_alice.sql";
import journal from "./meta/_journal.json";
export default {
journal,
migrations: {
m0000,
+ m0001,
},
};
diff --git a/package.json b/package.json
index 3610ef3..36e0710 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,8 @@
"lint:fix": "biome check . --write",
"format": "biome format . --write",
"typecheck": "tsc --noEmit",
- "test": "bun test"
+ "test": "bun test",
+ "upgrade-deps": "bunx expo install --fix"
},
"dependencies": {
"@expo/metro-runtime": "~55.0.6",
@@ -41,13 +42,13 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.45.1",
- "expo": "~55.0.4",
+ "expo": "~55.0.5",
"expo-constants": "~55.0.7",
- "expo-crypto": "55.0.8",
+ "expo-crypto": "~55.0.9",
"expo-drizzle-studio-plugin": "^0.2.1",
"expo-linking": "~55.0.7",
"expo-localization": "~55.0.8",
- "expo-router": "~55.0.3",
+ "expo-router": "~55.0.5",
"expo-splash-screen": "~55.0.10",
"expo-sqlite": "^55.0.10",
"expo-status-bar": "~55.0.4",
diff --git a/src/app/(auth)/forgot-password.tsx b/src/app/(auth)/forgot-password.tsx
index cb94aec..9a5da63 100644
--- a/src/app/(auth)/forgot-password.tsx
+++ b/src/app/(auth)/forgot-password.tsx
@@ -1,10 +1,5 @@
-import { View } from "react-native";
import { ForgotPasswordForm } from "@/shared/components/forgot-password-form";
export default function ForgotPassword() {
- return (
-
-
-
- );
+ return ;
}
diff --git a/src/app/(auth)/reset-password.tsx b/src/app/(auth)/reset-password.tsx
index 7e8ed17..5c3ffa6 100644
--- a/src/app/(auth)/reset-password.tsx
+++ b/src/app/(auth)/reset-password.tsx
@@ -1,10 +1,5 @@
-import { View } from "react-native";
import { ResetPasswordForm } from "@/shared/components/reset-password-form";
export default function ResetPassword() {
- return (
-
-
-
- );
+ return ;
}
diff --git a/src/app/(auth)/signin.tsx b/src/app/(auth)/signin.tsx
index 7bdd965..52a56ba 100644
--- a/src/app/(auth)/signin.tsx
+++ b/src/app/(auth)/signin.tsx
@@ -1,10 +1,5 @@
-import { View } from "react-native";
import { SignInForm } from "@/shared/components/sign-in-form";
export default function SignIn() {
- return (
-
-
-
- );
+ return ;
}
diff --git a/src/app/(auth)/signup.tsx b/src/app/(auth)/signup.tsx
index e20f6dd..40e27ff 100644
--- a/src/app/(auth)/signup.tsx
+++ b/src/app/(auth)/signup.tsx
@@ -1,10 +1,5 @@
-import { View } from "react-native";
import { SignUpForm } from "@/shared/components/sign-up-form";
export default function SignUp() {
- return (
-
-
-
- );
+ return ;
}
diff --git a/src/shared/components/forgot-password-form.tsx b/src/shared/components/forgot-password-form.tsx
index 7023e35..d453958 100644
--- a/src/shared/components/forgot-password-form.tsx
+++ b/src/shared/components/forgot-password-form.tsx
@@ -1,3 +1,5 @@
+import { useRouter } from "expo-router";
+import { useState } from "react";
import { View } from "react-native";
import { Button } from "@/shared/components/ui/button";
import {
@@ -10,10 +12,22 @@ import {
import { Input } from "@/shared/components/ui/input";
import { Label } from "@/shared/components/ui/label";
import { Text } from "@/shared/components/ui/text";
+import { createPasswordResetToken } from "@/shared/services/userService";
export function ForgotPasswordForm() {
- function onSubmit() {
- // TODO: Submit form and navigate to reset password screen if successful
+ const [email, setEmail] = useState("");
+ const [loading, setLoading] = useState(false);
+ const router = useRouter();
+
+ async function onSubmit() {
+ if (!email) return;
+ setLoading(true);
+ try {
+ const token = await createPasswordResetToken(email);
+ router.push({ pathname: "/(auth)/reset-password", params: { token } });
+ } finally {
+ setLoading(false);
+ }
}
return (
@@ -39,9 +53,11 @@ export function ForgotPasswordForm() {
autoCapitalize="none"
returnKeyType="send"
onSubmitEditing={onSubmit}
+ value={email}
+ onChangeText={setEmail}
/>
-
+
Reset your password
diff --git a/src/shared/components/reset-password-form.tsx b/src/shared/components/reset-password-form.tsx
index d4aeadc..d8ea03f 100644
--- a/src/shared/components/reset-password-form.tsx
+++ b/src/shared/components/reset-password-form.tsx
@@ -1,67 +1,85 @@
-import * as React from "react";
-import { type TextInput, View } from "react-native";
+import { useLocalSearchParams, useRouter } from "expo-router";
+import { CheckCircle2, XCircle } from "lucide-react-native";
+import { useState } from "react";
+import { View } from "react-native";
+import {
+ Alert,
+ AlertDescription,
+ AlertTitle,
+} from "@/shared/components/ui/alert";
import { Button } from "@/shared/components/ui/button";
import {
Card,
CardContent,
- CardDescription,
CardHeader,
CardTitle,
} from "@/shared/components/ui/card";
import { Input } from "@/shared/components/ui/input";
import { Label } from "@/shared/components/ui/label";
import { Text } from "@/shared/components/ui/text";
+import { resetPassword } from "@/shared/services/userService";
export function ResetPasswordForm() {
- const codeInputRef = React.useRef(null);
-
- function onPasswordSubmitEditing() {
- codeInputRef.current?.focus();
- }
+ const { token } = useLocalSearchParams<{ token: string }>();
+ const [password, setPassword] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [alert, setAlert] = useState<{
+ type: "success" | "error";
+ message: string;
+ } | null>(null);
+ const router = useRouter();
- function onSubmit() {
- // TODO: Submit form and navigate to protected screen if successful
+ async function onSubmit() {
+ if (!password || !token) return;
+ setLoading(true);
+ setAlert(null);
+ try {
+ await resetPassword(token, password);
+ setAlert({ type: "success", message: "Password updated successfully!" });
+ setTimeout(() => router.replace("/(auth)/signin"), 2000);
+ } catch (err) {
+ setAlert({ type: "error", message: (err as Error).message });
+ } finally {
+ setLoading(false);
+ }
}
return (
-
+
+ {alert && (
+
+
+ {alert.type === "success" ? "Success" : "Error"}
+
+ {alert.message}
+
+ )}
+
Reset password
-
- Enter the code sent to your email and set a new password
-
-
- New password
-
+ New password
-
-
- Verification code
-
-
- Reset Password
+
+ Save new password
diff --git a/src/shared/components/sign-up-form.tsx b/src/shared/components/sign-up-form.tsx
index 7a07354..07b145e 100644
--- a/src/shared/components/sign-up-form.tsx
+++ b/src/shared/components/sign-up-form.tsx
@@ -1,10 +1,17 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "expo-router";
+import { CheckCircle2, XCircle } from "lucide-react-native";
import * as React from "react";
+import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Pressable, type TextInput, View } from "react-native";
import { type SignUpSchema, signUpSchema } from "@/shared/auth/schemas";
import { SocialConnections } from "@/shared/components/social-connections";
+import {
+ Alert,
+ AlertDescription,
+ AlertTitle,
+} from "@/shared/components/ui/alert";
import { Button } from "@/shared/components/ui/button";
import {
Card,
@@ -22,12 +29,15 @@ import { useAuth } from "../hooks/useAuth";
export function SignUpForm() {
const router = useRouter();
const { signUp } = useAuth();
+ const [alert, setAlert] = useState<{
+ type: "success" | "error";
+ message: string;
+ } | null>(null);
const passwordInputRef = React.useRef(null);
const {
control,
handleSubmit,
- setError,
formState: { errors, isSubmitting },
} = useForm({
resolver: zodResolver(signUpSchema),
@@ -37,16 +47,30 @@ export function SignUpForm() {
async function onSubmit({ email, password }: SignUpSchema) {
try {
await signUp(email, password);
- router.replace("/(auth)/signin");
+ setAlert({ type: "success", message: "Account created successfully!" });
+ setTimeout(() => router.replace("/(auth)/signin"), 2000);
} catch (err) {
- setError("root", {
+ setAlert({
+ type: "error",
message: err instanceof Error ? err.message : "Something went wrong",
});
}
}
return (
-
+
+ {alert && (
+
+
+ {alert.type === "success" ? "Success" : "Error"}
+
+ {alert.message}
+
+ )}
+
diff --git a/src/shared/components/ui/alert.tsx b/src/shared/components/ui/alert.tsx
new file mode 100644
index 0000000..ba15335
--- /dev/null
+++ b/src/shared/components/ui/alert.tsx
@@ -0,0 +1,85 @@
+import type { LucideIcon } from "lucide-react-native";
+import * as React from "react";
+import { View, type ViewProps } from "react-native";
+import { Icon } from "@/shared/components/ui/icon";
+import { Text, TextClassContext } from "@/shared/components/ui/text";
+import { cn } from "@/shared/lib/utils";
+
+function Alert({
+ className,
+ variant,
+ children,
+ icon,
+ iconClassName,
+ ...props
+}: ViewProps &
+ React.RefAttributes & {
+ icon: LucideIcon;
+ variant?: "default" | "destructive";
+ iconClassName?: string;
+ }) {
+ return (
+
+
+
+
+
+ {children}
+
+
+ );
+}
+
+function AlertTitle({
+ className,
+ ...props
+}: React.ComponentProps & React.RefAttributes) {
+ return (
+
+ );
+}
+
+function AlertDescription({
+ className,
+ ...props
+}: React.ComponentProps & React.RefAttributes) {
+ const textClass = React.useContext(TextClassContext);
+ return (
+
+ );
+}
+
+export { Alert, AlertDescription, AlertTitle };
diff --git a/src/shared/db/schema.ts b/src/shared/db/schema.ts
index 341e883..a0ce6be 100644
--- a/src/shared/db/schema.ts
+++ b/src/shared/db/schema.ts
@@ -21,6 +21,13 @@ export const passwordResets = sqliteTable("password_resets", {
expiresAt: integer("expires_at").notNull(),
});
+export const passwordResetTokens = sqliteTable("password_reset_tokens", {
+ id: integer("id").primaryKey({ autoIncrement: true }),
+ userId: text("user_id").notNull(),
+ token: text("token").notNull().unique(),
+ createdAt: integer("created_at").notNull(),
+});
+
export const items = sqliteTable("items", {
id: integer("id").primaryKey().notNull(),
title: text("title").notNull(),
diff --git a/src/shared/services/userService.ts b/src/shared/services/userService.ts
index faaad78..c1c8599 100644
--- a/src/shared/services/userService.ts
+++ b/src/shared/services/userService.ts
@@ -1,7 +1,7 @@
import { eq } from "drizzle-orm";
import * as Crypto from "expo-crypto";
import { db } from "@/shared/db/client";
-import { sessions, users } from "@/shared/db/schema";
+import { passwordResetTokens, sessions, users } from "@/shared/db/schema";
import type { AuthUser } from "@/shared/types/auth";
async function hashPassword(password: string): Promise {
@@ -84,3 +84,62 @@ export async function signInUser(
export async function signOutUser(userId: number): Promise {
await db.delete(sessions).where(eq(sessions.userId, String(userId)));
}
+
+export async function createPasswordResetToken(email: string): Promise {
+ const [user] = await db
+ .select()
+ .from(users)
+ .where(eq(users.email, email))
+ .limit(1);
+
+ if (!user) return "ok";
+
+ const token = await Crypto.digestStringAsync(
+ Crypto.CryptoDigestAlgorithm.SHA256,
+ `${email}-${Date.now()}-${Math.random()}`,
+ );
+
+ await db
+ .delete(passwordResetTokens)
+ .where(eq(passwordResetTokens.userId, String(user.id)));
+
+ await db.insert(passwordResetTokens).values({
+ userId: String(user.id),
+ token,
+ createdAt: Date.now(),
+ });
+
+ return token;
+}
+
+export async function resetPassword(
+ token: string,
+ newPassword: string,
+): Promise {
+ const [resetToken] = await db
+ .select()
+ .from(passwordResetTokens)
+ .where(eq(passwordResetTokens.token, token))
+ .limit(1);
+
+ if (!resetToken) throw new Error("Invalid or expired token");
+
+ const FIFTEEN_MINUTES = 15 * 60 * 1000;
+ if (Date.now() - resetToken.createdAt > FIFTEEN_MINUTES) {
+ await db
+ .delete(passwordResetTokens)
+ .where(eq(passwordResetTokens.token, token));
+ throw new Error("Token expired");
+ }
+
+ const passwordHash = await hashPassword(newPassword);
+
+ await db
+ .update(users)
+ .set({ passwordHash })
+ .where(eq(users.id, Number(resetToken.userId)));
+
+ await db
+ .delete(passwordResetTokens)
+ .where(eq(passwordResetTokens.token, token));
+}