Skip to content

Astradial iOS — Native UI Specification

Goal: a user who picks up the app must believe it is the iOS Phone app. This page is the screen-by-screen contract. Reference screenshots are from the stock iOS Phone app (iOS 26, "Liquid Glass" design language) on the founder's iPhone — the canonical source of truth when this text and a screenshot disagree is the current stock Phone app on the latest iOS.

Design-system ground rules

These apply to every screen. They are what makes the app feel native — getting layouts right but these wrong still reads as fake.

Aspect Rule
Framework SwiftUI, iOS native controls only. No custom-drawn lookalikes where a system control exists.
Typography System font (SF Pro) via SwiftUI text styles only (.largeTitle, .title, .body, .caption…). Never hard-code font names or sizes.
Icons SF Symbols exclusively (star.fill, clock.fill, person.crop.circle.fill, circle.grid.3x3.fill, recordingtape, phone.fill, info.circle, mic.fill, plus, chevron.backward, line.3.horizontal.decrease).
Colors Semantic system colors only (Color(.systemBackground), .secondary, .systemGray5, system green for the call button, system blue for selection/links, system red for missed calls). Automatic dark-mode support comes free — never hard-code hex.
Tab bar Standard SwiftUI TabView with 5 tabs. On iOS 26 this renders as the floating "liquid glass" pill automatically — do not custom-build the pill. Order: Favourites, Recents, Contacts, Keypad, Voicemail.
Navigation NavigationStack with large titles where the stock app uses them (Recents, Favourites, Voicemail). Contacts pushed detail uses inline title with back chevron.
Search .searchable modifier → native search bar with the mic (dictation) affordance.
Haptics System defaults; keypad presses use the same click feedback as the stock dialer (UIImpactFeedbackGenerator light / keyboard click sound per stock behaviour).
Animations System defaults. No custom transition curves.
Accessibility Dynamic Type, VoiceOver labels on all controls — the stock app supports these, so must we.

Tab 1 — Keypad

Reference: screenshot IMG_6863 (stock keypad).

Layout, top to bottom:

  1. Line/identity chip (top-left): small rounded-square avatar with the account's initial + up/down chevrons. In the stock app this is the dual-SIM line selector; in Astradial it selects the SIP account / outbound identity when more than one account is configured. Hidden if only one account exists? No — stock shows it with one line too; match stock. Tapping presents the account picker sheet.
  2. Number display area: large, initially empty. As digits are entered they appear centred in large light type, shrinking as the number grows; an "Add Number" / matched-contact hint appears under it exactly as in stock. Backspace (delete.backward) appears to the right of the number once non-empty.
  3. Keypad grid: 4 rows × 3 columns of circular systemGray5 buttons. Digit large and centred; letters (ABC, DEF…) in small spaced caps below; 0 shows + beneath (long-press 0 → +); * and # have no sub-label. Long-press and multi-touch behave like stock.
  4. Call button: large green circle, white phone.fill, centred below the grid. Tap with empty number recalls the last dialled number into the display (stock behaviour).

Function mapping: digits feed liblinphone's outgoing-address builder; during an active call the same keypad sends DTMF (RFC 4733).

Tab 2 — Recents (call logs)

Reference: screenshot IMG_6865 (stock Recents).

  1. Header row: Edit button (left, glass capsule), All / Missed segmented pill (centre), filter icon (line.3.horizontal.decrease, right).
  2. Large title Recents, then a search bar (.searchable).
  3. List rows, each:
    • Avatar: contact photo, or initials in the system gradient circle, or generic person silhouette for unknown numbers.
    • Line 1: contact name or raw number, bold. Missed calls in red.
    • Line 2 (secondary): direction arrow (arrow.down.left incoming / arrow.up.right outgoing) + line badge (the small "H" square in stock = which line; for us, which SIP account) + number label (mobile, phone, or carrier/location string).
    • Right side: relative timestamp (Yesterday, 09:41, weekday, date — same formatting rules as stock) + info.circle button → pushes the call-detail / contact card.
    • Consecutive calls with the same party/direction collapse into one row with a (3) count, as stock does.
  4. Edit mode: swipe-to-delete per row, Edit → select/delete, Clear all.

Data source: liblinphone CallLog history (core.callLogs), merged and rendered with the rules above. Missed-call filter = logs with status missed/declined.

Tab 3 — Contacts

Reference: screenshot IMG_6864 (stock Contacts).

  1. Nav bar: back chevron in glass circle (left), inline title Contacts, + in glass circle (right) → native new-contact form.
  2. Search bar with mic icon.
  3. My Card row: user photo + name + My Card subtitle.
  4. Alphabetical sections with sticky section headers and the A–Z index rail on the right edge.
  5. Rows: initials-avatar or photo + name with surname bolded (respect the iOS name-display setting).
  6. Contact detail: native contact card; phone numbers tap-to-call via Astradial (liblinphone), with the standard actions row (message, call, video, mail) — non-call actions may hand off to system apps.

