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 viaPOST /api/v1/auth/user-login→ 24h org-scoped role-JWT (PlatformAuthactor caches) →Authorization: Beareron 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 revision4d10b72e— bumps are deliberate). Single-account enforced:AccountLoginViewModel.login()clears accounts/auth first (stacked accounts broke outbound). Defaultsdevsip.astradial.com+ UDP. - Repos (all free tier): upstream Belledonne → oss
astradial/astradial-ios-oss(public, primary dev, branchastradial, CI: TruffleHog + macOS build, branch protection) → originastradial/astradial-ios(private mirror,private-main). Claude cannot push to oss (permission hard-block) — Hari runsgit 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)¶
- API
calldatehas fractional seconds (.000Z) — plain ISO8601DateFormatter rejects it → all-zero analytics with no error.CDRCall.isoFractionalhandles it. Any new date field: parse likeTicket.parse(withFractionalSeconds). - linphonesw exports
Content— colliding with SwiftUIViewModifier.Content; use View extensions, not ViewModifiers, in files importing linphonesw. - Upstream forced light mode via
INFOPLIST_KEY_UIUserInterfaceStyle = Light(removed). - SDK
stablehead drifts from app master (beta-API) — shims live at top ofNativePhoneUI.swift;isMe(uri:)flip-flops between SDK versions. - 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).
- gitleaks-action requires paid license for orgs → CI uses TruffleHog.
- 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 rejectedmosaicMode()'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. - Multi-org emails:
user-loginpicks first org row (LIMIT 1) — use org-specific login; org name displayed under Analytics title. - Simulator:
xcrun simctl ui <udid> appearance dark; sim UDID used throughout:1B29CD4D-C16D-405D-BA77-F4FE40A89E5B(iPhone 17 Pro); build withDEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer. - Settings → Developer → API Diagnostics shows raw status/body for the app's exact requests — use it before guessing.
- Apple identifiers are globally unique, per-account. The old free personal team (
973K592P56) had already registeredcom.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 idcom.astradial.softphone, app groupgroup.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)". - PushKit registry must run on the MAIN queue, not
coreQueue. Running it oncoreQueuelet VoIP token delivery fire duringCoreContextstartup and reenter the core-startup queue → app hung on launch (transient). Fix:PushKitManagerowns a.main-queuePKPushRegistry;start()is idempotent (commitfc80b582e). Don't move it back tocoreQueue.
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