From d474331e274fb8a28a39821563f43684b30711cd Mon Sep 17 00:00:00 2001
From: Or Forshmit <162809292+OrF8@users.noreply.github.com>
Date: Wed, 8 Apr 2026 21:03:37 +0300
Subject: [PATCH 1/4] fix: resolve eslint hook state-in-effect violations
---
CHANGELOG.md | 46 ++++++
README.md | 245 ++++++++++++++++++++------------
SECURITY.md | 65 ++++-----
functions/package-lock.json | 2 +
functions/package.json | 2 +-
package-lock.json | 22 +--
package.json | 2 +-
src/hooks/useBoards.js | 73 ++++++----
src/hooks/useIncomingInvites.js | 53 ++++---
src/pages/BoardPage.jsx | 2 +-
10 files changed, 312 insertions(+), 200 deletions(-)
create mode 100644 CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..4f90c39
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,46 @@
+# Changelog
+
+All notable changes to this project are documented in this file.
+
+The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/).
+
+## [Unreleased]
+
+### Added
+- _No entries yet._
+
+## [1.1.0]
+
+### Added
+- Excel export for boards using `.xlsx` output.
+- Super-board export support with multiple worksheets (one worksheet per sub-board).
+- Optional summary worksheet in super-board exports.
+- Safer Excel export sanitization for sheet names, file names, and formula-like text values.
+- Explicit direct vs. inherited collaborator display in board collaboration management.
+
+### Changed
+- Documentation refresh across README, security policy, and release notes for the `v1.1.0` release.
+- Version alignment across frontend and Cloud Functions packages to `1.1.0`.
+
+### Fixed
+- Export date handling and workbook formatting improvements in generated Excel files.
+- Export-related CSP allowance updates for ExcelJS-hosted assets.
+- Board/super-board export UI flow improvements and related stability fixes.
+
+### Security
+- Security policy updated to supported-version policy for `1.1.x` and private reporting guidance.
+
+## [1.0.4]
+
+### Changed
+- Dependency and maintenance updates before `1.1.0`.
+
+## [1.0.1]
+
+### Changed
+- Early post-`1.0.0` maintenance updates.
+
+## [1.0.0]
+
+### Added
+- Initial public release of the Expense Management application.
diff --git a/README.md b/README.md
index 6ed0e90..4e079de 100644
--- a/README.md
+++ b/README.md
@@ -1,89 +1,112 @@
# Expense Management
-Expense Management is a real-time collaborative web app for managing shared expenses in Hebrew (RTL).
-It is designed for families, roommates, and partners who need one reliable place to track spending, collaborators,
-and board hierarchies without exposing broad user data.
+Expense Management is a Firebase + React web app for collaborative expense tracking, with a Hebrew RTL UI.
+It supports shared boards, one-level board hierarchies ("super boards" with sub-boards), invites, and Excel export.
-The project combines a React frontend with Firebase Authentication, Firestore, Hosting, and callable Cloud Functions.
-Invite, collaborator, and account management flows are implemented server-side to preserve a least-privilege security model.
-
-π **[Try the live app β](https://of8-expense-management.web.app/)**
+π **Live app:** https://of8-expense-management.web.app/
-
-
-
-
-
-
-
+
-## Table of Contents
-
-- [Overview](#overview)
-- [Key Features](#key-features)
-- [Tech Stack & Architecture](#tech-stack--architecture)
-- [Project Structure](#project-structure)
-- [Getting Started](#getting-started)
- - [Prerequisites](#prerequisites)
- - [Installation](#installation)
- - [Environment Configuration](#environment-configuration)
- - [Run Locally](#run-locally)
-- [Deployment](#deployment)
-- [Security Notes](#security-notes)
-- [License](#license)
-
## Overview
-The app centers around collaborative boards. Each board contains transactions, collaborators, and optional sub-boards.
-Members see real-time updates, while ownership and membership operations are enforced by Firestore rules and Cloud Functions.
+The app is designed for small groups (families, roommates, partners) who manage shared spending.
+Each board has members, transactions, and optional hierarchy relationships.
+Data is stored in Firestore and updates in real time.
+
+## Features
+
+- **Authentication:** Email/password and Google sign-in.
+- **Boards:** Create, rename, delete, and view shared boards.
+- **Super boards:** Group regular boards under one parent board (one-level hierarchy).
+- **Invitations:** Board owners can invite by email; invitees can accept or decline.
+- **Membership model:**
+ - `directMemberUids`: explicitly invited to a board.
+ - `memberUids`: effective access (direct + inherited from parent board).
+- **Inherited access:** Membership flows **down** from a super board to its sub-boards.
+- **Transactions:** Create, edit, and delete transactions on regular boards.
+- **Amounts:** Positive and negative amounts are supported (useful for refunds/credits).
+- **Future dates:** Optional `transactionDate` accepts valid `YYYY-MM-DD` dates, including future dates.
+- **Excel export:**
+ - Regular board: single worksheet export.
+ - Super board: multi-sheet export (one sheet per sub-board) + optional summary sheet.
+- **Account management:** Update nickname, sign out, and delete account (with server-side cleanup).
+
+## Boards vs. Super Boards
+
+- **Regular board:** A board without `subBoardIds`; it contains transactions directly.
+- **Super board:** A board with one or more `subBoardIds`; it aggregates totals from sub-boards and does not show a transaction-entry view.
+- **Sub-board:** A board with `parentBoardId` set.
+
+Hierarchy is intentionally **one level**:
+- A board can be top-level, or a child of one parent board.
+- A sub-board cannot itself have sub-boards.
+
+## Access Model (Direct vs. Inherited)
+
+This project uses two membership fields:
-## Key Features
+- `directMemberUids` = users directly added to that board.
+- `memberUids` = users with effective access to that board.
-- Firebase Auth (email/password + Google)
-- Shared expense boards with real-time transaction updates
-- Board hierarchy (parent/sub-board relationships)
-- Installment-aware credit-card tracking
-- Email-based invite flow (create/accept/decline/revoke)
-- Owner/member management (remove member, leave board)
-- Account deletion with server-side data cleanup
-- Hebrew RTL interface with light/dark theme
+Behavior:
+- Direct membership on a super board grants inherited access to descendant sub-boards.
+- Direct membership on a sub-board does **not** grant access to its parent.
+- Removing a direct member cascades membership recalculation through descendants.
-## Tech Stack & Architecture
+## Invitation Flow
-- **Frontend:** React 19, Vite 8, React Router 7, Tailwind CSS 4
-- **Backend:** Firebase Cloud Functions (Node.js 22)
-- **Data/Auth:** Firestore + Firebase Authentication
-- **Hosting:** Firebase Hosting
-- **Security:** Firestore rules + App Check enforcement on callable functions
+1. Board owner sends an invite by email.
+2. Invite document is created under `boards/{boardId}/invites`.
+3. Invitee sees incoming invites and can accept/decline.
+4. Accepting adds membership and deletes the invite.
+5. Declining deletes the invite.
+
+Notes:
+- Invites include `expiresAt` and are treated as pending while the document exists.
+- Functions enforce ownership/auth checks before invite and membership mutations.
+
+## Architecture
+
+- **Frontend:** React 19 + Vite 8 + React Router 7 + Tailwind CSS 4.
+- **Backend:** Firebase Cloud Functions (Node.js 22 runtime).
+- **Data/Auth:** Firestore + Firebase Authentication.
+- **Hosting:** Firebase Hosting.
+- **Security controls:** Firestore Security Rules, App Check integration in the client, and callable functions with `enforceAppCheck: true`.
## Project Structure
```text
ExpenseManagement/
βββ src/
-β βββ components/ # UI and board/collaborator components
-β βββ context/ # Auth and theme providers
-β βββ firebase/ # Firebase client modules (auth, boards, invites, users, config)
-β βββ hooks/ # Data hooks (boards, transactions, incoming invites)
-β βββ pages/ # Route pages (auth, boards, board view, legal pages)
-βββ functions/ # Callable Cloud Functions for invite/member/account flows
-βββ firestore.rules # Firestore authorization and validation rules
-βββ firebase.json # Hosting targets, headers, and Firebase service config
-βββ .github/workflows/ # Deploy + CodeQL workflows
+β βββ components/
+β βββ context/
+β βββ firebase/
+β βββ hooks/
+β βββ pages/
+β βββ utils/
+βββ functions/
+βββ firestore.rules
+βββ firebase.json
+βββ .github/workflows/
```
-## Getting Started
+## Setup
### Prerequisites
-- Node.js 22+
+- Node.js 22+ (recommended for local dev, including functions)
- npm
-- Firebase project with Authentication, Firestore, Hosting, and Functions enabled
+- Firebase project with:
+ - Authentication
+ - Cloud Firestore
+ - Cloud Functions
+ - Hosting
+ - App Check (recommended/enabled for production)
-### Installation
+### Install
```bash
git clone https://github.com/OrF8/ExpenseManagement.git
@@ -92,15 +115,15 @@ npm ci
npm --prefix functions ci
```
-### Environment Configuration
+### Environment Variables
-Create `.env` from `.env.example` and fill your Firebase values.
+Copy `.env.example` to `.env`:
```bash
cp .env.example .env
```
-Required frontend variables:
+Required variables:
- `VITE_FIREBASE_API_KEY`
- `VITE_FIREBASE_AUTH_DOMAIN`
@@ -110,64 +133,104 @@ Required frontend variables:
- `VITE_FIREBASE_APP_ID`
- `VITE_RECAPTCHA_V3_SITE_KEY`
-#### Optional: Preview deployment configuration (`.env.preview`)
-For preview deployments using the PowerShell script (`npm run deploy:preview -- -PrNumber pr_num`),
-create a `.env.preview` file with the same Firebase variables as `.env`,
-with two optional variables (if you want to enable App Check debug mode for the preview channel):
-- `VITE_APPCHECK_DEBUG=true`
-- `VITE_APPCHECK_DEBUG_TOKEN=your_app_check_debug_token_here`
-
-Another variable is `FIREBASE_PROJECT_ID`. This should be set to the same
-project as `.env` if you want to deploy to a preview channel in the same project,
-or it can be set to a different project ID if you want to deploy the preview channel to a separate Firebase project.
-
-This allows you to deploy a Firebase Hosting preview channel separately from your main environment.
+Optional preview deployment file:
```bash
cp .env.preview.example .env.preview
```
-This is useful for testing changes without affecting the live app.
+Additional preview variables:
-### Run Locally
+- `VITE_APPCHECK_DEBUG` (optional)
+- `VITE_APPCHECK_DEBUG_TOKEN` (optional)
+- `FIREBASE_PROJECT_ID` (required for preview deploy script)
+
+## Development
+
+### Run locally
```bash
npm run dev
```
-App URL: `http://localhost:5173`
+Vite dev server: `http://localhost:5173`
-Production build preview:
+### Production build preview
```bash
npm run build
npm run preview
```
-App URL: `http://localhost:4173`
+Preview server: `http://localhost:4173`
+
+### Linting
+
+- Frontend linting is configured via ESLint:
+
+```bash
+npm run lint
+```
+
+- Functions package currently has a placeholder lint script (`Skipping lint`).
## Deployment
-Main deployment is automated through GitHub Actions (`.github/workflows/deploy.yml`) using
-**Google Workload Identity Federation** (OIDC) with a deploy service account.
+### CI/CD (main branch)
-Typical manual deploy commands (**after** building with `npm run build`):
+GitHub Actions workflow `.github/workflows/deploy.yml`:
+- builds the frontend,
+- deploys **Functions + Hosting** to Firebase,
+- authenticates with Google via Workload Identity Federation (OIDC).
+
+### Manual deploy
```bash
-firebase deploy --only firestore:rules --project
+npm run build
firebase deploy --only functions,hosting --project
```
-The repo also includes a PowerShell preview script (`npm run deploy:preview -- -PrNumber pr_num`) that deploys functions and a hosting preview channel using `.env.preview`.
+To deploy Firestore rules explicitly:
+
+```bash
+firebase deploy --only firestore:rules --project
+```
+
+### Preview deployment script
+
+A PowerShell script is provided for preview channels:
+
+```bash
+npm run deploy:preview -- -PrNumber
+```
+
+This script:
+- loads `.env.preview`,
+- deploys functions,
+- builds with `--mode preview`,
+- deploys a Firebase Hosting preview channel.
+
+## Security & Privacy Notes
+
+- Firestore rules restrict board reads/writes to authorized users and owners by role.
+- Invite and membership mutations are handled through callable functions instead of broad client-side user reads.
+- Callable functions are configured with App Check enforcement.
+- Account deletion is handled server-side to remove owned data and membership links.
+- This is a collaborative app: data shared to a board is visible to that boardβs members.
+
+For vulnerability reporting, see [SECURITY.md](./SECURITY.md).
+
+## Release Status
+
+Current release target: **v1.1.0**.
-## Security Notes
+Notable release focus:
+- hierarchy-aware collaboration,
+- Excel export improvements (including super-board multi-sheet export),
+- documentation refresh and version alignment.
-- Firestore access is scoped to board membership and document ownership; `/users/{uid}` is owner-readable and owner-writable only.
-- Invite creation, acceptance, declination, revocation, member removal, and account deletion are all implemented as callable Cloud Functions to ensure server-side validation and least-privilege access.
-- Callable functions enforce App Check (`enforceAppCheck: true`), and Hosting serves strict security headers (including CSP, X-Frame-Options, and Referrer-Policy).
-- App Check is also enforced on Firestore and authentication operations to prevent abuse from unauthorized clients.
-- Invite queries use collection-group access constrained by authenticated email match in Firestore rules.
+See [CHANGELOG.md](./CHANGELOG.md) for release notes.
## License
-This project is licensed under the MIT license. For more information, see the [LICENSE](./LICENSE) file.
+MIT. See [LICENSE](./LICENSE).
diff --git a/SECURITY.md b/SECURITY.md
index 5d6857a..a2ddec5 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,53 +2,48 @@
## Supported Versions
-This project is currently in early development. Only the latest version is supported with security updates.
+Security fixes are provided for the latest minor line only.
-| Version | Supported |
-| -------- | ------------------ |
-| 1.0.x | :white_check_mark: |
-| < 1.0.0 | :x: |
+| Version | Supported |
+| --- | --- |
+| 1.1.x | β
|
+| 1.0.x | β |
+| < 1.0.0 | β |
-We strongly recommend always using the latest version of the application.
-
----
+If you run a self-hosted deployment, upgrade to the latest `1.1.x` patch release as soon as practical.
## Reporting a Vulnerability
-If you discover a security vulnerability, please report it responsibly.
+Please report vulnerabilities **privately**.
+
+### Preferred reporting channel
-### π¬ How to report
-- Open a **private security advisory** on GitHub (preferred), or
-- Contact the maintainer directly via GitHub, or
-- Contact us via email [expensemanagementwebsite@gmail.com](mailto:expensemanagementwebsite@gmail.com)
+Use GitHubβs private vulnerability reporting for this repository:
-Please include:
-- A clear description of the vulnerability
-- Steps to reproduce the issue
-- Potential impact
-- Suggested fixes (if available)
+- Open a private report via the repositoryβs **Security** tab ("Report a vulnerability") when available.
+- If private reporting is not enabled in your view, open a GitHub Security Advisory draft for the repository maintainers.
-### β±οΈ Response timeline
-- Initial response: within **48β72 hours**
-- Status update: within **5β7 days**
+Do **not** open a public issue for suspected vulnerabilities.
-### π Responsible disclosure
-Please **do not publicly disclose** the vulnerability until it has been reviewed and addressed.
+### What to include
-### β
What to expect
-- If the vulnerability is accepted:
- - It will be fixed as soon as possible
- - A patched version will be released
-- If declined:
- - You will receive an explanation
+Please include as much detail as possible:
----
+- Affected area (frontend, Firestore rules, Cloud Functions, deployment config, etc.)
+- Steps to reproduce
+- Expected vs. actual behavior
+- Impact assessment (confidentiality / integrity / availability)
+- Proof-of-concept details, logs, or screenshots (if safe to share)
+- Suggested remediation (optional)
-## Additional Notes
+### Disclosure expectations
-This project uses:
-- Firebase (Firestore, Authentication, Hosting)
+- Please do not publicly disclose the issue before a fix is available.
+- Maintainers will acknowledge reports and triage based on severity and reproducibility.
+- Resolution timelines vary by complexity and maintainer availability.
+- When a report is confirmed, the fix will be shipped in a supported release line.
-While Firebase provides strong security features, proper configuration (e.g., Firestore rules, API restrictions) is critical. Misconfiguration may lead to vulnerabilities.
+## Scope Notes
-If your report relates to Firebase configuration, please include relevant details.
+This project relies on Firebase services (Authentication, Firestore, Functions, Hosting).
+Configuration issues can be security-sensitive; include relevant Firebase project and rule/function context in reports.
diff --git a/functions/package-lock.json b/functions/package-lock.json
index 3e45e73..71ee4ba 100644
--- a/functions/package-lock.json
+++ b/functions/package-lock.json
@@ -1,10 +1,12 @@
{
"name": "expense-management-functions",
+ "version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "expense-management-functions",
+ "version": "1.1.0",
"dependencies": {
"firebase-admin": "^13.7.0",
"firebase-functions": "^7.2.3"
diff --git a/functions/package.json b/functions/package.json
index 42ae3ad..b5ea2a3 100644
--- a/functions/package.json
+++ b/functions/package.json
@@ -22,5 +22,5 @@
"eslint-config-google": "^0.14.0"
},
"private": true,
- "version": "1.0.4"
+ "version": "1.1.0"
}
diff --git a/package-lock.json b/package-lock.json
index da458b2..2a633a2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "expense-management",
- "version": "1.0.4",
+ "version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "expense-management",
- "version": "1.0.4",
+ "version": "1.1.0",
"dependencies": {
"firebase": "^12.11.0",
"react": "^19.2.4",
@@ -1451,9 +1451,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1471,9 +1468,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1491,9 +1485,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1511,9 +1502,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1531,9 +1519,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1551,9 +1536,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
diff --git a/package.json b/package.json
index 40bdfdc..954568b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "expense-management",
"private": true,
- "version": "1.0.4",
+ "version": "1.1.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/src/hooks/useBoards.js b/src/hooks/useBoards.js
index 24da487..1a3a37f 100644
--- a/src/hooks/useBoards.js
+++ b/src/hooks/useBoards.js
@@ -5,49 +5,64 @@ import { subscribeWithAppCheckRetry } from '../utils/appCheckRetry';
export function useBoards() {
const { user } = useAuth();
- const [boards, setBoards] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [retryingSecureConnection, setRetryingSecureConnection] = useState(false);
+ const uid = user?.uid ?? null;
- useEffect(() => {
- if (!user?.uid) {
- setBoards([]);
- setLoading(false);
- setError(null);
- setRetryingSecureConnection(false);
- return;
- }
+ const [state, setState] = useState({
+ boards: [],
+ error: null,
+ retryingSecureConnection: false,
+ forUid: null,
+ });
- setLoading(true);
- setError(null);
- setRetryingSecureConnection(false);
+ useEffect(() => {
+ if (!uid) return;
const unsubscribe = subscribeWithAppCheckRetry(
- (onData, onError) => subscribeToBoards(user.uid, onData, onError),
+ (onData, onError) => subscribeToBoards(uid, onData, onError),
(data) => {
- setBoards(data);
- setError(null);
- setRetryingSecureConnection(false);
- setLoading(false);
+ setState({
+ boards: data,
+ error: null,
+ retryingSecureConnection: false,
+ forUid: uid,
+ });
},
(err) => {
- setBoards([]);
- setError(err?.message || 'Χ©ΧΧΧΧ ΧΧΧ’ΧΧ Χͺ ΧΧΧΧΧΧͺ');
- setRetryingSecureConnection(false);
- setLoading(false);
+ setState({
+ boards: [],
+ error: err?.message || 'Χ©ΧΧΧΧ ΧΧΧ’ΧΧ Χͺ ΧΧΧΧΧΧͺ',
+ retryingSecureConnection: false,
+ forUid: uid,
+ });
},
{
onRetryAttempt: () => {
- setRetryingSecureConnection(true);
- setLoading(true);
- setError(null);
+ setState((prev) => ({
+ ...prev,
+ retryingSecureConnection: true,
+ error: null,
+ }));
},
},
);
return () => unsubscribe();
- }, [user?.uid]);
+ }, [uid]);
+
+ if (!uid) {
+ return {
+ boards: [],
+ loading: false,
+ error: null,
+ retryingSecureConnection: false,
+ };
+ }
- return { boards, loading, error, retryingSecureConnection };
+ const loading = state.forUid !== uid;
+ return {
+ boards: loading ? [] : state.boards,
+ loading,
+ error: state.error,
+ retryingSecureConnection: state.retryingSecureConnection,
+ };
}
diff --git a/src/hooks/useIncomingInvites.js b/src/hooks/useIncomingInvites.js
index dbd4944..9ad6e1e 100644
--- a/src/hooks/useIncomingInvites.js
+++ b/src/hooks/useIncomingInvites.js
@@ -4,37 +4,46 @@ import { useAuth } from '../context/AuthContext';
export function useIncomingInvites() {
const { user } = useAuth();
- const [invites, setInvites] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
+ const email = user?.email ?? null;
- useEffect(() => {
- if (!user?.email) {
- setInvites([]);
- setLoading(false);
- setError(null);
- return;
- }
+ const [state, setState] = useState({
+ invites: [],
+ error: null,
+ forEmail: null,
+ });
- setLoading(true);
- setError(null);
+ useEffect(() => {
+ if (!email) return;
const unsubscribe = subscribeToIncomingInvites(
- user.email,
+ email,
(data) => {
- setInvites(data);
- setError(null);
- setLoading(false);
+ setState({ invites: data, error: null, forEmail: email });
},
(err) => {
- setInvites([]);
- setError(err?.message || 'Χ©ΧΧΧΧ ΧΧΧ’ΧΧ Χͺ ΧΧΧΧΧ ΧΧͺ');
- setLoading(false);
- }
+ setState({
+ invites: [],
+ error: err?.message || 'Χ©ΧΧΧΧ ΧΧΧ’ΧΧ Χͺ ΧΧΧΧΧ ΧΧͺ',
+ forEmail: email,
+ });
+ },
);
return () => unsubscribe();
- }, [user?.email]);
+ }, [email]);
+
+ if (!email) {
+ return {
+ invites: [],
+ loading: false,
+ error: null,
+ };
+ }
- return { invites, loading, error };
+ const loading = state.forEmail !== email;
+ return {
+ invites: loading ? [] : state.invites,
+ loading,
+ error: state.error,
+ };
}
diff --git a/src/pages/BoardPage.jsx b/src/pages/BoardPage.jsx
index 2121912..88ee808 100644
--- a/src/pages/BoardPage.jsx
+++ b/src/pages/BoardPage.jsx
@@ -271,7 +271,7 @@ export function BoardPage() {
b.id !== boardId &&
isMergeValid(boardId, b.id, allBoards),
);
- }, [isOwner, board, allBoards, boardId, user?.uid]);
+ }, [isOwner, board, isSuperBoard, isSubBoard, allBoards, boardId, user?.uid]);
async function handleMoveUnder(parentId) {
const parent = allBoards.find((b) => b.id === parentId);
From 24cd7566be772178e70ee909ae14dee07631c01d Mon Sep 17 00:00:00 2001
From: or-forshmit8
Date: Wed, 8 Apr 2026 21:15:48 +0300
Subject: [PATCH 2/4] docs: refresh documentation for v1.1.0 release
---
CHANGELOG.md | 8 +++++---
README.md | 44 +++++++++++++++++++++++++++++++++-----------
SECURITY.md | 8 +++++++-
3 files changed, 45 insertions(+), 15 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f90c39..48ea01f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,10 +4,12 @@ All notable changes to this project are documented in this file.
The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/).
-## [Unreleased]
+[//]: # (## [Unreleased])
-### Added
-- _No entries yet._
+[//]: # ()
+[//]: # (### Added)
+
+[//]: # (- _No entries yet._)
## [1.1.0]
diff --git a/README.md b/README.md
index 4e079de..8fa9c11 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,31 @@ It supports shared boards, one-level board hierarchies ("super boards" with sub-
+
+
+
+
+
+
+
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Features](#Features)
+- [Boards vs. Super Boards](#boards-vs-super-boards)
+- [Access Model (Direct vs. Inherited)](#access-model-direct-vs-inherited)
+- [Invitation Flow](#invitation-flow)
+- [Architecture](#architecture)
+- [Project Structure](#project-structure)
+- [Setup](#setup)
+- [Development](#development)
+- [Deployment](#deployment)
+- [Security & Privacy Notes](#security--privacy-notes)
+- [Release Status](#release-status)
+- [License](#license)
## Overview
@@ -81,16 +104,15 @@ Notes:
```text
ExpenseManagement/
βββ src/
-β βββ components/
-β βββ context/
-β βββ firebase/
-β βββ hooks/
-β βββ pages/
-β βββ utils/
-βββ functions/
-βββ firestore.rules
-βββ firebase.json
-βββ .github/workflows/
+β βββ components/ # UI and board/collaborator components
+β βββ context/ # Auth and theme providers
+β βββ firebase/ # Firebase client modules (auth, boards, invites, users, config)
+β βββ hooks/ # Data hooks (boards, transactions, incoming invites)
+β βββ pages/ # Route pages (auth, boards, board view, legal pages)
+βββ functions/ # Callable Cloud Functions for invite/member/account flows
+βββ firestore.rules # Firestore authorization and validation rules
+βββ firebase.json # Hosting targets, headers, and Firebase service config
+βββ .github/workflows/ # Deploy + CodeQL workflows
```
## Setup
@@ -233,4 +255,4 @@ See [CHANGELOG.md](./CHANGELOG.md) for release notes.
## License
-MIT. See [LICENSE](./LICENSE).
+This project is licensed under the MIT license. For more information, see the [LICENSE](./LICENSE) file.
diff --git a/SECURITY.md b/SECURITY.md
index a2ddec5..c49ecf5 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -20,11 +20,17 @@ Please report vulnerabilities **privately**.
Use GitHubβs private vulnerability reporting for this repository:
-- Open a private report via the repositoryβs **Security** tab ("Report a vulnerability") when available.
+- Open a private report via the repositoryβs **Security** tab ("Report a vulnerability").
- If private reporting is not enabled in your view, open a GitHub Security Advisory draft for the repository maintainers.
Do **not** open a public issue for suspected vulnerabilities.
+#### Other reporting channels
+
+- Contact the maintainer directly via GitHub
+- Contact us via email [expensemanagementwebsite@gmail.com](mailto:expensemanagementwebsite@gmail.com)
+
+
### What to include
Please include as much detail as possible:
From b37602f41d7fe9eae650124b63858a4587522b68 Mon Sep 17 00:00:00 2001
From: Or Forshmit <162809292+OrF8@users.noreply.github.com>
Date: Wed, 8 Apr 2026 21:39:35 +0300
Subject: [PATCH 3/4] Fix stale hook state during auth identity switches
---
src/hooks/useBoards.js | 15 +++++++++++----
src/hooks/useIncomingInvites.js | 18 ++++++++++++++----
2 files changed, 25 insertions(+), 8 deletions(-)
diff --git a/src/hooks/useBoards.js b/src/hooks/useBoards.js
index 1a3a37f..a386d78 100644
--- a/src/hooks/useBoards.js
+++ b/src/hooks/useBoards.js
@@ -6,7 +6,6 @@ import { subscribeWithAppCheckRetry } from '../utils/appCheckRetry';
export function useBoards() {
const { user } = useAuth();
const uid = user?.uid ?? null;
-
const [state, setState] = useState({
boards: [],
error: null,
@@ -46,7 +45,15 @@ export function useBoards() {
},
);
- return () => unsubscribe();
+ return () => {
+ unsubscribe();
+ setState({
+ boards: [],
+ error: null,
+ retryingSecureConnection: false,
+ forUid: null,
+ });
+ };
}, [uid]);
if (!uid) {
@@ -62,7 +69,7 @@ export function useBoards() {
return {
boards: loading ? [] : state.boards,
loading,
- error: state.error,
- retryingSecureConnection: state.retryingSecureConnection,
+ error: loading ? null : state.error,
+ retryingSecureConnection: loading ? false : state.retryingSecureConnection,
};
}
diff --git a/src/hooks/useIncomingInvites.js b/src/hooks/useIncomingInvites.js
index 9ad6e1e..64b2609 100644
--- a/src/hooks/useIncomingInvites.js
+++ b/src/hooks/useIncomingInvites.js
@@ -5,7 +5,6 @@ import { useAuth } from '../context/AuthContext';
export function useIncomingInvites() {
const { user } = useAuth();
const email = user?.email ?? null;
-
const [state, setState] = useState({
invites: [],
error: null,
@@ -18,7 +17,11 @@ export function useIncomingInvites() {
const unsubscribe = subscribeToIncomingInvites(
email,
(data) => {
- setState({ invites: data, error: null, forEmail: email });
+ setState({
+ invites: data,
+ error: null,
+ forEmail: email,
+ });
},
(err) => {
setState({
@@ -29,7 +32,14 @@ export function useIncomingInvites() {
},
);
- return () => unsubscribe();
+ return () => {
+ unsubscribe();
+ setState({
+ invites: [],
+ error: null,
+ forEmail: null,
+ });
+ };
}, [email]);
if (!email) {
@@ -44,6 +54,6 @@ export function useIncomingInvites() {
return {
invites: loading ? [] : state.invites,
loading,
- error: state.error,
+ error: loading ? null : state.error,
};
}
From 8bc0471ec23fb7060605cfa7fc8fd097673e0569 Mon Sep 17 00:00:00 2001
From: Or Forshmit <162809292+OrF8@users.noreply.github.com>
Date: Wed, 8 Apr 2026 21:47:48 +0300
Subject: [PATCH 4/4] Address review follow-ups for board and invite hooks
---
src/hooks/useBoards.js | 24 ++++++++++++------------
src/hooks/useIncomingInvites.js | 16 ++++++----------
2 files changed, 18 insertions(+), 22 deletions(-)
diff --git a/src/hooks/useBoards.js b/src/hooks/useBoards.js
index a386d78..e700e2a 100644
--- a/src/hooks/useBoards.js
+++ b/src/hooks/useBoards.js
@@ -11,6 +11,8 @@ export function useBoards() {
error: null,
retryingSecureConnection: false,
forUid: null,
+ forUser: null,
+ retryingForUser: null,
});
useEffect(() => {
@@ -24,6 +26,8 @@ export function useBoards() {
error: null,
retryingSecureConnection: false,
forUid: uid,
+ forUser: user,
+ retryingForUser: null,
});
},
(err) => {
@@ -32,6 +36,8 @@ export function useBoards() {
error: err?.message || 'Χ©ΧΧΧΧ ΧΧΧ’ΧΧ Χͺ ΧΧΧΧΧΧͺ',
retryingSecureConnection: false,
forUid: uid,
+ forUser: user,
+ retryingForUser: null,
});
},
{
@@ -40,21 +46,14 @@ export function useBoards() {
...prev,
retryingSecureConnection: true,
error: null,
+ retryingForUser: user,
}));
},
},
);
- return () => {
- unsubscribe();
- setState({
- boards: [],
- error: null,
- retryingSecureConnection: false,
- forUid: null,
- });
- };
- }, [uid]);
+ return () => unsubscribe();
+ }, [uid, user]);
if (!uid) {
return {
@@ -65,11 +64,12 @@ export function useBoards() {
};
}
- const loading = state.forUid !== uid;
+ const loading = state.forUid !== uid || state.forUser !== user;
return {
boards: loading ? [] : state.boards,
loading,
error: loading ? null : state.error,
- retryingSecureConnection: loading ? false : state.retryingSecureConnection,
+ retryingSecureConnection:
+ state.retryingSecureConnection && state.retryingForUser === user,
};
}
diff --git a/src/hooks/useIncomingInvites.js b/src/hooks/useIncomingInvites.js
index 64b2609..5e9c5ce 100644
--- a/src/hooks/useIncomingInvites.js
+++ b/src/hooks/useIncomingInvites.js
@@ -9,6 +9,7 @@ export function useIncomingInvites() {
invites: [],
error: null,
forEmail: null,
+ forUser: null,
});
useEffect(() => {
@@ -21,6 +22,7 @@ export function useIncomingInvites() {
invites: data,
error: null,
forEmail: email,
+ forUser: user,
});
},
(err) => {
@@ -28,19 +30,13 @@ export function useIncomingInvites() {
invites: [],
error: err?.message || 'Χ©ΧΧΧΧ ΧΧΧ’ΧΧ Χͺ ΧΧΧΧΧ ΧΧͺ',
forEmail: email,
+ forUser: user,
});
},
);
- return () => {
- unsubscribe();
- setState({
- invites: [],
- error: null,
- forEmail: null,
- });
- };
- }, [email]);
+ return () => unsubscribe();
+ }, [email, user]);
if (!email) {
return {
@@ -50,7 +46,7 @@ export function useIncomingInvites() {
};
}
- const loading = state.forEmail !== email;
+ const loading = state.forEmail !== email || state.forUser !== user;
return {
invites: loading ? [] : state.invites,
loading,