# Borrower signup auth flow (v2 — email OTP only)

Use this document when building or updating signup UIs (borrower app, affiliate onboarding, admin test sim).

## Summary

Signup is **4 steps**. OTP is sent **once**, by **email only**. There is **no SMS OTP step** and **no second email OTP** after profile.

| Step | UI | Endpoint | Persists |
|------|-----|----------|----------|
| 1 | Email + phone + optional referral | `POST /auth/initiate-signup` | `referenceId` |
| 2 | Email OTP | `POST /auth/verify-otp` | `signupToken` |
| 3 | Profile (email read-only) | `POST /auth/update-profile` | same `signupToken` |
| 4 | Password | `POST /auth/create-password` | `accessToken` + user |

**Do not use** `POST /auth/verify-email-otp` in the signup funnel — it is legacy.

---

## Base URL

```
{NEXT_PUBLIC_API_URL}/auth/...
```

Default local: `http://localhost:8000/api/v1/auth/...`

**Headers (all steps):**

```http
Content-Type: application/json
Accept: application/json
```

No `Authorization` header until step 4 completes (then use `Bearer {accessToken}`).

---

## Step 1 — Initiate signup

**`POST /auth/initiate-signup`**

Collect email, phone, and optional affiliate referral code. Sends a **6-digit OTP to email**.

### Request body

```json
{
  "email": "john@example.com",
  "phoneNumber": "08100000000",
  "referralCode": "NPD-4492"
}
```

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `email` | string | yes | Valid email, max 191, **must not exist** in `users.email` |
| `phoneNumber` | string | yes | Max 20, **must not exist** in `users.phone` |
| `referralCode` | string | no | Affiliate referral code |

### Success `200`

```json
{
  "message": "OTP sent to email successfully",
  "referenceId": "req_abc123xyz0"
}
```

Store `referenceId` in state/localStorage. OTP cache TTL: **15 minutes**.

### Errors

| Status | When |
|--------|------|
| `422` | Validation (duplicate email/phone, invalid format) |
| `500` | Email send failure |

**422 body (Laravel validation):**

```json
{
  "message": "The email has already been taken. (and 1 more error)",
  "errors": {
    "email": ["The email has already been taken."],
    "phoneNumber": ["The phone number has already been taken."]
  }
}
```

---

## Step 2 — Verify email OTP

**`POST /auth/verify-otp`**

Verifies the OTP from step 1. **Uses `email` + `referenceId`, not phone.**

### Request body

```json
{
  "email": "john@example.com",
  "otp": "123456",
  "referenceId": "req_abc123xyz0"
}
```

| Field | Type | Required |
|-------|------|----------|
| `email` | string | yes — must match email from step 1 |
| `otp` | string | yes — 6 digits |
| `referenceId` | string | yes — from step 1 |

### Success `200`

```json
{
  "message": "Email OTP verified",
  "signupToken": "longRandomString60Chars..."
}
```

Store `signupToken`. Signup session TTL: **30 minutes**. Step 1 cache is deleted after success.

### Errors

| Status | Body |
|--------|------|
| `400` | `{ "message": "Invalid or expired OTP" }` |
| `422` | Missing/invalid fields |

---

## Step 3 — Update profile

**`POST /auth/update-profile`**

Saves personal details. **Do not send `email`** — it is already verified and stored server-side from step 1.

Show email as **read-only** in the UI (from step 1 form state).

### Request body

```json
{
  "signupToken": "longRandomString60Chars...",
  "firstName": "John",
  "lastName": "Doe",
  "dob": "1995-01-01",
  "stateOfOrigin": "Lagos",
  "lga": "Ikeja",
  "address": "12 Example Street",
  "occupation": "Engineer"
}
```

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `signupToken` | string | yes | From step 2 |
| `firstName` | string | yes | Max 100 |
| `lastName` | string | yes | Max 100 |
| `dob` | string | yes | Date `YYYY-MM-DD` |
| `stateOfOrigin` | string | yes | |
| `lga` | string | yes | |
| `address` | string | yes | |
| `occupation` | string | yes | |

### Success `200`

```json
{
  "message": "Profile saved successfully",
  "signupToken": "same-token-as-request"
}
```

No new token is issued. Proceed to step 4 with the same `signupToken`.

### Errors

| Status | Body |
|--------|------|
| `400` | `{ "message": "Invalid or expired signup token" }` — email not verified or token expired |
| `422` | Validation errors |

---

## Step 4 — Create password & finalize

**`POST /auth/create-password`**

Creates the user account and returns an API token.

### Request body

```json
{
  "signupToken": "longRandomString60Chars...",
  "password": "secret123",
  "fcm_token": "optional-push-token"
}
```

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `signupToken` | string | yes | From steps 2–3 |
| `password` | string | yes | Min **6** characters |
| `fcm_token` | string | no | Push notification token |

