From 2a8b6a7b62e6d8db231fcd3346eeeab6f33dd168 Mon Sep 17 00:00:00 2001 From: mohd Date: Sat, 14 Mar 2026 23:30:56 +0300 Subject: [PATCH] feat: added initial implementation --- docs/README.md | 1 + docs/frontend-spec-requirements.md | 216 ++++++++++++++++++ frontend/src/components/LocaleSwitch.test.jsx | 20 ++ frontend/src/components/PaymentForm.jsx | 10 +- frontend/src/components/PaymentForm.test.jsx | 92 ++++++++ .../src/components/ProtectedRoute.test.jsx | 33 +++ frontend/src/components/SalonSearch.test.jsx | 8 + frontend/src/contexts/AuthContext.jsx | 9 +- frontend/src/contexts/AuthContext.test.jsx | 70 ++++++ frontend/src/hooks/usePaymentForm.js | 66 +++--- frontend/src/hooks/useSalonSearch.js | 7 +- frontend/src/i18n/ar-sa.json | 89 +++++++- frontend/src/i18n/en.json | 89 +++++++- frontend/src/layouts/MainLayout.jsx | 51 +++-- frontend/src/pages/BookPage.jsx | 36 ++- frontend/src/pages/BookPage.test.jsx | 100 ++++++++ frontend/src/pages/BookingsPage.test.jsx | 65 ++++++ frontend/src/pages/LoginPage.jsx | 11 + frontend/src/pages/LoginPage.test.jsx | 70 +++++- 19 files changed, 964 insertions(+), 79 deletions(-) create mode 100644 docs/frontend-spec-requirements.md create mode 100644 frontend/src/components/LocaleSwitch.test.jsx create mode 100644 frontend/src/components/PaymentForm.test.jsx create mode 100644 frontend/src/components/ProtectedRoute.test.jsx create mode 100644 frontend/src/contexts/AuthContext.test.jsx create mode 100644 frontend/src/pages/BookPage.test.jsx create mode 100644 frontend/src/pages/BookingsPage.test.jsx diff --git a/docs/README.md b/docs/README.md index 66419c4..a203311 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,7 @@ Use this file first. - `docs/adr/`: architecture decisions - `docs/runbooks/`: operational response guides - `docs/templates/`: ADR/runbook templates +- `docs/frontend-spec-requirements.md`: frontend technical specs + requirements (customer MVP) ## Update Rules (short) - Behavior/flow changes: update architecture + affected runbook. diff --git a/docs/frontend-spec-requirements.md b/docs/frontend-spec-requirements.md new file mode 100644 index 0000000..03c6147 --- /dev/null +++ b/docs/frontend-spec-requirements.md @@ -0,0 +1,216 @@ +# Frontend Technical Specs and Requirements (MVP) + +## Purpose +Define the implementation contract for the React frontend against the current backend REST API so the customer flows (auth, search, booking, payment, profile) can ship reliably for KSA. + +## Scope +In scope: +- Customer web app (React + Vite) for Phase 1 flows. +- API integration contracts for current backend endpoints. +- UX/error/loading behavior and test requirements. +- i18n/RTL and KSA timezone handling. + +Out of scope (Phase 2+): +- Manager/staff admin dashboards. +- Advanced reporting/reviews moderation tools. +- Full observability product dashboards. + +## Product and Platform Constraints +- Market default: KSA first. +- Default locale: `ar-sa`; fallback: `en`. +- Timezone baseline: `Asia/Riyadh` (`+03:00`). +- API error contract: HTTP status + `detail` where applicable. +- Booking/payment operations must avoid duplicate side effects. + +## UX Priorities (Primary Contract) +- First screen for authenticated and guest users MUST be a feed of nearby/available salons. +- The feed is the default home experience and primary entry to booking. +- Main customer navigation MUST be bottom tabs (mobile-first), not header-first navigation. +- Bottom tabs MUST include at minimum: + - Home/Feed + - Bookings + - Profile +- Optional tabs (when implemented) may include Search/Explore and Payments, but must not displace Home/Feed as default. + +## Frontend Architecture Requirements + +## Runtime and Stack +- React 18 + Vite. +- React Router (`BrowserRouter`) for route navigation. +- `i18next` + `react-i18next` for translations and direction switching. +- `fetch` wrapper in `src/api/client.js` as single API boundary. +- Auth/session state in `AuthContext`. + +## Route Map (Customer) +- `/` home feed (nearby/available salons) + search/filter. +- `/salon/:id` salon detail. +- `/login` phone OTP login. +- `/book?salon=` booking creation. +- `/pay?booking=` payment initiation. +- `/pay/return` payment callback/return surface. +- `/bookings` customer booking history. +- `/profile` customer profile summary. + +## Module Boundaries +- `src/api/`: all HTTP logic, standardized errors. +- `src/contexts/`: auth/session lifecycle only. +- `src/hooks/`: domain-side UI logic (`useSalonSearch`, `usePaymentForm`). +- `src/pages/`: route-level composition. +- `src/components/`: reusable presentation and guarded wrappers. +- `src/i18n/`: locale dictionaries and locale/direction state. + +## Functional Requirements + +### FR-1 Phone-First Authentication +- Login MUST use: + - `POST /api/auth/phone/request/` + - `POST /api/auth/phone/verify/` +- Password auth endpoint (`/api/auth/token/`) MUST NOT be used (returns 410). +- Login request form MUST collect: + - `phone_number` (accept KSA local or E.164 input) + - `channel` (`sms` or `whatsapp`) + - optional: `device_id` (recommended for abuse controls) +- Verify step MUST submit `request_id` + 6-digit `code`. +- On success, frontend MUST persist `access` and `refresh` tokens and user payload. + +### FR-2 Session Restore and Token Refresh +- On app boot, if `access` exists: + - call `GET /api/auth/me/`. +- If `401`/token invalid: + - call `POST /api/auth/token/refresh/` once. + - retry `GET /api/auth/me/` with new access token. +- If refresh fails, frontend MUST clear tokens and require re-login. + +### FR-3 Salon Discovery +- Home MUST render a salon feed by default on first load. +- Feed data MUST come from `GET /api/salons/` and support query params for discovery. +- Home search MUST call `GET /api/salons/?q=`. +- Search SHOULD support additional filters when UI is added: + - `city` + - `service` +- Nearby/available ranking can be client-side initially, but server response MUST remain source of truth. +- Result cards MUST link to `/salon/:id`. + +### FR-4 Salon Detail +- Detail page MUST call `GET /api/salons/:id/`. +- UI MUST render: + - salon base info + - services (duration, amount, currency) + - staff list + - optional reviews/photos if present +- CTA MUST deep-link to booking flow with salon id. + +### FR-5 Booking Creation +- Booking page MUST require authenticated user. +- Create booking with `POST /api/bookings/` using: + - `service` + - `staff` (required) + - `start_time` + - `end_time` + - optional `notes` +- `end_time` MUST match service duration exactly. +- Datetime submitted to backend MUST include explicit offset (`+03:00` for KSA baseline). +- On success (`201`), frontend MUST navigate to payment flow with booking id. + +### FR-6 Booking History +- `/bookings` MUST call authenticated `GET /api/bookings/`. +- List MUST show booking id, status, salon/service labels, datetime, and price. +- Datetime rendering MUST use active locale formatting. + +### FR-7 Payment Initiation (Idempotent) +- Payment submission MUST call `POST /api/payments/`. +- Payload requirements: + - `booking_id` (number) + - `provider` = `moyasar` + - `idempotency_key` (UUID) + - `source` object with supported type (`stcpay`, `token`, `applepay`, `samsungpay`) + - `callback_url` required for `source.type=token` +- Frontend MUST disable duplicate submits while request is in-flight. +- Same payment attempt retry MUST reuse the same `idempotency_key`. +- New attempt MUST generate a new key. +- If response includes `redirect_url`, frontend MUST redirect. + +### FR-8 Payment Return Handling +- `/pay/return` MUST parse query params: + - `status` + - `id` +- Success statuses shown as success UX: `paid`, `captured`, `authorized`. +- Non-success statuses MUST show neutral/pending/failure guidance and link to profile/bookings. + +### FR-9 Locale and Direction +- App MUST allow switching between `ar-sa` and `en`. +- Locale switch MUST: + - persist preference in local storage + - set `` + - set `` (`rtl` for `ar-sa`, `ltr` for `en`) +- API calls MUST include `Accept-Language` header with active locale. + +## API Contract Requirements + +| Endpoint | Auth | Request | Success | Error handling | +|---|---|---|---|---| +| `POST /api/auth/phone/request/` | No | `phone_number`, `channel`, optional profile fields | `201` with `request_id`, `expires_at` | `429` may include `retry_after_seconds`; show wait message | +| `POST /api/auth/phone/verify/` | No | `request_id`, `code` | `200` with `access`, `refresh`, `user` | `400` invalid/expired code | +| `POST /api/auth/token/refresh/` | No | `refresh` | `200` with new `access` | logout on failure | +| `GET /api/auth/me/` | Bearer | - | `200` user payload | `401` triggers refresh flow | +| `GET /api/salons/` | No | `q`, optional `city`, `service` | `200` list | show localized generic fetch error | +| `GET /api/salons/:id/` | No | - | `200` detail object | show detail/fallback | +| `POST /api/bookings/` | Bearer | booking payload | `201` booking | `400` field validation errors | +| `GET /api/bookings/` | Bearer | - | `200` list | auth + generic errors | +| `POST /api/payments/` | Bearer | payment payload | `201` created or `200` reused idempotent record | `400/403` with details; never auto-retry with new key | + +## Error and State Handling Requirements +- API wrapper MUST throw structured errors with: + - HTTP status + - parsed response body + - best message (`detail` first, fallback to response text) +- For validation objects (`{field: [msg]}`), UI SHOULD render first field message near form and keep raw object in debug logs. +- For `429` with `retry_after_seconds`, UI MUST display server-provided cooldown. +- All mutating forms MUST expose: + - idle/loading/error/success states + - submit button disabled while loading + +## Security and Abuse-Resistance Requirements +- Use Bearer access token for authenticated endpoints only. +- Include optional `device_id` during phone auth request to strengthen backend abuse controls. +- Never send raw card PAN/CVV data to backend; use tokenized sources only. +- On logout, clear user and both tokens from memory + storage. + +## Accessibility and UX Requirements +- All interactive controls MUST have accessible labels. +- Auth/booking/payment forms MUST be keyboard usable. +- Error text MUST be visible and associated with active form context. +- Layout MUST remain usable on mobile widths (`>=320px`) and desktop. + +## Non-Functional Requirements +- Reliability: no duplicate payment submission side effects for one attempt. +- Consistency: API errors surfaced predictably and localized where available. +- Maintainability: domain behavior in hooks/services, not route components. +- Extensibility: route/module structure must support manager/staff pages later without rewrite. + +## Test Requirements (Frontend) +- Test stack: `vitest` + Testing Library. +- Required coverage for release: + - phone login request + verify success/failure + 429 cooldown message + - auth restore and refresh-token fallback + - protected route redirect behavior + - salon search loading/empty/results/error + - booking form validation + API error mapping + success redirect + - payment form source validation + idempotency key reuse on retry + redirect behavior + - locale switching persists and sets `lang`/`dir` + - bookings list rendering and localized datetime output + +Run: +- `cd frontend && npm run test` + +## Definition of Done (Frontend) +- All FR requirements implemented for in-scope routes. +- API integrations match endpoint/payload contract above. +- No use of deprecated password login API. +- All listed frontend tests pass. +- `ar-sa` and `en` UX verified on mobile + desktop. + +## Known Dependencies and Open Decisions +- OAuth/social-linking policy is not finalized; keep social login UI hidden for now. +- Cancellation and refund policies are not finalized; do not ship irreversible customer actions until policy finalization. +- Detailed business-hours/timezone policy beyond current backend validation remains open; keep KSA-offset submission and avoid client-side assumptions that override server validation. diff --git a/frontend/src/components/LocaleSwitch.test.jsx b/frontend/src/components/LocaleSwitch.test.jsx new file mode 100644 index 0000000..6db67f3 --- /dev/null +++ b/frontend/src/components/LocaleSwitch.test.jsx @@ -0,0 +1,20 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import LocaleSwitch from "./LocaleSwitch"; +import i18n from "../i18n"; + +describe("LocaleSwitch", () => { + beforeEach(async () => { + localStorage.clear(); + await i18n.changeLanguage("en"); + }); + + it("persists locale and sets html lang/dir", async () => { + render(); + fireEvent.click(screen.getByRole("button", { name: "العربية" })); + await waitFor(() => { + expect(document.documentElement.lang).toBe("ar-sa"); + }); + expect(document.documentElement.dir).toBe("rtl"); + expect(localStorage.getItem("locale")).toBe("ar-sa"); + }); +}); diff --git a/frontend/src/components/PaymentForm.jsx b/frontend/src/components/PaymentForm.jsx index b37ef5b..ec4cf56 100644 --- a/frontend/src/components/PaymentForm.jsx +++ b/frontend/src/components/PaymentForm.jsx @@ -33,15 +33,6 @@ export default function PaymentForm({ bookingId = "", token = "" }) { required /> - + {error &&

{error}

}