# Onboarding / Auth — Logic & Flows

Demo = React+TS+Zustand at `/private/tmp/Navagoo_MI_dev/navagoo-app/src`.
Ours = Yii2 advanced template (frontend shop portal + api mobile).

## 1. The demo's single source of truth: `nextOnboardingScreen()`

`lib/onboarding.ts:13-26` is a pure router that decides which onboarding screen the
owner belongs on, from three booleans on the shop plus one on the owner:

```
verificationStatus 'requested'|'rejected'  -> 'inactive'
!shop.phoneVerified                         -> 'authenticate'   (activation email -> OTP)
!owner.passwordSet                          -> 'set_password'
sessionUserId !== ownerUserId               -> 'login'
!shop.contractAccepted                      -> 'terms'          (first-login hard gate)
else                                        -> 'portal'
```

This is a **linear gate chain**. Each gate must clear before the next is reachable.

### Our equivalent
We have no single router. The same chain is spread across controllers and inferred from
DB columns:

| Demo gate | Our column / check | Ref |
|---|---|---|
| `verificationStatus` requested/rejected | `shop.verification_status` (`VERIFICATION_STATUS_PENDING=0`, `VERIFIED=1`) | `common/models/base/Shop.php:94-95` |
| `phoneVerified` | `user.phone_verified` (`PHONE_VERIFIED=1`) | `common/models/User.php:70` |
| `passwordSet` | **no equivalent** — password is set at signup; admin-created owners are not modelled | — |
| session vs owner | `Yii::$app->user` identity | `frontend/controllers/SignInController.php:177-179` |
| `contractAccepted` | `shop.status == AWAITING_CONTRACT` + KeyStorage `policies.accepted.{uid}.shop` | `frontend/controllers/SiteController.php:101-103,899` |

The gate **order** also differs. Demo checks verification first, then phone, then
password, then login, then terms. Ours checks login first (you must authenticate to be
evaluated at all), then phone-verified redirect, then verification+status, then contract.

## 2. Sign-up flow

**Demo** (`SignUp.tsx` + `store.ts:1152-1186 submitShopSignup`):
1. Single self-serve form collects store + owner details and a password.
2. Creates a `Shop` (`verificationStatus:'requested'`, `phoneVerified:false`,
   `contractAccepted:false`, `status:'inactive'`) and a `User` (`role:'owner'`,
   **`passwordSet:true`** because the user chose a password here).
3. Shows an in-page confirmation panel ("submitted, pending review"). No auto-login.
4. Admin later reviews -> `activateShop` sets `verificationStatus:'activated_pending_auth'`
   and emails an "Authenticate" link (`store.ts:1188-1207`).

**Ours** (`SignInController::actionSignup` + `frontend/models/SignupForm.php:106-211`):
1. Self-serve form -> `SignupForm::signup()` creates `User` (USER_TYPE_SHOP_OWNER,
   STATUS_ACTIVE since `shouldBeActivated()` returns `false` at line 221-224) and a Shop
   (`STATUS_NEW`, `VERIFICATION_STATUS_PENDING`) via `user->addShop()`.
2. Password is set during signup (`setPassword`, line 121) — matches demo `passwordSet:true`.
3. Categories linked into `shop_category_assignment` (line 185-198).
4. Controller **auto-logs the user in** (`SignInController.php:143`) and auto-accepts
   policies (line 146-148) — **divergent**: demo never auto-logs in and shows a
   pending-review confirmation panel instead.
5. City/district are **hard-coded to الرياض** (`SignupForm.php:154-155`) — demo leaves
   `city:''`.

There is **also a second, mobile-API signup** (`api/controllers/UserController.php:38
actionSingUp` + `api/models/UserSignup.php`) that sets `STATUS_ACTIVE` and immediately
sends an OTP. This is the app channel; the demo only models the web channel.

## 3. Activation email -> "Authenticate" -> OTP

**Demo** (`ActivateEmail.tsx` + `OtpEntry.tsx`):
- `ActivateEmail` renders a faux email; guarded to `verificationStatus ===
  'activated_pending_auth'` else shows "nothing to activate".
- "Authenticate" -> `sendActivationOtp(shopId)` generates a **4-digit** `pendingOtp` and
  pushes a demo SMS (`store.ts:1211-1221`), navigates to `/activate/otp`.
- `OtpEntry` verifies via `verifyOtp(shopId, code)` — plain string compare against
  `shop.pendingOtp`, clears it on success and sets `phoneVerified:true`
  (`store.ts:1223-1228`). On success it **re-reads fresh state** and routes via
  `nextOnboardingScreen` to `set_password` or `login` (`OtpEntry.tsx:35-40`).
- OTP input is numeric-only, `maxLength 4`, centered tracking, with a **demo hint chip**
  showing the actual code (`OtpEntry.tsx:55-60`).

