# ePhone — field-engineer internet calling (Asterisk + React Native)

A Zoiper-style softphone for field engineers. Outbound calls reach customer
mobiles via a SIP trunk; inbound customer calls to your DID ring the engineer's
app over WSS — waking a locked phone with a VoIP push and native call UI.

## End-to-end flows

```
OUTBOUND   App dialpad ─WSS─▶ Asterisk [from-mobile] ─▶ trunk2 ─▶ PSTN ─▶ Customer
INBOUND    Customer ─▶ DID ─▶ trunk2 ─▶ [from-trunk] ─▶ [ring-engineer]
                                          │
                                 ephone-push.sh (APNs/FCM)
                                          ▼
                              phone wakes → REGISTER → INVITE ─WSS─▶ App (CallKit rings)
```

## Repository map

### Asterisk backend — `asterisk/`
| File | What it does |
|------|--------------|
| `pjsip.conf` | WSS transport, TLS SIP trunk (`trunk2`), WebRTC endpoint template + per-engineer blocks |
| `rtp.conf` | RTP port range, STUN/TURN for ICE |
| `http.conf` | Terminates the `wss://` TLS handshake (required for app registration) |
| `extensions.conf` | `[from-mobile]` outbound, `[from-trunk]` inbound, `[ring-engineer]` wake+ring |
| `ephone-push.sh` | Called by the dialplan to fire APNs/FCM and wake the engineer's phone |

### Mobile app — `mobile/`
| File | What it does |
|------|--------------|
| `src/services/SipEngine.ts` | jssip UA: registration, ICE, session management, in-call controls |
| `src/services/CallManager.ts` | Bridges SIP ↔ native CallKeep (CallKit / ConnectionService) |
| `src/services/PushService.ts` | FCM (Android) + PushKit (iOS) wake-on-incoming |
| `src/services/authStore.ts` | Login + persistent SIP credential storage |
| `src/hooks/useSip.ts` | React binding: engine events → declarative UI state |
| `src/screens/LoginScreen.tsx` | First-run auth config |
| `src/screens/DialpadScreen.tsx` | Home dialpad + registration status |
| `src/screens/IncomingCallScreen.tsx` | Full-screen incoming call (foreground) |
| `src/screens/ActiveCallScreen.tsx` | In-call: Mute · Speaker · Hold · Keypad · Hang up + live timer |
| `src/components/*` | Avatar, DialKey (haptic), CallActionButton |
| `src/App.tsx` | Root state machine (login gate + call overlays) |
| `index.js` | Boot order + FCM background handler |
| `DEPENDENCIES.md` | npm packages + iOS/Android native setup |

## Bring-up checklist
1. **Certs** — install a publicly-trusted TLS cert (`asterisk.crt/key`) so `wss://` works.
2. **pjsip.conf** — fill trunk creds, provider FQDN, per-engineer passwords; `pjsip reload`.
3. **extensions.conf** — set `OUTBOUND_CID` to your DID, map each DID → engineer; `dialplan reload`.
4. **ephone-push.sh** — `chmod +x`, drop in `/usr/local/bin/`, point it at your push relay API.
5. **App** — `npm i` (see `mobile/DEPENDENCIES.md`), configure APNs `.p8` + `google-services.json`, run.
6. **Verify** — `pjsip show endpoint 6001` shows *Avail* after the app logs in; place a test call.

## Design notes / decisions you may want to revisit
- **Inbound = direct-to-person (DID→engineer).** For dispatch-style pooling
  (round-robin across available engineers, like Uber/Zomato), convert
  `[ring-engineer]` to an Asterisk `Queue()`.
- **TURN is optional in config but effectively required** for engineers on
  carrier CGNAT — stand up coturn and fill the TURN lines in `rtp.conf` +
  `config.ts` or you'll hit one-way / no audio.
- **SIP password on device** — the direct-login path stores the SIP secret
  locally. For production prefer `loginWithApi()` which exchanges company creds
  for a short-lived SIP secret.