### Success `200`

```json
{
  "message": "Registration complete",
  "accessToken": "1|plainTextSanctumToken...",
  "user": {
    "id": 42,
    "name": "John Doe",
    "email": "john@example.com",
    "phone": "08100000000"
  }
}
```

Persist `accessToken` (e.g. `localStorage` / secure storage). Use:

```http
Authorization: Bearer {accessToken}
```

User is created with `email_verified: 1`, `phone_verified: 0`.

### Errors

| Status | Message |
|--------|---------|
| `400` | `Invalid or expired signup token` |
| `400` | `Email must be verified before completing registration` |
| `400` | `Complete your profile before setting a password` |
| `400` | `Phone number or email already registered` |
| `422` | Password too short, missing fields |

---

## Deprecated endpoint

**`POST /auth/verify-email-otp`** — legacy second email OTP step.

```json
{ "signupToken": "...", "emailOtp": "123456" }
```

**Do not call this in new signup UIs.** Email verification happens entirely in step 2 (`verify-otp`).

---

## Client state checklist

Between steps, persist at minimum:

```typescript
interface SignupSession {
  email: string;           // step 1 — show read-only on step 3
  phoneNumber: string;     // step 1 — display only if needed
  referenceId: string | null;  // after step 1
  signupToken: string | null;  // after step 2
}
```

After step 4:

```typescript
interface AuthSession {
  accessToken: string;
  user: {
    id: number;
    name: string;
    email: string;
    phone: string;
  };
}
```

---

## TypeScript types (copy-paste)

```typescript
// Step 1
export interface InitiateSignupRequest {
  email: string;
  phoneNumber: string;
  referralCode?: string;
}
export interface InitiateSignupResponse {
  message: string;
  referenceId: string;
}

// Step 2
export interface VerifyOtpRequest {
  email: string;
  otp: string;
  referenceId: string;
}
export interface VerifyOtpResponse {
  message: string;
  signupToken: string;
}

// Step 3
export interface UpdateProfileRequest {
  signupToken: string;
  firstName: string;
  lastName: string;
  dob: string; // YYYY-MM-DD
  stateOfOrigin: string;
  lga: string;
  address: string;
  occupation: string;
}
export interface UpdateProfileResponse {
  message: string;
  signupToken: string;
}

// Step 4
export interface CreatePasswordRequest {
  signupToken: string;
  password: string;
  fcm_token?: string;
}
export interface CreatePasswordResponse {
  message: string;
  accessToken: string;
  user: {
    id: number;
    name: string;
    email: string;
    phone: string;
  };
}

export interface ApiValidationError {
  message: string;
  errors?: Record<string, string[]>;
}
export interface ApiMessageError {
  message: string;
}
```

---

## Example axios sequence

```typescript
const baseURL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000/api/v1";

// Step 1
const { data: s1 } = await axios.post(`${baseURL}/auth/initiate-signup`, {
  email: "john@example.com",
  phoneNumber: "08100000000",
  referralCode: "NPD-4492",
});

// Step 2
const { data: s2 } = await axios.post(`${baseURL}/auth/verify-otp`, {
  email: "john@example.com",
  otp: "123456",
  referenceId: s1.referenceId,
});

// Step 3
await axios.post(`${baseURL}/auth/update-profile`, {
  signupToken: s2.signupToken,
  firstName: "John",
  lastName: "Doe",
  dob: "1995-01-01",
  stateOfOrigin: "Lagos",
  lga: "Ikeja",
  address: "12 Example Street",
  occupation: "Engineer",
});

// Step 4
const { data: s4 } = await axios.post(`${baseURL}/auth/create-password`, {
  signupToken: s2.signupToken,
  password: "secret123",
});

// Authenticated requests
axios.defaults.headers.common.Authorization = `Bearer ${s4.accessToken}`;
```

---

## Local development

When `APP_ENV=local`, OTP is always **`123456`** (see `OtpService`).

---

## Old flow (do not implement)

| Old (removed) | New (v2) |
|---------------|----------|
| Step 1: phone only → SMS OTP | Step 1: email + phone → **email OTP** |
| Step 2: verify phone OTP | Step 2: verify **email** OTP |
| Step 3: profile + send email OTP | Step 3: profile only |
| Step 4: verify email OTP | *(removed)* |
| Step 5: password | Step 4: password |

---

## Reference implementation

Affiliate onboarding (v2):

- `affiliate/src/app/onboarding/page.tsx`
- `affiliate/src/app/onboarding/[id]/page.tsx`

Both use `ONBOARDING_FLOW_VERSION = '2'` to reset stale localStorage from the old 5-step flow.
