commit 015b528127cfe9e9f32c0d5b3d40f915d043f7b1 Author: r.mubarakzyanov Date: Wed Aug 26 23:01:07 2026 +0300 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c25011f --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# dependencies +node_modules/ + +# expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# native +/ios +/android +*.orig.* + +# metro +.metro-health-check* + +# misc +.DS_Store +*.log +npm-debug.* +yarn-debug.* +yarn-error.* + +# local env +.env*.local + +# ide +.idea/ + +# internal docs +docs/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..58bd706 --- /dev/null +++ b/README.md @@ -0,0 +1,178 @@ +# CasaDoc — mobile app + +Mobile client for the CasaDoc property-document platform, for iOS and Android +from a single codebase. + +**Stack:** Expo SDK 52 · React Native 0.76 · expo-router · TypeScript · +React Query · expo-secure-store · i18n-js. + +The app can run against the PHP backend or against in-memory demo data; the +choice is one constant in `src/api/config.ts`. + +--- + +## Features + +**Account** + +- Login with email and password, bearer token kept in SecureStore, auto-login + on next launch. +- Registration with e-mail code verification. +- Password recovery (request a code, set a new password) and password change + from inside the app. +- Profile editing, avatar, account deletion. +- Interface in Italian and English, following the device language, with a + manual override in the profile. + +**Properties** + +- List with cover image, address and document counters. +- Detail screen with document categories as folders and recent documents. +- Create and edit, including a map picker for the address (react-native-maps). + +**Documents** + +- Per-property list, filtered by category and by status. +- Search across every document of every property. +- Expiry tracking: valid, expiring and expired badges, warning banners, and a + dedicated tab listing everything that needs attention. +- Detail screen with an inline preview for images and a card that hands PDFs to + the phone's own viewer; sharing and deletion. +- Editing of name, category and expiry date. + +**Adding a document** + +- From the file picker, from the photo library, or with the built-in scanner. +- The scanner uses the native document scanner (ML Kit on Android, VisionKit on + iOS) with edge detection and perspective correction, captures several pages + and combines them into one PDF (`src/scanner/scanToPdf.ts`). + +**Offline** + +- Responses are cached and persisted, so a previously opened screen still shows + its content without a connection; a banner appears while the device is + offline and the data refreshes as soon as it is back. + +--- + +## Getting started + +```bash +npm install +``` + +The scanner, the maps and the file viewer are native modules, so the app needs +a development build rather than Expo Go: + +```bash +npx expo run:ios # or: npx expo run:android +npm start # subsequent runs +``` + +With EAS instead of a local build: + +```bash +npx eas build --profile development --platform ios # or android +``` + +`npm run prebuild` regenerates the native projects (`expo prebuild --clean`); +`ios/` and `android/` are not kept in the repository because everything native +is declared by config plugins in `app.json`, and `npm install` applies +`patches/` through `patch-package`. + +When the phone is connected over USB, `npm run reverse` forwards the Metro and +backend ports to it. + +--- + +## Pointing the app at a backend + +Everything goes through `src/api/config.ts`: + +```ts +export const USE_MOCK = false; // true -> in-memory demo data, no server +``` + +- `USE_MOCK = true` uses `src/api/mock.ts`: every flow works offline on demo + data, and any credentials are accepted at login. +- `USE_MOCK = false` uses `src/api/real.ts`, mapped to the endpoints in + `openapi.yaml`. + +The address of the server comes from two places: `DEV_HOST` in the same file +for development builds, and `expo.extra.apiUrl` in `app.json` for release +builds. A release build must use `https`, because both platforms block plain +HTTP traffic outside development. + +The screens import `api` from `@/api` and never know which of the two is +active. + +### How documents map to the backend + +The screens work with a flat model: a property owns documents, each with a +category and an optional expiry date. The backend nests the same data one level +deeper, as page (slug) → section → document requirement → uploaded files. +`src/api/real.ts` holds that mapping. Reads flatten the structure for display; +writes stay faithful, because the upload screen has the user pick a real +requirement and sends its id. + +Two consequences are worth knowing: + +- A property's documents are gathered with one request per category page, since + the endpoint serves a single page at a time. The properties list does not pay + that cost — it uses the counters `homes.php` returns. +- There is no endpoint for a single file's metadata, so document screens read + from the list they were opened from. + +--- + +## Project structure + +``` +app/ expo-router routes + _layout.tsx providers, auth gate + (auth)/ login, register, verify, forgot, reset + (tabs)/ properties · search · expiring · profile + property/[id].tsx property detail + property/add.tsx new property + property/edit/[id].tsx edit property + property/location.tsx map picker for the address + documents/[propertyId].tsx documents of a property + document/[id].tsx document detail and viewer + document/edit/[id].tsx edit a document + profile/edit.tsx profile + profile/password.tsx change password + upload.tsx add a document: file, gallery or scan + +src/ + api/ config.ts (backend switch), http.ts, types.ts, mock.ts, real.ts + auth/ AuthContext, token in SecureStore + i18n/ en.ts, it.ts, locale detection and override + scanner/ scanToPdf.ts + theme/ design tokens (colours, spacing, radii, typography) + components/ ui.tsx, domain.tsx, forms, AppBar, PdfView, OfflineBanner + lib/ formatting, permissions, files, network, query client +``` + +--- + +## Scripts + +| | | +|---|---| +| `npm start` | Metro for a development build | +| `npm run ios` / `npm run android` | build and install on device or simulator | +| `npm run prebuild` | regenerate `ios/` and `android/` | +| `npm run typecheck` | TypeScript, no emit | +| `npm run lint` | ESLint | +| `npm run reverse` | forward ports 8081 and 8080 to a USB-connected Android device | + +--- + +## Notes + +- Design tokens live in `src/theme/tokens.ts` and are the single source for + colours, spacing, radii and typography. Typography currently maps to the + system font; loading Space Grotesk and Inter through `expo-font` is a change + in that one file. +- Sharing between owners, ownership transfer and reports exist in the backend + contract but are outside the scope of this app. diff --git a/app.json b/app.json new file mode 100644 index 0000000..b1a8949 --- /dev/null +++ b/app.json @@ -0,0 +1,105 @@ +{ + "expo": { + "name": "CasaDoc", + "slug": "casadoc", + "version": "1.0.0", + "orientation": "portrait", + "scheme": "casadoc", + "userInterfaceStyle": "light", + "newArchEnabled": true, + "ios": { + "supportsTablet": true, + "bundleIdentifier": "io.cesoft.casadoc", + "infoPlist": { + "NSCameraUsageDescription": "CasaDoc uses the camera to scan property documents into PDF.", + "NSPhotoLibraryUsageDescription": "CasaDoc needs access to your photos to attach images to documents.", + "NSLocationWhenInUseUsageDescription": "CasaDoc uses your location to place a property on the map.", + "ITSAppUsesNonExemptEncryption": false, + "NSAppTransportSecurity": { + "NSExceptionDomains": { + "192.168.88.82": { + "NSExceptionAllowsInsecureHTTPLoads": true, + "NSIncludesSubdomains": true + } + } + } + }, + "buildNumber": "2" + }, + "android": { + "package": "io.cesoft.casadoc", + "permissions": [ + "android.permission.ACCESS_COARSE_LOCATION", + "android.permission.ACCESS_FINE_LOCATION", + "android.permission.CAMERA" + ], + "config": { + "googleMaps": { + "apiKey": "AIzaSyADtQRKgCpJNnQCP8QvBeKDcm0TrTPpsGY" + } + }, + "versionCode": 2, + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#237687" + }, + "blockedPermissions": [ + "android.permission.RECORD_AUDIO", + "android.permission.SYSTEM_ALERT_WINDOW", + "android.permission.WRITE_EXTERNAL_STORAGE" + ] + }, + "plugins": [ + "expo-router", + "expo-secure-store", + [ + "expo-image-picker", + { + "photosPermission": "CasaDoc needs access to your photos to attach images to documents.", + "cameraPermission": "CasaDoc uses the camera to scan property documents into PDF." + } + ], + [ + "react-native-document-scanner-plugin", + { + "cameraPermission": "CasaDoc uses the camera to scan property documents into PDF." + } + ], + "expo-asset", + "expo-font", + [ + "expo-build-properties", + { + "android": { + "ndkVersion": "27.1.12297006", + "targetSdkVersion": 35, + "compileSdkVersion": 35 + } + } + ], + "expo-localization", + "@react-native-community/datetimepicker", + [ + "expo-location", + { + "locationWhenInUsePermission": "CasaDoc uses your location to place a property on the map." + } + ] + ], + "experiments": { + "typedRoutes": true + }, + "icon": "./assets/icon.png", + "splash": { + "image": "./assets/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#237687" + }, + "web": { + "favicon": "./assets/favicon.png" + }, + "extra": { + "apiUrl": "https://casadoc.cesoft.io/public/userportal/api" + } + } +} diff --git a/app/(auth)/_layout.tsx b/app/(auth)/_layout.tsx new file mode 100644 index 0000000..bde2d12 --- /dev/null +++ b/app/(auth)/_layout.tsx @@ -0,0 +1,10 @@ +import { Stack } from 'expo-router'; +import { colors } from '@/theme'; + +export default function AuthLayout() { + return ( + + ); +} diff --git a/app/(auth)/forgot.tsx b/app/(auth)/forgot.tsx new file mode 100644 index 0000000..54300b4 --- /dev/null +++ b/app/(auth)/forgot.tsx @@ -0,0 +1,62 @@ +import React, { useState } from 'react'; +import { useRouter } from 'expo-router'; +import { api } from '@/api'; +import { useT } from '@/i18n'; +import { AuthShell } from '@/components/AuthShell'; +import { Button, Field, TextField } from '@/components/ui'; +import { spacing } from '@/theme'; + +export default function ForgotPasswordScreen() { + const router = useRouter(); + const t = useT(); + + const [email, setEmail] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + async function onSubmit() { + const mail = email.trim(); + if (!mail || !mail.includes('@')) return setError(t('register.badEmail')); + + setError(null); + setLoading(true); + try { + await api.auth.forgotPassword(mail); + } catch (e: any) { + setError(e?.message ?? t('common.tryAgain')); + setLoading(false); + return; + } + setLoading(false); + // The endpoint succeeds even for unknown addresses, and the screen must + // not reveal whether the account exists. + router.replace(`/reset?email=${encodeURIComponent(mail)}`); + } + + return ( + + + + + +