Skip to content

iOS App — Status, Architecture & Continuation Guide

Living document — update after every working session. Last updated: 2026-07-03 (session: VoIP push wake working E2E on staging — killed/background/locked/offline/reconnect all verified by Hari. Wake decision v2: push-ack delivery receipt + qualify verification, dead phones hit failover in ~5-6s; apns-expiration: 0 kills fake calls on reconnect; registration capped 300s / floored 30s with an in-app expiry picker. App: liblinphone phantom-pushkit session killed on INVITE (10s answer delay fixed), caller number on the wake screen, false "Wrong username or password" toast fixed, user-visible Linphone strings renamed to Astradial. Design + runbook: voip-push-wake.md; bugs → troubleshooting Errors 78–83. Platform PRs #494–#497 merged; app commits 3cf093746 → 06d4044a8 + popup/rename build. Next: App Store prep — Phase 6 of ios-uat-test-plan.md, and the OPEN GPLv3 licensing decision in decisions.md which gates submission.) Purpose: lets any future session (human or Claude) continue exactly where we left off.

📱 App Store — 2.1(a) rejection → build 6 resubmit (2026-07-13)

Store app = com.astradial.astracall = repo Mobile App/astradial-astracall (the GPL-free AstraCall build). The old Linphone astradial-ios / com.astradial.softphone is NOT on the store — don't edit it for store work.

Rejected twice, Guideline 2.1(a) ("app unresponsive after login," reviewed on iPad Air). Root cause: after Firebase login the app dumped the reviewer on a mandatory "Connect Your Line" SIP screen with no obvious path forward. Fixes shipped in build 6:

  • Reworked Connect Your Line (Quick/Manual/Scan, glassy single-card SIP chooser, floating Proceed) + Settings → Connect Line → Choose reusing it; Startup Page setting; Sign Out = full reset. See ios-ui-spec.md.
  • Quick-connect visibility fix: gate on sip_port='wss' only, never routing — phone-routed wss lines now appear. See mobile-app-access.md.
  • In-app Privacy Policy link; account-deletion handled by the enterprise exemption (no in-app sign-up) stated in review notes.
  • Device family = iPhone only (TARGETED_DEVICE_FAMILY=1) — reviewed on iPad in compatibility mode; the "unresponsive" fix is the login flow, not iPad layout.

Build-number gotcha (fixed): xcodegen wrote a literal CFBundleVersion=1 into Info.plist that overrode CURRENT_PROJECT_VERSION, so an upload shipped as build 1 (duplicate). project.yml now sets CFBundleVersion: "$(CURRENT_PROJECT_VERSION)". Build-number rule + log: App Store/A tour to app store.md. Marketing 1.0, build 6.

🚀 AstraCall engine — Phases A+B PASSED (2026-07-03 evening)

Our own GPL-free engine made real calls. Same day as the decision: AstraSIP (Swift SIP-over-WSS, 11 unit tests) registered to staging (REGISTER→digest→200, qualify answered, expiry honored) and completed bidirectional 2-way-audio calls app↔Mac(1001) — recorded by MixMonitor, clean hangups both ways, ICE+DTLS clean on first attempt. Repo: astradial/astradial-astracall (private, main). Test rig: ext 1003 = e1003ra9v72 on sip_port='wss' (platform PR #498). Next: Phase C (CallKit + PushKit + wake-v2 client port — server side reused unchanged). Continuation doc for ANY agent: astracall-execution-blueprint.md.

Android own-engine build (same GPL-free decision, mirrors this design): astracall-android-plan.md — Phase 0 (freeze + clean-room scaffold + ours/derived inventory) underway 2026-07-04; server already WSS-ready, so Android starts at Phase A.

⚠️ Preservation point — the Linphone app is safely frozen

The liblinphone-based app (everything above, fully working) is permanently recoverable: tag linphone-stable-2026-07-03 and branch linphone-stable on astradial/astradial-ios (commit 259704a14), pushed to GitHub 2026-07-03. Revert/build it anytime: git checkout linphone-stable-2026-07-03 → build → install. The working build also remains installed on Hari's iPhone. The AstraCall engine replacement (astracall-engine-plan.md) lives in a separate repo (astradial-astracall) and cannot touch this one.

What the app is now

Astradial Phone (com.astradial.softphone) — native iOS dialer on the Linphone engine + MD analytics for hospitals. Signed by the Astradial Private Limited paid developer team (L7RZB3SY47) as of 2026-06-29, so device builds no longer expire every 7 days. The Xcode project/target/scheme are named Astradial (Astradial.xcodeproj, scheme Astradial); the Linphone source folder + linphonesw SDK keep their names (that's the actual library — renaming breaks imports). App display name was already Astradial. Verified with live Thangavelu Hospital / Mithra scans data.

Tabs: Analytics (Fitness-style, sign-in gated) · Recents (device calls + Company scope w/ recordings) · Contacts (org Users & Extensions directory, Work/Out toggle — NO device contacts, no permission) · Keypad (stock dialer; profile/gear button → Settings) · Tickets (sign-in gated, remark-required status changes, open-count badge on tab + app icon). Settings: Accounts section (multi-account switch, swipe-to-remove, Add Account), SIP line (SIP-user dropdown + QR + manual, live status), Appearance picker (System/Light/Dark), ₹/patient, API Diagnostics (Developer).

Multi-account (June 13): AstradialAccounts.swift — AccountStore keeps saved platform accounts in a single Keychain blob (astradial_accounts_v1: Firebase email+password, org/role meta, and the SIP user attached while that account was active). Switching accounts re-signs into Firebase and restores that org's SIP line (clears the line if the target org has none attached; a plain first sign-in never wipes a pre-existing manual/QR line). MDSession.signIn is the single switch path: resets PlatformAuth JWT, clears the (non-org-keyed) PulseViewModel cache, reloads the tickets badge. The SIP-user dropdown (Settings + onboarding) lists human SIP users only (routing_type=sip, ring_target≠phone) and provisions from GET /api/v1/users/:id/sip-credentials with the editor-QR parameters (username = asterisk_endpoint, devsip.astradial.com:5080, UDP — AstradialSIPDefaults). Every successful AccountLoginViewModel.login() records the attachment; the keypad's top-left chip shows the attached SIP user's name.

Verified working: Firebase login → org JWT → live analytics + tickets + org directory + recordings; outbound SIP calls (device); QR SIP provisioning; dark mode; onboarding [Sign In | Skip]. Not yet verified: incoming call with app foreground (test pending). Incoming with app killed: iOS side built (Part B, dormant) — needs Part A (Apple Push key) + Part C (server gateway) to actually ring.

Architecture (decided + implemented)

  • Auth — NO API keys: Firebase email/password (project misssellerai, same as webapp) → app exchanges ID token via POST /api/v1/auth/user-login → 24h org-scoped role-JWT (PlatformAuth actor caches) → Authorization: Bearer on all API calls. Role/org shown in UI (owner gating = signed-in, role displayed). Real plist (misssellerai) is in the repo dir but skip-worktree'd (local only); the committed plist is a dummy placeholder — no real Firebase key ships in git (2026-06-29).
  • VoIP push (iOS side DONE 2026-06-29, dormant): rings-when-killed uses PushKit + CallKit (Apple-required; FCM can't on iOS — and PushKit is the battery-light path). PushKitManager (EarlyPushkitDelegate.swift) requests the VoIP token, forwards it to liblinphone, and on a push reports to CallKit + processPushNotification. Needs Part A (Push capability + APNs .p8) and Part C (server gateway, separate platform session) before it actually rings. See decisions §2.
  • Data: GET /api/v1/calls (client dedup by linkedid, ANSWERED-preferring — matches editor), GET/PUT /api/v1/users, GET/PATCH/POST /api/v1/tickets (+/events). Server base: https://devpbx.astradial.com (prod; stagepbx is behind CF Access — unusable from app). Cache-free ephemeral URLSession.
  • SIP: liblinphone (GitHub mirror BelledonneCommunications/linphone-sdk-swift-ios, pinned to revision 4d10b72e — bumps are deliberate). Single-account enforced: AccountLoginViewModel.login() clears accounts/auth first (stacked accounts broke outbound). Defaults devsip.astradial.com + UDP.
  • Repos (all free tier): upstream Belledonne → oss astradial/astradial-ios-oss (public, primary dev, branch astradial, CI: TruffleHog + macOS build, branch protection) → origin astradial/astradial-ios (private mirror, private-main). Claude cannot push to oss (permission hard-block) — Hari runs git push oss astradial.
  • Key files (all in Linphone/UI/Main/ unless noted): NativePhoneUI.swift (tabs/keypad/recents/org-contacts + SDK shims), AstradialMD.swift (auth, API, analytics tiles+details, settings, diagnostics, shimmer), TicketsUI.swift, OnboardingUI.swift, NativeCallUI.swift (in-call + QR scan), ../../SplashScreen.swift, Launch Screen.storyboard.

Hard-won gotchas (do not re-learn these)

  1. API calldate has fractional seconds (.000Z) — plain ISO8601DateFormatter rejects it → all-zero analytics with no error. CDRCall.isoFractional handles it. Any new date field: parse like Ticket.parse (withFractionalSeconds).
  2. linphonesw exports Content — colliding with SwiftUI ViewModifier.Content; use View extensions, not ViewModifiers, in files importing linphonesw.
  3. Upstream forced light mode via INFOPLIST_KEY_UIUserInterfaceStyle = Light (removed).
  4. SDK stable head drifts from app master (beta-API) — shims live at top of NativePhoneUI.swift; isMe(uri:) flip-flops between SDK versions.
  5. Free personal team: no push entitlements (removed; restore on paid), no NSE — the 3 extension targets are removed from the project (sources on disk; restore wiring from git history when paid).
  6. gitleaks-action requires paid license for orgs → CI uses TruffleHog.
  7. GitHub runners can't reach gitlab.linphone.org → SDK via GitHub mirror. CI build job is GREEN as of 2026-06-22 (commit 80945328b) — both jobs pass (TruffleHog secret scan + macOS build). Getting there needed a Swift fix: the CI toolchain is stricter than local Xcode 26.5 and rejected mosaicMode()'s mixed CGFloat/Double / operands (CallView.swift) as ambiguous — keep grid-sizing math all-CGFloat. Local Xcode silently accepts the mix, so build green locally ≠ green on CI.
  8. Multi-org emails: user-login picks first org row (LIMIT 1) — use org-specific login; org name displayed under Analytics title.
  9. Simulator: xcrun simctl ui <udid> appearance dark; sim UDID used throughout: 1B29CD4D-C16D-405D-BA77-F4FE40A89E5B (iPhone 17 Pro); build with DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer.
  10. Settings → Developer → API Diagnostics shows raw status/body for the app's exact requests — use it before guessing.
  11. Apple identifiers are globally unique, per-account. The old free personal team (973K592P56) had already registered com.astradial.phone + its app groups, so the new company team got "not available / enter a different string" for both. You can't reclaim a free-team identifier (no portal access) — just rename to a fresh one. Result: bundle id com.astradial.softphone, app group group.com.astradial.phone.shared (= APP_GROUP_NAME, the liblinphone data container). Also: a free personal team can't register App Groups at all — make sure the Astradial Private Limited team (L7RZB3SY47) is selected, not "Hari Prasath (Personal Team)".
  12. PushKit registry must run on the MAIN queue, not coreQueue. Running it on coreQueue let VoIP token delivery fire during CoreContext startup and reenter the core-startup queue → app hung on launch (transient). Fix: PushKitManager owns a .main-queue PKPushRegistry; start() is idempotent (commit fc80b582e). Don't move it back to coreQueue.

Pending work (priority order)

# Task Blocked on
1 Incoming call foreground test (app open → call DID → CallKit rings). Closes #10 verification with DTMF/hold/speaker checks. Hari testing
2 Paid team ACTIVE (Astradial Private Limited L7RZB3SY47, approved 2026-06-29; app signs on-device). Remaining: restore push entitlements + extensions, VoIP cert, TestFlight (#3 remainder, #14) platform/app session
3 VoIP push (#12). iOS DONE + CI-green; launch-hang fixed (PushKit on main queue, fc80b582e). Part A DONE (Push cap + APNs key PF5LTXJ66D). Part C BUILT — PR #483 → staging (token endpoint + APNs sender + gated pre-dial hook), check-routes green. Remaining: deploy PR #483 to staging (merge → migration → env + .p8 + restart → res_curl → dialplan regen, all VPS-side = Hari) + on-device test. Prod-safe (staging-branch-only + dialplan gated on token+env). See decisions §2. staging deploy (Hari)
4 Missed-call FCM/APNs notifications (#16) + user-sessions report API (#15 — source: audit_log user.login + org_users.last_login) platform-repo session
5 OSS push + CI green check after each batch. CI now passes green (both jobs) — keep it that way; remember the CI Swift toolchain is stricter than local Xcode (see gotcha 7).
6 Polish backlog: number formatting as-you-type, Dynamic Type/VoiceOver on keypad, contacts registration dots (/users/registrations), A–Z rail scrubbing, ticket SSE live refresh, GPL source link in About before first external build (#13 condition)
7 Android (phase 2): fork linphone-android, same flow; FCM high-priority for calls after iOS ships

Continuation runbook

cd "/Users/hari/AstradialDevelopment/Mobile App/astradial-ios"   # branch: astradial
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
xcodebuild -project Astradial.xcodeproj -scheme Astradial \
  -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build
# Device: Hari presses ⌘R in Xcode (scheme Astradial → hari's iPhone).
# Never run terminal builds while Hari is building in Xcode (build-db lock).
# Commits: push origin astradial + private-main; Hari pushes oss.
# Real GoogleService-Info.plist: present locally, skip-worktree — leave alone.

Related docs: overview · astradial-ios · ui-spec · md-analytics · decisions · repo-cicd