**Ours** (`frontend/modules/user/controllers/SignInController.php`):
- Activation is via an **emailed signed token** (`UserToken::TYPE_ACTIVATION`,
  `SignInController.php:419-491` builds `/user/sign-in/activation?token=...`), not a faux
  email screen. `actionActivation` (line 190-300) validates+expires the token, stores it
  in session, then **auto-sends a 6-digit OTP** via `SmsLog::create` /
  `SMSHelper::sendVerify` (line 252-293).
- `actionVerifyActivationOtp` (line 307-482) verifies the OTP against the `sms_log` table
  (`SmsLog::TYPE_REGISTER`), checks **expiry + rate-limit + resend cooldown**, deletes the
  OTP and token, sets `status=ACTIVE, phone_verified=PHONE_VERIFIED` (line 435-438),
  logs the user out, and redirects to login with a success flash.
- `actionResendActivationOtp` (line 489-595) provides resend with cooldown — **no demo
  equivalent** (demo has no resend).
- Test-number bypass (`0505050506` etc, non-prod only) creates an OTP without SMS
  (line 257-274).

**Key divergences**
- Demo OTP = 4 digits, in-memory string compare, no expiry/rate-limit. Ours = 6 digits,
  DB-backed, with expiry + brute-force cooldown (more robust; production-grade).
- Demo shows the code on screen (demo hint); ours never does (correct for prod).
- Demo separates `activated_pending_auth` as a distinct status; ours folds activation into
  the token lifecycle + `phone_verified` flag. There is **no** `activated_pending_auth`
  equivalent status in `Shop`.

## 4. Set-password screen

**Demo** (`SetPassword.tsx`): a dedicated screen reachable when `!owner.passwordSet` (i.e.
admin-created owners who never chose a password). Collects + confirms a password with a
live strength meter, calls `setOwnerPassword` (`store.ts:1230-1231`), routes to `/login`.

**Ours**: **MISSING as a distinct screen.** Password is always chosen at signup
(`SignupForm` requires `password`), so the demo's "admin creates owner without password ->
owner sets it later" path does not exist. The only password-setting-after-the-fact paths
are reset-password (`actionResetPassword`) and account edit (`actionAccount`), which are
different flows. There IS a console/admin path that sets a random password
(`SignInController.php:698 $user->setPassword($password)` in `actionResendEmail`), but no
owner-facing set-password screen.

## 5. Login

**Demo** (`Login.tsx` + `store.ts:1233-1240`):
- Email + password. `login()` finds the user by email, **rejects if `!passwordSet`**
  (returns null), sets `activeUserId`. Password value is ignored (demo `_password`).
- On `null` -> warning toast "no account". On success -> `/shop/dashboard`.
- Post-login terms/portal routing handled elsewhere by `nextOnboardingScreen` (`terms`
  gate).

**Ours** (`SignInController::actionLogin` + `frontend/models/LoginForm.php`):
- Username **or** email + password (`LoginForm::getUser` line 86-87).
- Real password hashing (`validatePassword`), real activation check (`validateActivation`),
  RBAC check `can('shopOwner')` (`LoginForm.php:121-123`), and **brute-force IP lockout**
  with escalating block durations (`LoginForm.php:101-173`) — far beyond the demo.
- Post-login routing (`SignInController.php:181-219`):
  - if `phone_verified != PHONE_VERIFIED` -> `/site/activation-status` (≈ demo
    `authenticate` gate).
  - if verified + status ACTIVE/AWAITING_CONTRACT -> dashboard (terms consent shown there)
    (≈ demo `terms`/`portal`).
  - new active shop without city/district -> `/sign-in/profile`.

## 6. Terms / contract gate

**Demo**: `acceptShopTerms(shopId)` (`store.ts:1242-1245`) sets
`contractAccepted:true, verificationStatus:'active', status:'active'`. Hard gate after
first login before reaching the portal.

**Ours**: policy consent stored in KeyStorage (`policies.accepted.{uid}.{role}`) and shop
flips `AWAITING_CONTRACT -> ACTIVE` on acceptance (`SiteController.php:899-919`). The gate
lives on the dashboard, not as a standalone screen. Functionally equivalent.

## Summary
Our backend is more hardened (token-based activation, 6-digit DB OTP with expiry +
rate-limit, RBAC, IP brute-force lockout) but **diverges in flow**: (a) signup auto-logs in
and auto-accepts policies instead of showing a pending-review panel; (b) there is **no
owner set-password screen** (the admin-created-owner branch of the demo is unmodelled);
(c) activation is token+SMS-OTP rather than a faux-email + on-screen 4-digit code; (d) no
single `nextOnboardingScreen` router — gate order differs.
