Astradial Phone (iOS) — Agile Delivery Plan & UAT Test Cases
Living document. Owns the road from feature-complete to App Store launch, following: Plan → Gather requirements → Develop → UAT (per feature) → Staging test (entire app) → Launch.
Last updated: 2026-07-02. Per-build regression checklist (run every build): test-plan.md. Status board / continuation notes: status.md. Open decisions: decisions.md.
Legend: ✅ pass · ❌ fail · ⬜ not run · 🚫 blocked · P1 = launch-blocker, P2 = should-fix, P3 = nice-to-have.
Phase 1 — Plan
| Item | Value |
| Scope | iOS softphone (branded linphone-iphone) for Astradial cloud PBX users |
| Environments | Staging: stagesip.astradial.com:5080 / API stagepbx.astradial.com · Prod: devsip.astradial.com:5060 / API devpbx.astradial.com (app default base) |
| Builds | Debug → APNs sandbox, token env dev · Release/TestFlight → APNs production, token env prod |
| Test devices | iPhone 12 mini (primary, in hand) · one additional device ≥ iOS 17 before launch |
| Roles | Claude: sim-testable flows + build gate, log analysis, fixes · Hari: all 🅓 device-only rows (push, CallKit, audio) |
| Exit criteria (whole plan) | All P1 UAT cases ✅ on staging AND prod, zero open P1 defects, Phase 5 regression ✅, Launch checklist ✅ |
Phase gate: this table agreed → requirements below frozen for v1.
Phase 2 — Gather requirements
Feature inventory = requirement list. Acceptance criterion for each = its UAT section passing.
| # | Requirement | UAT section | v1? |
| R1 | Sign in (Firebase → org JWT), SIP line attach, logout | AUTH | ✅ |
| R2 | SIP provisioning: QR, manual (host:port), SIP-user dropdown | PROV | ✅ |
| R3 | Outbound calls with full in-call controls | OUT | ✅ |
| R4 | Inbound calls: foreground, background, killed (VoIP push wake), locked | INB / WAKE | ✅ |
| R5 | CallKit correctness: answer/decline/end from every surface, native call UI | CK | ✅ |
| R6 | Audio: earpiece/speaker/bluetooth, mute, DTMF, hold, proximity | AUD | ✅ |
| R7 | Resilience: network flap, re-registration, offline UX | NET | ✅ |
| R8 | Call history (platform CDR fetch) | HIST | ✅ |
| R9 | Contacts (org directory) | CONT | ✅ |
| R10 | Platform integration: VoIP token registration, analytics tiles, tickets | PLAT | ✅ |
| R11 | Settings, dark mode, Help | MISC | ✅ |
| R12 | Conversations (chat), Meetings, Recordings tabs | MISC | ⬜ decide — hide or ship (see decisions.md) |
Phase gate: every v1=✅ row maps to UAT cases below; R12 decision made before Phase 5.
Phase 3 — Develop
Definition of done per feature: code merged to app repo master + xcodebuild green + covered UAT rows runnable. Current engineering state (2026-07-02):
| Work item | State |
VoIP push wake — app side (refreshRegisters on empty call-id, wake keepalive, placeholder re-keying, answer-before-INVITE) | ✅ built, on device; uncommitted in working tree |
VoIP push wake — server side (APNs sender, /asterisk/voip-push-wait, dialplan CURL) | ✅ merged to main; live staging + prod (prod env armed 2026-06-30, .env.bak-20260630-233937) |
| Firebase early-config race fix | ✅ built, on device; uncommitted; verify in next capture |
| SDK log level | ⚠ temporarily .Message for debugging — revert to .Warning before release build |
| Hangup from iPhone doesn't end far side (DEF-001) | ❌ open — next dev item |
| Prod wake token seeding + regen | 🚫 blocked on prod endpoint + device token (option B agreed) |
| Known launch bugs from test-plan.md (first-launch splash hang, first-call hang) | ❌ open, P1 |
Phase gate: working tree committed, DEF-001 fixed, P1 bugs fixed → enter Phase 4 formally.
Phase 4 — UAT, per feature (device unless marked 🅢)
Run on staging/Debug first; repeat the P1 rows on prod/Release before launch. Record date + build in the Status column.
AUTH — Sign-in & accounts
| ID | P | Steps | Expected | Status |
| AUTH-01 | P1 | Fresh install → sign in with valid Firebase account | Org JWT obtained; MD tabs (History/Tickets/Analytics) populate | ⬜ |
| AUTH-02 | P1 | Attach SIP user from dropdown | Registers within ~5 s; status chip shows user | ⬜ |
| AUTH-03 | P1 | Log out | SIP line cleared (unREGISTER), chip gone, no stale org data | ⬜ |
| AUTH-04 | P2 | Wrong password | Clear error, no hang | ⬜ |
| AUTH-05 | P2 | Skip login | Dialer usable; gated tabs show sign-in prompt | ⬜ |
| AUTH-06 | P2 | Multi-account switch | Old line cleared, new org's SIP line restored | ⬜ |
| AUTH-07 | P2 | Sign Out | Full reset: Firebase + saved accounts (Keychain) + all prefs/caches wiped; app returns to first onboarding screen | ⬜ |
| AUTH-08 | P2 | Settings → Startup Page = Keypad/Tickets/Analytics, relaunch | App opens on the chosen tab; persists across launches; role default when unset (owner→Analytics, agent→Tickets) | ⬜ |
PROV — SIP provisioning 🅢 (registration result on device)
| ID | P | Steps | Expected | Status |
| PROV-01 | P1 | QR scan from editor | Account created + registers | ⬜ |
| PROV-02 | P1 | Manual entry with custom host:port (staging :5080) | Registers | ⬜ |
| PROV-03 | P2 | Quick-connect list contents | Every user with sip_port='wss' + an asterisk_endpoint, regardless of routing; AI agents (sip_port='5080') hidden | ⬜ |
| PROV-04 | P1 | Switch SIP user | Old contact removed from Asterisk (pjsip show contacts), new registers | ⬜ |
| PROV-05 | P1 | Set a wss user's routing to phone number, reopen Quick connect | User still appears and Proceed registers (routing ≠ visibility gate) | ⬜ |
| PROV-06 | P2 | Settings → Connect Line → Choose → pick user → Proceed | Pushes the connect screen; pops back to Settings once registered | ⬜ |
| PROV-07 | P3 | Tap Proceed | Button does not resize or shift text (opacity-only press) | ⬜ |
OUT — Outbound calls
| ID | P | Steps | Expected | Status |
| OUT-01 | P1 | First call immediately after cold launch | Connects + 2-way audio (known-bug row) | ❌ open bug |
| OUT-02 | P1 | Subsequent call | Connects + 2-way audio | ⬜ |
| OUT-03 | P1 | Hang up from app | Far side ends immediately (BYE sent) | ⬜ |
| OUT-04 | P1 | Far side hangs up | App call ends, UI returns cleanly | ⬜ |
| OUT-05 | P2 | Call invalid/unroutable number | Proper error tone/message, no stuck screen | ⬜ |
| OUT-06 | P2 | Duration timer | Counts up, not stuck at 0:00 | ⬜ |
| OUT-07 | P2 | Outbound caller-ID | Far side sees the org's configured outbound DID | ⬜ |
INB — Inbound, app foreground / background
| ID | P | Steps | Expected | Status |
| INB-01 | P1 | App open, call arrives | CallKit rings; answer → 2-way audio | ⬜ |
| INB-02 | P1 | Decline | Caller leg follows org failover (voicemail/announce), not dead air | ⬜ |
| INB-03 | P1 | App backgrounded (not killed), call arrives | Full-screen CallKit; answer works | ⬜ |
| INB-04 | P1 | Answer from lock screen (backgrounded) | Audio connects | ⬜ |
| INB-05 | P2 | Second inbound during active call (call waiting) | CallKit offers hold&accept / decline; both behave | ⬜ |
WAKE — Inbound, app KILLED (VoIP push wake) — headline feature
Staging validation 2026-06-30, iPhone 12 mini, Debug/sandbox: WAKE-01..04 ✅ (log d1.rtf: push → REGISTER 200 ~1 s → INVITE ~3 s → StreamsRunning).
| ID | P | Steps | Expected | Status |
| WAKE-01 | P1 | Swipe-kill app → call the extension | Push wakes app; CallKit rings in ≤ ~5 s | ✅ stg 06-30 |
| WAKE-02 | P1 | Answer after a moment of ringing | Connects, 2-way audio | ✅ stg 06-30 |
| WAKE-03 | P1 | Answer from locked screen | Connects, 2-way audio | ✅ stg 06-30 |
| WAKE-04 | P1 | Caller hangs up before answer | Ring stops, call screen dismisses cleanly | ✅ stg 06-30 |
| WAKE-05 | P1 | Hang up from iPhone after answering a wake call | Far side ends immediately (BYE) | ✅ our stack (07-03, packet-proven); far-side stuck UI = third-party client bug (Error 83) |
| WAKE-06 | P1 | Answer instantly (before INVITE lands) | Deferred-accept connects it; no dead call | ✅ stg 07-03 (phantom-kill fix) |
| WAKE-07 | P1 | Don't answer at all | Placeholder ends ≤ 30 s; no stuck CallKit call; caller hits failover | ⬜ |
| WAKE-08 | P2 | Kill app, phone on WiFi off / LTE only | Wake still works over cellular | ⬜ |
| WAKE-13 | P1 | Phone fully offline (airplane) → call | Failover to phone number in ~5-6s (wake v2: no ack → no) | ✅ stg 07-03 |
| WAKE-14 | P1 | After WAKE-13, internet returns | No fake call (apns-expiration:0 drops undelivered pushes) | ✅ stg 07-03 |
| WAKE-09 | P2 | After device reboot + first unlock | Wake works (iOS requires one unlock) | ⬜ |
| WAKE-10 | P1 | Prod repeat of WAKE-01..05 (Release build, prod APNs, prod extension) | Same as staging | 🚫 token seed pending |
| WAKE-11 | P2 | App already foreground when push would fire | No duplicate ring (gateway skips push when registered) | ⬜ |
| WAKE-12 | P3 | Two devices, same user | Sane behaviour: both ring or documented single-device rule | ⬜ |
CK — CallKit correctness
| ID | P | Steps | Expected | Status |
| CK-01 | P1 | Native call UI shows caller name/number for inbound | Real caller-ID, updates from placeholder on wake calls | ⬜ |
| CK-02 | P1 | End call from native (lock-screen) UI vs in-app red button | Both end both legs — log [Hangup]/CXEndCallAction | ⬜ (DEF-001 related) |
| CK-03 | P2 | Native phone call arrives during Astradial call | Hold/interleave sanely; our call survives or ends cleanly | ⬜ |
| CK-04 | P2 | Recents: Astradial calls in iOS call log | Present with correct handle (or documented off) | ⬜ |
AUD — Audio & in-call controls
| ID | P | Steps | Expected | Status |
| AUD-01 | P1 | Mute / unmute | Far side stops/starts hearing you | ⬜ |
| AUD-02 | P1 | Speaker toggle | Route changes; survives screen lock | ⬜ |
| AUD-03 | P1 | DTMF in IVR | Digits register (test against org IVR) | ⬜ |
| AUD-04 | P2 | Hold / resume | Far side hears silence/MOH; resume restores audio | ⬜ |
| AUD-05 | P2 | Bluetooth headset connect mid-call | Route follows; disconnect falls back | ⬜ |
| AUD-06 | P2 | Proximity sensor | Screen off at ear, on when away | ⬜ |
NET — Network resilience
| ID | P | Steps | Expected | Status |
| NET-01 | P1 | WiFi → LTE mid-call | Call survives (or reconnects) within a few s | ⬜ |
| NET-02 | P1 | Airplane mode 10 s mid-call → off | Recovers or ends cleanly; app re-registers after | ⬜ |
| NET-03 | P2 | Launch offline | UI reachable, offline state shown, no splash hang | ⬜ |
| NET-04 | P2 | Registration expiry / stale NAT | App re-registers without user action; inbound still works after 1 h idle | ⬜ |
HIST / CONT / PLAT / MISC — App surfaces 🅢 (data needs device+org)
| ID | P | Steps | Expected | Status |
| HIST-01 | P1 | History tab after test calls | CDRs appear (dated fetch; self-heal path works) | ⬜ |
| HIST-02 | P2 | Tap history entry → call back | Dials correct number | ⬜ |
| CONT-01 | P2 | Contacts tab | Org directory listed, no device-contacts permission prompt | ⬜ |
| CONT-02 | P2 | Call a contact | Dials | ⬜ |
| PLAT-01 | P1 | Fresh install + SIP attach posts VoIP token | Row in platform DB with right env (dev/prod); HTTP 200 in log | ✅ stg 06-29 |
| PLAT-02 | P2 | Analytics tiles load | Numbers match editor | ⬜ |
| PLAT-03 | P2 | Tickets: badge, open, change status | Syncs with platform | ⬜ |
| MISC-01 | P2 | Dark mode across all tabs | No unreadable/white-flash screens | ⬜ |
| MISC-02 | P2 | Settings toggles persist across relaunch | Persisted | ⬜ |
| MISC-03 | P3 | Help screens open | No dead links | ⬜ |
| MISC-04 | P1 | Launch checks (A1–A6 of test-plan.md) | Per that doc — includes the two open P1 launch bugs | ❌ 2 open |
Phase gate: all P1 rows ✅ on staging/Debug → Phase 5. (Prod/Release P1 repeats may run in parallel with Phase 5 once tokens are seeded.)
Phase 5 — Staging test, entire app (regression + soak)
Full-pass, one sitting, on a staging org that mirrors prod (queues, IVR, failover, recording):
| ID | Scenario | Status |
| REG-01 | Entire test-plan.md release gate (A–H) in order | ⬜ |
| REG-02 | Full P1 UAT sweep above, same build, no code changes between | ⬜ |
| REG-03 | Cross-feature: receive wake call → while in-call browse History/Tickets → hang up → history updates | ⬜ |
| REG-04 | Queue member scenario: extension in a queue, killed app — queue call wakes it; agent pause/unpause via *5 if applicable | ⬜ |
| REG-05 | 24 h soak: app killed overnight, first morning call wakes it (token + registration longevity) | ⬜ |
| REG-06 | Battery: killed between calls, no abnormal drain overnight (iOS battery screen) | ⬜ |
| REG-07 | Two-user test: two devices, A calls B both directions, hangups both directions clean | ⬜ |
| REG-08 | Recording-enabled call: indicator behaviour + recording lands platform-side | ⬜ |
Phase gate: REG-01..08 ✅, zero P1 defects open, build ID frozen → Launch.
Phase 6 — Launch
| # | Gate | Status |
| L-00 | GPLv3 licensing resolved — app is a linphone-iphone (GPLv3) fork; GPL and App Store terms are legally incompatible (FSF position; why Belledonne sells commercial SDK licenses). Options: buy Belledonne commercial license, or accept the legal risk knowingly. Tracked in decisions.md. Blocks submission. | ⬜ |
| L-01 | SDK log level reverted to .Warning; debug ★★★ VOIP_TOKEN line removed/DEBUG-only confirmed | ⬜ |
| L-02 | All working-tree changes committed + pushed (app repo) | ⬜ |
| L-03 | Release build (prod APNs entitlement production, API base devpbx.astradial.com) archives + TestFlight upload | ⬜ |
| L-04 | Prod E2E: WAKE-01..05 on TestFlight build against prod extension (real token self-seed → regen run → test). Note: token POST does not auto-regen the dialplan — run regen-all-org-configs.js or ship the auto-regen fix first | ⬜ |
| L-05 | Rollback plan written: prod .env backup exists (.env.bak-20260630-233937); pulling APNS_*/INTERNAL_API_URL + regen reverts wake feature server-side; app rollback = previous TestFlight build | ✅ |
| L-06 | App Store assets: screenshots, privacy manifest/labels (mic, contacts-none, push), review notes incl. demo SIP account for Apple review | ⬜ |
| L-07 | Post-launch monitoring: APNs error alerting (push-4xx reasons in api logs), Crashlytics watch for first 48 h | ⬜ |
| L-09 | PrivacyInfo.xcprivacy — none exists in the repo (audit 07-03); required by Apple since May 2024 (UserDefaults/File-timestamp API declarations + data-collection categories for Firebase/Crashlytics). Rejection risk without it. | ⬜ |
| L-10 | ITSAppUsesNonExemptEncryption=false in Info.plist (standard-crypto exemption) — missing today, App Store Connect will block/ask every upload | ⬜ |
| L-11 | Verify mic permission string reaches the built Info.plist (not in source Info.plist — likely INFOPLIST_KEY_ build setting; confirm on archive) + review remaining org.linphone.phone URL-scheme/category ids (cosmetic, check QR flow before renaming) | ⬜ |
| L-12 | Real GoogleService-Info.plist in the archive (repo copies were replaced with dummies in 4ff9f2013; root-level copy present — verify it's the real prod one) + Crashlytics dSYM upload | ⬜ |
| L-13 | Review-features pass: R12 decision (hide unfinished Conversations/Meetings/Recordings tabs — reviewers reject empty/broken surfaces), DEF-006 Quick Connect empty-list fixed (reviewer's first login experience), demo SIP account + review notes for Apple | ⬜ |
| L-08 | Docs: update status.md + this file to "launched vX.Y"; troubleshooting entries for any defect found in UAT | ⬜ |
Defect log
| ID | Sev | Description | Found | Status |
| DEF-001 | P1 | iPhone hangup doesn't end far side | 2026-06-30 UAT | Closed 07-03: our stack packet-proven correct; far-side client (Telephone 1.6) ignores BYE — troubleshooting Error 83 |
| DEF-002 | P2 | Firebase "default app not configured" race on push-launch | 2026-06-30 log d1 | Verified fixed 07-03 (0 occurrences in R5 capture) |
| DEF-003 | P1 | First-launch 10-min splash hang (device-only) | test-plan.md | Open |
| DEF-004 | P1 | First outbound call after launch hangs; second works | test-plan.md | Open |
| DEF-005 | P2 | Platform: token POST didn't trigger dialplan regen | 2026-06-30 review | Fixed PR #494 (regen on token-presence flip) |
| DEF-007 | P1 | Background-answer: in-call UI ~10s late (liblinphone phantom pushkit session blocked accept) | 2026-07-03 R5 | Fixed + verified 07-03 (phantom-kill on IncomingReceived, app 06d4044a8) |
| DEF-008 | P2 | "Wrong username or password" toast on every app open (PUBLISH 489 + unscoped toast) | 2026-07-03 | Fixed (publish off + toast scoped to interactive logins) — verify on next install |
Traceability
R1→AUTH · R2→PROV · R3→OUT · R4→INB+WAKE · R5→CK · R6→AUD · R7→NET · R8→HIST · R9→CONT · R10→PLAT · R11/R12→MISC. Launch = Phase 6 only.