diff --git a/.env.example b/.env.example deleted file mode 100644 index 5dac96c..0000000 --- a/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# Supabase Configuration -# Copy this file to .env and add your actual credentials -SUPABASE_URL=your_supabase_project_url_here -SUPABASE_ANON_KEY=your_supabase_anon_key_here -GOOGLE_CLIENT_ID=your_google_web_client_id_here \ No newline at end of file diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies deleted file mode 100644 index 79315b2..0000000 --- a/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"app_links","path":"/home/dct/.pub-cache/hosted/pub.dev/app_links-6.4.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_core","path":"/home/dct/.pub-cache/hosted/pub.dev/firebase_core-4.5.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_messaging","path":"/home/dct/.pub-cache/hosted/pub.dev/firebase_messaging-16.1.2/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/dct/.pub-cache/hosted/pub.dev/flutter_local_notifications-21.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/dct/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_ios","path":"/home/dct/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.9.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_ios","path":"/home/dct/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.13+6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/dct/.pub-cache/hosted/pub.dev/package_info_plus-9.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/dct/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/dct/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/dct/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/dct/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"/home/dct/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.6/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"app_links","path":"/home/dct/.pub-cache/hosted/pub.dev/app_links-6.4.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_core","path":"/home/dct/.pub-cache/hosted/pub.dev/firebase_core-4.5.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_messaging","path":"/home/dct/.pub-cache/hosted/pub.dev/firebase_messaging-16.1.2/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/dct/.pub-cache/hosted/pub.dev/flutter_local_notifications-21.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_plugin_android_lifecycle","path":"/home/dct/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.33/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_android","path":"/home/dct/.pub-cache/hosted/pub.dev/geolocator_android-5.0.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_android","path":"/home/dct/.pub-cache/hosted/pub.dev/google_sign_in_android-6.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_android","path":"/home/dct/.pub-cache/hosted/pub.dev/image_picker_android-0.8.13+13/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"package_info_plus","path":"/home/dct/.pub-cache/hosted/pub.dev/package_info_plus-9.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/home/dct/.pub-cache/hosted/pub.dev/path_provider_android-2.2.22/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/dct/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"/home/dct/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.18/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_android","path":"/home/dct/.pub-cache/hosted/pub.dev/sqflite_android-2.4.2+3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"/home/dct/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.28/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"app_links","path":"/home/dct/.pub-cache/hosted/pub.dev/app_links-6.4.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_selector_macos","path":"/home/dct/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_core","path":"/home/dct/.pub-cache/hosted/pub.dev/firebase_core-4.5.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_messaging","path":"/home/dct/.pub-cache/hosted/pub.dev/firebase_messaging-16.1.2/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/dct/.pub-cache/hosted/pub.dev/flutter_local_notifications-21.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/dct/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_ios","path":"/home/dct/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.9.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_macos","path":"/home/dct/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.2+1/","native_build":false,"dependencies":["file_selector_macos"],"dev_dependency":false},{"name":"package_info_plus","path":"/home/dct/.pub-cache/hosted/pub.dev/package_info_plus-9.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/dct/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/dct/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/dct/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/dct/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"/home/dct/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"app_links_linux","path":"/home/dct/.pub-cache/hosted/pub.dev/app_links_linux-1.0.3/","native_build":false,"dependencies":["gtk"],"dev_dependency":false},{"name":"file_selector_linux","path":"/home/dct/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications_linux","path":"/home/dct/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-8.0.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"geolocator_linux","path":"/home/dct/.pub-cache/hosted/pub.dev/geolocator_linux-0.2.4/","native_build":false,"dependencies":["package_info_plus"],"dev_dependency":false},{"name":"gtk","path":"/home/dct/.pub-cache/hosted/pub.dev/gtk-2.1.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_linux","path":"/home/dct/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.2/","native_build":false,"dependencies":["file_selector_linux"],"dev_dependency":false},{"name":"package_info_plus","path":"/home/dct/.pub-cache/hosted/pub.dev/package_info_plus-9.0.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/dct/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/dct/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","native_build":false,"dependencies":["url_launcher_linux"],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/home/dct/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"/home/dct/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"app_links","path":"/home/dct/.pub-cache/hosted/pub.dev/app_links-6.4.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_selector_windows","path":"/home/dct/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_core","path":"/home/dct/.pub-cache/hosted/pub.dev/firebase_core-4.5.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications_windows","path":"/home/dct/.pub-cache/hosted/pub.dev/flutter_local_notifications_windows-3.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_windows","path":"/home/dct/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_windows","path":"/home/dct/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.2/","native_build":false,"dependencies":["file_selector_windows"],"dev_dependency":false},{"name":"package_info_plus","path":"/home/dct/.pub-cache/hosted/pub.dev/package_info_plus-9.0.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/dct/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/dct/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","native_build":true,"dependencies":["url_launcher_windows"],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/home/dct/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"/home/dct/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"app_links_web","path":"/home/dct/.pub-cache/hosted/pub.dev/app_links_web-1.0.4/","dependencies":[],"dev_dependency":false},{"name":"firebase_core_web","path":"/home/dct/.pub-cache/hosted/pub.dev/firebase_core_web-3.5.0/","dependencies":[],"dev_dependency":false},{"name":"firebase_messaging_web","path":"/home/dct/.pub-cache/hosted/pub.dev/firebase_messaging_web-4.1.3/","dependencies":["firebase_core_web"],"dev_dependency":false},{"name":"geolocator_web","path":"/home/dct/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/","dependencies":[],"dev_dependency":false},{"name":"google_sign_in_web","path":"/home/dct/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.4+4/","dependencies":[],"dev_dependency":false},{"name":"image_picker_for_web","path":"/home/dct/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.1/","dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/dct/.pub-cache/hosted/pub.dev/package_info_plus-9.0.0/","dependencies":[],"dev_dependency":false},{"name":"share_plus","path":"/home/dct/.pub-cache/hosted/pub.dev/share_plus-10.1.4/","dependencies":["url_launcher_web"],"dev_dependency":false},{"name":"shared_preferences_web","path":"/home/dct/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"/home/dct/.pub-cache/hosted/pub.dev/url_launcher_web-2.4.2/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"app_links","dependencies":["app_links_linux","app_links_web"]},{"name":"app_links_linux","dependencies":["gtk"]},{"name":"app_links_web","dependencies":[]},{"name":"file_selector_linux","dependencies":[]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_windows","dependencies":[]},{"name":"firebase_core","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","dependencies":[]},{"name":"firebase_messaging","dependencies":["firebase_core","firebase_messaging_web"]},{"name":"firebase_messaging_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux","flutter_local_notifications_windows"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"flutter_local_notifications_windows","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows","geolocator_linux"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_linux","dependencies":["package_info_plus"]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"gtk","dependencies":[]},{"name":"image_picker","dependencies":["image_picker_android","image_picker_for_web","image_picker_ios","image_picker_linux","image_picker_macos","image_picker_windows"]},{"name":"image_picker_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"image_picker_ios","dependencies":[]},{"name":"image_picker_linux","dependencies":["file_selector_linux"]},{"name":"image_picker_macos","dependencies":["file_selector_macos"]},{"name":"image_picker_windows","dependencies":["file_selector_windows"]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"share_plus","dependencies":["url_launcher_web","url_launcher_windows","url_launcher_linux"]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2026-05-27 17:25:12.730242","version":"3.38.8","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/ANTIGRAVITY_BOOKBRIDGE_BRIEF_v2.md b/ANTIGRAVITY_BOOKBRIDGE_BRIEF_v2.md deleted file mode 100644 index b43e981..0000000 --- a/ANTIGRAVITY_BOOKBRIDGE_BRIEF_v2.md +++ /dev/null @@ -1,464 +0,0 @@ -# πŸ€– Antigravity 2.0 β€” BookBridge Development Agent Brief (v2) -> Feed this entire document as your starting prompt into Antigravity 2.0. -> NOTE: You have already pulled the latest repo and analyzed its state. -> Issue #14 (APK download link) is confirmed fixed in commit `a0a4b7a`. -> Begin from Issue #15 (screenshot overflow) and Issue #9 (Terms & Conditions), -> then proceed through Priorities 2–7 in order. - ---- - -## 🎯 YOUR MISSION - -You are a senior Flutter/Dart engineer resuming development on **BookBridge** β€” a peer-to-peer textbook marketplace for Cameroonian students. The lead developer (Mr. DCT) has been away from this repo for ~2 months. Your job is to pick up exactly where he left off and execute the next development sprint based on both the existing roadmap and the strategic recommendations document below. - -**Repository:** https://github.com/DCT-Berinyuy/book-bridge -**Active Branch:** `dev` -**Current Version:** v1.4.0+4 -**Tech Stack:** Flutter 3.10.7+ / Dart, Supabase (PostgreSQL + Auth + Storage + Realtime), Provider, GetIt, go_router, dartz, Fapshi (payments) - ---- - -## πŸ“¦ REPO STATE (Already Confirmed) - -- βœ… `dev` and `main` are fully synced, clean, no conflicts -- βœ… Issue #14 (APK link) fixed in commit `a0a4b7a` -- βœ… Fapshi webhook phone formatting fixed (issues #20/#21) -- βœ… Default light theme set -- βœ… 135 commits, 5 releases, 47 Dart files, ~15,000 LOC - -**Read these files before writing any code:** -- `GEMINI.md` β€” full AI context with architectural decisions -- `lib/injection_container.dart` β€” all registered dependencies -- `lib/config/router.dart` β€” all routes -- `lib/features/listings/data/datasources/supabase_listings_datasource.dart` β€” datasource pattern -- `pubspec.yaml` β€” current dependencies - ---- - -## πŸ—οΈ ARCHITECTURE OVERVIEW - -### Clean Architecture β€” 3 Layers -``` -lib/ -β”œβ”€β”€ core/ # Errors, theme, base use cases, shared widgets -β”œβ”€β”€ features/ -β”‚ β”œβ”€β”€ auth/ # 11 files β€” Supabase Auth, sign in/up, profile -β”‚ β”œβ”€β”€ listings/ # 22 files β€” Browse, sell, search, listing details -β”‚ └── notifications/ # Placeholder only -β”œβ”€β”€ config/ # app_config.dart, router.dart -β”œβ”€β”€ injection_container.dart -└── main.dart -``` - -### Key Patterns -- **State:** `Provider` + `ChangeNotifier` ViewModels -- **DI:** `GetIt` service locator (30+ registered deps) -- **Navigation:** `go_router` with `StatefulShellRoute` -- **Error handling:** `Either` via `dartz` -- **DB:** Supabase PostgreSQL with RLS on all tables - -### Brand Identity: "Knowledge & Trust" -- Scholar Blue: `#1A4D8C` (primary) -- Bridge Orange: `#F2994A` (secondary) -- Growth Green: `#27AE60` (tertiary) -- Font: Montserrat (headings), Inter (body) - ---- - -## βœ… ALREADY BUILT β€” DO NOT REBUILD - -- User authentication (email/password via Supabase) -- Browse listings (grid, infinite scroll, 50/page) -- Full-text search (PostgreSQL tsvector) -- Sell books (image upload, form validation) -- Profile management -- WhatsApp seller contact -- Category filtering -- Fapshi payments (MTN MoMo + Orange Money) β€” v1.4.0 -- Multi-language support (EN/FR) -- SvelteKit landing page (book-bridge-three.vercel.app) -- Clean Architecture with 0 flutter analyze issues - ---- - -## 🚧 DEVELOPMENT SPRINT β€” PRIORITY ORDER - -Work through these in strict order. Complete and commit each before moving to the next. - ---- - -### PRIORITY 1 β€” Fix Remaining Open Issues - -**Issue #15:** Screenshot overflow on landing page phone widget -- File: `landingPage/` (SvelteKit) -- Find the phone mockup component and fix CSS overflow/clipping on the screenshot images - -**Issue #9:** Missing Terms & Conditions page -- Create `lib/features/auth/presentation/screens/terms_screen.dart` -- Add route `/terms` in `config/router.dart` -- Link from Sign Up screen with checkbox: "I agree to the Terms & Conditions" -- Content to include: - - User responsibilities (accurate listings, honest condition grading) - - Prohibited items (counterfeit, stolen books) - - Payment policy (Fapshi terms, refund conditions) - - Safety guidelines for in-person meetups - - Data privacy statement - - Platform commission disclosure (5–15% success fee) - ---- - -### PRIORITY 2 β€” Standardized Book Condition Grading System - -**Goal:** Reduce buyer/seller disputes by standardizing how book conditions are described and displayed. Directly addresses the #1 complaint on global platforms like Better World Books. - -**Implementation:** - -1. Update the `condition` field display throughout the app with color-coded badges: - - `new` β†’ Green `#27AE60` - - `like_new` β†’ Teal `#1ABC9C` - - `good` β†’ Scholar Blue `#1A4D8C` - - `fair` β†’ Bridge Orange `#F2994A` - - `poor` β†’ Red `#E74C3C` - -2. Create `lib/core/widgets/condition_badge.dart` β€” reusable badge widget used on ListingCard and ListingDetailsScreen - -3. Add a **Condition Guide Modal** β€” tappable info icon on SellScreen and ListingDetailsScreen: - - Create `lib/core/widgets/condition_guide_modal.dart` - - Each condition shows: name, color, description, what to expect - - Content: - ``` - NEW: Unused, unopened, original packaging if applicable - LIKE NEW: Read once, no markings, minimal wear - GOOD: Some highlighting/notes, intact binding, all pages present - FAIR: Heavy use, significant markings, may have creases - POOR: Functional but heavily worn, all content readable - ``` - -4. On SellScreen, replace plain condition dropdown with visual condition selector cards showing color + description - ---- - -### PRIORITY 3 β€” Offline Cache with sqflite - -**Goal:** Users can browse listings without active internet β€” critical for Cameroonian low-bandwidth context. - -**Dependencies to add in pubspec.yaml:** -```yaml -sqflite: ^2.3.0 -path: ^1.9.0 -``` - -**Files to create:** -- `lib/core/local_db/local_database.dart` β€” SQLite setup and schema -- `lib/features/listings/data/datasources/local_listings_datasource.dart` - -**Schema:** -```sql -CREATE TABLE cached_listings ( - id TEXT PRIMARY KEY, - title TEXT, - author TEXT, - price_fcfa INTEGER, - condition TEXT, - image_url TEXT, - description TEXT, - seller_id TEXT, - status TEXT, - category TEXT, - cached_at INTEGER -); -``` - -**Modify `ListingsRepositoryImpl` to:** -- On successful fetch β†’ cache results to SQLite -- On network failure β†’ serve from cache with `OfflineBanner` widget -- Cache TTL: 24 hours (check `cached_at` Unix timestamp) - -**Add `OfflineBanner` widget:** -- Yellow banner at top of HomeScreen: "You're offline β€” showing saved results" -- Only visible when serving from cache - -**Register `SqfliteCacheService` in `injection_container.dart`** - ---- - -### PRIORITY 4 β€” Social Impact Tracker (Book-for-Book Model) - -**Goal:** Formalize BookBridge's social mission with measurable impact metrics. Based on Better World Books' model β€” every transaction generates trackable social good. Critical for investor pitch at PROMOTE 2026. - -**Database β€” Add to Supabase:** -```sql -CREATE TABLE impact_metrics ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - metric_type TEXT NOT NULL CHECK (metric_type IN ( - 'book_circulated', - 'money_saved', - 'co2_avoided', - 'student_reached' - )), - value FLOAT NOT NULL, - transaction_id UUID, - recorded_at TIMESTAMP DEFAULT NOW() -); - -CREATE TABLE platform_stats ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - total_books_circulated INTEGER DEFAULT 0, - total_students_reached INTEGER DEFAULT 0, - total_money_saved_fcfa BIGINT DEFAULT 0, - total_co2_avoided_kg FLOAT DEFAULT 0, - updated_at TIMESTAMP DEFAULT NOW() -); -``` - -**Calculation logic:** -- `money_saved` = average new book price (15,000 FCFA) βˆ’ sale price -- `co2_avoided` = 2.7kg CO2 per book reused (industry standard) -- `student_reached` = unique buyers + unique sellers - -**Flutter implementation:** -1. Create `lib/features/impact/` feature -2. `ImpactStatsWidget` β€” displayed on HomeScreen below the featured carousel: - ``` - πŸ“š 1,247 books circulated - πŸ’° 18.7M FCFA saved by students - 🌱 3.4 tonnes CO2 avoided - ``` -3. Stats fetched from `platform_stats` table (cached locally, updated on each transaction) -4. Animated number counters on first load (use `CountUp` animation) -5. Update `platform_stats` via Supabase trigger after each completed transaction - ---- - -### PRIORITY 5 β€” Seller Ratings & Trust System - -**Goal:** Verified post-transaction ratings. Only buyers who completed a purchase can rate sellers. Core trust mechanism for investor pitch. - -**Database:** -```sql -CREATE TABLE ratings ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - listing_id UUID REFERENCES listings(id) ON DELETE CASCADE, - buyer_id UUID REFERENCES profiles(id) ON DELETE CASCADE, - seller_id UUID REFERENCES profiles(id) ON DELETE CASCADE, - score INTEGER CHECK (score BETWEEN 1 AND 5), - comment TEXT, - created_at TIMESTAMP DEFAULT NOW(), - UNIQUE(listing_id, buyer_id) -); - -ALTER TABLE profiles ADD COLUMN rating_score FLOAT DEFAULT 0; -ALTER TABLE profiles ADD COLUMN rating_count INTEGER DEFAULT 0; -``` - -**Bayesian Rating Formula:** -``` -adjusted_rating = (v / (v + m)) * R + (m / (v + m)) * C -where: v = rating_count, R = raw average, m = 5, C = global average -``` - -**Flutter:** -1. Create `lib/features/ratings/` feature (domain/data/presentation) -2. `RatingEntity`, `SubmitRatingUseCase`, `GetSellerRatingsUseCase` -3. `RatingViewModel` with Provider -4. `SubmitRatingSheet` β€” bottom sheet with 5-star selector + optional comment -5. Seller rating badge on `ListingDetailsScreen` and `ProfileScreen` -6. Trigger rating prompt after Fapshi transaction marked complete - ---- - -### PRIORITY 6 β€” Safety Guidelines & Campus Meetup Zones - -**Goal:** Increase trust and safety for in-person book exchanges. Based on P2P marketplace best practices β€” explicit safety infrastructure reduces fraud and increases platform confidence. - -**Flutter:** -1. Create `lib/features/safety/presentation/screens/safety_guidelines_screen.dart` -2. Add route `/safety` accessible from ListingDetailsScreen ("Arrange Meetup" section) -3. Content sections: - - **Safe Meetup Locations**: Public campus areas, library entrances, student union buildings - - **What to Check**: Verify book condition before paying, match ISBN if possible - - **Payment Safety**: Always use in-app Fapshi payment, never send money in advance via WhatsApp - - **Report Issues**: Link to report form -4. Add `MeetupTipsCard` widget on `ListingDetailsScreen` below the WhatsApp contact button -5. Create a `campus_zones` table in Supabase for verified safe exchange points: -```sql -CREATE TABLE campus_zones ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - name TEXT NOT NULL, - university TEXT NOT NULL, - city TEXT NOT NULL, - description TEXT, - latitude FLOAT, - longitude FLOAT, - is_verified BOOLEAN DEFAULT FALSE -); -``` -Pre-populate with known ICT University, University of YaoundΓ© I, University of Buea locations. - ---- - -### PRIORITY 7 β€” Push Notifications (FCM) - -**Goal:** Notify users about payment confirmations, new listings matching their interests, and listing inquiries. - -**Dependencies:** -```yaml -firebase_messaging: ^14.0.0 -flutter_local_notifications: ^17.0.0 -``` - -**Database:** -```sql -CREATE TABLE notifications ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - user_id UUID REFERENCES profiles(id) ON DELETE CASCADE, - type TEXT NOT NULL, - title TEXT NOT NULL, - body TEXT, - data JSONB, - is_read BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP DEFAULT NOW() -); -``` - -**Notification types:** -- `payment_confirmed` β€” Fapshi payment success -- `new_inquiry` β€” Someone contacted about your listing -- `new_listing_match` β€” New book in a category you've browsed -- `impact_milestone` β€” "BookBridge just circulated its 500th book! πŸŽ‰" - -**Implementation:** -1. Create `lib/core/notifications/notification_service.dart` -2. Update `NotificationsScreen` (currently placeholder) with real notification list -3. Unread badge on bottom nav Notifications icon -4. Impact milestone notifications tie into Priority 4's impact tracker - ---- - -### PRIORITY 8 β€” Wishlist / Favorites - -**Database:** -```sql -CREATE TABLE wishlists ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - user_id UUID REFERENCES profiles(id) ON DELETE CASCADE, - listing_id UUID REFERENCES listings(id) ON DELETE CASCADE, - created_at TIMESTAMP DEFAULT NOW(), - UNIQUE(user_id, listing_id) -); -``` - -**Flutter:** -1. Create `lib/features/wishlist/` feature -2. Heart icon on `ListingCard` and `ListingDetailsScreen` -3. `WishlistScreen` accessible from Profile tab -4. Optimistic UI updates (toggle immediately, sync to Supabase) -5. `WishlistViewModel` with Provider - ---- - -### PRIORITY 9 β€” Escrow Payment Flow - -**Goal:** Trust layer on top of existing Fapshi integration. - -**Flow:** -``` -Buyer pays β†’ Funds held in escrow β†’ -Buyer confirms receipt (or 48h auto-release) β†’ -Funds released to seller β†’ Rating prompt shown β†’ Impact metrics updated -``` - -**Database:** -```sql -CREATE TABLE escrow_transactions ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - listing_id UUID REFERENCES listings(id), - buyer_id UUID REFERENCES profiles(id), - seller_id UUID REFERENCES profiles(id), - amount_fcfa INTEGER NOT NULL, - fapshi_transaction_id TEXT, - status TEXT DEFAULT 'held' CHECK ( - status IN ('held', 'released', 'refunded', 'disputed') - ), - held_at TIMESTAMP DEFAULT NOW(), - released_at TIMESTAMP, - release_deadline TIMESTAMP -); -``` - -**Steps:** -1. Modify Fapshi payment flow to route through escrow -2. "Confirm Receipt" button on buyer's transaction history -3. Supabase Edge Function for auto-release after 48h -4. Dispute flow: "There's a problem" β†’ manual review queue -5. On release β†’ trigger rating prompt (Priority 5) + update impact metrics (Priority 4) - ---- - -## 🎨 UI/UX IMPROVEMENTS (Implement Alongside Above) - -- **Shimmer skeleton loaders** on HomeScreen grid β€” replace `CircularProgressIndicator` -- **Image compression** before upload β€” max 800px width, 80% quality using `flutter_image_compress` -- **Empty state illustrations** β€” custom SVG for empty search, empty wishlist, empty profile -- **Book swap badge** on listings β€” sellers can optionally mark a book as "Swap Available" in addition to sale price -- **Featured listings carousel** refinement β€” ensure impact stats widget sits cleanly below it - ---- - -## πŸ“‹ CODING STANDARDS β€” NON-NEGOTIABLE - -1. **Zero flutter analyze issues** β€” run before every commit -2. **Clean Architecture** β€” every new feature: domain/data/presentation layers -3. **Either pattern** β€” all repository methods return `Either` -4. **Provider for state** β€” new ViewModels extend `ChangeNotifier` -5. **Register in GetIt** β€” all new services/repos/use cases in `injection_container.dart` -6. **Before every commit:** - ```bash - dart fix --apply - flutter analyze - dart format . - ``` -7. **Branch naming:** `feature/condition-grading`, `feature/offline-cache`, `feature/impact-tracker`, etc. -8. **Commit messages:** Conventional commits β€” `feat:`, `fix:`, `refactor:`, `docs:` - ---- - -## πŸ† CONTEXT: WHY THIS MATTERS - -BookBridge is a candidate for **PROMOTE 2026** β€” an investor showcase at Palais des CongrΓ¨s, YaoundΓ© (June 15–21, 2026). ICT University will present 5 student projects to 200+ investors. **Pre-selection deadline: June 1, 2026.** - -**Features investors will care about most (in order):** -1. **Impact metrics** β€” books circulated, money saved, CO2 avoided (Priority 4) -2. **Seller ratings** β€” trust = monetization potential (Priority 5) -3. **Escrow payments** β€” fraud prevention = scale (Priority 9) -4. **Offline cache** β€” African market fit = TAM story (Priority 3) -5. **Safety infrastructure** β€” platform maturity signal (Priority 6) - -The social mission framing (book-for-book model, SDG alignment, B-Corp direction) is what differentiates BookBridge from a generic marketplace. Every feature should reinforce that story. - -Build like investors are watching. πŸš€ - ---- - -## πŸ“Š STRATEGIC CONTEXT (From Analysis Document) - -BookBridge should model itself after these global platforms while staying African-first: - -| Platform | Key Lesson for BookBridge | -|---|---| -| Better World Books | Book-for-book donations, impact reporting, B-Corp mission | -| World of Books (Wob) | Author revenue sharing, zero-waste pledge, library sourcing | -| Bookshop.org | 80% margin to local ecosystem partners | -| ThriftBooks | Standardized condition grading, tiered loyalty | -| Kenya's Text Book Centre | Curriculum-aligned inventory, East African market proof | - -**Target metrics to display at PROMOTE 2026:** -- Books circulated: track from day 1 -- Students reached: unique active users -- Money saved: (avg new price βˆ’ sale price) Γ— transactions -- CO2 avoided: 2.7kg Γ— books reused - ---- - -*Brief v2 prepared by Claude (Anthropic) for Mr. DCT / DevSafe* -*BookBridge v1.4.0 β†’ v2.0.0 sprint | PROMOTE 2026 ready* diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index d92b041..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,137 +0,0 @@ -# CHANGELOG - -## 1.3.0 - February 15, 2026 - -### Added - -- **Multi-language Support**: Full support for English and French localization across the app. -- **Fapshi Integration**: Integrated community donation support via a dedicated Fapshi checkout link. -- **SvelteKit Landing Page**: Initial production build of the web front-end. - -### Changed - -- **UI Refinements**: Compacted promotional section for a more native feel (155px height). -- **Design System**: Harmonized UI with "pill" style capsule containers for icons and buttons. -- **Project Structure**: Cleaned up 20+ redundant files and migrated to local `l10n` imports for stability. -- **Git Hygiene**: Comprehensive `.gitignore` updates for a cleaner development environment. - -### Fixed - -- **Layout Overflows**: Resolved persistent "RIGHT OVERFLOWED" and "BOTTOM OVERFLOWED" issues on the home screen. -- **Localization Bug**: Fixed critical "Undefined name 'AppLocalizations'" regression. - ---- - -## 1.2.0 - February 14, 2026 - -### Added - -- **Nearby Books**: Initial implementation of location-based listing discovery. -- **Promo Banners**: Dynamic PageView for promotional messaging. - ---- - -### Added - -- **Custom App Launcher Icon**: Beautiful BookBridge logo with intricate patterns on an open book design -- **App Icon Assets**: Added `assets/app_icon.png` to project resources -- **Adaptive Icons**: Configured adaptive icons for Android with black background - -### Changed - -- Updated `pubspec.yaml` to include app icon configuration -- Integrated `flutter_launcher_icons` package for icon generation - -### Technical - -- Generated launcher icons for both Android and iOS platforms -- Configured adaptive icon foreground and background for Android - ---- - -## 1.0.0 - January 2026 - -### Features Implemented - -#### Authentication - -- Email/password sign-up and sign-in via Supabase Auth -- User profile creation with full name and locality -- Password reset functionality -- Automatic profile creation via database trigger -- Auth state-based navigation - -#### Listings Management - -- Browse available book listings in grid layout -- Infinite scroll pagination (50 items per page) -- Create new listings with image upload -- Image upload to Supabase Storage (`book_images` bucket) -- Delete own listings with confirmation -- View listing details with seller information - -#### Search - -- Full-text search across book titles and authors -- PostgreSQL tsvector-based search with ranking -- Real-time search results - -#### Profile - -- View and edit user profile -- Manage user's own listings -- WhatsApp contact integration -- Sign out functionality - -#### UI/UX - -- Material Design 3 implementation -- "Knowledge & Trust" visual identity - - Scholar Blue (#1A4D8C) primary color - - Bridge Orange (#F2994A) secondary color - - Growth Green (#27AE60) tertiary color -- Google Fonts integration (Montserrat, Inter) -- Bottom navigation bar (Home, Search, Sell, Profile) -- Pull-to-refresh on home screen -- Loading states and error handling -- Empty states with helpful messages - -### Architecture - -- Clean Architecture with Domain/Data/Presentation layers -- Provider state management with ChangeNotifier -- Dependency injection with GetIt -- Navigation with go_router and auth-based redirection -- Error handling with Either pattern (dartz) - -### Database - -- PostgreSQL tables: `profiles`, `listings` -- Row Level Security (RLS) policies -- Full-text search function -- Automatic profile creation trigger -- Indexes for optimized queries - -### Storage - -- Supabase Storage for book images -- Public bucket with RLS policies -- User-specific folder structure - -### Documentation - -- Comprehensive README with architecture overview -- Detailed Supabase setup guide (665 lines) -- Implementation documentation -- Brand identity guide -- Social venture vision document - ---- - -## 0.1.0 - Initial Setup - -- Initial release of BookBridge application -- Flutter project setup with Clean Architecture and MVVM -- Supabase integration planned for Auth, PostgreSQL, and Storage -- Dark theme applied -- Core features (Auth, Home Feed, Search, Sell, Profile, Book Details) outlined diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index a853bb5..0000000 --- a/GEMINI.md +++ /dev/null @@ -1,89 +0,0 @@ -# Gemini CLI Context for BookBridge - -This document provides instructional context for interacting with the BookBridge project. - -## Project Overview - -BookBridge is a mobile application built with **Flutter** that serves as a peer-to-peer marketplace for students in Cameroon to buy and sell used physical books. The backend is powered by **Supabase**, handling user authentication and the database of book listings. - -The project follows a **Clean Architecture** pattern, separating concerns into `Domain`, `Data`, and `Presentation` layers. - -Additionally, there is a `landingPage` directory containing a separate web project built with **SvelteKit** and **Vite**, presumably for marketing or as a web front-end for the service. - -### Key Technologies - -- **Mobile App (Flutter):** - - **State Management:** `provider` with `ChangeNotifier` - - **Navigation:** `go_router` - - **Dependency Injection:** `get_it` - - **Backend Integration:** `supabase_flutter` - - **Error Handling:** `dartz` for functional error handling (`Either`) -- **Landing Page (Web):** - - **Framework:** SvelteKit - - **Build Tool:** Vite -- **Backend:** - - Supabase (Auth, PostgreSQL Database, Storage) - -## Building and Running - -### Flutter Mobile App - -1. **Install Dependencies:** - - ```bash - flutter pub get - ``` - -2. **Configure Environment:** - Create a `.env` file in the project root. You can copy the example: - - ```bash - cp .env.example .env - ``` - - Then, fill in your Supabase project credentials in the `.env` file: - - ``` - SUPABASE_URL=your_supabase_project_url - SUPABASE_ANON_KEY=your_supabase_anon_key - ``` - -3. **Run the App:** - Use the `--dart-define` flags to pass the environment variables from the `.env` file to the Flutter app. - ```bash - flutter run \ - --dart-define="SUPABASE_URL=$(grep SUPABASE_URL .env | cut -d'=' -f2)" \ - --dart-define="SUPABASE_ANON_KEY=$(grep SUPABASE_ANON_KEY .env | cut -d'=' -f2)" \ - --dart-define="GOOGLE_CLIENT_ID=$(grep GOOGLE_CLIENT_ID .env | cut -d'=' -f2)" \ - --dart-define="FAPSHI_API_USER=$(grep FAPSHI_API_USER .env | cut -d'=' -f2)" \ - --dart-define="FAPSHI_API_KEY=$(grep FAPSHI_API_KEY .env | cut -d'=' -f2)" \ - --dart-define="FAPSHI_BASE_URL=$(grep FAPSHI_BASE_URL .env | cut -d'=' -f2)" - ``` - -### SvelteKit Landing Page - -1. **Navigate to Directory:** - - ```bash - cd landingPage - ``` - -2. **Install Dependencies:** - - ```bash - npm install - ``` - -3. **Run the Development Server:** - ```bash - npm run dev - ``` - -## Development Conventions - -- **Architecture:** The codebase is structured using Clean Architecture principles. When adding new features, create or modify files within the `lib/features` directory, respecting the `domain`, `data`, and `presentation` layers. -- **Code Quality:** Maintain high code quality by using the standard Dart/Flutter tools. Before committing, it's good practice to run: - - `flutter analyze` - To check for any static analysis issues. - - `dart format .` - To ensure consistent code formatting. -- **State Management:** App state is managed using the `provider` package. For new UI features requiring state, create a `ChangeNotifier` and provide it to the relevant widget tree. -- **Navigation:** All routing is handled by `go_router`. Add new routes or modify existing ones in `lib/config/router.dart`. diff --git a/README.md b/README.md index 9c0e7b1..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,247 +0,0 @@ -# BookBridge πŸ“š - -[![Flutter](https://img.shields.io/badge/Flutter-3.10.7+-blue?logo=flutter&style=flat-square)](https://flutter.dev/) -[![Supabase](https://img.shields.io/badge/Supabase-Backend-green?logo=supabase&style=flat-square)](https://supabase.com/) -[![SvelteKit](https://img.shields.io/badge/SvelteKit-Landing%20Page-orange?logo=svelte&style=flat-square)](https://kit.svelte.dev/) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) -[![Platform](https://img.shields.io/badge/platform-android%20%7C%20ios%20%7C%20web-green?style=flat-square)](https://flutter.dev/) -[![Version](https://img.shields.io/badge/version-1.5.0-brightgreen?style=flat-square)](https://github.com/DCT-Berinyuy/book-bridge) - -> **A Social Venture to End Learning Poverty in Cameroon** - -BookBridge is a peer-to-peer marketplace designed for Cameroonian students to buy and sell used physical books. Powered by Flutter and Supabase, it facilitates affordable access to textbooks and educational resources while enabling students to recycle and monetize their book collections. - -**Our Mission**: To democratize access to education in Cameroon, addressing the crisis where **72% of children** cannot read and understand simple text by age 10. - ---- - -## πŸ“Έ Screenshots - -
- Screenshot 1 - Screenshot 2 - Screenshot 3 -
- ---- - -## 🌟 Key Features - -### Core Marketplace -* **User Authentication**: Secure sign-in and profile synchronization via Supabase Auth. -* **Smart Search**: Full-text indexing across book titles and authors using PostgreSQL `tsvector` with relevance ranking. -* **Direct Handover Flow**: Intuitive purchase flow featuring user-to-user coordination. -* **Category Filtering**: Browse books by category (textbooks, novels, references, etc.) with responsive chips. -* **Secure Storage**: Public book cover images hosted securely via bucket policies in Supabase Storage. - -### Automated Escrow System (MoMo Integration) -* **Secure Holds**: Funds are collected via **Fapshi Direct Pay (MoMo/Orange Money)** and held in escrow until the buyer confirms physical handover. -* **5-Day Auto-Release**: Prevents sellers from being ghosted. Escrows are automatically released to the seller after 5 days if no dispute is filed. -* **Status Polling**: A pg_cron background worker Edge Function checks payment status every 5 minutes to automatically resolve transactions stuck in `pending_payment`. -* **Dispute Freeze**: Buyers can report problems to freeze the auto-release timer and trigger admin review. -* **Secure Payouts**: Payout execution is handled entirely server-side (Edge Functions) using database secrets via `app_secrets` (RLS enforced). -* **Audit Logging**: Every API transaction with Fapshi is logged in `fapshi_audit_logs` for transaction history, tracing, and fraud prevention. - ---- - -## πŸ—οΈ Architecture - -BookBridge follows **Clean Architecture** patterns separating business logic, UI, and data layers: - -``` -lib/ -β”œβ”€β”€ core/ # Shared assets, utilities, and components -β”‚ β”œβ”€β”€ error/ # Functional error handling (Failures, Exceptions) -β”‚ β”œβ”€β”€ theme/ # Custom Material Design 3 theme -β”‚ └── usecases/ # Base abstract UseCase contracts -β”œβ”€β”€ features/ # Modules encapsulating distinct functionality -β”‚ β”œβ”€β”€ auth/ # Domain, data, and presentation layers for Auth -β”‚ β”œβ”€β”€ chat/ # Real-time message exchange -β”‚ β”œβ”€β”€ favorites/ # Wishlists and saved listings -β”‚ β”œβ”€β”€ listings/ # Browsing, listing creation, and category search -β”‚ β”œβ”€β”€ payments/ # Fapshi Direct Pay integration & ViewModels -β”‚ β”œβ”€β”€ reviews/ # Buyer/Seller trust rating system -β”‚ └── transactions/ # Escrow confirm and dispute handlers -β”œβ”€β”€ config/ # Global configuration -β”‚ β”œβ”€β”€ app_config.dart # Dart define environment bindings -β”‚ └── router.dart # Route configurations (go_router) -β”œβ”€β”€ injection_container.dart # GetIt dependency injection setup -└── main.dart # Application entry point -``` - -### Escrow Architecture Sequence - -```mermaid -sequenceDiagram - actor Buyer - actor Seller - participant App as Flutter Mobile App - participant Fapshi as Fapshi API - participant Webhook as SvelteKit Webhook - participant DB as Supabase DB - participant Cron as pg_cron / Edge Functions - - Buyer->>App: Clicks "Buy Now" & enters MoMo details - App->>Fapshi: Direct Pay request - Fapshi-->>App: Returns transId (CREATED) - Fapshi->>Webhook: Webhook notification (CREATED/PENDING) - Webhook->>DB: Inserts transaction as 'pending_payment' - Buyer->>Fapshi: Approves USSD Push (MoMo Payment) - Fapshi->>Webhook: Webhook notification (SUCCESSFUL) - Webhook->>DB: Updates transaction status to 'held' & creates escrow - Webhook->>DB: Marks listing as 'sold' - Note over DB: 5-Day Auto-Release timer starts - Seller->>Buyer: Hands over physical book - alt Buyer Confirms Delivery - Buyer->>App: Clicks "Confirm Receipt" - App->>Cron: Calls process-escrow Edge Function (release) - else Cooldown Expired (5 days) - Cron->>DB: Auto-release-expired job triggers - end - Cron->>Fapshi: Payout API request to Seller - Fapshi-->>Cron: Payout SUCCESSFUL - Cron->>DB: Updates status to 'released' and payout successful - DB->>Seller: MoMo Payout Received -``` - ---- - -## πŸš€ Getting Started - -### Prerequisites -* Flutter SDK (v3.10.7 or higher) -* Supabase CLI / Account -* Node.js (for SvelteKit Landing Page) - -### Repository Setup -1. Clone the project: - ```bash - git clone https://github.com/DCT-Berinyuy/book-bridge.git - cd book-bridge - ``` -2. Fetch packages: - ```bash - flutter pub get - ``` - -### Configuration -Create a `.env` file in the project root: -```env -SUPABASE_URL=https://your-project.supabase.co -SUPABASE_ANON_KEY=your-anon-key -FAPSHI_API_USER=your-fapshi-user -FAPSHI_API_KEY=your-fapshi-key -FAPSHI_BASE_URL=https://live.fapshi.com -``` - -### Launching the App -Run the application with environments injected using `--dart-define`: -```bash -flutter run \ - --dart-define="SUPABASE_URL=$(grep SUPABASE_URL .env | cut -d'=' -f2)" \ - --dart-define="SUPABASE_ANON_KEY=$(grep SUPABASE_ANON_KEY .env | cut -d'=' -f2)" \ - --dart-define="FAPSHI_API_USER=$(grep FAPSHI_API_USER .env | cut -d'=' -f2)" \ - --dart-define="FAPSHI_API_KEY=$(grep FAPSHI_API_KEY .env | cut -d'=' -f2)" -``` - -### Running Landing Page (SvelteKit) -```bash -cd landingPage -npm install -npm run dev -``` - ---- - -## πŸ“Š Database Schema Highlights - -### `transactions` Table -Stores buyer purchase logs and commissions. -```sql -CREATE TABLE public.transactions ( - id UUID DEFAULT gen_random_uuid() PRIMARY KEY, - listing_id UUID NOT NULL REFERENCES public.listings(id) ON DELETE CASCADE, - buyer_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - seller_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - amount INTEGER NOT NULL CHECK (amount > 0), - payment_reference TEXT UNIQUE NOT NULL, - status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'pending_payment', 'successful', 'failed', 'held', 'disputed')), - payout_status TEXT DEFAULT 'pending', - payout_reference TEXT, - commission_amount INTEGER, - created_at TIMESTAMPTZ DEFAULT NOW() -); -``` - -### `escrow_transactions` Table -Manages the auto-release deadline timer. -```sql -CREATE TABLE public.escrow_transactions ( - id UUID DEFAULT gen_random_uuid() PRIMARY KEY, - transaction_id UUID NOT NULL REFERENCES public.transactions(id) ON DELETE CASCADE, - status TEXT NOT NULL DEFAULT 'held' CHECK (status IN ('held', 'released', 'refunded', 'disputed')), - dispute_reason TEXT, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - release_deadline TIMESTAMP WITH TIME ZONE GENERATED ALWAYS AS (public.add_5_days(created_at)) STORED -); -``` - -### `fapshi_audit_logs` Table -Maintains payout auditing logs for administrative review. -```sql -CREATE TABLE public.fapshi_audit_logs ( - id UUID DEFAULT gen_random_uuid() PRIMARY KEY, - transaction_id UUID REFERENCES public.transactions(id) ON DELETE SET NULL, - endpoint TEXT NOT NULL, - request_payload JSONB, - response_payload JSONB, - status_code INTEGER, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); -``` - ---- - -## 🧭 Navigation & Routes - -The application leverages `go_router` supporting authentication redirects and shell layouts: - -| Route | Screen | Auth? | Description | -| :--- | :--- | :--- | :--- | -| `/` | SplashScreen | No | Sessions startup and auth routing | -| `/sign-in` | SignInScreen | No | User login portal | -| `/home` | HomeScreen | Yes | Browse books listings feed | -| `/search` | SearchScreen | Yes | Run FTS indexing search queries | -| `/sell` | SellScreen | Yes | Book details registration and storage uploads | -| `/profile` | ProfileScreen | Yes | User details, feedback, and active listings | -| `/listing/:id`| ListingDetailsScreen | Yes | Specific book details & checkout portal | - ---- - -## 🀝 Contributing Guidelines - -1. Fork the Repository. -2. Create a Feature Branch (`git checkout -b feature/AmazingFeature`). -3. Follow the Clean Architecture design rules. -4. Ensure files are properly formatted: - ```bash - dart format . - flutter analyze - ``` -5. Commit your Changes (`git commit -m 'feat: Add AmazingFeature'`). -6. Push to Branch (`git push origin feature/AmazingFeature`). -7. Open a Pull Request. - ---- - -## πŸ“„ License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - ---- - -
- Made with ❀️ for Cameroonian students
- Democratizing access to knowledge, one book at a time. -
diff --git a/.gitignore b/app/.gitignore similarity index 100% rename from .gitignore rename to app/.gitignore diff --git a/.metadata b/app/.metadata similarity index 100% rename from .metadata rename to app/.metadata diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..9c0e7b1 --- /dev/null +++ b/app/README.md @@ -0,0 +1,247 @@ +# BookBridge πŸ“š + +[![Flutter](https://img.shields.io/badge/Flutter-3.10.7+-blue?logo=flutter&style=flat-square)](https://flutter.dev/) +[![Supabase](https://img.shields.io/badge/Supabase-Backend-green?logo=supabase&style=flat-square)](https://supabase.com/) +[![SvelteKit](https://img.shields.io/badge/SvelteKit-Landing%20Page-orange?logo=svelte&style=flat-square)](https://kit.svelte.dev/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) +[![Platform](https://img.shields.io/badge/platform-android%20%7C%20ios%20%7C%20web-green?style=flat-square)](https://flutter.dev/) +[![Version](https://img.shields.io/badge/version-1.5.0-brightgreen?style=flat-square)](https://github.com/DCT-Berinyuy/book-bridge) + +> **A Social Venture to End Learning Poverty in Cameroon** + +BookBridge is a peer-to-peer marketplace designed for Cameroonian students to buy and sell used physical books. Powered by Flutter and Supabase, it facilitates affordable access to textbooks and educational resources while enabling students to recycle and monetize their book collections. + +**Our Mission**: To democratize access to education in Cameroon, addressing the crisis where **72% of children** cannot read and understand simple text by age 10. + +--- + +## πŸ“Έ Screenshots + +
+ Screenshot 1 + Screenshot 2 + Screenshot 3 +
+ +--- + +## 🌟 Key Features + +### Core Marketplace +* **User Authentication**: Secure sign-in and profile synchronization via Supabase Auth. +* **Smart Search**: Full-text indexing across book titles and authors using PostgreSQL `tsvector` with relevance ranking. +* **Direct Handover Flow**: Intuitive purchase flow featuring user-to-user coordination. +* **Category Filtering**: Browse books by category (textbooks, novels, references, etc.) with responsive chips. +* **Secure Storage**: Public book cover images hosted securely via bucket policies in Supabase Storage. + +### Automated Escrow System (MoMo Integration) +* **Secure Holds**: Funds are collected via **Fapshi Direct Pay (MoMo/Orange Money)** and held in escrow until the buyer confirms physical handover. +* **5-Day Auto-Release**: Prevents sellers from being ghosted. Escrows are automatically released to the seller after 5 days if no dispute is filed. +* **Status Polling**: A pg_cron background worker Edge Function checks payment status every 5 minutes to automatically resolve transactions stuck in `pending_payment`. +* **Dispute Freeze**: Buyers can report problems to freeze the auto-release timer and trigger admin review. +* **Secure Payouts**: Payout execution is handled entirely server-side (Edge Functions) using database secrets via `app_secrets` (RLS enforced). +* **Audit Logging**: Every API transaction with Fapshi is logged in `fapshi_audit_logs` for transaction history, tracing, and fraud prevention. + +--- + +## πŸ—οΈ Architecture + +BookBridge follows **Clean Architecture** patterns separating business logic, UI, and data layers: + +``` +lib/ +β”œβ”€β”€ core/ # Shared assets, utilities, and components +β”‚ β”œβ”€β”€ error/ # Functional error handling (Failures, Exceptions) +β”‚ β”œβ”€β”€ theme/ # Custom Material Design 3 theme +β”‚ └── usecases/ # Base abstract UseCase contracts +β”œβ”€β”€ features/ # Modules encapsulating distinct functionality +β”‚ β”œβ”€β”€ auth/ # Domain, data, and presentation layers for Auth +β”‚ β”œβ”€β”€ chat/ # Real-time message exchange +β”‚ β”œβ”€β”€ favorites/ # Wishlists and saved listings +β”‚ β”œβ”€β”€ listings/ # Browsing, listing creation, and category search +β”‚ β”œβ”€β”€ payments/ # Fapshi Direct Pay integration & ViewModels +β”‚ β”œβ”€β”€ reviews/ # Buyer/Seller trust rating system +β”‚ └── transactions/ # Escrow confirm and dispute handlers +β”œβ”€β”€ config/ # Global configuration +β”‚ β”œβ”€β”€ app_config.dart # Dart define environment bindings +β”‚ └── router.dart # Route configurations (go_router) +β”œβ”€β”€ injection_container.dart # GetIt dependency injection setup +└── main.dart # Application entry point +``` + +### Escrow Architecture Sequence + +```mermaid +sequenceDiagram + actor Buyer + actor Seller + participant App as Flutter Mobile App + participant Fapshi as Fapshi API + participant Webhook as SvelteKit Webhook + participant DB as Supabase DB + participant Cron as pg_cron / Edge Functions + + Buyer->>App: Clicks "Buy Now" & enters MoMo details + App->>Fapshi: Direct Pay request + Fapshi-->>App: Returns transId (CREATED) + Fapshi->>Webhook: Webhook notification (CREATED/PENDING) + Webhook->>DB: Inserts transaction as 'pending_payment' + Buyer->>Fapshi: Approves USSD Push (MoMo Payment) + Fapshi->>Webhook: Webhook notification (SUCCESSFUL) + Webhook->>DB: Updates transaction status to 'held' & creates escrow + Webhook->>DB: Marks listing as 'sold' + Note over DB: 5-Day Auto-Release timer starts + Seller->>Buyer: Hands over physical book + alt Buyer Confirms Delivery + Buyer->>App: Clicks "Confirm Receipt" + App->>Cron: Calls process-escrow Edge Function (release) + else Cooldown Expired (5 days) + Cron->>DB: Auto-release-expired job triggers + end + Cron->>Fapshi: Payout API request to Seller + Fapshi-->>Cron: Payout SUCCESSFUL + Cron->>DB: Updates status to 'released' and payout successful + DB->>Seller: MoMo Payout Received +``` + +--- + +## πŸš€ Getting Started + +### Prerequisites +* Flutter SDK (v3.10.7 or higher) +* Supabase CLI / Account +* Node.js (for SvelteKit Landing Page) + +### Repository Setup +1. Clone the project: + ```bash + git clone https://github.com/DCT-Berinyuy/book-bridge.git + cd book-bridge + ``` +2. Fetch packages: + ```bash + flutter pub get + ``` + +### Configuration +Create a `.env` file in the project root: +```env +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_ANON_KEY=your-anon-key +FAPSHI_API_USER=your-fapshi-user +FAPSHI_API_KEY=your-fapshi-key +FAPSHI_BASE_URL=https://live.fapshi.com +``` + +### Launching the App +Run the application with environments injected using `--dart-define`: +```bash +flutter run \ + --dart-define="SUPABASE_URL=$(grep SUPABASE_URL .env | cut -d'=' -f2)" \ + --dart-define="SUPABASE_ANON_KEY=$(grep SUPABASE_ANON_KEY .env | cut -d'=' -f2)" \ + --dart-define="FAPSHI_API_USER=$(grep FAPSHI_API_USER .env | cut -d'=' -f2)" \ + --dart-define="FAPSHI_API_KEY=$(grep FAPSHI_API_KEY .env | cut -d'=' -f2)" +``` + +### Running Landing Page (SvelteKit) +```bash +cd landingPage +npm install +npm run dev +``` + +--- + +## πŸ“Š Database Schema Highlights + +### `transactions` Table +Stores buyer purchase logs and commissions. +```sql +CREATE TABLE public.transactions ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + listing_id UUID NOT NULL REFERENCES public.listings(id) ON DELETE CASCADE, + buyer_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + seller_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + amount INTEGER NOT NULL CHECK (amount > 0), + payment_reference TEXT UNIQUE NOT NULL, + status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'pending_payment', 'successful', 'failed', 'held', 'disputed')), + payout_status TEXT DEFAULT 'pending', + payout_reference TEXT, + commission_amount INTEGER, + created_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +### `escrow_transactions` Table +Manages the auto-release deadline timer. +```sql +CREATE TABLE public.escrow_transactions ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + transaction_id UUID NOT NULL REFERENCES public.transactions(id) ON DELETE CASCADE, + status TEXT NOT NULL DEFAULT 'held' CHECK (status IN ('held', 'released', 'refunded', 'disputed')), + dispute_reason TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + release_deadline TIMESTAMP WITH TIME ZONE GENERATED ALWAYS AS (public.add_5_days(created_at)) STORED +); +``` + +### `fapshi_audit_logs` Table +Maintains payout auditing logs for administrative review. +```sql +CREATE TABLE public.fapshi_audit_logs ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + transaction_id UUID REFERENCES public.transactions(id) ON DELETE SET NULL, + endpoint TEXT NOT NULL, + request_payload JSONB, + response_payload JSONB, + status_code INTEGER, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +--- + +## 🧭 Navigation & Routes + +The application leverages `go_router` supporting authentication redirects and shell layouts: + +| Route | Screen | Auth? | Description | +| :--- | :--- | :--- | :--- | +| `/` | SplashScreen | No | Sessions startup and auth routing | +| `/sign-in` | SignInScreen | No | User login portal | +| `/home` | HomeScreen | Yes | Browse books listings feed | +| `/search` | SearchScreen | Yes | Run FTS indexing search queries | +| `/sell` | SellScreen | Yes | Book details registration and storage uploads | +| `/profile` | ProfileScreen | Yes | User details, feedback, and active listings | +| `/listing/:id`| ListingDetailsScreen | Yes | Specific book details & checkout portal | + +--- + +## 🀝 Contributing Guidelines + +1. Fork the Repository. +2. Create a Feature Branch (`git checkout -b feature/AmazingFeature`). +3. Follow the Clean Architecture design rules. +4. Ensure files are properly formatted: + ```bash + dart format . + flutter analyze + ``` +5. Commit your Changes (`git commit -m 'feat: Add AmazingFeature'`). +6. Push to Branch (`git push origin feature/AmazingFeature`). +7. Open a Pull Request. + +--- + +## πŸ“„ License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +--- + +
+ Made with ❀️ for Cameroonian students
+ Democratizing access to knowledge, one book at a time. +
diff --git a/analysis_options.yaml b/app/analysis_options.yaml similarity index 100% rename from analysis_options.yaml rename to app/analysis_options.yaml diff --git a/android/.gitignore b/app/android/.gitignore similarity index 100% rename from android/.gitignore rename to app/android/.gitignore diff --git a/android/app/build.gradle.kts b/app/android/app/build.gradle.kts similarity index 100% rename from android/app/build.gradle.kts rename to app/android/app/build.gradle.kts diff --git a/android/app/google-services.json b/app/android/app/google-services.json similarity index 100% rename from android/app/google-services.json rename to app/android/app/google-services.json diff --git a/android/app/src/debug/AndroidManifest.xml b/app/android/app/src/debug/AndroidManifest.xml similarity index 100% rename from android/app/src/debug/AndroidManifest.xml rename to app/android/app/src/debug/AndroidManifest.xml diff --git a/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml similarity index 100% rename from android/app/src/main/AndroidManifest.xml rename to app/android/app/src/main/AndroidManifest.xml diff --git a/android/app/src/main/kotlin/com/example/book_bridge/MainActivity.kt b/app/android/app/src/main/kotlin/com/example/book_bridge/MainActivity.kt similarity index 100% rename from android/app/src/main/kotlin/com/example/book_bridge/MainActivity.kt rename to app/android/app/src/main/kotlin/com/example/book_bridge/MainActivity.kt diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png similarity index 100% rename from android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png rename to app/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png similarity index 100% rename from android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png rename to app/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/app/android/app/src/main/res/drawable-v21/launch_background.xml similarity index 100% rename from android/app/src/main/res/drawable-v21/launch_background.xml rename to app/android/app/src/main/res/drawable-v21/launch_background.xml diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png similarity index 100% rename from android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png rename to app/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png similarity index 100% rename from android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png rename to app/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png similarity index 100% rename from android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png rename to app/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png diff --git a/android/app/src/main/res/drawable/launch_background.xml b/app/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from android/app/src/main/res/drawable/launch_background.xml rename to app/android/app/src/main/res/drawable/launch_background.xml diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/android/app/src/main/res/values-night/styles.xml b/app/android/app/src/main/res/values-night/styles.xml similarity index 100% rename from android/app/src/main/res/values-night/styles.xml rename to app/android/app/src/main/res/values-night/styles.xml diff --git a/android/app/src/main/res/values/colors.xml b/app/android/app/src/main/res/values/colors.xml similarity index 100% rename from android/app/src/main/res/values/colors.xml rename to app/android/app/src/main/res/values/colors.xml diff --git a/android/app/src/main/res/values/styles.xml b/app/android/app/src/main/res/values/styles.xml similarity index 100% rename from android/app/src/main/res/values/styles.xml rename to app/android/app/src/main/res/values/styles.xml diff --git a/android/app/src/profile/AndroidManifest.xml b/app/android/app/src/profile/AndroidManifest.xml similarity index 100% rename from android/app/src/profile/AndroidManifest.xml rename to app/android/app/src/profile/AndroidManifest.xml diff --git a/android/build.gradle.kts b/app/android/build.gradle.kts similarity index 100% rename from android/build.gradle.kts rename to app/android/build.gradle.kts diff --git a/android/gradle.properties b/app/android/gradle.properties similarity index 100% rename from android/gradle.properties rename to app/android/gradle.properties diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/app/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from android/gradle/wrapper/gradle-wrapper.properties rename to app/android/gradle/wrapper/gradle-wrapper.properties diff --git a/android/settings.gradle.kts b/app/android/settings.gradle.kts similarity index 100% rename from android/settings.gradle.kts rename to app/android/settings.gradle.kts diff --git a/assets/Antigravity_Prompt_BookBridge_Rust_Phase1.md b/app/assets/Antigravity_Prompt_BookBridge_Rust_Phase1.md similarity index 100% rename from assets/Antigravity_Prompt_BookBridge_Rust_Phase1.md rename to app/assets/Antigravity_Prompt_BookBridge_Rust_Phase1.md diff --git a/assets/Antigravity_Prompt_BookBridge_Rust_Phase2.md b/app/assets/Antigravity_Prompt_BookBridge_Rust_Phase2.md similarity index 100% rename from assets/Antigravity_Prompt_BookBridge_Rust_Phase2.md rename to app/assets/Antigravity_Prompt_BookBridge_Rust_Phase2.md diff --git a/assets/BookBridge_v2_Implementation_Plan.md b/app/assets/BookBridge_v2_Implementation_Plan.md similarity index 100% rename from assets/BookBridge_v2_Implementation_Plan.md rename to app/assets/BookBridge_v2_Implementation_Plan.md diff --git a/assets/Founder.jpg b/app/assets/Founder.jpg similarity index 100% rename from assets/Founder.jpg rename to app/assets/Founder.jpg diff --git a/assets/app_icon.png b/app/assets/app_icon.png similarity index 100% rename from assets/app_icon.png rename to app/assets/app_icon.png diff --git a/assets/flow_diagram.png b/app/assets/flow_diagram.png similarity index 100% rename from assets/flow_diagram.png rename to app/assets/flow_diagram.png diff --git a/assets/logo.png b/app/assets/logo.png similarity index 100% rename from assets/logo.png rename to app/assets/logo.png diff --git a/assets/sreenshots/screenshot1.png b/app/assets/sreenshots/screenshot1.png similarity index 100% rename from assets/sreenshots/screenshot1.png rename to app/assets/sreenshots/screenshot1.png diff --git a/assets/sreenshots/screenshot2.png b/app/assets/sreenshots/screenshot2.png similarity index 100% rename from assets/sreenshots/screenshot2.png rename to app/assets/sreenshots/screenshot2.png diff --git a/assets/sreenshots/screenshot3.png b/app/assets/sreenshots/screenshot3.png similarity index 100% rename from assets/sreenshots/screenshot3.png rename to app/assets/sreenshots/screenshot3.png diff --git a/firebase.json b/app/firebase.json similarity index 100% rename from firebase.json rename to app/firebase.json diff --git a/ios/.gitignore b/app/ios/.gitignore similarity index 100% rename from ios/.gitignore rename to app/ios/.gitignore diff --git a/ios/Flutter/AppFrameworkInfo.plist b/app/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from ios/Flutter/AppFrameworkInfo.plist rename to app/ios/Flutter/AppFrameworkInfo.plist diff --git a/ios/Flutter/Debug.xcconfig b/app/ios/Flutter/Debug.xcconfig similarity index 100% rename from ios/Flutter/Debug.xcconfig rename to app/ios/Flutter/Debug.xcconfig diff --git a/ios/Flutter/Release.xcconfig b/app/ios/Flutter/Release.xcconfig similarity index 100% rename from ios/Flutter/Release.xcconfig rename to app/ios/Flutter/Release.xcconfig diff --git a/ios/Runner.xcodeproj/project.pbxproj b/app/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from ios/Runner.xcodeproj/project.pbxproj rename to app/ios/Runner.xcodeproj/project.pbxproj diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/app/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from ios/Runner.xcworkspace/contents.xcworkspacedata rename to app/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/ios/Runner/AppDelegate.swift b/app/ios/Runner/AppDelegate.swift similarity index 100% rename from ios/Runner/AppDelegate.swift rename to app/ios/Runner/AppDelegate.swift diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/app/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from ios/Runner/Base.lproj/LaunchScreen.storyboard rename to app/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/ios/Runner/Base.lproj/Main.storyboard b/app/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from ios/Runner/Base.lproj/Main.storyboard rename to app/ios/Runner/Base.lproj/Main.storyboard diff --git a/ios/Runner/Info.plist b/app/ios/Runner/Info.plist similarity index 100% rename from ios/Runner/Info.plist rename to app/ios/Runner/Info.plist diff --git a/ios/Runner/Runner-Bridging-Header.h b/app/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from ios/Runner/Runner-Bridging-Header.h rename to app/ios/Runner/Runner-Bridging-Header.h diff --git a/ios/RunnerTests/RunnerTests.swift b/app/ios/RunnerTests/RunnerTests.swift similarity index 100% rename from ios/RunnerTests/RunnerTests.swift rename to app/ios/RunnerTests/RunnerTests.swift diff --git a/lib/config/app_config.dart b/app/lib/config/app_config.dart similarity index 100% rename from lib/config/app_config.dart rename to app/lib/config/app_config.dart diff --git a/lib/config/router.dart b/app/lib/config/router.dart similarity index 100% rename from lib/config/router.dart rename to app/lib/config/router.dart diff --git a/lib/core/constants/categories.dart b/app/lib/core/constants/categories.dart similarity index 100% rename from lib/core/constants/categories.dart rename to app/lib/core/constants/categories.dart diff --git a/lib/core/error/exceptions.dart b/app/lib/core/error/exceptions.dart similarity index 100% rename from lib/core/error/exceptions.dart rename to app/lib/core/error/exceptions.dart diff --git a/lib/core/error/failures.dart b/app/lib/core/error/failures.dart similarity index 100% rename from lib/core/error/failures.dart rename to app/lib/core/error/failures.dart diff --git a/lib/core/local_db/local_database.dart b/app/lib/core/local_db/local_database.dart similarity index 100% rename from lib/core/local_db/local_database.dart rename to app/lib/core/local_db/local_database.dart diff --git a/lib/core/presentation/viewmodels/theme_viewmodel.dart b/app/lib/core/presentation/viewmodels/theme_viewmodel.dart similarity index 100% rename from lib/core/presentation/viewmodels/theme_viewmodel.dart rename to app/lib/core/presentation/viewmodels/theme_viewmodel.dart diff --git a/lib/core/presentation/widgets/notification_icon.dart b/app/lib/core/presentation/widgets/notification_icon.dart similarity index 100% rename from lib/core/presentation/widgets/notification_icon.dart rename to app/lib/core/presentation/widgets/notification_icon.dart diff --git a/lib/core/presentation/widgets/offline_banner.dart b/app/lib/core/presentation/widgets/offline_banner.dart similarity index 100% rename from lib/core/presentation/widgets/offline_banner.dart rename to app/lib/core/presentation/widgets/offline_banner.dart diff --git a/lib/core/presentation/widgets/scaffold_with_navbar.dart b/app/lib/core/presentation/widgets/scaffold_with_navbar.dart similarity index 100% rename from lib/core/presentation/widgets/scaffold_with_navbar.dart rename to app/lib/core/presentation/widgets/scaffold_with_navbar.dart diff --git a/lib/core/theme/app_theme.dart b/app/lib/core/theme/app_theme.dart similarity index 100% rename from lib/core/theme/app_theme.dart rename to app/lib/core/theme/app_theme.dart diff --git a/lib/core/usecases/usecase.dart b/app/lib/core/usecases/usecase.dart similarity index 100% rename from lib/core/usecases/usecase.dart rename to app/lib/core/usecases/usecase.dart diff --git a/lib/features/auth/data/datasources/supabase_auth_data_source.dart b/app/lib/features/auth/data/datasources/supabase_auth_data_source.dart similarity index 100% rename from lib/features/auth/data/datasources/supabase_auth_data_source.dart rename to app/lib/features/auth/data/datasources/supabase_auth_data_source.dart diff --git a/lib/features/auth/data/models/user_model.dart b/app/lib/features/auth/data/models/user_model.dart similarity index 100% rename from lib/features/auth/data/models/user_model.dart rename to app/lib/features/auth/data/models/user_model.dart diff --git a/lib/features/auth/data/repositories/auth_repository_impl.dart b/app/lib/features/auth/data/repositories/auth_repository_impl.dart similarity index 100% rename from lib/features/auth/data/repositories/auth_repository_impl.dart rename to app/lib/features/auth/data/repositories/auth_repository_impl.dart diff --git a/lib/features/auth/domain/entities/user.dart b/app/lib/features/auth/domain/entities/user.dart similarity index 100% rename from lib/features/auth/domain/entities/user.dart rename to app/lib/features/auth/domain/entities/user.dart diff --git a/lib/features/auth/domain/repositories/auth_repository.dart b/app/lib/features/auth/domain/repositories/auth_repository.dart similarity index 100% rename from lib/features/auth/domain/repositories/auth_repository.dart rename to app/lib/features/auth/domain/repositories/auth_repository.dart diff --git a/lib/features/auth/domain/usecases/get_current_user_usecase.dart b/app/lib/features/auth/domain/usecases/get_current_user_usecase.dart similarity index 100% rename from lib/features/auth/domain/usecases/get_current_user_usecase.dart rename to app/lib/features/auth/domain/usecases/get_current_user_usecase.dart diff --git a/lib/features/auth/domain/usecases/send_password_reset_email_usecase.dart b/app/lib/features/auth/domain/usecases/send_password_reset_email_usecase.dart similarity index 100% rename from lib/features/auth/domain/usecases/send_password_reset_email_usecase.dart rename to app/lib/features/auth/domain/usecases/send_password_reset_email_usecase.dart diff --git a/lib/features/auth/domain/usecases/sign_in_usecase.dart b/app/lib/features/auth/domain/usecases/sign_in_usecase.dart similarity index 100% rename from lib/features/auth/domain/usecases/sign_in_usecase.dart rename to app/lib/features/auth/domain/usecases/sign_in_usecase.dart diff --git a/lib/features/auth/domain/usecases/sign_in_with_google_usecase.dart b/app/lib/features/auth/domain/usecases/sign_in_with_google_usecase.dart similarity index 100% rename from lib/features/auth/domain/usecases/sign_in_with_google_usecase.dart rename to app/lib/features/auth/domain/usecases/sign_in_with_google_usecase.dart diff --git a/lib/features/auth/domain/usecases/sign_out_usecase.dart b/app/lib/features/auth/domain/usecases/sign_out_usecase.dart similarity index 100% rename from lib/features/auth/domain/usecases/sign_out_usecase.dart rename to app/lib/features/auth/domain/usecases/sign_out_usecase.dart diff --git a/lib/features/auth/domain/usecases/sign_up_usecase.dart b/app/lib/features/auth/domain/usecases/sign_up_usecase.dart similarity index 100% rename from lib/features/auth/domain/usecases/sign_up_usecase.dart rename to app/lib/features/auth/domain/usecases/sign_up_usecase.dart diff --git a/lib/features/auth/domain/usecases/update_user_usecase.dart b/app/lib/features/auth/domain/usecases/update_user_usecase.dart similarity index 100% rename from lib/features/auth/domain/usecases/update_user_usecase.dart rename to app/lib/features/auth/domain/usecases/update_user_usecase.dart diff --git a/lib/features/auth/presentation/screens/complete_profile_screen.dart b/app/lib/features/auth/presentation/screens/complete_profile_screen.dart similarity index 100% rename from lib/features/auth/presentation/screens/complete_profile_screen.dart rename to app/lib/features/auth/presentation/screens/complete_profile_screen.dart diff --git a/lib/features/auth/presentation/screens/edit_profile_screen.dart b/app/lib/features/auth/presentation/screens/edit_profile_screen.dart similarity index 100% rename from lib/features/auth/presentation/screens/edit_profile_screen.dart rename to app/lib/features/auth/presentation/screens/edit_profile_screen.dart diff --git a/lib/features/auth/presentation/screens/sign_in_screen.dart b/app/lib/features/auth/presentation/screens/sign_in_screen.dart similarity index 100% rename from lib/features/auth/presentation/screens/sign_in_screen.dart rename to app/lib/features/auth/presentation/screens/sign_in_screen.dart diff --git a/lib/features/auth/presentation/screens/sign_up_screen.dart b/app/lib/features/auth/presentation/screens/sign_up_screen.dart similarity index 100% rename from lib/features/auth/presentation/screens/sign_up_screen.dart rename to app/lib/features/auth/presentation/screens/sign_up_screen.dart diff --git a/lib/features/auth/presentation/screens/terms_screen.dart b/app/lib/features/auth/presentation/screens/terms_screen.dart similarity index 100% rename from lib/features/auth/presentation/screens/terms_screen.dart rename to app/lib/features/auth/presentation/screens/terms_screen.dart diff --git a/lib/features/auth/presentation/viewmodels/auth_viewmodel.dart b/app/lib/features/auth/presentation/viewmodels/auth_viewmodel.dart similarity index 100% rename from lib/features/auth/presentation/viewmodels/auth_viewmodel.dart rename to app/lib/features/auth/presentation/viewmodels/auth_viewmodel.dart diff --git a/lib/features/auth/presentation/widgets/marketplace_agreement_sheet.dart b/app/lib/features/auth/presentation/widgets/marketplace_agreement_sheet.dart similarity index 100% rename from lib/features/auth/presentation/widgets/marketplace_agreement_sheet.dart rename to app/lib/features/auth/presentation/widgets/marketplace_agreement_sheet.dart diff --git a/lib/features/chat/data/datasources/supabase_chat_data_source.dart b/app/lib/features/chat/data/datasources/supabase_chat_data_source.dart similarity index 100% rename from lib/features/chat/data/datasources/supabase_chat_data_source.dart rename to app/lib/features/chat/data/datasources/supabase_chat_data_source.dart diff --git a/lib/features/chat/data/models/message_model.dart b/app/lib/features/chat/data/models/message_model.dart similarity index 100% rename from lib/features/chat/data/models/message_model.dart rename to app/lib/features/chat/data/models/message_model.dart diff --git a/lib/features/chat/data/repositories/chat_repository_impl.dart b/app/lib/features/chat/data/repositories/chat_repository_impl.dart similarity index 100% rename from lib/features/chat/data/repositories/chat_repository_impl.dart rename to app/lib/features/chat/data/repositories/chat_repository_impl.dart diff --git a/lib/features/chat/domain/entities/conversation.dart b/app/lib/features/chat/domain/entities/conversation.dart similarity index 100% rename from lib/features/chat/domain/entities/conversation.dart rename to app/lib/features/chat/domain/entities/conversation.dart diff --git a/lib/features/chat/domain/entities/message.dart b/app/lib/features/chat/domain/entities/message.dart similarity index 100% rename from lib/features/chat/domain/entities/message.dart rename to app/lib/features/chat/domain/entities/message.dart diff --git a/lib/features/chat/domain/repositories/chat_repository.dart b/app/lib/features/chat/domain/repositories/chat_repository.dart similarity index 100% rename from lib/features/chat/domain/repositories/chat_repository.dart rename to app/lib/features/chat/domain/repositories/chat_repository.dart diff --git a/lib/features/chat/presentation/screens/chat_list_screen.dart b/app/lib/features/chat/presentation/screens/chat_list_screen.dart similarity index 100% rename from lib/features/chat/presentation/screens/chat_list_screen.dart rename to app/lib/features/chat/presentation/screens/chat_list_screen.dart diff --git a/lib/features/chat/presentation/screens/chat_screen.dart b/app/lib/features/chat/presentation/screens/chat_screen.dart similarity index 100% rename from lib/features/chat/presentation/screens/chat_screen.dart rename to app/lib/features/chat/presentation/screens/chat_screen.dart diff --git a/lib/features/chat/presentation/viewmodels/chat_viewmodel.dart b/app/lib/features/chat/presentation/viewmodels/chat_viewmodel.dart similarity index 100% rename from lib/features/chat/presentation/viewmodels/chat_viewmodel.dart rename to app/lib/features/chat/presentation/viewmodels/chat_viewmodel.dart diff --git a/lib/features/favorites/data/datasources/supabase_favorites_data_source.dart b/app/lib/features/favorites/data/datasources/supabase_favorites_data_source.dart similarity index 100% rename from lib/features/favorites/data/datasources/supabase_favorites_data_source.dart rename to app/lib/features/favorites/data/datasources/supabase_favorites_data_source.dart diff --git a/lib/features/favorites/data/models/favorite_model.dart b/app/lib/features/favorites/data/models/favorite_model.dart similarity index 100% rename from lib/features/favorites/data/models/favorite_model.dart rename to app/lib/features/favorites/data/models/favorite_model.dart diff --git a/lib/features/favorites/data/repositories/favorites_repository_impl.dart b/app/lib/features/favorites/data/repositories/favorites_repository_impl.dart similarity index 100% rename from lib/features/favorites/data/repositories/favorites_repository_impl.dart rename to app/lib/features/favorites/data/repositories/favorites_repository_impl.dart diff --git a/lib/features/favorites/domain/entities/favorite.dart b/app/lib/features/favorites/domain/entities/favorite.dart similarity index 100% rename from lib/features/favorites/domain/entities/favorite.dart rename to app/lib/features/favorites/domain/entities/favorite.dart diff --git a/lib/features/favorites/domain/repositories/favorites_repository.dart b/app/lib/features/favorites/domain/repositories/favorites_repository.dart similarity index 100% rename from lib/features/favorites/domain/repositories/favorites_repository.dart rename to app/lib/features/favorites/domain/repositories/favorites_repository.dart diff --git a/lib/features/favorites/domain/usecases/get_favorites_usecase.dart b/app/lib/features/favorites/domain/usecases/get_favorites_usecase.dart similarity index 100% rename from lib/features/favorites/domain/usecases/get_favorites_usecase.dart rename to app/lib/features/favorites/domain/usecases/get_favorites_usecase.dart diff --git a/lib/features/favorites/domain/usecases/is_favorite_usecase.dart b/app/lib/features/favorites/domain/usecases/is_favorite_usecase.dart similarity index 100% rename from lib/features/favorites/domain/usecases/is_favorite_usecase.dart rename to app/lib/features/favorites/domain/usecases/is_favorite_usecase.dart diff --git a/lib/features/favorites/domain/usecases/toggle_favorite_usecase.dart b/app/lib/features/favorites/domain/usecases/toggle_favorite_usecase.dart similarity index 100% rename from lib/features/favorites/domain/usecases/toggle_favorite_usecase.dart rename to app/lib/features/favorites/domain/usecases/toggle_favorite_usecase.dart diff --git a/lib/features/favorites/presentation/screens/favorites_screen.dart b/app/lib/features/favorites/presentation/screens/favorites_screen.dart similarity index 100% rename from lib/features/favorites/presentation/screens/favorites_screen.dart rename to app/lib/features/favorites/presentation/screens/favorites_screen.dart diff --git a/lib/features/favorites/presentation/viewmodels/favorites_viewmodel.dart b/app/lib/features/favorites/presentation/viewmodels/favorites_viewmodel.dart similarity index 100% rename from lib/features/favorites/presentation/viewmodels/favorites_viewmodel.dart rename to app/lib/features/favorites/presentation/viewmodels/favorites_viewmodel.dart diff --git a/lib/features/favorites/presentation/widgets/pulse_heart_icon.dart b/app/lib/features/favorites/presentation/widgets/pulse_heart_icon.dart similarity index 100% rename from lib/features/favorites/presentation/widgets/pulse_heart_icon.dart rename to app/lib/features/favorites/presentation/widgets/pulse_heart_icon.dart diff --git a/lib/features/favorites/presentation/widgets/skeleton_listing_card.dart b/app/lib/features/favorites/presentation/widgets/skeleton_listing_card.dart similarity index 100% rename from lib/features/favorites/presentation/widgets/skeleton_listing_card.dart rename to app/lib/features/favorites/presentation/widgets/skeleton_listing_card.dart diff --git a/lib/features/impact/data/datasources/supabase_impact_data_source.dart b/app/lib/features/impact/data/datasources/supabase_impact_data_source.dart similarity index 100% rename from lib/features/impact/data/datasources/supabase_impact_data_source.dart rename to app/lib/features/impact/data/datasources/supabase_impact_data_source.dart diff --git a/lib/features/impact/data/models/platform_stats_model.dart b/app/lib/features/impact/data/models/platform_stats_model.dart similarity index 100% rename from lib/features/impact/data/models/platform_stats_model.dart rename to app/lib/features/impact/data/models/platform_stats_model.dart diff --git a/lib/features/impact/data/repositories/impact_repository_impl.dart b/app/lib/features/impact/data/repositories/impact_repository_impl.dart similarity index 100% rename from lib/features/impact/data/repositories/impact_repository_impl.dart rename to app/lib/features/impact/data/repositories/impact_repository_impl.dart diff --git a/lib/features/impact/domain/entities/platform_stats.dart b/app/lib/features/impact/domain/entities/platform_stats.dart similarity index 100% rename from lib/features/impact/domain/entities/platform_stats.dart rename to app/lib/features/impact/domain/entities/platform_stats.dart diff --git a/lib/features/impact/domain/repositories/impact_repository.dart b/app/lib/features/impact/domain/repositories/impact_repository.dart similarity index 100% rename from lib/features/impact/domain/repositories/impact_repository.dart rename to app/lib/features/impact/domain/repositories/impact_repository.dart diff --git a/lib/features/impact/domain/usecases/get_platform_stats_usecase.dart b/app/lib/features/impact/domain/usecases/get_platform_stats_usecase.dart similarity index 100% rename from lib/features/impact/domain/usecases/get_platform_stats_usecase.dart rename to app/lib/features/impact/domain/usecases/get_platform_stats_usecase.dart diff --git a/lib/features/impact/presentation/widgets/count_up_text.dart b/app/lib/features/impact/presentation/widgets/count_up_text.dart similarity index 100% rename from lib/features/impact/presentation/widgets/count_up_text.dart rename to app/lib/features/impact/presentation/widgets/count_up_text.dart diff --git a/lib/features/impact/presentation/widgets/impact_stats_widget.dart b/app/lib/features/impact/presentation/widgets/impact_stats_widget.dart similarity index 100% rename from lib/features/impact/presentation/widgets/impact_stats_widget.dart rename to app/lib/features/impact/presentation/widgets/impact_stats_widget.dart diff --git a/lib/features/listings/data/datasources/local_listings_datasource.dart b/app/lib/features/listings/data/datasources/local_listings_datasource.dart similarity index 100% rename from lib/features/listings/data/datasources/local_listings_datasource.dart rename to app/lib/features/listings/data/datasources/local_listings_datasource.dart diff --git a/lib/features/listings/data/datasources/supabase_listings_data_source.dart b/app/lib/features/listings/data/datasources/supabase_listings_data_source.dart similarity index 100% rename from lib/features/listings/data/datasources/supabase_listings_data_source.dart rename to app/lib/features/listings/data/datasources/supabase_listings_data_source.dart diff --git a/lib/features/listings/data/datasources/supabase_storage_data_source.dart b/app/lib/features/listings/data/datasources/supabase_storage_data_source.dart similarity index 100% rename from lib/features/listings/data/datasources/supabase_storage_data_source.dart rename to app/lib/features/listings/data/datasources/supabase_storage_data_source.dart diff --git a/lib/features/listings/data/models/listing_model.dart b/app/lib/features/listings/data/models/listing_model.dart similarity index 100% rename from lib/features/listings/data/models/listing_model.dart rename to app/lib/features/listings/data/models/listing_model.dart diff --git a/lib/features/listings/data/repositories/listing_repository_impl.dart b/app/lib/features/listings/data/repositories/listing_repository_impl.dart similarity index 100% rename from lib/features/listings/data/repositories/listing_repository_impl.dart rename to app/lib/features/listings/data/repositories/listing_repository_impl.dart diff --git a/lib/features/listings/domain/entities/book_condition.dart b/app/lib/features/listings/domain/entities/book_condition.dart similarity index 100% rename from lib/features/listings/domain/entities/book_condition.dart rename to app/lib/features/listings/domain/entities/book_condition.dart diff --git a/lib/features/listings/domain/entities/category.dart b/app/lib/features/listings/domain/entities/category.dart similarity index 100% rename from lib/features/listings/domain/entities/category.dart rename to app/lib/features/listings/domain/entities/category.dart diff --git a/lib/features/listings/domain/entities/listing.dart b/app/lib/features/listings/domain/entities/listing.dart similarity index 100% rename from lib/features/listings/domain/entities/listing.dart rename to app/lib/features/listings/domain/entities/listing.dart diff --git a/lib/features/listings/domain/repositories/listing_repository.dart b/app/lib/features/listings/domain/repositories/listing_repository.dart similarity index 100% rename from lib/features/listings/domain/repositories/listing_repository.dart rename to app/lib/features/listings/domain/repositories/listing_repository.dart diff --git a/lib/features/listings/domain/usecases/create_listing_usecase.dart b/app/lib/features/listings/domain/usecases/create_listing_usecase.dart similarity index 100% rename from lib/features/listings/domain/usecases/create_listing_usecase.dart rename to app/lib/features/listings/domain/usecases/create_listing_usecase.dart diff --git a/lib/features/listings/domain/usecases/delete_listing_usecase.dart b/app/lib/features/listings/domain/usecases/delete_listing_usecase.dart similarity index 100% rename from lib/features/listings/domain/usecases/delete_listing_usecase.dart rename to app/lib/features/listings/domain/usecases/delete_listing_usecase.dart diff --git a/lib/features/listings/domain/usecases/get_listing_details_usecase.dart b/app/lib/features/listings/domain/usecases/get_listing_details_usecase.dart similarity index 100% rename from lib/features/listings/domain/usecases/get_listing_details_usecase.dart rename to app/lib/features/listings/domain/usecases/get_listing_details_usecase.dart diff --git a/lib/features/listings/domain/usecases/get_listings_usecase.dart b/app/lib/features/listings/domain/usecases/get_listings_usecase.dart similarity index 100% rename from lib/features/listings/domain/usecases/get_listings_usecase.dart rename to app/lib/features/listings/domain/usecases/get_listings_usecase.dart diff --git a/lib/features/listings/domain/usecases/get_user_listings_usecase.dart b/app/lib/features/listings/domain/usecases/get_user_listings_usecase.dart similarity index 100% rename from lib/features/listings/domain/usecases/get_user_listings_usecase.dart rename to app/lib/features/listings/domain/usecases/get_user_listings_usecase.dart diff --git a/lib/features/listings/domain/usecases/search_listings_usecase.dart b/app/lib/features/listings/domain/usecases/search_listings_usecase.dart similarity index 100% rename from lib/features/listings/domain/usecases/search_listings_usecase.dart rename to app/lib/features/listings/domain/usecases/search_listings_usecase.dart diff --git a/lib/features/listings/domain/usecases/update_listing_usecase.dart b/app/lib/features/listings/domain/usecases/update_listing_usecase.dart similarity index 100% rename from lib/features/listings/domain/usecases/update_listing_usecase.dart rename to app/lib/features/listings/domain/usecases/update_listing_usecase.dart diff --git a/lib/features/listings/presentation/screens/about_screen.dart b/app/lib/features/listings/presentation/screens/about_screen.dart similarity index 100% rename from lib/features/listings/presentation/screens/about_screen.dart rename to app/lib/features/listings/presentation/screens/about_screen.dart diff --git a/lib/features/listings/presentation/screens/categories_screen.dart b/app/lib/features/listings/presentation/screens/categories_screen.dart similarity index 100% rename from lib/features/listings/presentation/screens/categories_screen.dart rename to app/lib/features/listings/presentation/screens/categories_screen.dart diff --git a/lib/features/listings/presentation/screens/contact_us_screen.dart b/app/lib/features/listings/presentation/screens/contact_us_screen.dart similarity index 100% rename from lib/features/listings/presentation/screens/contact_us_screen.dart rename to app/lib/features/listings/presentation/screens/contact_us_screen.dart diff --git a/lib/features/listings/presentation/screens/faq_screen.dart b/app/lib/features/listings/presentation/screens/faq_screen.dart similarity index 100% rename from lib/features/listings/presentation/screens/faq_screen.dart rename to app/lib/features/listings/presentation/screens/faq_screen.dart diff --git a/lib/features/listings/presentation/screens/feedback_screen.dart b/app/lib/features/listings/presentation/screens/feedback_screen.dart similarity index 100% rename from lib/features/listings/presentation/screens/feedback_screen.dart rename to app/lib/features/listings/presentation/screens/feedback_screen.dart diff --git a/lib/features/listings/presentation/screens/home_screen.dart b/app/lib/features/listings/presentation/screens/home_screen.dart similarity index 100% rename from lib/features/listings/presentation/screens/home_screen.dart rename to app/lib/features/listings/presentation/screens/home_screen.dart diff --git a/lib/features/listings/presentation/screens/listing_details_screen.dart b/app/lib/features/listings/presentation/screens/listing_details_screen.dart similarity index 100% rename from lib/features/listings/presentation/screens/listing_details_screen.dart rename to app/lib/features/listings/presentation/screens/listing_details_screen.dart diff --git a/lib/features/listings/presentation/screens/my_books_screen.dart b/app/lib/features/listings/presentation/screens/my_books_screen.dart similarity index 100% rename from lib/features/listings/presentation/screens/my_books_screen.dart rename to app/lib/features/listings/presentation/screens/my_books_screen.dart diff --git a/lib/features/listings/presentation/screens/privacy_policy_screen.dart b/app/lib/features/listings/presentation/screens/privacy_policy_screen.dart similarity index 100% rename from lib/features/listings/presentation/screens/privacy_policy_screen.dart rename to app/lib/features/listings/presentation/screens/privacy_policy_screen.dart diff --git a/lib/features/listings/presentation/screens/profile_screen.dart b/app/lib/features/listings/presentation/screens/profile_screen.dart similarity index 100% rename from lib/features/listings/presentation/screens/profile_screen.dart rename to app/lib/features/listings/presentation/screens/profile_screen.dart diff --git a/lib/features/listings/presentation/screens/search_screen.dart b/app/lib/features/listings/presentation/screens/search_screen.dart similarity index 100% rename from lib/features/listings/presentation/screens/search_screen.dart rename to app/lib/features/listings/presentation/screens/search_screen.dart diff --git a/lib/features/listings/presentation/screens/sell_screen.dart b/app/lib/features/listings/presentation/screens/sell_screen.dart similarity index 100% rename from lib/features/listings/presentation/screens/sell_screen.dart rename to app/lib/features/listings/presentation/screens/sell_screen.dart diff --git a/lib/features/listings/presentation/screens/seller_profile_screen.dart b/app/lib/features/listings/presentation/screens/seller_profile_screen.dart similarity index 100% rename from lib/features/listings/presentation/screens/seller_profile_screen.dart rename to app/lib/features/listings/presentation/screens/seller_profile_screen.dart diff --git a/lib/features/listings/presentation/viewmodels/home_viewmodel.dart b/app/lib/features/listings/presentation/viewmodels/home_viewmodel.dart similarity index 100% rename from lib/features/listings/presentation/viewmodels/home_viewmodel.dart rename to app/lib/features/listings/presentation/viewmodels/home_viewmodel.dart diff --git a/lib/features/listings/presentation/viewmodels/listing_details_viewmodel.dart b/app/lib/features/listings/presentation/viewmodels/listing_details_viewmodel.dart similarity index 100% rename from lib/features/listings/presentation/viewmodels/listing_details_viewmodel.dart rename to app/lib/features/listings/presentation/viewmodels/listing_details_viewmodel.dart diff --git a/lib/features/listings/presentation/viewmodels/locale_viewmodel.dart b/app/lib/features/listings/presentation/viewmodels/locale_viewmodel.dart similarity index 100% rename from lib/features/listings/presentation/viewmodels/locale_viewmodel.dart rename to app/lib/features/listings/presentation/viewmodels/locale_viewmodel.dart diff --git a/lib/features/listings/presentation/viewmodels/location_viewmodel.dart b/app/lib/features/listings/presentation/viewmodels/location_viewmodel.dart similarity index 100% rename from lib/features/listings/presentation/viewmodels/location_viewmodel.dart rename to app/lib/features/listings/presentation/viewmodels/location_viewmodel.dart diff --git a/lib/features/listings/presentation/viewmodels/profile_viewmodel.dart b/app/lib/features/listings/presentation/viewmodels/profile_viewmodel.dart similarity index 100% rename from lib/features/listings/presentation/viewmodels/profile_viewmodel.dart rename to app/lib/features/listings/presentation/viewmodels/profile_viewmodel.dart diff --git a/lib/features/listings/presentation/viewmodels/search_viewmodel.dart b/app/lib/features/listings/presentation/viewmodels/search_viewmodel.dart similarity index 100% rename from lib/features/listings/presentation/viewmodels/search_viewmodel.dart rename to app/lib/features/listings/presentation/viewmodels/search_viewmodel.dart diff --git a/lib/features/listings/presentation/viewmodels/sell_viewmodel.dart b/app/lib/features/listings/presentation/viewmodels/sell_viewmodel.dart similarity index 100% rename from lib/features/listings/presentation/viewmodels/sell_viewmodel.dart rename to app/lib/features/listings/presentation/viewmodels/sell_viewmodel.dart diff --git a/lib/features/listings/presentation/viewmodels/seller_profile_viewmodel.dart b/app/lib/features/listings/presentation/viewmodels/seller_profile_viewmodel.dart similarity index 100% rename from lib/features/listings/presentation/viewmodels/seller_profile_viewmodel.dart rename to app/lib/features/listings/presentation/viewmodels/seller_profile_viewmodel.dart diff --git a/lib/features/listings/presentation/widgets/listing_card.dart b/app/lib/features/listings/presentation/widgets/listing_card.dart similarity index 100% rename from lib/features/listings/presentation/widgets/listing_card.dart rename to app/lib/features/listings/presentation/widgets/listing_card.dart diff --git a/lib/features/notifications/data/datasources/supabase_notifications_data_source.dart b/app/lib/features/notifications/data/datasources/supabase_notifications_data_source.dart similarity index 100% rename from lib/features/notifications/data/datasources/supabase_notifications_data_source.dart rename to app/lib/features/notifications/data/datasources/supabase_notifications_data_source.dart diff --git a/lib/features/notifications/data/models/notification_model.dart b/app/lib/features/notifications/data/models/notification_model.dart similarity index 100% rename from lib/features/notifications/data/models/notification_model.dart rename to app/lib/features/notifications/data/models/notification_model.dart diff --git a/lib/features/notifications/data/repositories/notifications_repository_impl.dart b/app/lib/features/notifications/data/repositories/notifications_repository_impl.dart similarity index 100% rename from lib/features/notifications/data/repositories/notifications_repository_impl.dart rename to app/lib/features/notifications/data/repositories/notifications_repository_impl.dart diff --git a/lib/features/notifications/data/services/push_notification_service.dart b/app/lib/features/notifications/data/services/push_notification_service.dart similarity index 100% rename from lib/features/notifications/data/services/push_notification_service.dart rename to app/lib/features/notifications/data/services/push_notification_service.dart diff --git a/lib/features/notifications/domain/entities/notification.dart b/app/lib/features/notifications/domain/entities/notification.dart similarity index 100% rename from lib/features/notifications/domain/entities/notification.dart rename to app/lib/features/notifications/domain/entities/notification.dart diff --git a/lib/features/notifications/domain/repositories/notifications_repository.dart b/app/lib/features/notifications/domain/repositories/notifications_repository.dart similarity index 100% rename from lib/features/notifications/domain/repositories/notifications_repository.dart rename to app/lib/features/notifications/domain/repositories/notifications_repository.dart diff --git a/lib/features/notifications/presentation/screens/notifications_screen.dart b/app/lib/features/notifications/presentation/screens/notifications_screen.dart similarity index 100% rename from lib/features/notifications/presentation/screens/notifications_screen.dart rename to app/lib/features/notifications/presentation/screens/notifications_screen.dart diff --git a/lib/features/notifications/presentation/viewmodels/notifications_viewmodel.dart b/app/lib/features/notifications/presentation/viewmodels/notifications_viewmodel.dart similarity index 100% rename from lib/features/notifications/presentation/viewmodels/notifications_viewmodel.dart rename to app/lib/features/notifications/presentation/viewmodels/notifications_viewmodel.dart diff --git a/lib/features/payments/data/datasources/fapshi_data_source.dart b/app/lib/features/payments/data/datasources/fapshi_data_source.dart similarity index 100% rename from lib/features/payments/data/datasources/fapshi_data_source.dart rename to app/lib/features/payments/data/datasources/fapshi_data_source.dart diff --git a/lib/features/payments/data/repositories/payment_repository_impl.dart b/app/lib/features/payments/data/repositories/payment_repository_impl.dart similarity index 100% rename from lib/features/payments/data/repositories/payment_repository_impl.dart rename to app/lib/features/payments/data/repositories/payment_repository_impl.dart diff --git a/lib/features/payments/domain/repositories/payment_repository.dart b/app/lib/features/payments/domain/repositories/payment_repository.dart similarity index 100% rename from lib/features/payments/domain/repositories/payment_repository.dart rename to app/lib/features/payments/domain/repositories/payment_repository.dart diff --git a/lib/features/payments/domain/usecases/collect_payment_usecase.dart b/app/lib/features/payments/domain/usecases/collect_payment_usecase.dart similarity index 100% rename from lib/features/payments/domain/usecases/collect_payment_usecase.dart rename to app/lib/features/payments/domain/usecases/collect_payment_usecase.dart diff --git a/lib/features/payments/domain/usecases/get_payment_status_usecase.dart b/app/lib/features/payments/domain/usecases/get_payment_status_usecase.dart similarity index 100% rename from lib/features/payments/domain/usecases/get_payment_status_usecase.dart rename to app/lib/features/payments/domain/usecases/get_payment_status_usecase.dart diff --git a/lib/features/payments/presentation/viewmodels/payment_viewmodel.dart b/app/lib/features/payments/presentation/viewmodels/payment_viewmodel.dart similarity index 100% rename from lib/features/payments/presentation/viewmodels/payment_viewmodel.dart rename to app/lib/features/payments/presentation/viewmodels/payment_viewmodel.dart diff --git a/lib/features/payments/presentation/widgets/payment_bottom_sheet.dart b/app/lib/features/payments/presentation/widgets/payment_bottom_sheet.dart similarity index 100% rename from lib/features/payments/presentation/widgets/payment_bottom_sheet.dart rename to app/lib/features/payments/presentation/widgets/payment_bottom_sheet.dart diff --git a/lib/features/reviews/data/datasources/supabase_reviews_data_source.dart b/app/lib/features/reviews/data/datasources/supabase_reviews_data_source.dart similarity index 100% rename from lib/features/reviews/data/datasources/supabase_reviews_data_source.dart rename to app/lib/features/reviews/data/datasources/supabase_reviews_data_source.dart diff --git a/lib/features/reviews/data/repositories/review_repository_impl.dart b/app/lib/features/reviews/data/repositories/review_repository_impl.dart similarity index 100% rename from lib/features/reviews/data/repositories/review_repository_impl.dart rename to app/lib/features/reviews/data/repositories/review_repository_impl.dart diff --git a/lib/features/reviews/domain/entities/review_entity.dart b/app/lib/features/reviews/domain/entities/review_entity.dart similarity index 100% rename from lib/features/reviews/domain/entities/review_entity.dart rename to app/lib/features/reviews/domain/entities/review_entity.dart diff --git a/lib/features/reviews/domain/repositories/review_repository.dart b/app/lib/features/reviews/domain/repositories/review_repository.dart similarity index 100% rename from lib/features/reviews/domain/repositories/review_repository.dart rename to app/lib/features/reviews/domain/repositories/review_repository.dart diff --git a/lib/features/reviews/domain/usecases/create_review_usecase.dart b/app/lib/features/reviews/domain/usecases/create_review_usecase.dart similarity index 100% rename from lib/features/reviews/domain/usecases/create_review_usecase.dart rename to app/lib/features/reviews/domain/usecases/create_review_usecase.dart diff --git a/lib/features/reviews/domain/usecases/get_user_reviews_usecase.dart b/app/lib/features/reviews/domain/usecases/get_user_reviews_usecase.dart similarity index 100% rename from lib/features/reviews/domain/usecases/get_user_reviews_usecase.dart rename to app/lib/features/reviews/domain/usecases/get_user_reviews_usecase.dart diff --git a/lib/features/reviews/domain/usecases/has_reviewed_usecase.dart b/app/lib/features/reviews/domain/usecases/has_reviewed_usecase.dart similarity index 100% rename from lib/features/reviews/domain/usecases/has_reviewed_usecase.dart rename to app/lib/features/reviews/domain/usecases/has_reviewed_usecase.dart diff --git a/lib/features/reviews/presentation/viewmodels/review_viewmodel.dart b/app/lib/features/reviews/presentation/viewmodels/review_viewmodel.dart similarity index 100% rename from lib/features/reviews/presentation/viewmodels/review_viewmodel.dart rename to app/lib/features/reviews/presentation/viewmodels/review_viewmodel.dart diff --git a/lib/features/reviews/presentation/widgets/review_dialog.dart b/app/lib/features/reviews/presentation/widgets/review_dialog.dart similarity index 100% rename from lib/features/reviews/presentation/widgets/review_dialog.dart rename to app/lib/features/reviews/presentation/widgets/review_dialog.dart diff --git a/lib/features/reviews/presentation/widgets/seller_rating_badge.dart b/app/lib/features/reviews/presentation/widgets/seller_rating_badge.dart similarity index 100% rename from lib/features/reviews/presentation/widgets/seller_rating_badge.dart rename to app/lib/features/reviews/presentation/widgets/seller_rating_badge.dart diff --git a/lib/features/reviews/presentation/widgets/seller_trust_card.dart b/app/lib/features/reviews/presentation/widgets/seller_trust_card.dart similarity index 100% rename from lib/features/reviews/presentation/widgets/seller_trust_card.dart rename to app/lib/features/reviews/presentation/widgets/seller_trust_card.dart diff --git a/lib/features/reviews/presentation/widgets/star_rating_display.dart b/app/lib/features/reviews/presentation/widgets/star_rating_display.dart similarity index 100% rename from lib/features/reviews/presentation/widgets/star_rating_display.dart rename to app/lib/features/reviews/presentation/widgets/star_rating_display.dart diff --git a/lib/features/safety/data/datasources/safety_remote_datasource.dart b/app/lib/features/safety/data/datasources/safety_remote_datasource.dart similarity index 100% rename from lib/features/safety/data/datasources/safety_remote_datasource.dart rename to app/lib/features/safety/data/datasources/safety_remote_datasource.dart diff --git a/lib/features/safety/data/models/campus_zone_model.dart b/app/lib/features/safety/data/models/campus_zone_model.dart similarity index 100% rename from lib/features/safety/data/models/campus_zone_model.dart rename to app/lib/features/safety/data/models/campus_zone_model.dart diff --git a/lib/features/safety/data/repositories/safety_repository_impl.dart b/app/lib/features/safety/data/repositories/safety_repository_impl.dart similarity index 100% rename from lib/features/safety/data/repositories/safety_repository_impl.dart rename to app/lib/features/safety/data/repositories/safety_repository_impl.dart diff --git a/lib/features/safety/domain/entities/campus_zone.dart b/app/lib/features/safety/domain/entities/campus_zone.dart similarity index 100% rename from lib/features/safety/domain/entities/campus_zone.dart rename to app/lib/features/safety/domain/entities/campus_zone.dart diff --git a/lib/features/safety/domain/repositories/safety_repository.dart b/app/lib/features/safety/domain/repositories/safety_repository.dart similarity index 100% rename from lib/features/safety/domain/repositories/safety_repository.dart rename to app/lib/features/safety/domain/repositories/safety_repository.dart diff --git a/lib/features/safety/domain/usecases/get_campus_zones_usecase.dart b/app/lib/features/safety/domain/usecases/get_campus_zones_usecase.dart similarity index 100% rename from lib/features/safety/domain/usecases/get_campus_zones_usecase.dart rename to app/lib/features/safety/domain/usecases/get_campus_zones_usecase.dart diff --git a/lib/features/safety/presentation/screens/safety_guidelines_screen.dart b/app/lib/features/safety/presentation/screens/safety_guidelines_screen.dart similarity index 100% rename from lib/features/safety/presentation/screens/safety_guidelines_screen.dart rename to app/lib/features/safety/presentation/screens/safety_guidelines_screen.dart diff --git a/lib/features/safety/presentation/viewmodels/safety_viewmodel.dart b/app/lib/features/safety/presentation/viewmodels/safety_viewmodel.dart similarity index 100% rename from lib/features/safety/presentation/viewmodels/safety_viewmodel.dart rename to app/lib/features/safety/presentation/viewmodels/safety_viewmodel.dart diff --git a/lib/features/safety/presentation/widgets/meetup_tips_card.dart b/app/lib/features/safety/presentation/widgets/meetup_tips_card.dart similarity index 100% rename from lib/features/safety/presentation/widgets/meetup_tips_card.dart rename to app/lib/features/safety/presentation/widgets/meetup_tips_card.dart diff --git a/lib/features/transactions/data/datasources/supabase_transactions_data_source.dart b/app/lib/features/transactions/data/datasources/supabase_transactions_data_source.dart similarity index 100% rename from lib/features/transactions/data/datasources/supabase_transactions_data_source.dart rename to app/lib/features/transactions/data/datasources/supabase_transactions_data_source.dart diff --git a/lib/features/transactions/data/repositories/transaction_repository_impl.dart b/app/lib/features/transactions/data/repositories/transaction_repository_impl.dart similarity index 100% rename from lib/features/transactions/data/repositories/transaction_repository_impl.dart rename to app/lib/features/transactions/data/repositories/transaction_repository_impl.dart diff --git a/lib/features/transactions/domain/entities/transaction_entity.dart b/app/lib/features/transactions/domain/entities/transaction_entity.dart similarity index 100% rename from lib/features/transactions/domain/entities/transaction_entity.dart rename to app/lib/features/transactions/domain/entities/transaction_entity.dart diff --git a/lib/features/transactions/domain/repositories/transaction_repository.dart b/app/lib/features/transactions/domain/repositories/transaction_repository.dart similarity index 100% rename from lib/features/transactions/domain/repositories/transaction_repository.dart rename to app/lib/features/transactions/domain/repositories/transaction_repository.dart diff --git a/lib/features/transactions/domain/usecases/get_transaction_by_external_ref_usecase.dart b/app/lib/features/transactions/domain/usecases/get_transaction_by_external_ref_usecase.dart similarity index 100% rename from lib/features/transactions/domain/usecases/get_transaction_by_external_ref_usecase.dart rename to app/lib/features/transactions/domain/usecases/get_transaction_by_external_ref_usecase.dart diff --git a/lib/features/transactions/domain/usecases/get_user_transactions_usecase.dart b/app/lib/features/transactions/domain/usecases/get_user_transactions_usecase.dart similarity index 100% rename from lib/features/transactions/domain/usecases/get_user_transactions_usecase.dart rename to app/lib/features/transactions/domain/usecases/get_user_transactions_usecase.dart diff --git a/lib/features/transactions/presentation/screens/transaction_history_screen.dart b/app/lib/features/transactions/presentation/screens/transaction_history_screen.dart similarity index 100% rename from lib/features/transactions/presentation/screens/transaction_history_screen.dart rename to app/lib/features/transactions/presentation/screens/transaction_history_screen.dart diff --git a/lib/features/transactions/presentation/viewmodels/transaction_history_viewmodel.dart b/app/lib/features/transactions/presentation/viewmodels/transaction_history_viewmodel.dart similarity index 100% rename from lib/features/transactions/presentation/viewmodels/transaction_history_viewmodel.dart rename to app/lib/features/transactions/presentation/viewmodels/transaction_history_viewmodel.dart diff --git a/lib/firebase_options.dart b/app/lib/firebase_options.dart similarity index 100% rename from lib/firebase_options.dart rename to app/lib/firebase_options.dart diff --git a/lib/injection_container.dart b/app/lib/injection_container.dart similarity index 100% rename from lib/injection_container.dart rename to app/lib/injection_container.dart diff --git a/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb similarity index 100% rename from lib/l10n/app_en.arb rename to app/lib/l10n/app_en.arb diff --git a/lib/l10n/app_fr.arb b/app/lib/l10n/app_fr.arb similarity index 100% rename from lib/l10n/app_fr.arb rename to app/lib/l10n/app_fr.arb diff --git a/lib/l10n/app_localizations.dart b/app/lib/l10n/app_localizations.dart similarity index 100% rename from lib/l10n/app_localizations.dart rename to app/lib/l10n/app_localizations.dart diff --git a/lib/l10n/app_localizations_en.dart b/app/lib/l10n/app_localizations_en.dart similarity index 100% rename from lib/l10n/app_localizations_en.dart rename to app/lib/l10n/app_localizations_en.dart diff --git a/lib/l10n/app_localizations_fr.dart b/app/lib/l10n/app_localizations_fr.dart similarity index 100% rename from lib/l10n/app_localizations_fr.dart rename to app/lib/l10n/app_localizations_fr.dart diff --git a/lib/main.dart b/app/lib/main.dart similarity index 100% rename from lib/main.dart rename to app/lib/main.dart diff --git a/linux/.gitignore b/app/linux/.gitignore similarity index 100% rename from linux/.gitignore rename to app/linux/.gitignore diff --git a/linux/CMakeLists.txt b/app/linux/CMakeLists.txt similarity index 100% rename from linux/CMakeLists.txt rename to app/linux/CMakeLists.txt diff --git a/linux/flutter/CMakeLists.txt b/app/linux/flutter/CMakeLists.txt similarity index 100% rename from linux/flutter/CMakeLists.txt rename to app/linux/flutter/CMakeLists.txt diff --git a/linux/flutter/generated_plugin_registrant.cc b/app/linux/flutter/generated_plugin_registrant.cc similarity index 100% rename from linux/flutter/generated_plugin_registrant.cc rename to app/linux/flutter/generated_plugin_registrant.cc diff --git a/linux/flutter/generated_plugin_registrant.h b/app/linux/flutter/generated_plugin_registrant.h similarity index 100% rename from linux/flutter/generated_plugin_registrant.h rename to app/linux/flutter/generated_plugin_registrant.h diff --git a/linux/flutter/generated_plugins.cmake b/app/linux/flutter/generated_plugins.cmake similarity index 100% rename from linux/flutter/generated_plugins.cmake rename to app/linux/flutter/generated_plugins.cmake diff --git a/linux/runner/CMakeLists.txt b/app/linux/runner/CMakeLists.txt similarity index 100% rename from linux/runner/CMakeLists.txt rename to app/linux/runner/CMakeLists.txt diff --git a/linux/runner/main.cc b/app/linux/runner/main.cc similarity index 100% rename from linux/runner/main.cc rename to app/linux/runner/main.cc diff --git a/linux/runner/my_application.cc b/app/linux/runner/my_application.cc similarity index 100% rename from linux/runner/my_application.cc rename to app/linux/runner/my_application.cc diff --git a/linux/runner/my_application.h b/app/linux/runner/my_application.h similarity index 100% rename from linux/runner/my_application.h rename to app/linux/runner/my_application.h diff --git a/macos/.gitignore b/app/macos/.gitignore similarity index 100% rename from macos/.gitignore rename to app/macos/.gitignore diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/app/macos/Flutter/Flutter-Debug.xcconfig similarity index 100% rename from macos/Flutter/Flutter-Debug.xcconfig rename to app/macos/Flutter/Flutter-Debug.xcconfig diff --git a/macos/Flutter/Flutter-Release.xcconfig b/app/macos/Flutter/Flutter-Release.xcconfig similarity index 100% rename from macos/Flutter/Flutter-Release.xcconfig rename to app/macos/Flutter/Flutter-Release.xcconfig diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift similarity index 100% rename from macos/Flutter/GeneratedPluginRegistrant.swift rename to app/macos/Flutter/GeneratedPluginRegistrant.swift diff --git a/macos/Runner.xcodeproj/project.pbxproj b/app/macos/Runner.xcodeproj/project.pbxproj similarity index 100% rename from macos/Runner.xcodeproj/project.pbxproj rename to app/macos/Runner.xcodeproj/project.pbxproj diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/app/macos/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from macos/Runner.xcworkspace/contents.xcworkspacedata rename to app/macos/Runner.xcworkspace/contents.xcworkspacedata diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/macos/Runner/AppDelegate.swift b/app/macos/Runner/AppDelegate.swift similarity index 100% rename from macos/Runner/AppDelegate.swift rename to app/macos/Runner/AppDelegate.swift diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png similarity index 100% rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png rename to app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png similarity index 100% rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png rename to app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png similarity index 100% rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png rename to app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png similarity index 100% rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png rename to app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png similarity index 100% rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png rename to app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png similarity index 100% rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png rename to app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png similarity index 100% rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png rename to app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/app/macos/Runner/Base.lproj/MainMenu.xib similarity index 100% rename from macos/Runner/Base.lproj/MainMenu.xib rename to app/macos/Runner/Base.lproj/MainMenu.xib diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/app/macos/Runner/Configs/AppInfo.xcconfig similarity index 100% rename from macos/Runner/Configs/AppInfo.xcconfig rename to app/macos/Runner/Configs/AppInfo.xcconfig diff --git a/macos/Runner/Configs/Debug.xcconfig b/app/macos/Runner/Configs/Debug.xcconfig similarity index 100% rename from macos/Runner/Configs/Debug.xcconfig rename to app/macos/Runner/Configs/Debug.xcconfig diff --git a/macos/Runner/Configs/Release.xcconfig b/app/macos/Runner/Configs/Release.xcconfig similarity index 100% rename from macos/Runner/Configs/Release.xcconfig rename to app/macos/Runner/Configs/Release.xcconfig diff --git a/macos/Runner/Configs/Warnings.xcconfig b/app/macos/Runner/Configs/Warnings.xcconfig similarity index 100% rename from macos/Runner/Configs/Warnings.xcconfig rename to app/macos/Runner/Configs/Warnings.xcconfig diff --git a/macos/Runner/DebugProfile.entitlements b/app/macos/Runner/DebugProfile.entitlements similarity index 100% rename from macos/Runner/DebugProfile.entitlements rename to app/macos/Runner/DebugProfile.entitlements diff --git a/macos/Runner/Info.plist b/app/macos/Runner/Info.plist similarity index 100% rename from macos/Runner/Info.plist rename to app/macos/Runner/Info.plist diff --git a/macos/Runner/MainFlutterWindow.swift b/app/macos/Runner/MainFlutterWindow.swift similarity index 100% rename from macos/Runner/MainFlutterWindow.swift rename to app/macos/Runner/MainFlutterWindow.swift diff --git a/macos/Runner/Release.entitlements b/app/macos/Runner/Release.entitlements similarity index 100% rename from macos/Runner/Release.entitlements rename to app/macos/Runner/Release.entitlements diff --git a/macos/RunnerTests/RunnerTests.swift b/app/macos/RunnerTests/RunnerTests.swift similarity index 100% rename from macos/RunnerTests/RunnerTests.swift rename to app/macos/RunnerTests/RunnerTests.swift diff --git a/pubspec.yaml b/app/pubspec.yaml similarity index 100% rename from pubspec.yaml rename to app/pubspec.yaml diff --git a/test/.gitkeep b/app/test/.gitkeep similarity index 100% rename from test/.gitkeep rename to app/test/.gitkeep diff --git a/test/features/chat/presentation/viewmodels/chat_viewmodel_test.dart b/app/test/features/chat/presentation/viewmodels/chat_viewmodel_test.dart similarity index 100% rename from test/features/chat/presentation/viewmodels/chat_viewmodel_test.dart rename to app/test/features/chat/presentation/viewmodels/chat_viewmodel_test.dart diff --git a/test/features/listings/presentation/screens/seller_profile_screen_test.dart b/app/test/features/listings/presentation/screens/seller_profile_screen_test.dart similarity index 100% rename from test/features/listings/presentation/screens/seller_profile_screen_test.dart rename to app/test/features/listings/presentation/screens/seller_profile_screen_test.dart diff --git a/test/features/listings/presentation/viewmodels/seller_profile_viewmodel_test.dart b/app/test/features/listings/presentation/viewmodels/seller_profile_viewmodel_test.dart similarity index 100% rename from test/features/listings/presentation/viewmodels/seller_profile_viewmodel_test.dart rename to app/test/features/listings/presentation/viewmodels/seller_profile_viewmodel_test.dart diff --git a/web/favicon.png b/app/web/favicon.png similarity index 100% rename from web/favicon.png rename to app/web/favicon.png diff --git a/web/icons/Icon-192.png b/app/web/icons/Icon-192.png similarity index 100% rename from web/icons/Icon-192.png rename to app/web/icons/Icon-192.png diff --git a/web/icons/Icon-512.png b/app/web/icons/Icon-512.png similarity index 100% rename from web/icons/Icon-512.png rename to app/web/icons/Icon-512.png diff --git a/web/icons/Icon-maskable-192.png b/app/web/icons/Icon-maskable-192.png similarity index 100% rename from web/icons/Icon-maskable-192.png rename to app/web/icons/Icon-maskable-192.png diff --git a/web/icons/Icon-maskable-512.png b/app/web/icons/Icon-maskable-512.png similarity index 100% rename from web/icons/Icon-maskable-512.png rename to app/web/icons/Icon-maskable-512.png diff --git a/web/index.html b/app/web/index.html similarity index 100% rename from web/index.html rename to app/web/index.html diff --git a/web/manifest.json b/app/web/manifest.json similarity index 100% rename from web/manifest.json rename to app/web/manifest.json diff --git a/windows/.gitignore b/app/windows/.gitignore similarity index 100% rename from windows/.gitignore rename to app/windows/.gitignore diff --git a/windows/CMakeLists.txt b/app/windows/CMakeLists.txt similarity index 100% rename from windows/CMakeLists.txt rename to app/windows/CMakeLists.txt diff --git a/windows/flutter/CMakeLists.txt b/app/windows/flutter/CMakeLists.txt similarity index 100% rename from windows/flutter/CMakeLists.txt rename to app/windows/flutter/CMakeLists.txt diff --git a/windows/flutter/generated_plugin_registrant.cc b/app/windows/flutter/generated_plugin_registrant.cc similarity index 100% rename from windows/flutter/generated_plugin_registrant.cc rename to app/windows/flutter/generated_plugin_registrant.cc diff --git a/windows/flutter/generated_plugin_registrant.h b/app/windows/flutter/generated_plugin_registrant.h similarity index 100% rename from windows/flutter/generated_plugin_registrant.h rename to app/windows/flutter/generated_plugin_registrant.h diff --git a/windows/flutter/generated_plugins.cmake b/app/windows/flutter/generated_plugins.cmake similarity index 100% rename from windows/flutter/generated_plugins.cmake rename to app/windows/flutter/generated_plugins.cmake diff --git a/windows/runner/CMakeLists.txt b/app/windows/runner/CMakeLists.txt similarity index 100% rename from windows/runner/CMakeLists.txt rename to app/windows/runner/CMakeLists.txt diff --git a/windows/runner/Runner.rc b/app/windows/runner/Runner.rc similarity index 100% rename from windows/runner/Runner.rc rename to app/windows/runner/Runner.rc diff --git a/windows/runner/flutter_window.cpp b/app/windows/runner/flutter_window.cpp similarity index 100% rename from windows/runner/flutter_window.cpp rename to app/windows/runner/flutter_window.cpp diff --git a/windows/runner/flutter_window.h b/app/windows/runner/flutter_window.h similarity index 100% rename from windows/runner/flutter_window.h rename to app/windows/runner/flutter_window.h diff --git a/windows/runner/main.cpp b/app/windows/runner/main.cpp similarity index 100% rename from windows/runner/main.cpp rename to app/windows/runner/main.cpp diff --git a/windows/runner/resource.h b/app/windows/runner/resource.h similarity index 100% rename from windows/runner/resource.h rename to app/windows/runner/resource.h diff --git a/windows/runner/resources/app_icon.ico b/app/windows/runner/resources/app_icon.ico similarity index 100% rename from windows/runner/resources/app_icon.ico rename to app/windows/runner/resources/app_icon.ico diff --git a/windows/runner/runner.exe.manifest b/app/windows/runner/runner.exe.manifest similarity index 100% rename from windows/runner/runner.exe.manifest rename to app/windows/runner/runner.exe.manifest diff --git a/windows/runner/utils.cpp b/app/windows/runner/utils.cpp similarity index 100% rename from windows/runner/utils.cpp rename to app/windows/runner/utils.cpp diff --git a/windows/runner/utils.h b/app/windows/runner/utils.h similarity index 100% rename from windows/runner/utils.h rename to app/windows/runner/utils.h diff --git a/windows/runner/win32_window.cpp b/app/windows/runner/win32_window.cpp similarity index 100% rename from windows/runner/win32_window.cpp rename to app/windows/runner/win32_window.cpp diff --git a/windows/runner/win32_window.h b/app/windows/runner/win32_window.h similarity index 100% rename from windows/runner/win32_window.h rename to app/windows/runner/win32_window.h diff --git a/backend/Cargo.toml b/backend/Cargo.toml new file mode 100644 index 0000000..2927186 --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "backend" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/backend/src/main.rs b/backend/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/backend/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/bookbridge-rust-core/.env.example b/bookbridge-rust-core/.env.example deleted file mode 100644 index 7aa8bd3..0000000 --- a/bookbridge-rust-core/.env.example +++ /dev/null @@ -1,10 +0,0 @@ -# PostgreSQL Connection string -# Use Supabase's Supavisor Session-Mode Pooler (Port 5432) for IPv4 compatibility on Render: -# postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:5432/postgres -DATABASE_URL="postgresql://postgres.jacnsvcwmhoicuuzmrmr:[YOUR_DB_PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres" - -# Shared secret key for X-Internal-Secret validation (timing-attack protected) -INTERNAL_API_SECRET="your-generate-32-byte-hex-secret-here" - -# Port to bind the server listener to (defaults to 8080 if not set) -PORT=8080 diff --git a/bookbridge-rust-core/Cargo.toml b/bookbridge-rust-core/Cargo.toml deleted file mode 100644 index c71f122..0000000 --- a/bookbridge-rust-core/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "bookbridge-rust-core" -version = "0.1.0" -edition = "2021" - -[dependencies] -axum = "0.7.5" -tokio = { version = "1.38", features = ["full"] } -sqlx = { version = "0.7.4", features = ["postgres", "runtime-tokio-rustls", "macros", "chrono", "uuid"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -reqwest = { version = "0.12", features = ["json", "rustls-tls"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -thiserror = "1.0" -subtle = "2.5" -uuid = { version = "1.8", features = ["v4", "serde"] } -chrono = { version = "0.4", features = ["serde"] } -dotenvy = "0.15" -anyhow = "1.0" -tower = { version = "0.4", features = ["util"] } diff --git a/bookbridge-rust-core/src/auth.rs b/bookbridge-rust-core/src/auth.rs deleted file mode 100644 index 583ef71..0000000 --- a/bookbridge-rust-core/src/auth.rs +++ /dev/null @@ -1,38 +0,0 @@ -use axum::{ - body::Body, - extract::State, - http::{Request, StatusCode}, - middleware::Next, - response::Response, -}; -use subtle::ConstantTimeEq; -use crate::config::AppConfig; -use std::sync::Arc; - -pub async fn require_internal_auth( - State(config): State>, - request: Request, - next: Next, -) -> Result { - let header_val = request - .headers() - .get("X-Internal-Secret") - .and_then(|h| h.to_str().ok()); - - if let Some(incoming_secret) = header_val { - let expected_secret = &config.internal_api_secret; - if constant_time_compare(incoming_secret.as_bytes(), expected_secret.as_bytes()) { - return Ok(next.run(request).await); - } - } - - tracing::warn!("Unauthorized request: missing or invalid X-Internal-Secret header"); - Err(StatusCode::UNAUTHORIZED) -} - -pub fn constant_time_compare(a: &[u8], b: &[u8]) -> bool { - let lengths_match = a.len() == b.len(); - let actual_compare = if lengths_match { b } else { a }; - let cmp = a.ct_eq(actual_compare).unwrap_u8() == 1; - lengths_match && cmp -} diff --git a/bookbridge-rust-core/src/config.rs b/bookbridge-rust-core/src/config.rs deleted file mode 100644 index cc9e152..0000000 --- a/bookbridge-rust-core/src/config.rs +++ /dev/null @@ -1,35 +0,0 @@ -#[derive(Clone, Debug)] -pub struct AppConfig { - pub database_url: String, - pub internal_api_secret: String, - pub fapshi_base_url: String, - pub port: u16, -} - -impl AppConfig { - pub fn load() -> anyhow::Result { - // Load .env file if present - let _ = dotenvy::dotenv(); - - let database_url = std::env::var("DATABASE_URL") - .map_err(|_| anyhow::anyhow!("DATABASE_URL environment variable is not set"))?; - - let internal_api_secret = std::env::var("INTERNAL_API_SECRET") - .map_err(|_| anyhow::anyhow!("INTERNAL_API_SECRET environment variable is not set"))?; - - let fapshi_base_url = std::env::var("FAPSHI_BASE_URL") - .unwrap_or_else(|_| "https://live.fapshi.com".to_string()); - - let port = std::env::var("PORT") - .ok() - .and_then(|p| p.parse().ok()) - .unwrap_or(8080); - - Ok(Self { - database_url, - internal_api_secret, - fapshi_base_url, - port, - }) - } -} diff --git a/bookbridge-rust-core/src/error.rs b/bookbridge-rust-core/src/error.rs deleted file mode 100644 index b94ece3..0000000 --- a/bookbridge-rust-core/src/error.rs +++ /dev/null @@ -1,72 +0,0 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, - Json, -}; -use serde_json::json; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum AppError { - #[error("Database error: {0}")] - Database(#[from] sqlx::Error), - - #[error("HTTP client error: {0}")] - Reqwest(#[from] reqwest::Error), - - #[error("JSON error: {0}")] - Json(#[from] serde_json::Error), - - #[error("Unauthorized: {0}")] - Unauthorized(String), - - #[error("Fapshi error: {0}")] - Fapshi(String), - - #[error("Bad Request: {0}")] - BadRequest(String), - - #[error("Internal server error: {0}")] - Internal(String), -} - -impl IntoResponse for AppError { - fn into_response(self) -> Response { - let (status, error_message) = match &self { - AppError::Database(e) => { - tracing::error!("Database error: {:?}", e); - (StatusCode::INTERNAL_SERVER_ERROR, "Database error".to_string()) - } - AppError::Reqwest(e) => { - tracing::error!("Reqwest error: {:?}", e); - (StatusCode::BAD_GATEWAY, "Gateway error".to_string()) - } - AppError::Json(e) => { - tracing::error!("JSON serialization error: {:?}", e); - (StatusCode::BAD_REQUEST, "Malformed JSON request".to_string()) - } - AppError::Unauthorized(msg) => { - tracing::warn!("Unauthorized access: {}", msg); - (StatusCode::UNAUTHORIZED, msg.clone()) - } - AppError::Fapshi(msg) => { - tracing::error!("Fapshi integration error: {}", msg); - (StatusCode::BAD_GATEWAY, msg.clone()) - } - AppError::BadRequest(msg) => { - tracing::warn!("Bad request: {}", msg); - (StatusCode::BAD_REQUEST, msg.clone()) - } - AppError::Internal(msg) => { - tracing::error!("Internal error: {}", msg); - (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()) - } - }; - - let body = Json(json!({ - "error": error_message, - })); - - (status, body).into_response() - } -} diff --git a/bookbridge-rust-core/src/fapshi.rs b/bookbridge-rust-core/src/fapshi.rs deleted file mode 100644 index 8ead206..0000000 --- a/bookbridge-rust-core/src/fapshi.rs +++ /dev/null @@ -1,284 +0,0 @@ -use crate::error::AppError; -use sqlx::PgPool; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -pub struct FapshiClient { - http: reqwest::Client, - base_url: String, -} - -#[derive(Serialize, Clone)] -pub struct PayoutPayload { - pub amount: f64, - pub phone: String, - pub name: String, - #[serde(rename = "externalId")] - pub external_id: String, - pub message: String, -} - -#[derive(Deserialize, Serialize, Debug)] -pub struct PayoutResponse { - #[serde(rename = "statusCode")] - pub status_code: u16, - #[serde(rename = "transId")] - pub trans_id: Option, - pub message: Option, -} - -#[derive(Deserialize, Serialize, Debug)] -pub struct PollResponse { - pub status: Option, - pub state: Option, - #[serde(rename = "statusCode")] - pub status_code: Option, - pub message: Option, -} - -#[derive(Deserialize, Serialize, Debug)] -pub struct FapshiSearchItem { - #[serde(rename = "transId")] - pub trans_id: String, - pub status: String, - #[serde(rename = "externalId")] - pub external_id: Option, -} - -impl FapshiClient { - pub fn new(base_url: String) -> Self { - Self { - http: reqwest::Client::new(), - base_url, - } - } - - /// Fetches a key from the `app_secrets` table. - async fn get_secret(pool: &PgPool, key: &str) -> Result { - let row: (String,) = sqlx::query_as("SELECT value FROM app_secrets WHERE key = $1") - .bind(key) - .fetch_one(pool) - .await?; - Ok(row.0) - } - - /// Clean phone number: remove non-digits and strip leading "237". - pub fn clean_phone(phone: &str) -> String { - let mut cleaned: String = phone.chars().filter(|c| c.is_ascii_digit()).collect(); - if cleaned.starts_with("237") && cleaned.len() > 3 { - cleaned = cleaned[3..].to_string(); - } - cleaned - } - - /// Get payout endpoint. - fn payout_url(&self) -> String { - if self.base_url.contains("live.fapshi.com") || self.base_url.contains("api.fapshi.com") { - "https://live.fapshi.com/payout".to_string() - } else { - format!("{}/payout", self.base_url) - } - } - - /// Get status endpoint. - fn status_url(&self, reference: &str) -> String { - if self.base_url.contains("live.fapshi.com") || self.base_url.contains("api.fapshi.com") { - format!("https://api.fapshi.com/payment-status/{}", reference) - } else { - format!("{}/payment-status/{}", self.base_url, reference) - } - } - - /// Checks if a payout has already been processed successfully on Fapshi by searching by externalId. - pub async fn check_existing_payout( - &self, - pool: &PgPool, - external_id: &str, - ) -> Result, AppError> { - let api_user = Self::get_secret(pool, "fapshi_payout_api_user").await?; - let api_key = Self::get_secret(pool, "fapshi_payout_api_key").await?; - - let endpoint = if self.base_url.contains("live.fapshi.com") || self.base_url.contains("api.fapshi.com") { - "https://live.fapshi.com/search".to_string() - } else { - format!("{}/search", self.base_url) - }; - - let response = self.http.get(&endpoint) - .header("apiuser", &api_user) - .header("apikey", &api_key) - .query(&[("externalId", external_id)]) - .send() - .await; - - match response { - Ok(res) => { - let status = res.status().as_u16(); - if status == 200 { - let items: Vec = res.json().await?; - for item in items { - if let Some(ref ext_id) = item.external_id { - if ext_id == external_id { - let status_upper = item.status.to_uppercase(); - if status_upper == "SUCCESSFUL" || status_upper == "SUCCESS" { - return Ok(Some(item.trans_id)); - } - } - } - } - Ok(None) - } else { - let body_text = res.text().await.unwrap_or_else(|_| "No body".to_string()); - Err(AppError::Fapshi(format!("Fapshi search returned status {}: {}", status, body_text))) - } - } - Err(e) => { - Err(AppError::Reqwest(e)) - } - } - } - - /// Performs payout via Fapshi and logs to fapshi_audit_logs. - #[allow(clippy::too_many_arguments)] - pub async fn execute_payout( - &self, - pool: &PgPool, - tx_id: Uuid, - amount: f64, - phone: &str, - seller_name: &str, - external_id: &str, - listing_id: Uuid, - ) -> Result { - let api_user = Self::get_secret(pool, "fapshi_payout_api_user").await?; - let api_key = Self::get_secret(pool, "fapshi_payout_api_key").await?; - - let cleaned_phone = Self::clean_phone(phone); - let request_payload = PayoutPayload { - amount, - phone: cleaned_phone, - name: seller_name.to_string(), - external_id: external_id.to_string(), - message: format!("BookBridge escrow payout for listing {}", listing_id), - }; - - let endpoint = self.payout_url(); - let req_val = serde_json::to_value(&request_payload)?; - - let response = self.http.post(&endpoint) - .header("Content-Type", "application/json") - .header("apiuser", &api_user) - .header("apikey", &api_key) - .json(&request_payload) - .send() - .await; - - let (status_code, resp_val, result) = match response { - Ok(res) => { - let status = res.status().as_u16(); - let body_res: Result = res.json().await; - match body_res { - Ok(body) => { - let val = serde_json::to_value(&body)?; - if status == 200 && body.status_code == 200 { - if let Some(trans_id) = body.trans_id { - (status, val, Ok(trans_id)) - } else { - (status, val, Err(AppError::Fapshi("Payout succeeded but transId was missing".to_string()))) - } - } else { - let msg = body.message.unwrap_or_else(|| "Unknown Fapshi payout error".to_string()); - (status, val, Err(AppError::Fapshi(format!("Fapshi payout error ({}): {}", body.status_code, msg)))) - } - } - Err(e) => { - let err_msg = format!("JSON decode error: {}", e); - (status, serde_json::json!({ "error": err_msg }), Err(AppError::Reqwest(e))) - } - } - } - Err(e) => { - let err_msg = format!("Network error: {}", e); - (500, serde_json::json!({ "error": err_msg }), Err(AppError::Reqwest(e))) - } - }; - - // Write to fapshi_audit_logs (gracefully catch errors to prevent double-payouts on db connection drops) - if let Err(e) = sqlx::query( - "INSERT INTO fapshi_audit_logs (transaction_id, endpoint, request_payload, response_payload, status_code) VALUES ($1, $2, $3, $4, $5)" - ) - .bind(tx_id) - .bind(&endpoint) - .bind(req_val) - .bind(resp_val) - .bind(status_code as i32) - .execute(pool) - .await { - tracing::error!("Failed to write to fapshi_audit_logs during payout: {:?}", e); - } - - result - } - - /// Queries Fapshi status for polling stuck transactions. - pub async fn poll_payment_status( - &self, - pool: &PgPool, - tx_id: Uuid, - reference: &str, - ) -> Result { - let api_user = Self::get_secret(pool, "fapshi_api_user").await?; - let api_key = Self::get_secret(pool, "fapshi_api_key").await?; - - let endpoint = self.status_url(reference); - - let response = self.http.get(&endpoint) - .header("apiuser", &api_user) - .header("apikey", &api_key) - .send() - .await; - - let (status_code, resp_val, result) = match response { - Ok(res) => { - let status = res.status().as_u16(); - let body_res: Result = res.json().await; - match body_res { - Ok(body) => { - let val = serde_json::to_value(&body)?; - if status == 200 { - let fapshi_status = body.status.or(body.state).unwrap_or_default(); - (status, val, Ok(fapshi_status)) - } else { - let msg = body.message.unwrap_or_else(|| "Unknown status check error".to_string()); - (status, val, Err(AppError::Fapshi(format!("Fapshi poll error ({}): {}", status, msg)))) - } - } - Err(e) => { - let err_msg = format!("JSON decode error: {}", e); - (status, serde_json::json!({ "error": err_msg }), Err(AppError::Reqwest(e))) - } - } - } - Err(e) => { - let err_msg = format!("Network error: {}", e); - (500, serde_json::json!({ "error": err_msg }), Err(AppError::Reqwest(e))) - } - }; - - // Write to fapshi_audit_logs for audit trail (gracefully log errors) - if let Err(e) = sqlx::query( - "INSERT INTO fapshi_audit_logs (transaction_id, endpoint, request_payload, response_payload, status_code) VALUES ($1, $2, $3, $4, $5)" - ) - .bind(tx_id) - .bind(&endpoint) - .bind(serde_json::Value::Null) - .bind(resp_val) - .bind(status_code as i32) - .execute(pool) - .await { - tracing::error!("Failed to write to fapshi_audit_logs during polling: {:?}", e); - } - - result - } -} diff --git a/bookbridge-rust-core/src/lib.rs b/bookbridge-rust-core/src/lib.rs deleted file mode 100644 index 6afd929..0000000 --- a/bookbridge-rust-core/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -pub mod auth; -pub mod config; -pub mod error; -pub mod fapshi; -pub mod routes; - -use std::sync::Arc; -use std::sync::Mutex; -use std::collections::HashSet; -use uuid::Uuid; -use sqlx::PgPool; - -#[derive(Clone)] -pub struct AppState { - pub pool: PgPool, - pub in_progress_payouts: Arc>>, - pub fapshi_base_url: String, -} diff --git a/bookbridge-rust-core/src/main.rs b/bookbridge-rust-core/src/main.rs deleted file mode 100644 index 0108e86..0000000 --- a/bookbridge-rust-core/src/main.rs +++ /dev/null @@ -1,69 +0,0 @@ -use axum::{ - middleware, - routing::{get, post}, - Router, -}; -use sqlx::PgPool; -use std::sync::Arc; -use tokio::net::TcpListener; - -use bookbridge_rust_core::config::AppConfig; -use bookbridge_rust_core::routes::health::health_handler; -use bookbridge_rust_core::routes::escrow::{process_releases_handler, poll_pending_handler}; -use bookbridge_rust_core::routes::webhook::fapshi_webhook_handler; -use bookbridge_rust_core::auth::require_internal_auth; -use bookbridge_rust_core::AppState; - -#[tokio::main] -async fn main() -> Result<(), Box> { - // Initialize tracing subscriber - let _ = tracing_subscriber::fmt() - .with_env_filter( - std::env::var("RUST_LOG") - .unwrap_or_else(|_| "info,bookbridge_rust_core=debug".into()), - ) - .try_init(); - - tracing::info!("Initializing bookbridge-rust-core on Render..."); - - // 1. Load config - let config = AppConfig::load()?; - let shared_config = Arc::new(config.clone()); - - // 2. Setup database connection pool - let pool = PgPool::connect(&config.database_url).await?; - - - - // 3. Setup AppState - let state = AppState { - pool, - in_progress_payouts: Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())), - fapshi_base_url: config.fapshi_base_url, - }; - - // 4. Build authenticated routes - let internal_routes = Router::new() - .route("/escrow/process-releases", post(process_releases_handler)) - .route("/escrow/poll-pending", post(poll_pending_handler)) - .layer(middleware::from_fn_with_state( - shared_config.clone(), - require_internal_auth, - )) - .with_state(state.clone()); - - // 5. Main router (health and webhooks are public / custom authenticated) - let app = Router::new() - .route("/health", get(health_handler)) - .route("/webhooks/fapshi", post(fapshi_webhook_handler)) - .nest("/internal", internal_routes) - .with_state(state); - - // 6. Run Axum server using standard TcpListener - let addr = format!("0.0.0.0:{}", config.port); - tracing::info!("Starting Axum server on {}", addr); - let listener = TcpListener::bind(&addr).await?; - axum::serve(listener, app).await?; - - Ok(()) -} diff --git a/bookbridge-rust-core/src/routes/escrow.rs b/bookbridge-rust-core/src/routes/escrow.rs deleted file mode 100644 index c1eb786..0000000 --- a/bookbridge-rust-core/src/routes/escrow.rs +++ /dev/null @@ -1,392 +0,0 @@ -use axum::{extract::State, Json, response::IntoResponse}; -use serde::Serialize; -use crate::error::AppError; -use crate::fapshi::FapshiClient; -use crate::AppState; -use sqlx::Row; -use sqlx::PgConnection; -use uuid::Uuid; -use chrono::Utc; - -#[derive(Serialize)] -pub struct ProcessReleasesResponse { - pub processed: usize, - pub released: usize, - pub failed: usize, - pub results: Vec, -} - -#[derive(Serialize)] -pub struct ReleaseResult { - pub transaction_id: Uuid, - pub ok: bool, - pub error: Option, -} - -#[derive(Serialize)] -pub struct PollPendingResponse { - pub processed: usize, - pub results: Vec, -} - -#[derive(Serialize)] -pub struct PollResult { - pub id: Uuid, - pub reference: String, - pub status: String, - pub error: Option, -} - -/// Shared helper function to process all database side effects for a successful book purchase. -/// This includes marking the listing as sold, setting the transaction to 'held', upserting the escrow row, -/// and creating the initial buyer/seller chat message. -pub async fn handle_purchase_success_db( - conn: &mut PgConnection, - listing_id: Uuid, - buyer_id: Uuid, - seller_id: Uuid, - tx_id: Uuid, - now: chrono::DateTime, -) -> Result<(), AppError> { - // 1. Update listing status to 'sold' - sqlx::query("UPDATE listings SET status = 'sold' WHERE id = $1") - .bind(listing_id) - .execute(&mut *conn) - .await?; - - // 2. Update transaction status to 'held' - sqlx::query("UPDATE transactions SET status = 'held' WHERE id = $1") - .bind(tx_id) - .execute(&mut *conn) - .await?; - - // 3. Upsert escrow transaction atomically - sqlx::query( - "INSERT INTO escrow_transactions (transaction_id, status, created_at, updated_at) \ - VALUES ($1, 'held', $2, $2) \ - ON CONFLICT (transaction_id) DO UPDATE SET status = 'held', updated_at = $2" - ) - .bind(tx_id) - .bind(now) - .execute(&mut *conn) - .await?; - - // 4. Insert automated buyer/seller chat message - sqlx::query( - "INSERT INTO messages (listing_id, sender_id, receiver_id, content, is_read, created_at) \ - VALUES ($1, $2, $3, $4, false, $5)" - ) - .bind(listing_id) - .bind(buyer_id) - .bind(seller_id) - .bind("Hello! I just purchased this book via BookBridge. When can we meet to complete the exchange?") - .bind(now) - .execute(&mut *conn) - .await?; - - Ok(()) -} - -pub async fn release_escrow( - state: &AppState, - tx_id: Uuid, -) -> Result<(), AppError> { - // 1. Claim in-progress lock to mitigate concurrent execution / double payout risk - { - let mut in_progress = state.in_progress_payouts.lock().map_err(|_| { - AppError::Internal("Failed to acquire in-progress lock".to_string()) - })?; - if in_progress.contains(&tx_id) { - return Err(AppError::BadRequest(format!("Payout for transaction {} is already in progress", tx_id))); - } - in_progress.insert(tx_id); - } - - // Ensure we release lock on exit - struct LockGuard<'a> { - tx_id: Uuid, - in_progress: &'a std::sync::Mutex>, - } - impl<'a> Drop for LockGuard<'a> { - fn drop(&mut self) { - if let Ok(mut lock) = self.in_progress.lock() { - lock.remove(&self.tx_id); - } - } - } - let _guard = LockGuard { - tx_id, - in_progress: &state.in_progress_payouts, - }; - - let fapshi = FapshiClient::new(state.fapshi_base_url.clone()); - - // 2. Fetch transaction and escrow status to ensure both are in 'held' status - let tx_row = sqlx::query( - "SELECT t.listing_id, t.seller_id, t.amount, t.commission_amount, t.payment_reference, \ - t.status as tx_status, e.status as escrow_status \ - FROM transactions t \ - JOIN escrow_transactions e ON t.id = e.transaction_id \ - WHERE t.id = $1" - ) - .bind(tx_id) - .fetch_optional(&state.pool) - .await?; - - let tx = match tx_row { - Some(row) => { - let tx_status: String = row.get("tx_status"); - let escrow_status: String = row.get("escrow_status"); - if tx_status != "held" || escrow_status != "held" { - return Err(AppError::BadRequest(format!( - "Transaction or Escrow {} is not in 'held' status (tx: {}, escrow: {})", - tx_id, tx_status, escrow_status - ))); - } - row - } - None => return Err(AppError::BadRequest(format!("Transaction {} not found", tx_id))), - }; - - let listing_id: Uuid = tx.get("listing_id"); - let seller_id: Uuid = tx.get("seller_id"); - let amount: f64 = tx.get("amount"); - let commission_amount: Option = tx.get("commission_amount"); - let payment_reference: String = tx.get("payment_reference"); - - // 3. Fetch seller profile - let seller_row = sqlx::query( - "SELECT whatsapp_number, full_name FROM profiles WHERE id = $1" - ) - .bind(seller_id) - .fetch_optional(&state.pool) - .await?; - - let seller = match seller_row { - Some(row) => row, - None => return Err(AppError::BadRequest(format!("Seller profile {} not found", seller_id))), - }; - - let whatsapp_number: Option = seller.get("whatsapp_number"); - let full_name: Option = seller.get("full_name"); - - let phone = match whatsapp_number { - Some(num) if !num.trim().is_empty() => num, - _ => return Err(AppError::BadRequest("Seller has no payout number configured".to_string())), - }; - - let payout_amount = amount - commission_amount.unwrap_or(0.0); - let external_id = format!("escrow_payout_{}", payment_reference); - let seller_name_str = full_name.unwrap_or_else(|| "BookBridge Seller".to_string()); - - // Check if payout was already executed successfully on Fapshi in a previous run - let mut trans_id = match fapshi.check_existing_payout(&state.pool, &external_id).await { - Ok(Some(tid)) => { - tracing::info!("Payout for transaction {} already completed on Fapshi (transId: {}). Skipping payout call.", tx_id, tid); - Some(tid) - } - Ok(None) => None, // Safe to proceed with payout - Err(e) => { - tracing::error!("Could not verify prior payout status for tx {}: {:?}. Aborting to prevent potential double-payout.", tx_id, e); - return Err(e); // Fail closed - let the next cron run retry - } - }; - - // If not already processed, execute payout - if trans_id.is_none() { - let tid = fapshi.execute_payout( - &state.pool, - tx_id, - payout_amount, - &phone, - &seller_name_str, - &external_id, - listing_id, - ) - .await?; - trans_id = Some(tid); - } - - let trans_id_str = trans_id.unwrap_or_default(); - - // 5. Update databases only after payout is verified successful - let now = Utc::now(); - let mut transaction = state.pool.begin().await?; - - sqlx::query( - "UPDATE escrow_transactions SET status = 'released', updated_at = $1 \ - WHERE transaction_id = $2 AND status = 'held'" - ) - .bind(now) - .bind(tx_id) - .execute(&mut *transaction) - .await?; - - sqlx::query( - "UPDATE transactions SET status = 'successful', payout_status = 'successful', payout_reference = $1 \ - WHERE id = $2" - ) - .bind(&trans_id_str) - .bind(tx_id) - .execute(&mut *transaction) - .await?; - - transaction.commit().await?; - - Ok(()) -} - -pub async fn process_releases_handler( - State(state): State, -) -> Result { - let now = Utc::now(); - - // Find all held escrows whose release_deadline has passed - let expired_rows = sqlx::query( - "SELECT transaction_id FROM escrow_transactions \ - WHERE status = 'held' AND release_deadline <= $1" - ) - .bind(now) - .fetch_all(&state.pool) - .await?; - - let mut results = Vec::new(); - let mut released_count = 0; - let mut failed_count = 0; - - for row in expired_rows { - let tx_id: Uuid = row.get("transaction_id"); - match release_escrow(&state, tx_id).await { - Ok(_) => { - released_count += 1; - results.push(ReleaseResult { - transaction_id: tx_id, - ok: true, - error: None, - }); - } - Err(e) => { - failed_count += 1; - let err_str = e.to_string(); - tracing::error!("Failed to auto-release escrow {}: {}", tx_id, err_str); - results.push(ReleaseResult { - transaction_id: tx_id, - ok: false, - error: Some(err_str), - }); - } - } - } - - Ok(Json(ProcessReleasesResponse { - processed: results.len(), - released: released_count, - failed: failed_count, - results, - })) -} - -pub async fn poll_pending_handler( - State(state): State, -) -> Result { - let fapshi = FapshiClient::new(state.fapshi_base_url.clone()); - - // Find all transactions stuck in pending_payment for more than 10 minutes - let ten_minutes_ago = Utc::now() - chrono::Duration::minutes(10); - - let txs = sqlx::query( - "SELECT id, payment_reference, listing_id, buyer_id, seller_id, amount \ - FROM transactions \ - WHERE status = 'pending_payment' AND created_at <= $1" - ) - .bind(ten_minutes_ago) - .fetch_all(&state.pool) - .await?; - - let mut results = Vec::new(); - - for row in txs { - let tx_id: Uuid = row.get("id"); - let reference: String = row.get("payment_reference"); - let listing_id: Uuid = row.get("listing_id"); - let buyer_id: Uuid = row.get("buyer_id"); - let seller_id: Uuid = row.get("seller_id"); - - tracing::info!("Checking Fapshi status for transaction ref: {}", reference); - - match fapshi.poll_payment_status(&state.pool, tx_id, &reference).await { - Ok(fapshi_status) => { - let status_upper = fapshi_status.to_uppercase(); - if status_upper == "SUCCESSFUL" || status_upper == "SUCCESS" { - // Process successful payment - let now = Utc::now(); - let mut transaction = state.pool.begin().await?; - - if let Err(e) = handle_purchase_success_db( - &mut transaction, - listing_id, - buyer_id, - seller_id, - tx_id, - now, - ).await { - tracing::error!("Error writing successful purchase updates to DB during poll: {:?}", e); - transaction.rollback().await?; - results.push(PollResult { - id: tx_id, - reference: reference.clone(), - status: "error".to_string(), - error: Some(e.to_string()), - }); - continue; - } - - transaction.commit().await?; - - results.push(PollResult { - id: tx_id, - reference: reference.clone(), - status: "held".to_string(), - error: None, - }); - } else if status_upper == "FAILED" || status_upper == "EXPIRED" { - // Mark transaction as failed - sqlx::query("UPDATE transactions SET status = 'failed' WHERE id = $1") - .bind(tx_id) - .execute(&state.pool) - .await?; - - results.push(PollResult { - id: tx_id, - reference: reference.clone(), - status: "failed".to_string(), - error: None, - }); - } else { - // Still pending/created - results.push(PollResult { - id: tx_id, - reference: reference.clone(), - status: fapshi_status.clone(), - error: None, - }); - } - } - Err(e) => { - let err_str = e.to_string(); - tracing::error!("Error polling transaction status for {}: {}", reference, err_str); - results.push(PollResult { - id: tx_id, - reference: reference.clone(), - status: "error".to_string(), - error: Some(err_str), - }); - } - } - } - - Ok(Json(PollPendingResponse { - processed: results.len(), - results, - })) -} diff --git a/bookbridge-rust-core/src/routes/health.rs b/bookbridge-rust-core/src/routes/health.rs deleted file mode 100644 index 2858b7b..0000000 --- a/bookbridge-rust-core/src/routes/health.rs +++ /dev/null @@ -1,6 +0,0 @@ -use axum::{response::IntoResponse, Json}; -use serde_json::json; - -pub async fn health_handler() -> impl IntoResponse { - Json(json!({ "status": "ok" })) -} diff --git a/bookbridge-rust-core/src/routes/mod.rs b/bookbridge-rust-core/src/routes/mod.rs deleted file mode 100644 index 8ac07fe..0000000 --- a/bookbridge-rust-core/src/routes/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod escrow; -pub mod health; -pub mod webhook; diff --git a/bookbridge-rust-core/src/routes/webhook.rs b/bookbridge-rust-core/src/routes/webhook.rs deleted file mode 100644 index a8b440f..0000000 --- a/bookbridge-rust-core/src/routes/webhook.rs +++ /dev/null @@ -1,372 +0,0 @@ -use axum::{extract::State, http::HeaderMap, response::IntoResponse, Json}; -use serde::{Deserialize, Serialize}; -use crate::error::AppError; -use crate::auth::constant_time_compare; -use crate::routes::escrow::handle_purchase_success_db; -use crate::AppState; -use sqlx::{PgPool, Row}; -use uuid::Uuid; -use chrono::Utc; - -#[derive(Deserialize, Serialize, Debug)] -pub struct FapshiWebhookPayload { - pub status: Option, - pub state: Option, - #[serde(rename = "transId")] - pub trans_id: Option, - #[serde(rename = "transactionId")] - pub transaction_id: Option, - pub id: Option, - pub amount: Option, - #[serde(rename = "externalId")] - pub external_id: Option, - #[serde(rename = "externalReference")] - pub external_reference: Option, - pub custom: Option, -} - -async fn verify_webhook_auth(pool: &PgPool, headers: &HeaderMap) -> Result<(), AppError> { - // A. Check x-wh-secret header (dashboard verification key) - if let Some(wh_secret_header) = headers.get("x-wh-secret").and_then(|h| h.to_str().ok()) { - let secret_row: Option<(String,)> = sqlx::query_as("SELECT value FROM app_secrets WHERE key = 'fapshi_webhook_secret'") - .fetch_optional(pool) - .await?; - - if let Some(row) = secret_row { - if constant_time_compare(wh_secret_header.as_bytes(), row.0.as_bytes()) { - return Ok(()); - } - } - } - - - - - - let mut headers_debug = std::collections::HashMap::new(); - for (k, v) in headers.iter() { - if let Ok(val) = v.to_str() { - // Mask the actual sensitive values but show prefix/length for verification - let masked = if val.len() > 8 { - format!("{}... (len: {})", &val[..8], val.len()) - } else { - "***".to_string() - }; - headers_debug.insert(k.as_str().to_string(), masked); - } - } - tracing::warn!("Unauthorized webhook request: invalid credentials or headers. Received headers: {:?}", headers_debug); - Err(AppError::Unauthorized("Invalid authentication credentials".to_string())) -} - -fn parse_purchase_ref(external_ref: &str) -> Result<(Uuid, Uuid), AppError> { - let separator = if external_ref.contains(':') { ':' } else { '_' }; - let parts: Vec<&str> = external_ref.split(separator).collect(); - if parts.len() < 3 { - return Err(AppError::BadRequest("Malformed external_reference for purchase".to_string())); - } - let listing_id = Uuid::parse_str(parts[1]) - .map_err(|_| AppError::BadRequest("Invalid listing_id UUID".to_string()))?; - let buyer_id = Uuid::parse_str(parts[2]) - .map_err(|_| AppError::BadRequest("Invalid buyer_id UUID".to_string()))?; - Ok((listing_id, buyer_id)) -} - -fn parse_boost_ref(external_ref: &str) -> Result<(Uuid, i32), AppError> { - let separator = if external_ref.contains(':') { ':' } else { '_' }; - let parts: Vec<&str> = external_ref.split(separator).collect(); - if parts.len() < 2 { - return Err(AppError::BadRequest("Malformed external_reference for boost".to_string())); - } - let listing_id = Uuid::parse_str(parts[1]) - .map_err(|_| AppError::BadRequest("Invalid listing_id UUID".to_string()))?; - let duration = if parts.len() >= 3 { - parts[2].parse::().unwrap_or(7) - } else { - 7 - }; - Ok((listing_id, duration)) -} - -fn parse_donation_ref(external_ref: &str) -> Result, AppError> { - let separator = if external_ref.contains(':') { ':' } else { '_' }; - let parts: Vec<&str> = external_ref.split(separator).collect(); - if parts.len() < 2 { - return Err(AppError::BadRequest("Malformed external_reference for donation".to_string())); - } - let user_id_str = parts[1]; - if user_id_str == "anonymous" { - Ok(None) - } else { - let uid = Uuid::parse_str(user_id_str) - .map_err(|_| AppError::BadRequest("Invalid user_id UUID".to_string()))?; - Ok(Some(uid)) - } -} - -async fn handle_boost_success( - pool: &PgPool, - reference: &str, - amount: f64, - external_ref: &str, -) -> Result<(), AppError> { - let (listing_id, duration) = parse_boost_ref(external_ref)?; - - let listing_row = sqlx::query("SELECT seller_id FROM listings WHERE id = $1") - .bind(listing_id) - .fetch_optional(pool) - .await?; - - let listing = match listing_row { - Some(row) => row, - None => return Err(AppError::BadRequest(format!("Listing {} not found for boost", listing_id))), - }; - let user_id: Uuid = listing.get("seller_id"); - - let now = Utc::now(); - let rows_affected = sqlx::query( - "INSERT INTO boost_payments ( \ - payment_reference, listing_id, user_id, amount, duration_days, status, created_at \ - ) \ - VALUES ($1, $2, $3, $4, $5, 'successful', $6) \ - ON CONFLICT (payment_reference) DO NOTHING" - ) - .bind(reference) - .bind(listing_id) - .bind(user_id) - .bind(amount) - .bind(duration) - .bind(now) - .execute(pool) - .await? - .rows_affected(); - - if rows_affected == 0 { - tracing::info!("Boost payment with reference {} already recorded. Skipping listing boost update.", reference); - return Ok(()); - } - - let expires_at = now + chrono::Duration::days(duration as i64); - sqlx::query( - "UPDATE listings SET is_boosted = true, boost_expires_at = $1 WHERE id = $2" - ) - .bind(expires_at) - .bind(listing_id) - .execute(pool) - .await?; - - Ok(()) -} - -async fn handle_donation_success( - pool: &PgPool, - reference: &str, - amount: f64, - external_ref: &str, -) -> Result<(), AppError> { - let user_id = parse_donation_ref(external_ref)?; - let now = Utc::now(); - - sqlx::query( - "INSERT INTO donations (payment_reference, user_id, amount, status, created_at) \ - VALUES ($1, $2, $3, 'successful', $4) \ - ON CONFLICT (payment_reference) DO NOTHING" - ) - .bind(reference) - .bind(user_id) - .bind(amount) - .bind(now) - .execute(pool) - .await?; - - Ok(()) -} - -async fn handle_purchase_success( - state: &AppState, - reference: &str, - amount: f64, - external_ref: &str, -) -> Result<(), AppError> { - let (listing_id, buyer_id) = parse_purchase_ref(external_ref)?; - - let listing_row = sqlx::query("SELECT seller_id FROM listings WHERE id = $1") - .bind(listing_id) - .fetch_optional(&state.pool) - .await?; - - let listing = match listing_row { - Some(row) => row, - None => return Err(AppError::BadRequest(format!("Listing {} not found for purchase", listing_id))), - }; - let seller_id: Uuid = listing.get("seller_id"); - - let payout_amount = (amount * 0.95).floor(); - let commission_amount = amount - payout_amount; - - let now = Utc::now(); - - // Atomically upsert the transaction using ON CONFLICT DO UPDATE. - // If the transaction status is already 'held' or 'successful' (completed), the DO UPDATE will fail the WHERE clause - // and return 0 rows. - let tx_row = sqlx::query( - "INSERT INTO transactions ( \ - payment_reference, listing_id, buyer_id, seller_id, amount, status, \ - payout_status, payout_reference, commission_amount, created_at \ - ) \ - VALUES ($1, $2, $3, $4, $5, 'held', 'pending', NULL, $6, $7) \ - ON CONFLICT (payment_reference) \ - DO UPDATE SET \ - status = 'held' \ - WHERE transactions.status = 'pending_payment' \ - RETURNING id" - ) - .bind(reference) - .bind(listing_id) - .bind(buyer_id) - .bind(seller_id) - .bind(amount) - .bind(commission_amount) - .bind(now) - .fetch_optional(&state.pool) - .await?; - - let tx_id = match tx_row { - Some(row) => { - let id: Uuid = row.get("id"); - id - } - None => { - tracing::info!("Transaction with reference {} already claimed or processed. Skipping success logic.", reference); - return Ok(()); - } - }; - - let mut transaction = state.pool.begin().await?; - - if let Err(e) = handle_purchase_success_db( - &mut transaction, - listing_id, - buyer_id, - seller_id, - tx_id, - now, - ).await { - tracing::error!("Error writing webhook purchase success updates: {:?}", e); - transaction.rollback().await?; - return Err(e); - } - - transaction.commit().await?; - - Ok(()) -} - -async fn handle_purchase_pending( - pool: &PgPool, - reference: &str, - amount: f64, - external_ref: &str, -) -> Result<(), AppError> { - let (listing_id, buyer_id) = parse_purchase_ref(external_ref)?; - - let listing_row = sqlx::query("SELECT seller_id FROM listings WHERE id = $1") - .bind(listing_id) - .fetch_optional(pool) - .await?; - - let listing = match listing_row { - Some(row) => row, - None => return Err(AppError::BadRequest(format!("Listing {} not found", listing_id))), - }; - let seller_id: Uuid = listing.get("seller_id"); - - let payout_amount = (amount * 0.95).floor(); - let commission_amount = amount - payout_amount; - - let now = Utc::now(); - sqlx::query( - "INSERT INTO transactions ( \ - payment_reference, listing_id, buyer_id, seller_id, amount, status, \ - payout_status, payout_reference, commission_amount, created_at \ - ) \ - VALUES ($1, $2, $3, $4, $5, 'pending_payment', 'pending', NULL, $6, $7) \ - ON CONFLICT (payment_reference) \ - DO UPDATE SET status = 'pending_payment' \ - WHERE transactions.status = 'pending_payment'" - ) - .bind(reference) - .bind(listing_id) - .bind(buyer_id) - .bind(seller_id) - .bind(amount) - .bind(commission_amount) - .bind(now) - .execute(pool) - .await?; - - Ok(()) -} - -pub async fn fapshi_webhook_handler( - State(state): State, - headers: HeaderMap, - Json(body): Json, -) -> Result { - verify_webhook_auth(&state.pool, &headers).await?; - - let payload_val = if let Some(arr) = body.as_array() { - if arr.is_empty() { - return Err(AppError::BadRequest("Empty payload array".to_string())); - } - &arr[0] - } else if body.is_object() { - &body - } else { - return Err(AppError::BadRequest("Invalid webhook payload format".to_string())); - }; - - let payload: FapshiWebhookPayload = serde_json::from_value(payload_val.clone()) - .map_err(|e| AppError::BadRequest(format!("Failed to parse webhook payload: {}", e)))?; - - let status = payload.status.or(payload.state).ok_or_else(|| AppError::BadRequest("Missing status/state field".to_string()))?; - let reference = payload.trans_id.or(payload.transaction_id).or(payload.id).ok_or_else(|| AppError::BadRequest("Missing transId/id reference field".to_string()))?; - let amount_val = payload.amount.ok_or_else(|| AppError::BadRequest("Missing amount field".to_string()))?; - let external_reference = payload.external_id.or(payload.external_reference).or(payload.custom).unwrap_or_default(); - - let amount = match amount_val { - serde_json::Value::Number(num) => num.as_f64().unwrap_or(0.0), - serde_json::Value::String(s) => s.parse::().unwrap_or(0.0), - _ => return Err(AppError::BadRequest("Invalid amount type".to_string())), - }; - - let status_upper = status.to_uppercase(); - - tracing::info!("Processing incoming Fapshi webhook for ref: {}, status: {}, external: {}", reference, status_upper, external_reference); - - if status_upper == "SUCCESSFUL" || status_upper == "SUCCESS" { - if external_reference.starts_with("boost:") || external_reference.starts_with("boost_") { - handle_boost_success(&state.pool, &reference, amount, &external_reference).await?; - } else if external_reference.starts_with("donation:") || external_reference.starts_with("donation_") { - handle_donation_success(&state.pool, &reference, amount, &external_reference).await?; - } else if external_reference.starts_with("purchase:") || external_reference.starts_with("purchase_") { - handle_purchase_success(&state, &reference, amount, &external_reference).await?; - } else { - tracing::warn!("Unknown externalReference: {}", external_reference); - } - } else if (status_upper == "CREATED" || status_upper == "PENDING" || status_upper == "PENDING_PAYMENT") - && (external_reference.starts_with("purchase:") || external_reference.starts_with("purchase_")) - { - handle_purchase_pending(&state.pool, &reference, amount, &external_reference).await?; - } else if (status_upper == "FAILED" || status_upper == "EXPIRED") - && (external_reference.starts_with("purchase:") || external_reference.starts_with("purchase_")) - { - tracing::info!("Marking transaction failed: Ref={}", reference); - sqlx::query("UPDATE transactions SET status = 'failed' WHERE payment_reference = $1") - .bind(&reference) - .execute(&state.pool) - .await?; - } - - Ok((axum::http::StatusCode::OK, Json(serde_json::json!({ "success": true })))) -} diff --git a/bookbridge-rust-core/tests/auth_tests.rs b/bookbridge-rust-core/tests/auth_tests.rs deleted file mode 100644 index 710c1bc..0000000 --- a/bookbridge-rust-core/tests/auth_tests.rs +++ /dev/null @@ -1,56 +0,0 @@ -use axum::{ - body::Body, - http::{Request, StatusCode}, - middleware, - routing::get, - Router, -}; -use std::sync::Arc; -use tower::ServiceExt; -use bookbridge_rust_core::{ - auth::require_internal_auth, - config::AppConfig, -}; - -#[tokio::test] -async fn test_auth_middleware() { - let config = Arc::new(AppConfig { - database_url: "postgres://dummy".to_string(), - internal_api_secret: "super_secret_key_12345".to_string(), - fapshi_base_url: "https://sandbox.fapshi.com".to_string(), - port: 8080, - }); - - let app = Router::new() - .route("/test", get(|| async { "ok" })) - .layer(middleware::from_fn_with_state( - config.clone(), - require_internal_auth, - )); - - // Case 1: Missing Header - let req = Request::builder() - .uri("/test") - .body(Body::empty()) - .unwrap(); - let response = app.clone().oneshot(req).await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - - // Case 2: Incorrect Header Value - let req = Request::builder() - .uri("/test") - .header("X-Internal-Secret", "wrong_secret") - .body(Body::empty()) - .unwrap(); - let response = app.clone().oneshot(req).await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - - // Case 3: Correct Header Value - let req = Request::builder() - .uri("/test") - .header("X-Internal-Secret", "super_secret_key_12345") - .body(Body::empty()) - .unwrap(); - let response = app.clone().oneshot(req).await.unwrap(); - assert_eq!(response.status(), StatusCode::OK); -} diff --git a/bookbridge-rust-core/tests/escrow_tests.rs b/bookbridge-rust-core/tests/escrow_tests.rs deleted file mode 100644 index ba91613..0000000 --- a/bookbridge-rust-core/tests/escrow_tests.rs +++ /dev/null @@ -1,294 +0,0 @@ -use bookbridge_rust_core::fapshi::FapshiClient; -use bookbridge_rust_core::routes::escrow::release_escrow; -use bookbridge_rust_core::AppState; -use sqlx::PgPool; -use uuid::Uuid; -use std::sync::Arc; -use std::sync::Mutex; -use std::collections::HashSet; - -#[test] -fn test_clean_phone() { - assert_eq!(FapshiClient::clean_phone("+237 677 889 900"), "677889900"); - assert_eq!(FapshiClient::clean_phone("655 444 333"), "655444333"); - assert_eq!(FapshiClient::clean_phone("237699111222"), "699111222"); - assert_eq!(FapshiClient::clean_phone("12345"), "12345"); - assert_eq!(FapshiClient::clean_phone(""), ""); -} - -#[tokio::test] -async fn test_release_escrow_db_integration() -> Result<(), Box> { - let _ = dotenvy::dotenv(); - - // Safety check: only run on explicit local development/test databases. - // Invert the skip check to guarantee we NEVER run against production databases like Supabase. - let db_url = match std::env::var("DATABASE_URL") { - Ok(url) if (url.contains("localhost") || url.contains("127.0.0.1") || url.contains("test") || url.contains("dev")) - && !url.contains("pooler.supabase.com") - && !url.contains("db.jacnsvcwmhoicuuzmrmr") => url, - _ => { - println!("Skipping DB integration test (requires a local/test DATABASE_URL, skipping production settings)"); - return Ok(()); - } - }; - - let pool = match PgPool::connect(&db_url).await { - Ok(p) => p, - Err(e) => { - println!("Skipping DB integration test (database connection failed: {})", e); - return Ok(()); - } - }; - - // Run setup within a transaction - let mut tx = pool.begin().await?; - - let seller_id = Uuid::new_v4(); - let buyer_id = Uuid::new_v4(); - let listing_id = Uuid::new_v4(); - let transaction_id = Uuid::new_v4(); - - // Insert mock auth users to satisfy foreign key constraints if possible. - let user_setup = sqlx::query( - "INSERT INTO auth.users (id, email) VALUES ($1, $2), ($3, $4)" - ) - .bind(seller_id) - .bind("seller@example.com") - .bind(buyer_id) - .bind("buyer@example.com") - .execute(&mut *tx) - .await; - - if let Err(e) = user_setup { - println!("Skipping query execution check (cannot insert dummy auth.users: {}).", e); - tx.rollback().await?; - return Ok(()); - } - - // Insert profile details - sqlx::query( - "INSERT INTO profiles (id, full_name, whatsapp_number) VALUES ($1, $2, $3)" - ) - .bind(seller_id) - .bind("Integration Test Seller") - .bind("+237677777777") - .execute(&mut *tx) - .await?; - - // Insert listing details - sqlx::query( - "INSERT INTO listings (id, seller_id, title, status) VALUES ($1, $2, $3, $4)" - ) - .bind(listing_id) - .bind(seller_id) - .bind("Integration Test Book") - .bind("active") - .execute(&mut *tx) - .await?; - - // Insert transactions details (status must be 'held' to match status check constraint & release rule) - sqlx::query( - "INSERT INTO transactions (id, listing_id, buyer_id, seller_id, amount, commission_amount, payment_reference, status) \ - VALUES ($1, $2, $3, $4, 5000, 250, 'integration_test_ref_99', 'held')" - ) - .bind(transaction_id) - .bind(listing_id) - .bind(buyer_id) - .bind(seller_id) - .execute(&mut *tx) - .await?; - - // Insert escrow transactions details - sqlx::query( - "INSERT INTO escrow_transactions (transaction_id, status) VALUES ($1, 'held')" - ) - .bind(transaction_id) - .execute(&mut *tx) - .await?; - - // Ensure our Fapshi credentials table contains mock values to satisfy lookups - sqlx::query( - "INSERT INTO app_secrets (key, value) VALUES ($1, $2), ($3, $4) \ - ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value" - ) - .bind("fapshi_payout_api_user") - .bind("mock_payout_user") - .bind("fapshi_payout_api_key") - .bind("mock_payout_key") - .execute(&mut *tx) - .await?; - - // Commit the setup transaction so the database pool connections can read it - tx.commit().await?; - - let state = AppState { - pool: pool.clone(), - in_progress_payouts: Arc::new(Mutex::new(HashSet::new())), - fapshi_base_url: "https://sandbox.fapshi.com".to_string(), // Mock sandbox URL for testing - }; - - // Executing the escrow release. - let result = release_escrow(&state, transaction_id).await; - - // Manual cleanup to leave the database clean - let _ = sqlx::query("DELETE FROM app_secrets WHERE key IN ('fapshi_payout_api_user', 'fapshi_payout_api_key')").execute(&pool).await; - let _ = sqlx::query("DELETE FROM escrow_transactions WHERE transaction_id = $1").bind(transaction_id).execute(&pool).await; - let _ = sqlx::query("DELETE FROM transactions WHERE id = $1").bind(transaction_id).execute(&pool).await; - let _ = sqlx::query("DELETE FROM listings WHERE id = $1").bind(listing_id).execute(&pool).await; - let _ = sqlx::query("DELETE FROM profiles WHERE id = $1").bind(seller_id).execute(&pool).await; - let _ = sqlx::query("DELETE FROM auth.users WHERE id IN ($1, $2)").bind(seller_id).bind(buyer_id).execute(&pool).await; - - // Verify database status checks compiled and succeeded by ensuring the execution failed - // ONLY at the network boundary (Reqwest error attempting to reach sandbox.fapshi.com) - match result { - Ok(_) => { - panic!("Expected Fapshi payout network call to fail, but it returned Ok"); - } - Err(bookbridge_rust_core::error::AppError::Reqwest(e)) => { - let err_str = e.to_string(); - println!("Success: Database queries verified successfully up to the network boundary: {}", err_str); - assert!( - err_str.contains("dns") - || err_str.contains("connect") - || err_str.contains("builder") - || err_str.contains("resolve") - ); - } - Err(other) => { - panic!("Escrow release failed at database query/state checking stage before network: {:?}", other); - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_poll_pending_db_integration() -> Result<(), Box> { - let _ = dotenvy::dotenv(); - - // Safety check: skip running against production database strings - let db_url = match std::env::var("DATABASE_URL") { - Ok(url) if (url.contains("localhost") || url.contains("127.0.0.1") || url.contains("test") || url.contains("dev")) - && !url.contains("pooler.supabase.com") - && !url.contains("db.jacnsvcwmhoicuuzmrmr") => url, - _ => { - println!("Skipping DB poll pending integration test (requires a local/test DATABASE_URL)"); - return Ok(()); - } - }; - - let pool = match PgPool::connect(&db_url).await { - Ok(p) => p, - Err(e) => { - println!("Skipping DB poll pending integration test (database connection failed: {})", e); - return Ok(()); - } - }; - - let mut tx = pool.begin().await?; - - let seller_id = Uuid::new_v4(); - let buyer_id = Uuid::new_v4(); - let listing_id = Uuid::new_v4(); - let transaction_id = Uuid::new_v4(); - - // Insert mock auth users - let user_setup = sqlx::query( - "INSERT INTO auth.users (id, email) VALUES ($1, $2), ($3, $4)" - ) - .bind(seller_id) - .bind("seller-poll@example.com") - .bind(buyer_id) - .bind("buyer-poll@example.com") - .execute(&mut *tx) - .await; - - if let Err(e) = user_setup { - println!("Skipping poll pending query check (cannot insert dummy auth.users: {}).", e); - tx.rollback().await?; - return Ok(()); - } - - // Insert profile - sqlx::query( - "INSERT INTO profiles (id, full_name, whatsapp_number) VALUES ($1, $2, $3)" - ) - .bind(seller_id) - .bind("Poll Test Seller") - .bind("+237677777778") - .execute(&mut *tx) - .await?; - - // Insert listing - sqlx::query( - "INSERT INTO listings (id, seller_id, title, status) VALUES ($1, $2, $3, $4)" - ) - .bind(listing_id) - .bind(seller_id) - .bind("Poll Test Book") - .bind("active") - .execute(&mut *tx) - .await?; - - // Insert transaction stuck in pending_payment (using an old date to trigger the 10-minute rule) - let ten_minutes_ago = chrono::Utc::now() - chrono::Duration::minutes(15); - sqlx::query( - "INSERT INTO transactions (id, listing_id, buyer_id, seller_id, amount, commission_amount, payment_reference, status, created_at) \ - VALUES ($1, $2, $3, $4, 5000, 250, 'poll_test_ref_999', 'pending_payment', $5)" - ) - .bind(transaction_id) - .bind(listing_id) - .bind(buyer_id) - .bind(seller_id) - .bind(ten_minutes_ago) - .execute(&mut *tx) - .await?; - - // Mock app secrets for polling - sqlx::query( - "INSERT INTO app_secrets (key, value) VALUES ($1, $2), ($3, $4) \ - ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value" - ) - .bind("fapshi_api_user") - .bind("mock_api_user") - .bind("fapshi_api_key") - .bind("mock_api_key") - .execute(&mut *tx) - .await?; - - // Commit setup transaction so poll_payment_status can read from pool connections - tx.commit().await?; - - let fapshi = FapshiClient::new("https://sandbox.fapshi.com".to_string()); - - // Call status checking. Since we use a mock api key and are offline, this will hit connection/Fapshi error. - let result = fapshi.poll_payment_status(&pool, transaction_id, "poll_test_ref_999").await; - - // Manual cleanup - let _ = sqlx::query("DELETE FROM app_secrets WHERE key IN ('fapshi_api_user', 'fapshi_api_key')").execute(&pool).await; - let _ = sqlx::query("DELETE FROM transactions WHERE id = $1").bind(transaction_id).execute(&pool).await; - let _ = sqlx::query("DELETE FROM listings WHERE id = $1").bind(listing_id).execute(&pool).await; - let _ = sqlx::query("DELETE FROM profiles WHERE id = $1").bind(seller_id).execute(&pool).await; - let _ = sqlx::query("DELETE FROM auth.users WHERE id IN ($1, $2)").bind(seller_id).bind(buyer_id).execute(&pool).await; - - match result { - Ok(status) => { - println!("Status query completed with state: {}", status); - } - Err(bookbridge_rust_core::error::AppError::Reqwest(e)) => { - let err_str = e.to_string(); - println!("Success: Polling DB setup verified up to the network boundary: {}", err_str); - assert!( - err_str.contains("dns") - || err_str.contains("connect") - || err_str.contains("builder") - || err_str.contains("resolve") - ); - } - Err(other) => { - panic!("Polling status check failed during database querying/state validation: {:?}", other); - } - } - - Ok(()) -} diff --git a/bookbridge-rust-core/tests/webhook_tests.rs b/bookbridge-rust-core/tests/webhook_tests.rs deleted file mode 100644 index 5ffcb32..0000000 --- a/bookbridge-rust-core/tests/webhook_tests.rs +++ /dev/null @@ -1,235 +0,0 @@ -use bookbridge_rust_core::routes::webhook::fapshi_webhook_handler; -use bookbridge_rust_core::AppState; -use axum::{routing::post, Router, body::Body, http::{Request, StatusCode}}; -use sqlx::PgPool; -use uuid::Uuid; -use std::sync::Arc; -use std::sync::Mutex; -use std::collections::HashSet; -use tower::ServiceExt; - -#[tokio::test] -async fn test_webhook_integration() -> Result<(), Box> { - let _ = dotenvy::dotenv(); - - // Safety check: only run on local test databases - let db_url = match std::env::var("DATABASE_URL") { - Ok(url) if (url.contains("localhost") || url.contains("127.0.0.1") || url.contains("test") || url.contains("dev")) - && !url.contains("pooler.supabase.com") - && !url.contains("db.jacnsvcwmhoicuuzmrmr") => url, - _ => { - println!("Skipping webhook DB integration tests (non-local DATABASE_URL)"); - return Ok(()); - } - }; - - let pool = match PgPool::connect(&db_url).await { - Ok(p) => p, - Err(e) => { - println!("Skipping webhook DB integration tests (connection failed: {})", e); - return Ok(()); - } - }; - - // Setup mock rows in a transaction, then commit - let mut tx = pool.begin().await?; - - let seller_id = Uuid::new_v4(); - let buyer_id = Uuid::new_v4(); - let listing_id = Uuid::new_v4(); - let transaction_id = Uuid::new_v4(); - let payment_ref = format!("web_test_ref_{}", Uuid::new_v4().to_string().replace("-", "")); - - // Mock auth users - let user_setup = sqlx::query( - "INSERT INTO auth.users (id, email) VALUES ($1, $2), ($3, $4)" - ) - .bind(seller_id) - .bind("wh-seller@example.com") - .bind(buyer_id) - .bind("wh-buyer@example.com") - .execute(&mut *tx) - .await; - - if let Err(e) = user_setup { - println!("Skipping webhook DB check (cannot insert dummy auth.users: {}).", e); - tx.rollback().await?; - return Ok(()); - } - - // Profiles - sqlx::query("INSERT INTO profiles (id, full_name, whatsapp_number) VALUES ($1, $2, $3)") - .bind(seller_id).bind("Webhook Seller").bind("+237612345678") - .execute(&mut *tx).await?; - - // Listing - sqlx::query("INSERT INTO listings (id, seller_id, title, status) VALUES ($1, $2, $3, $4)") - .bind(listing_id).bind(seller_id).bind("Webhook Book").bind("active") - .execute(&mut *tx).await?; - - // Initial Transaction in pending_payment state - sqlx::query( - "INSERT INTO transactions (id, listing_id, buyer_id, seller_id, amount, status, payout_status, payment_reference) \ - VALUES ($1, $2, $3, $4, 5000, 'pending_payment', 'pending', $5)" - ) - .bind(transaction_id) - .bind(listing_id) - .bind(buyer_id) - .bind(seller_id) - .bind(&payment_ref) - .execute(&mut *tx) - .await?; - - // App secrets for webhook authentication - sqlx::query( - "INSERT INTO app_secrets (key, value) VALUES ($1, $2), ($3, $4), ($5, $6) \ - ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value" - ) - .bind("fapshi_webhook_secret").bind("wh_test_key_xyz") - .bind("fapshi_api_user").bind("wh_test_payout_user") - .bind("fapshi_api_key").bind("wh_test_payout_key") - .execute(&mut *tx) - .await?; - - tx.commit().await?; - - // Initialize State - let state = AppState { - pool: pool.clone(), - in_progress_payouts: Arc::new(Mutex::new(HashSet::new())), - fapshi_base_url: "https://sandbox.fapshi.com".to_string(), - }; - - let app = Router::new() - .route("/webhooks/fapshi", post(fapshi_webhook_handler)) - .with_state(state); - - // --- TEST CASE 1: Unauthorized Webhook (Invalid Key) --- - { - let payload = serde_json::json!({ - "status": "SUCCESSFUL", - "transId": "fapshi_tx_111", - "amount": 5000, - "externalId": format!("purchase:{}:{}", listing_id, buyer_id) - }); - - let req = Request::builder() - .method("POST") - .uri("/webhooks/fapshi") - .header("Content-Type", "application/json") - .header("x-wh-secret", "invalid_secret") - .body(Body::from(serde_json::to_string(&payload)?)) - .unwrap(); - - let response = app.clone().oneshot(req).await.unwrap(); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - // --- TEST CASE 2: Malformed Payload (400 Bad Request) --- - { - let req = Request::builder() - .method("POST") - .uri("/webhooks/fapshi") - .header("Content-Type", "application/json") - .header("x-wh-secret", "wh_test_key_xyz") - .body(Body::from("{ malformed json }")) - .unwrap(); - - let response = app.clone().oneshot(req).await.unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - } - - // --- TEST CASE 3: Successful Purchase Webhook --- - { - // Target the standard reference we setup - let mock_payload = serde_json::json!([{ - "status": "SUCCESSFUL", - "transId": &payment_ref, // Use the payment_reference as Fapshi transId - "amount": "5000", - "externalId": format!("purchase:{}:{}", listing_id, buyer_id) - }]); - - let req = Request::builder() - .method("POST") - .uri("/webhooks/fapshi") - .header("Content-Type", "application/json") - .header("x-wh-secret", "wh_test_key_xyz") - .body(Body::from(serde_json::to_string(&mock_payload)?)) - .unwrap(); - - let response = app.clone().oneshot(req).await.unwrap(); - assert_eq!(response.status(), StatusCode::OK); - - // Verify database updates - let tx_state: (String, String) = sqlx::query_as("SELECT status, payout_status FROM transactions WHERE id = $1") - .bind(transaction_id) - .fetch_one(&pool) - .await?; - assert_eq!(tx_state.0, "held"); - assert_eq!(tx_state.1, "pending"); - - let listing_state: (String,) = sqlx::query_as("SELECT status FROM listings WHERE id = $1") - .bind(listing_id) - .fetch_one(&pool) - .await?; - assert_eq!(listing_state.0, "sold"); - - let escrow_count: (i64,) = sqlx::query_as("SELECT count(*) FROM escrow_transactions WHERE transaction_id = $1") - .bind(transaction_id) - .fetch_one(&pool) - .await?; - assert_eq!(escrow_count.0, 1); - - let msg_count: (i64,) = sqlx::query_as("SELECT count(*) FROM messages WHERE listing_id = $1") - .bind(listing_id) - .fetch_one(&pool) - .await?; - assert_eq!(msg_count.0, 1); - } - - // --- TEST CASE 4: Duplicate Webhook Delivery (No-op check) --- - { - // Re-deliver same success payload - let mock_payload = serde_json::json!([{ - "status": "SUCCESSFUL", - "transId": &payment_ref, - "amount": "5000", - "externalId": format!("purchase:{}:{}", listing_id, buyer_id) - }]); - - let req = Request::builder() - .method("POST") - .uri("/webhooks/fapshi") - .header("Content-Type", "application/json") - .header("x-wh-secret", "wh_test_key_xyz") - .body(Body::from(serde_json::to_string(&mock_payload)?)) - .unwrap(); - - let response = app.clone().oneshot(req).await.unwrap(); - assert_eq!(response.status(), StatusCode::OK); - - // Verify no duplicates were created - let escrow_count: (i64,) = sqlx::query_as("SELECT count(*) FROM escrow_transactions WHERE transaction_id = $1") - .bind(transaction_id) - .fetch_one(&pool) - .await?; - assert_eq!(escrow_count.0, 1); // Remains exactly 1 (no duplicate row) - - let msg_count: (i64,) = sqlx::query_as("SELECT count(*) FROM messages WHERE listing_id = $1") - .bind(listing_id) - .fetch_one(&pool) - .await?; - assert_eq!(msg_count.0, 1); // Remains exactly 1 - } - - // Manual database cleanup - let _ = sqlx::query("DELETE FROM app_secrets WHERE key IN ('fapshi_webhook_secret', 'fapshi_api_user', 'fapshi_api_key')").execute(&pool).await; - let _ = sqlx::query("DELETE FROM messages WHERE listing_id = $1").bind(listing_id).execute(&pool).await; - let _ = sqlx::query("DELETE FROM escrow_transactions WHERE transaction_id = $1").bind(transaction_id).execute(&pool).await; - let _ = sqlx::query("DELETE FROM transactions WHERE id = $1").bind(transaction_id).execute(&pool).await; - let _ = sqlx::query("DELETE FROM listings WHERE id = $1").bind(listing_id).execute(&pool).await; - let _ = sqlx::query("DELETE FROM profiles WHERE id = $1").bind(seller_id).execute(&pool).await; - let _ = sqlx::query("DELETE FROM auth.users WHERE id IN ($1, $2)").bind(seller_id).bind(buyer_id).execute(&pool).await; - - Ok(()) -} diff --git a/git_final_out.txt b/git_final_out.txt deleted file mode 100644 index 8b76e05..0000000 --- a/git_final_out.txt +++ /dev/null @@ -1,9 +0,0 @@ -[dev 908a831] feat: implement boost listing priority, visual badge, and fix keyboard overlap in payment sheet - 1 file changed, 0 insertions(+), 0 deletions(-) - create mode 100644 git_final_out.txt -To https://github.com/DCT-Berinyuy/book-bridge.git - f5834ea..908a831 dev -> dev -error: Your local changes to the following files would be overwritten by checkout: - git_final_out.txt -Please commit your changes or stash them before you switch branches. -Aborting diff --git a/git_log_final.txt b/git_log_final.txt deleted file mode 100644 index d53bae2..0000000 --- a/git_log_final.txt +++ /dev/null @@ -1,39 +0,0 @@ -commit c53a46e3a4072d7b10d7429524a87b41d8f7bd3d -Author: DCT-Berinyuy -Date: Sat Feb 28 03:40:25 2026 +0100 - - feat: add Sold and Boosted badges to ListingDetailsScreen - - - Fix missing 'boosted' translation key in ARB files - - Implement _buildSoldBadge and _buildBoostedBadge in ListingDetailsScreen - - Add badges to the header of the details view - -commit b7ccb736ac5e3df203a2861c040f36b687f7d4f3 -Author: DCT-Berinyuy -Date: Sat Feb 28 02:51:21 2026 +0100 - - feat: complete Phase 4 In-App Payments - - - Create and migrate transactions table - - Implement Purchase logic in Svelte webhook to update 'Sold' listing status - - Handle initial automated Buyer<>Seller message creation on successful purchase - - Show localized 'Sold' UI badge and disable buying/chatting - - Added English/French localizations for Sold state - -commit 908a831d591efd7ae60d58739e4e191c708a6f9f -Author: DCT-Berinyuy -Date: Fri Feb 27 18:47:15 2026 +0100 - - feat: implement boost listing priority, visual badge, and fix keyboard overlap in payment sheet - -commit 4e5c7ff0b1aaa3684e5437695cffffce14883f49 -Author: DCT-Berinyuy -Date: Fri Feb 27 18:15:57 2026 +0100 - - feat: implement boost listing priority, visual badge, and fix keyboard overlap in payment sheet - -commit f5834eaaafb6955532b9ad280c925b560cfa4a51 -Author: DCT-Berinyuy -Date: Fri Feb 27 17:29:40 2026 +0100 - - feat: implement in-app chat, unread badge, and buy/pay now buttons diff --git a/git_log_now.txt b/git_log_now.txt deleted file mode 100644 index 7ab8e03..0000000 --- a/git_log_now.txt +++ /dev/null @@ -1,11 +0,0 @@ -commit b7ccb736ac5e3df203a2861c040f36b687f7d4f3 -Author: DCT-Berinyuy -Date: Sat Feb 28 02:51:21 2026 +0100 - - feat: complete Phase 4 In-App Payments - - - Create and migrate transactions table - - Implement Purchase logic in Svelte webhook to update 'Sold' listing status - - Handle initial automated Buyer<>Seller message creation on successful purchase - - Show localized 'Sold' UI badge and disable buying/chatting - - Added English/French localizations for Sold state diff --git a/git_merge_push_log.txt b/git_merge_push_log.txt deleted file mode 100644 index 93013ad..0000000 --- a/git_merge_push_log.txt +++ /dev/null @@ -1,2 +0,0 @@ -To https://github.com/DCT-Berinyuy/book-bridge.git - f335180..f5834ea dev -> dev diff --git a/git_status_debug.txt b/git_status_debug.txt deleted file mode 100644 index d748e31..0000000 --- a/git_status_debug.txt +++ /dev/null @@ -1,25 +0,0 @@ -On branch dev -Your branch is up to date with 'origin/dev'. - -Changes not staged for commit: - (use "git add ..." to update what will be committed) - (use "git restore ..." to discard changes in working directory) - modified: lib/config/router.dart - modified: lib/core/presentation/widgets/scaffold_with_navbar.dart - modified: lib/features/listings/presentation/screens/listing_details_screen.dart - modified: lib/injection_container.dart - modified: lib/l10n/app_en.arb - modified: lib/l10n/app_fr.arb - modified: lib/l10n/app_localizations.dart - modified: lib/l10n/app_localizations_en.dart - modified: lib/l10n/app_localizations_fr.dart - modified: lib/main.dart - modified: pubspec.yaml - -Untracked files: - (use "git add ..." to include in what will be committed) - git_status_debug.txt - lib/features/chat/ - test/ - -no changes added to commit (use "git add" and/or "git commit -a") diff --git a/git_status_now.txt b/git_status_now.txt deleted file mode 100644 index 9e49354..0000000 --- a/git_status_now.txt +++ /dev/null @@ -1,9 +0,0 @@ -On branch dev -Your branch is ahead of 'origin/dev' by 1 commit. - (use "git push" to publish your local commits) - -Untracked files: - (use "git add ..." to include in what will be committed) - git_status_now.txt - -nothing added to commit but untracked files present (use "git add" to track) diff --git a/supabase/favorites_migration.sql b/supabase/favorites_migration.sql deleted file mode 100644 index 90bcad0..0000000 --- a/supabase/favorites_migration.sql +++ /dev/null @@ -1,53 +0,0 @@ --- SQL Migration: Favorites Functionality - --- 1. Create favorites table -CREATE TABLE IF NOT EXISTS public.favorites ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - listing_id UUID NOT NULL REFERENCES public.listings(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ DEFAULT now(), - -- Prevent duplicate favorites - UNIQUE(user_id, listing_id) -); - --- 2. Enable RLS -ALTER TABLE public.favorites ENABLE ROW LEVEL SECURITY; - --- 3. RLS Policies -CREATE POLICY "Users can view their own favorites" -ON public.favorites FOR SELECT -USING (auth.uid() = user_id); - -CREATE POLICY "Users can create their own favorites" -ON public.favorites FOR INSERT -WITH CHECK (auth.uid() = user_id); - -CREATE POLICY "Users can delete their own favorites" -ON public.favorites FOR DELETE -USING (auth.uid() = user_id); - --- 4. Create wishlists table -CREATE TABLE IF NOT EXISTS public.wishlists ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - listing_id UUID NOT NULL REFERENCES public.listings(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ DEFAULT now(), - -- Prevent duplicate wishlists - UNIQUE(user_id, listing_id) -); - --- 5. Enable RLS for wishlists -ALTER TABLE public.wishlists ENABLE ROW LEVEL SECURITY; - --- 6. RLS Policies for wishlists -CREATE POLICY "Users can view their own wishlists" -ON public.wishlists FOR SELECT -USING (auth.uid() = user_id); - -CREATE POLICY "Users can create their own wishlists" -ON public.wishlists FOR INSERT -WITH CHECK (auth.uid() = user_id); - -CREATE POLICY "Users can delete their own wishlists" -ON public.wishlists FOR DELETE -USING (auth.uid() = user_id); diff --git a/supabase/feedback_migration.sql b/supabase/feedback_migration.sql deleted file mode 100644 index e641e84..0000000 --- a/supabase/feedback_migration.sql +++ /dev/null @@ -1,20 +0,0 @@ --- Create feedback table -CREATE TABLE IF NOT EXISTS public.feedback ( - id UUID DEFAULT gen_random_uuid() PRIMARY KEY, - user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, - content TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT now() -); - --- Enable RLS -ALTER TABLE public.feedback ENABLE ROW LEVEL SECURITY; - --- Create policy to allow users to insert their own feedback -CREATE POLICY "Users can insert their own feedback" -ON public.feedback -FOR INSERT -TO authenticated -WITH CHECK (auth.uid() = user_id); - --- Create policy to allow team to read all feedback (Optional: you can adjust this later) --- For now, let's keep it private or for service_role only. diff --git a/supabase/functions/fapshi-polling/index.ts b/supabase/functions/fapshi-polling/index.ts deleted file mode 100644 index 927745c..0000000 --- a/supabase/functions/fapshi-polling/index.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; - -const supabaseUrl = Deno.env.get("SUPABASE_URL")!; -const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; - -function adminClient() { - return createClient(supabaseUrl, supabaseServiceKey); -} - -function resp(status: number, body: unknown): Response { - return new Response(JSON.stringify(body), { - status, - headers: { "Content-Type": "application/json" }, - }); -} - -Deno.serve(async (req: Request) => { - try { - // Validate cron secret - const supabase = adminClient(); - const cronSecret = req.headers.get("x-cron-secret"); - const { data: secretRow } = await supabase - .from("app_secrets") - .select("value") - .eq("key", "broadcast_secret") - .single(); - - if (!cronSecret || cronSecret !== secretRow?.value) { - return resp(401, { error: "Unauthorized" }); - } - - // Retrieve Fapshi credentials from database secrets - const { data: userRow } = await supabase - .from("app_secrets") - .select("value") - .eq("key", "fapshi_api_user") - .single(); - const { data: keyRow } = await supabase - .from("app_secrets") - .select("value") - .eq("key", "fapshi_api_key") - .single(); - - const fapshiApiUser = userRow?.value; - const fapshiApiKey = keyRow?.value; - - if (!fapshiApiUser || !fapshiApiKey) { - return resp(500, { error: "Fapshi API credentials not configured in app_secrets" }); - } - - // Find all transactions stuck in pending_payment for more than 10 minutes - const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000).toISOString(); - const { data: txs, error: txsErr } = await supabase - .from("transactions") - .select("id, payment_reference, listing_id, buyer_id, seller_id, amount") - .eq("status", "pending_payment") - .lte("created_at", tenMinutesAgo); - - if (txsErr) { - return resp(500, { error: txsErr.message }); - } - - const results: Array<{ id: string; reference: string; status: string; error?: string }> = []; - - for (const tx of txs ?? []) { - const reference = tx.payment_reference; - try { - console.log(`Checking Fapshi status for transaction ref: ${reference}`); - const fapshiRes = await fetch(`https://api.fapshi.com/payment-status/${reference}`, { - method: "GET", - headers: { - "apiuser": fapshiApiUser, - "apikey": fapshiApiKey, - }, - }); - - if (!fapshiRes.ok) { - const errMsg = await fapshiRes.text(); - results.push({ id: tx.id, reference, status: "error", error: `Fapshi error: ${fapshiRes.status} - ${errMsg}` }); - console.error(`Fapshi status check failed for ${reference}: ${fapshiRes.status} - ${errMsg}`); - continue; - } - - const data = await fapshiRes.json(); - const fapshiStatus = data.status || data.state; - console.log(`Fapshi status for ${reference} is ${fapshiStatus}`); - - if (fapshiStatus === "SUCCESSFUL" || fapshiStatus === "SUCCESS") { - // Process successful payment - // 1. Mark listing as sold - await supabase - .from("listings") - .update({ status: "sold" }) - .eq("id", tx.listing_id); - - // 2. Update transaction status to held (since escrow is enabled) - await supabase - .from("transactions") - .update({ status: "held" }) - .eq("id", tx.id); - - // 3. Create escrow transaction - await supabase - .from("escrow_transactions") - .upsert({ - transaction_id: tx.id, - status: "held", - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - }, { onConflict: "transaction_id" }); - - // 4. Send automated chat message - await supabase - .from("messages") - .insert({ - listing_id: tx.listing_id, - sender_id: tx.buyer_id, - receiver_id: tx.seller_id, - content: "Hello! I just purchased this book via BookBridge. When can we meet to complete the exchange?", - is_read: false, - created_at: new Date().toISOString(), - }); - - results.push({ id: tx.id, reference, status: "held" }); - } else if (fapshiStatus === "FAILED" || fapshiStatus === "EXPIRED") { - // Mark transaction as failed - await supabase - .from("transactions") - .update({ status: "failed" }) - .eq("id", tx.id); - - results.push({ id: tx.id, reference, status: "failed" }); - } else { - // Still pending/created - results.push({ id: tx.id, reference, status: fapshiStatus }); - } - } catch (err) { - results.push({ id: tx.id, reference, status: "error", error: String(err) }); - console.error(`Error processing status check for ${reference}:`, err); - } - } - - return resp(200, { processed: results.length, results }); - } catch (err) { - console.error("fapshi-polling unhandled error:", err); - return resp(500, { error: String(err) }); - } -}); diff --git a/supabase/functions/process-escrow/index.ts b/supabase/functions/process-escrow/index.ts deleted file mode 100644 index 5cbc51d..0000000 --- a/supabase/functions/process-escrow/index.ts +++ /dev/null @@ -1,382 +0,0 @@ -import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; - -const supabaseUrl = Deno.env.get("SUPABASE_URL")!; -const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; -const FAPSHI_PAYOUT_URL = "https://live.fapshi.com/payout"; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function adminClient() { - return createClient(supabaseUrl, supabaseServiceKey); -} - -function userClient(jwt: string) { - return createClient(supabaseUrl, Deno.env.get("SUPABASE_ANON_KEY")!, { - global: { headers: { Authorization: `Bearer ${jwt}` } }, - }); -} - -/** Returns the bearer token from the Authorization header, or null. */ -function extractJwt(req: Request): string | null { - const auth = req.headers.get("Authorization"); - if (!auth?.startsWith("Bearer ")) return null; - return auth.slice(7); -} - -/** Verify the caller is an admin via service_role key or a trusted cron secret. */ -function isAdmin(req: Request): boolean { - const cronSecret = req.headers.get("x-cron-secret"); - if (cronSecret) { - // validated below in auto-release; here we just flag that it came via cron header - return true; - } - const authKey = req.headers.get("x-supabase-admin-key"); - return authKey === supabaseServiceKey; -} - -/** - * Execute a payout via Fapshi and return { ok, transId, error }. - * Statuses are NEVER updated unless this returns ok = true. - */ -async function executeFapshiPayout(params: { - amount: number; - phone: string; - sellerName: string; - externalId: string; - listingId: string; - transactionId: string; -}): Promise<{ ok: boolean; transId?: string; error?: string }> { - let cleanedPhone = params.phone.replace(/\D/g, ""); - if (cleanedPhone.startsWith("237")) cleanedPhone = cleanedPhone.slice(3); - - const requestPayload = { - amount: params.amount, - phone: cleanedPhone, - name: params.sellerName, - externalId: params.externalId, - message: `BookBridge escrow payout for listing ${params.listingId}`, - }; - - let res: Response; - let responsePayload: any = null; - let statusCode = 0; - - const supabase = adminClient(); - - // Retrieve Fapshi payout credentials from database secrets - const { data: userRow } = await supabase - .from("app_secrets") - .select("value") - .eq("key", "fapshi_payout_api_user") - .single(); - const { data: keyRow } = await supabase - .from("app_secrets") - .select("value") - .eq("key", "fapshi_payout_api_key") - .single(); - - const fapshiPayoutUser = userRow?.value; - const fapshiPayoutKey = keyRow?.value; - - if (!fapshiPayoutUser || !fapshiPayoutKey) { - return { ok: false, error: "Fapshi payout credentials not configured in app_secrets" }; - } - - try { - res = await fetch(FAPSHI_PAYOUT_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - apiuser: fapshiPayoutUser, - apikey: fapshiPayoutKey, - }, - body: JSON.stringify(requestPayload), - }); - statusCode = res.status; - responsePayload = await res.json().catch(() => ({})); - } catch (err) { - await supabase.from("fapshi_audit_logs").insert({ - transaction_id: params.transactionId, - endpoint: FAPSHI_PAYOUT_URL, - request_payload: requestPayload, - response_payload: { error: String(err) }, - status_code: 500, - }); - return { ok: false, error: `Network error: ${String(err)}` }; - } - - await supabase.from("fapshi_audit_logs").insert({ - transaction_id: params.transactionId, - endpoint: FAPSHI_PAYOUT_URL, - request_payload: requestPayload, - response_payload: responsePayload, - status_code: statusCode, - }); - - if (res.ok && responsePayload?.statusCode === 200) { - return { ok: true, transId: responsePayload.transId }; - } - return { - ok: false, - error: `Fapshi error ${res.status}: ${JSON.stringify(responsePayload)}`, - }; -} - -/** - * Release a single escrow: - * 1. Fetch transaction + seller profile. - * 2. Execute Fapshi payout. - * 3. ONLY on success β†’ update escrow + transaction statuses. - * Returns { ok, error? }. - */ -async function releaseEscrow( - transactionId: string, - supabase = adminClient() -): Promise<{ ok: boolean; error?: string }> { - // Fetch transaction - const { data: tx, error: txErr } = await supabase - .from("transactions") - .select("id, listing_id, seller_id, amount, commission_amount, payment_reference") - .eq("id", transactionId) - .single(); - - if (txErr || !tx) { - return { ok: false, error: `Transaction not found: ${txErr?.message}` }; - } - - // Fetch seller profile - const { data: profile } = await supabase - .from("profiles") - .select("whatsapp_number, full_name") - .eq("id", tx.seller_id) - .single(); - - if (!profile?.whatsapp_number) { - return { ok: false, error: "Seller has no payout number configured." }; - } - - const payoutAmount = tx.amount - (tx.commission_amount ?? 0); - const externalId = `escrow_payout_${tx.payment_reference}`; - - // ── PAYOUT FIRST ──────────────────────────────────────────────────────── - const payout = await executeFapshiPayout({ - amount: payoutAmount, - phone: profile.whatsapp_number, - sellerName: profile.full_name ?? "BookBridge Seller", - externalId, - listingId: tx.listing_id, - transactionId: tx.id, - }); - - if (!payout.ok) { - return { ok: false, error: payout.error }; - } - // ── ONLY UPDATE DB AFTER CONFIRMED PAYOUT ──────────────────────────────── - - const now = new Date().toISOString(); - - const { error: escrowErr } = await supabase - .from("escrow_transactions") - .update({ status: "released", updated_at: now }) - .eq("transaction_id", transactionId) - .eq("status", "held"); - - if (escrowErr) { - console.error("escrow update failed after payout:", escrowErr); - // Payout already sent β€” log but don't surface as failure - } - - const { error: txUpdateErr } = await supabase - .from("transactions") - .update({ - status: "successful", - payout_status: "successful", - payout_reference: payout.transId, - }) - .eq("id", transactionId); - - if (txUpdateErr) { - console.error("transaction update failed after payout:", txUpdateErr); - } - - return { ok: true }; -} - -// --------------------------------------------------------------------------- -// Route handlers -// --------------------------------------------------------------------------- - -async function handleRelease(req: Request): Promise { - const jwt = extractJwt(req); - if (!jwt) return resp(401, { error: "Unauthorized" }); - - const { transaction_id } = await req.json(); - if (!transaction_id) return resp(400, { error: "transaction_id required" }); - - // Verify caller is the buyer - const client = userClient(jwt); - const { data: { user } } = await client.auth.getUser(); - if (!user) return resp(401, { error: "Invalid token" }); - - const { data: tx } = await adminClient() - .from("transactions") - .select("buyer_id, status") - .eq("id", transaction_id) - .single(); - - if (!tx) return resp(404, { error: "Transaction not found" }); - if (tx.buyer_id !== user.id) return resp(403, { error: "Forbidden" }); - if (tx.status !== "held") return resp(409, { error: `Transaction is '${tx.status}', not 'held'` }); - - const result = await releaseEscrow(transaction_id); - return result.ok - ? resp(200, { ok: true }) - : resp(500, { error: result.error }); -} - -async function handleDispute(req: Request): Promise { - const jwt = extractJwt(req); - if (!jwt) return resp(401, { error: "Unauthorized" }); - - const { transaction_id, dispute_reason } = await req.json(); - if (!transaction_id || !dispute_reason) { - return resp(400, { error: "transaction_id and dispute_reason required" }); - } - - const client = userClient(jwt); - const { data: { user } } = await client.auth.getUser(); - if (!user) return resp(401, { error: "Invalid token" }); - - const supabase = adminClient(); - - const { data: tx } = await supabase - .from("transactions") - .select("buyer_id, status") - .eq("id", transaction_id) - .single(); - - if (!tx) return resp(404, { error: "Transaction not found" }); - if (tx.buyer_id !== user.id) return resp(403, { error: "Forbidden" }); - if (tx.status !== "held") return resp(409, { error: `Transaction is '${tx.status}', not 'held'` }); - - const now = new Date().toISOString(); - - await supabase - .from("escrow_transactions") - .update({ status: "disputed", dispute_reason, updated_at: now }) - .eq("transaction_id", transaction_id); - - await supabase - .from("transactions") - .update({ status: "disputed" }) - .eq("id", transaction_id); - - return resp(200, { ok: true }); -} - -async function handleAutoReleaseExpired(req: Request): Promise { - // Validate cron secret - const supabase = adminClient(); - const cronSecret = req.headers.get("x-cron-secret"); - const { data: secretRow } = await supabase - .from("app_secrets") - .select("value") - .eq("key", "broadcast_secret") - .single(); - - if (!cronSecret || cronSecret !== secretRow?.value) { - return resp(401, { error: "Unauthorized" }); - } - - // Find all held escrows whose release_deadline has passed - const { data: expired, error } = await supabase - .from("escrow_transactions") - .select("transaction_id") - .eq("status", "held") - .lte("release_deadline", new Date().toISOString()); - - if (error) return resp(500, { error: error.message }); - - const results: Array<{ transaction_id: string; ok: boolean; error?: string }> = []; - - for (const row of expired ?? []) { - const result = await releaseEscrow(row.transaction_id, supabase); - results.push({ transaction_id: row.transaction_id, ...result }); - console.log(`auto-release ${row.transaction_id}: ${result.ok ? "OK" : result.error}`); - } - - return resp(200, { released: results.length, results }); -} - -async function handleResolveDispute(req: Request): Promise { - // Admin only β€” validate service_role key - const adminKey = req.headers.get("x-supabase-admin-key"); - if (adminKey !== supabaseServiceKey) { - return resp(401, { error: "Admin authorization required" }); - } - - const { transaction_id, action } = await req.json(); - if (!transaction_id || !["release", "refund"].includes(action)) { - return resp(400, { error: "transaction_id and action ('release'|'refund') required" }); - } - - const supabase = adminClient(); - const now = new Date().toISOString(); - - if (action === "release") { - const result = await releaseEscrow(transaction_id, supabase); - return result.ok - ? resp(200, { ok: true, action: "released" }) - : resp(500, { error: result.error }); - } - - // action === "refund" - await supabase - .from("escrow_transactions") - .update({ status: "refunded", updated_at: now }) - .eq("transaction_id", transaction_id); - - await supabase - .from("transactions") - .update({ status: "failed" }) - .eq("id", transaction_id); - - return resp(200, { ok: true, action: "refunded" }); -} - -// --------------------------------------------------------------------------- -// Entry point -// --------------------------------------------------------------------------- - -function resp(status: number, body: unknown): Response { - return new Response(JSON.stringify(body), { - status, - headers: { "Content-Type": "application/json" }, - }); -} - -Deno.serve(async (req: Request) => { - try { - const { action } = await req.clone().json().catch(() => ({ action: null })); - - switch (action) { - case "release": - return await handleRelease(req); - case "dispute": - return await handleDispute(req); - case "auto-release-expired": - return await handleAutoReleaseExpired(req); - case "resolve-dispute": - return await handleResolveDispute(req); - default: - return resp(400, { - error: "Unknown action. Valid: release | dispute | auto-release-expired | resolve-dispute", - }); - } - } catch (err) { - console.error("process-escrow unhandled error:", err); - return resp(500, { error: String(err) }); - } -}); diff --git a/supabase/migrations/20260602130000_escrow_payment_flow.sql b/supabase/migrations/20260602130000_escrow_payment_flow.sql deleted file mode 100644 index 1921450..0000000 --- a/supabase/migrations/20260602130000_escrow_payment_flow.sql +++ /dev/null @@ -1,98 +0,0 @@ --- ============================================================ --- Priority 9: Escrow Payment Flow --- Migration: 20260602130000_escrow_payment_flow.sql --- ============================================================ - --- 1. Immutable helper function required for GENERATED column on TIMESTAMPTZ --- (PostgreSQL rejects non-immutable expressions in generated columns; --- this wrapper is declared IMMUTABLE so the planner accepts it.) -CREATE OR REPLACE FUNCTION public.add_48_hours(t TIMESTAMPTZ) -RETURNS TIMESTAMPTZ -LANGUAGE SQL IMMUTABLE AS $$ - SELECT t + INTERVAL '48 hours'; -$$; - --- 2. Create escrow_transactions table -CREATE TABLE IF NOT EXISTS public.escrow_transactions ( - id UUID DEFAULT gen_random_uuid() PRIMARY KEY, - transaction_id UUID NOT NULL REFERENCES public.transactions(id) ON DELETE CASCADE, - status TEXT NOT NULL DEFAULT 'held' - CHECK (status IN ('held', 'released', 'refunded', 'disputed')), - dispute_reason TEXT, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - release_deadline TIMESTAMP WITH TIME ZONE - GENERATED ALWAYS AS (public.add_48_hours(created_at)) STORED -); - --- 3. Performance indexes -CREATE INDEX IF NOT EXISTS idx_escrow_transaction_id ON public.escrow_transactions (transaction_id); -CREATE INDEX IF NOT EXISTS idx_escrow_status ON public.escrow_transactions (status); -CREATE INDEX IF NOT EXISTS idx_escrow_release_deadline ON public.escrow_transactions (release_deadline) - WHERE status = 'held'; - --- 4. updated_at auto-refresh trigger -CREATE OR REPLACE FUNCTION public.set_escrow_updated_at() -RETURNS TRIGGER LANGUAGE plpgsql AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$; - -DROP TRIGGER IF EXISTS trg_escrow_updated_at ON public.escrow_transactions; -CREATE TRIGGER trg_escrow_updated_at - BEFORE UPDATE ON public.escrow_transactions - FOR EACH ROW EXECUTE FUNCTION public.set_escrow_updated_at(); - --- 5. Row-Level Security -ALTER TABLE public.escrow_transactions ENABLE ROW LEVEL SECURITY; - --- Buyers and sellers can view their own escrow records -DROP POLICY IF EXISTS "escrow_select_parties" ON public.escrow_transactions; -CREATE POLICY "escrow_select_parties" - ON public.escrow_transactions FOR SELECT - USING ( - EXISTS ( - SELECT 1 FROM public.transactions t - WHERE t.id = escrow_transactions.transaction_id - AND (t.buyer_id = auth.uid() OR t.seller_id = auth.uid()) - ) - ); - --- Only the buyer can dispute or release (INSERT not needed β€” webhook inserts via service_role) -DROP POLICY IF EXISTS "escrow_update_buyer" ON public.escrow_transactions; -CREATE POLICY "escrow_update_buyer" - ON public.escrow_transactions FOR UPDATE - USING ( - EXISTS ( - SELECT 1 FROM public.transactions t - WHERE t.id = escrow_transactions.transaction_id - AND t.buyer_id = auth.uid() - ) - ); - --- 6. Enable pg_net (required for pg_cron HTTP calls) -CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions; - --- 7. Schedule hourly pg_cron job to trigger auto-release of expired escrows --- The job uses the broadcast_secret from app_secrets for authorization. --- Runs at minute 0 of every hour (0 * * * *). -SELECT cron.unschedule('auto-release-escrows-job') WHERE EXISTS ( - SELECT 1 FROM cron.job WHERE jobname = 'auto-release-escrows-job' -); - -SELECT cron.schedule( - 'auto-release-escrows-job', - '0 * * * *', - $$ - SELECT public.http_post( - url := 'https://jacnsvcwmhoicuuzmrmr.supabase.co/functions/v1/process-escrow', - headers := jsonb_build_object( - 'Content-Type', 'application/json', - 'x-cron-secret', (SELECT value FROM public.app_secrets WHERE key = 'broadcast_secret' LIMIT 1) - ), - body := '{"action":"auto-release-expired"}'::jsonb - ); - $$ -); diff --git a/supabase/migrations/20260602224500_update_transactions_status_check.sql b/supabase/migrations/20260602224500_update_transactions_status_check.sql deleted file mode 100644 index 17cf6f2..0000000 --- a/supabase/migrations/20260602224500_update_transactions_status_check.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Drop existing status check constraint -ALTER TABLE public.transactions DROP CONSTRAINT IF EXISTS transactions_status_check; - --- Add updated status check constraint supporting 'held' and 'disputed' -ALTER TABLE public.transactions ADD CONSTRAINT transactions_status_check - CHECK (status IN ('pending', 'successful', 'failed', 'held', 'disputed')); diff --git a/supabase/migrations/20260611180000_fintech_architecture_fixes.sql b/supabase/migrations/20260611180000_fintech_architecture_fixes.sql deleted file mode 100644 index 28767fe..0000000 --- a/supabase/migrations/20260611180000_fintech_architecture_fixes.sql +++ /dev/null @@ -1,90 +0,0 @@ --- ============================================================ --- Fintech Architecture Fixes --- Migration: 20260611180000_fintech_architecture_fixes.sql --- ============================================================ - --- 1. Extend Auto-Release from 48h to 5 days --- First, drop the generated column that depends on add_48_hours -ALTER TABLE public.escrow_transactions DROP COLUMN IF EXISTS release_deadline; - --- Drop the old helper function -DROP FUNCTION IF EXISTS public.add_48_hours(TIMESTAMPTZ); - --- Create new helper function for 5 days -CREATE OR REPLACE FUNCTION public.add_5_days(t TIMESTAMPTZ) -RETURNS TIMESTAMPTZ -LANGUAGE SQL IMMUTABLE AS $$ - SELECT t + INTERVAL '5 days'; -$$; - --- Re-add release_deadline column with 5 days calculation -ALTER TABLE public.escrow_transactions -ADD COLUMN release_deadline TIMESTAMP WITH TIME ZONE -GENERATED ALWAYS AS (public.add_5_days(created_at)) STORED; - --- 2. Update status check constraint on transactions table -ALTER TABLE public.transactions DROP CONSTRAINT IF EXISTS transactions_status_check; -ALTER TABLE public.transactions ADD CONSTRAINT transactions_status_check - CHECK (status IN ('pending', 'pending_payment', 'successful', 'failed', 'held', 'disputed')); - --- 3. Create fapshi_audit_logs table -CREATE TABLE IF NOT EXISTS public.fapshi_audit_logs ( - id UUID DEFAULT gen_random_uuid() PRIMARY KEY, - transaction_id UUID REFERENCES public.transactions(id) ON DELETE SET NULL, - endpoint TEXT NOT NULL, - request_payload JSONB, - response_payload JSONB, - status_code INTEGER, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - --- Enable RLS for the audit logs -ALTER TABLE public.fapshi_audit_logs ENABLE ROW LEVEL SECURITY; - --- 4. Store Project URL as Vault Secret -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM vault.decrypted_secrets WHERE name = 'app.supabase_url') THEN - PERFORM vault.create_secret('https://jacnsvcwmhoicuuzmrmr.supabase.co', 'app.supabase_url', 'Supabase Project URL'); - END IF; -END $$; - --- 5. Re-schedule auto-release-escrows-job (using net.http_post & vault secret) -SELECT cron.unschedule('auto-release-escrows-job') WHERE EXISTS ( - SELECT 1 FROM cron.job WHERE jobname = 'auto-release-escrows-job' -); - -SELECT cron.schedule( - 'auto-release-escrows-job', - '0 * * * *', - $$ - SELECT net.http_post( - url := (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'app.supabase_url' LIMIT 1) || '/functions/v1/process-escrow', - headers := jsonb_build_object( - 'Content-Type', 'application/json', - 'x-cron-secret', (SELECT value FROM public.app_secrets WHERE key = 'broadcast_secret' LIMIT 1) - ), - body := '{"action":"auto-release-expired"}'::jsonb - ); - $$ -); - --- 6. Schedule new fapshi-polling-job (every 5 minutes) -SELECT cron.unschedule('fapshi-polling-job') WHERE EXISTS ( - SELECT 1 FROM cron.job WHERE jobname = 'fapshi-polling-job' -); - -SELECT cron.schedule( - 'fapshi-polling-job', - '*/5 * * * *', - $$ - SELECT net.http_post( - url := (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'app.supabase_url' LIMIT 1) || '/functions/v1/fapshi-polling', - headers := jsonb_build_object( - 'Content-Type', 'application/json', - 'x-cron-secret', (SELECT value FROM public.app_secrets WHERE key = 'broadcast_secret' LIMIT 1) - ), - body := '{}'::jsonb - ); - $$ -); diff --git a/test.txt b/test.txt deleted file mode 100644 index 9daeafb..0000000 --- a/test.txt +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/landingPage/.gitignore b/web/.gitignore similarity index 100% rename from landingPage/.gitignore rename to web/.gitignore diff --git a/landingPage/.npmrc b/web/.npmrc similarity index 100% rename from landingPage/.npmrc rename to web/.npmrc diff --git a/landingPage/check_db.py b/web/check_db.py similarity index 100% rename from landingPage/check_db.py rename to web/check_db.py diff --git a/landingPage/check_supabase_fast.py b/web/check_supabase_fast.py similarity index 100% rename from landingPage/check_supabase_fast.py rename to web/check_supabase_fast.py diff --git a/landingPage/check_tx.js b/web/check_tx.js similarity index 100% rename from landingPage/check_tx.js rename to web/check_tx.js diff --git a/landingPage/jsconfig.json b/web/jsconfig.json similarity index 100% rename from landingPage/jsconfig.json rename to web/jsconfig.json diff --git a/landingPage/package-lock.json b/web/package-lock.json similarity index 100% rename from landingPage/package-lock.json rename to web/package-lock.json diff --git a/landingPage/package.json b/web/package.json similarity index 100% rename from landingPage/package.json rename to web/package.json diff --git a/landingPage/src/app.css b/web/src/app.css similarity index 100% rename from landingPage/src/app.css rename to web/src/app.css diff --git a/landingPage/src/app.d.ts b/web/src/app.d.ts similarity index 100% rename from landingPage/src/app.d.ts rename to web/src/app.d.ts diff --git a/landingPage/src/app.html b/web/src/app.html similarity index 100% rename from landingPage/src/app.html rename to web/src/app.html diff --git a/landingPage/src/lib/assets/Founder.jpg b/web/src/lib/assets/Founder.jpg similarity index 100% rename from landingPage/src/lib/assets/Founder.jpg rename to web/src/lib/assets/Founder.jpg diff --git a/landingPage/src/lib/assets/favicon.svg b/web/src/lib/assets/favicon.svg similarity index 100% rename from landingPage/src/lib/assets/favicon.svg rename to web/src/lib/assets/favicon.svg diff --git a/landingPage/src/lib/assets/logo.png b/web/src/lib/assets/logo.png similarity index 100% rename from landingPage/src/lib/assets/logo.png rename to web/src/lib/assets/logo.png diff --git a/landingPage/src/lib/assets/screenshot.png b/web/src/lib/assets/screenshot.png similarity index 100% rename from landingPage/src/lib/assets/screenshot.png rename to web/src/lib/assets/screenshot.png diff --git a/landingPage/src/lib/components/AppShowcase.svelte b/web/src/lib/components/AppShowcase.svelte similarity index 100% rename from landingPage/src/lib/components/AppShowcase.svelte rename to web/src/lib/components/AppShowcase.svelte diff --git a/landingPage/src/lib/components/Features.svelte b/web/src/lib/components/Features.svelte similarity index 100% rename from landingPage/src/lib/components/Features.svelte rename to web/src/lib/components/Features.svelte diff --git a/landingPage/src/lib/components/Footer.svelte b/web/src/lib/components/Footer.svelte similarity index 100% rename from landingPage/src/lib/components/Footer.svelte rename to web/src/lib/components/Footer.svelte diff --git a/landingPage/src/lib/components/Founder.svelte b/web/src/lib/components/Founder.svelte similarity index 100% rename from landingPage/src/lib/components/Founder.svelte rename to web/src/lib/components/Founder.svelte diff --git a/landingPage/src/lib/components/Hero.svelte b/web/src/lib/components/Hero.svelte similarity index 100% rename from landingPage/src/lib/components/Hero.svelte rename to web/src/lib/components/Hero.svelte diff --git a/landingPage/src/lib/components/HowItWorks.svelte b/web/src/lib/components/HowItWorks.svelte similarity index 100% rename from landingPage/src/lib/components/HowItWorks.svelte rename to web/src/lib/components/HowItWorks.svelte diff --git a/landingPage/src/lib/components/Navbar.svelte b/web/src/lib/components/Navbar.svelte similarity index 100% rename from landingPage/src/lib/components/Navbar.svelte rename to web/src/lib/components/Navbar.svelte diff --git a/landingPage/src/lib/components/TrustBar.svelte b/web/src/lib/components/TrustBar.svelte similarity index 100% rename from landingPage/src/lib/components/TrustBar.svelte rename to web/src/lib/components/TrustBar.svelte diff --git a/landingPage/src/lib/index.js b/web/src/lib/index.js similarity index 100% rename from landingPage/src/lib/index.js rename to web/src/lib/index.js diff --git a/landingPage/src/lib/links.js b/web/src/lib/links.js similarity index 100% rename from landingPage/src/lib/links.js rename to web/src/lib/links.js diff --git a/landingPage/src/lib/theme.svelte.js b/web/src/lib/theme.svelte.js similarity index 100% rename from landingPage/src/lib/theme.svelte.js rename to web/src/lib/theme.svelte.js diff --git a/landingPage/src/routes/+layout.svelte b/web/src/routes/+layout.svelte similarity index 100% rename from landingPage/src/routes/+layout.svelte rename to web/src/routes/+layout.svelte diff --git a/landingPage/src/routes/+page.svelte b/web/src/routes/+page.svelte similarity index 100% rename from landingPage/src/routes/+page.svelte rename to web/src/routes/+page.svelte diff --git a/landingPage/src/routes/api/webhooks/campay/+server.js b/web/src/routes/api/webhooks/campay/+server.js similarity index 100% rename from landingPage/src/routes/api/webhooks/campay/+server.js rename to web/src/routes/api/webhooks/campay/+server.js diff --git a/landingPage/src/routes/api/webhooks/fapshi-payouts/+server.js b/web/src/routes/api/webhooks/fapshi-payouts/+server.js similarity index 100% rename from landingPage/src/routes/api/webhooks/fapshi-payouts/+server.js rename to web/src/routes/api/webhooks/fapshi-payouts/+server.js diff --git a/landingPage/src/routes/api/webhooks/fapshi/+server.js b/web/src/routes/api/webhooks/fapshi/+server.js similarity index 100% rename from landingPage/src/routes/api/webhooks/fapshi/+server.js rename to web/src/routes/api/webhooks/fapshi/+server.js diff --git a/landingPage/static/favicon.png b/web/static/favicon.png similarity index 100% rename from landingPage/static/favicon.png rename to web/static/favicon.png diff --git a/landingPage/static/robots.txt b/web/static/robots.txt similarity index 100% rename from landingPage/static/robots.txt rename to web/static/robots.txt diff --git a/landingPage/svelte.config.js b/web/svelte.config.js similarity index 100% rename from landingPage/svelte.config.js rename to web/svelte.config.js diff --git a/landingPage/vite.config.js b/web/vite.config.js similarity index 100% rename from landingPage/vite.config.js rename to web/vite.config.js