Data source: the iOS address book (CNContactStore) exactly like stock, plus Linphone SIP-address contacts merged in (Linphone's Friend list / remote directory if configured). Permission prompt on first launch, with the standard limited-access flow on iOS 18+.

Tab 4 — Favourites

Superseded (June 2026): the Favourites tab was replaced by the MD Analytics tab (Apple Health-style dashboard) at Hari's request — see MD Analytics Dashboard. The spec below is kept in case favourites return as a sub-feature.

(No screenshot yet — match stock.)

  • Large title Favourites, + to add (picker over contacts → choose number + action), Edit to reorder/remove.
  • Rows: avatar + name + label of the favourited number; tap = call immediately via Astradial.

Tab 5 — Voicemail

  • Large title Voicemail. Stock shows a list with inline playback (scrubber, speaker, delete) and transcription.
  • Astradial backend: Asterisk voicemail (app_voicemail). v1 may ship as: list via VM APIs if/when exposed, or a "Call Voicemail" action dialling *97-style feature code while the full visual- voicemail backend is designed. UI must still be the native layout — an empty state ("No Voicemail") is acceptable for v1 if the backend isn't ready. Decision tracked in iOS App → Open questions.

In-call screen

Use CallKit's native in-call UI (as Linphone already does when CallKit is enabled). This gives the system call screen, lock-screen answering, call waiting, and the green status pill for free — the strongest possible "native" signal. The in-app call screen (reached by tapping the green pill) must mirror the stock layout: large name/number top-centre, call timer, 6-button grid (mute, keypad, speaker / add call, FaceTime-slot → hidden or transfer, contacts), red end-call button bottom-centre.

Onboarding — "Connect Your Line" (SIP line setup)

Reached after Firebase sign-in (owners then land on Analytics), and reused verbatim from Settings → Connect Line. Screen (OnboardingSIPView / OnboardingSIPQuickView, AstraCall/Screens/OnboardingUI.swift):

  • Connect Type segmented control at the top: Quick (default) · Manual · Scan QR.
  • Quick — a native, greyed search bar below the toggle, then a single rounded card containing all mobile-enabled SIP users, rows separated by hairline dividers (Contacts/Call-Logs style). Each row: initials InitialsAvatar (blue-grey gradient) + name + Extension ####; the selected row shows a trailing blue checkmark (no ring/box). A fixed "Proceed with ####" solid-accent capsule floats pinned at the bottom (SolidCapsuleButtonStyle — opacity-only press feedback, never scales/shifts). Default selection = the signed-in user's own extension, else the first row.
  • Visibility gate: a user appears iff sip_port='wss' and has an asterisk_endpoint — never gated on call routing. See Mobile App Access.
  • Manual — Username / Password / Domain / Transport + Connect. Scan QR — camera scan.
  • Later (toolbar) or connecting a line dismisses the screen. Owners with a single line auto-select it; no password needed for Quick.

Settings

Native iOS grouped Form. Relevant sections:

  • SIP Account: Status (Registered dot), Line, Server URL, and Connect LineChoose › — a nav row pushing the "Connect Your Line" screen above; it auto-pops once a line registers. (The old inline "SIP Users" picker + Connect Type dropdown were removed.)
  • General → Startup Page: picker for which tab opens on launch — Keypad · Tickets · Analytics. Role-aware default (owner/manager → Analytics, agent → Tickets, signed-out → Keypad), user-overridable, persisted device-locally (astradial_launch_page).
  • Accounts: current account row (name + Org · Role, with a 🗑 trash to remove), other saved accounts (tap to switch), Add Account, and Sign Out. Sign Out is a full reset — clears Firebase, saved accounts (Keychain), all app prefs/caches, and returns to the first onboarding screen (fresh-install state).

What we deliberately do NOT carry over from Linphone's UI

Linphone 6's own SwiftUI UI (sidebar/assistant/chat/meetings styling, orange branding, its own tab structure) is entirely discarded. Keep the view-model ↔ core wiring where convenient, but no Linphone visual element may survive into Astradial builds: app icon, splash, colors, layouts, fonts, the assistant onboarding — all replaced. Provisioning is Astradial QR scan + manual entry only.

Acceptance test for "100% native"

Put the app side-by-side with the stock Phone app on the same iPhone:

  1. Same five tabs, same icons, same order, same selected-state look.
  2. Keypad digits, letter labels, spacing, and call-button geometry indistinguishable at a glance.
  3. Recents rows: same two-line structure, arrows, badges, timestamp wording, info button.
  4. Contacts: same search, My Card, section index rail, surname bolding.
  5. Dark mode, Dynamic Type (largest size), and VoiceOver all behave like stock.
  6. A failed item = a bug, not a nice-to-have.