# Calls for iPhone

A SwiftUI iOS 17+ app using the same Go authentication/signaling core and native
WebRTC engine as Calls for Mac. No embedded webview is used. The Xcode project is
generated from `project.yml`; edit that specification and the Swift sources.

## Build

Requires Xcode, Go, and XcodeGen (`brew install xcodegen`). Dependencies are pinned
in the shared Swift package and the isolated native Go module.

```sh
make iphone
# For a signed physical-device build:
CALLS_DEVELOPMENT_TEAM=YOUR_TEAM_ID make iphone-device
```

Build products are `native/build/ios-simulator/Build/Products/Debug-iphonesimulator/CallsPhone.app`
and `native/build/ios-device/Build/Products/Debug-iphoneos/CallsPhone.app`.
The device build uses automatic development signing with your selected team; it
does not upload an app to App Store Connect or TestFlight.

Generate/open the Xcode project for normal device development:

```sh
xcodegen generate --spec native/ios/project.yml
open native/ios/CallsPhone.xcodeproj
```

Select your development team and connected iPhone, then Run. Alternatively,
install a signed app using `xcrun devicectl device install app --device DEVICE_ID`
followed by the device build's `.app` path. The iPhone must be connected and
available to Xcode, with Developer Mode enabled.

## First phone milestone

- **Sign in** to the Calls server (calls.oficina.works unless changed with the
  home screen's gear), or **Join a call as a guest** with a pasted invite link;
  sign in in Safari/the default browser.
- Open the email link normally, compare the browser's code with Calls's code,
  approve, then return to Calls. A member's session is kept in the Keychain
  (this device only) and checked with the server at the next launch; see
  [../README.md](../README.md). **Sign out** forgets it.
- Join using the front camera and microphone; the local video display is mirrored.
  Sent video and remote video retain their original orientation.
- Native remote video, microphone/camera controls, guest admission, chat, and leave.
- Chat is always available beside the call controls. Its button shows new-message
  counts; opening it shows the latest messages and a composer without scrolling
  past the video tiles. Opening chat keeps the call active.
- Members have a Share button beside Chat, with Copy link and the system share menu.
- Camera/microphone permission refusals appear in the app. Retry after changing
  permission in Settings. The simulator has no physical camera.

Calls are foreground-only in this milestone. Switching away ends the call,
except while you are sharing your screen; sign-in survives switching to the
browser. Camera switching, CallKit/background calling, and distribution are
follow-up work. Physical camera, audible audio, Bluetooth routing, rotation while
capturing, and interruption recovery need a real-device acceptance pass.

## Sharing the screen

**Share your screen** in the call is the system's broadcast button
(`RPSystemBroadcastPickerView`, offering only Calls' own extension). The whole
screen is shared, which is the only thing iOS lets any app share. The frames
reach a **broadcast upload extension** (`Broadcast/SampleHandler.swift`, target
`CallsBroadcast`), a separate process with a memory ceiling of about fifty
megabytes. It links only `CallsBroadcastKit` and no WebRTC, which keeps it near
600 KB. It shrinks each frame to 1280 on the long side, at up to fifteen a second,
and passes it to the app over a Unix socket in the **App Group** container
(`calls.sock`). The app is already in the call and sends the frames on the same
`sendonly` screen transceiver the Mac and the browser use. Only one frame is in
flight at a time; any frame that arrives while one is being written is dropped,
never queued.

Stopping works from either end. **Stop sharing** in the app closes the socket;
the extension's next write fails and it ends the broadcast with "You stopped
sharing your screen in Calls." Stopping from Control Center or the red status
bar closes the socket from the other side, and the app stops the share. A
broadcast started with no call to take it ends with "Join a call in Calls, then
share your screen from there."

A call that is sharing stays up when you leave the app, because showing other
apps is the point. That needs the `audio` background mode. Every other call
still ends when the app goes to the background. If the broadcast is stopped
from outside while Calls is in the background, the call ends too.

The App Group is one build setting, `CALLS_APP_GROUP` in `project.yml`
(`group.net.amadan.oficina.calls`). It feeds both targets' entitlements and the
`CallsAppGroup` key in both Info.plists. The extension's bundle identifier is the
app's plus `.broadcast`. The app looks for its extension by that rule, so the
two must stay in step.

**Signing.** The simulator build is unsigned, so it has no App Group and cannot
broadcast; ReplayKit broadcasts need a device anyway. Before the first signed
device build, in the Apple Developer portal (Certificates, Identifiers &
Profiles):

1. Identifiers → App Groups → **+** → `group.net.amadan.oficina.calls`.
2. Identifiers → `net.amadan.oficina.calls.ios` → enable **App Groups** →
   choose that group. (Push stays as it is.)
3. Identifiers → **+** → App IDs → App → `net.amadan.oficina.calls.ios.broadcast`,
   description "Calls broadcast", with **App Groups** enabled and the same group.
4. `CALLS_DEVELOPMENT_TEAM=<team> make iphone-device`. Automatic signing makes
   both development profiles once the identifiers exist, and
   `-allowProvisioningUpdates` can also create steps 1–3 for you if you would
   rather let Xcode do it.

A TestFlight or App Store build needs distribution profiles for **both** bundle
identifiers, and the extension goes up inside the app.

**The TestFlight release branch differs.** `release/calls-testflight` ships
under the bundle identifier `works.oficina.meet`, not
`net.amadan.oficina.calls.ios`. When screen sharing reaches that branch it
needs, in its `project.yml`:

- `CALLS_APP_GROUP: group.works.oficina.meet`
- the `CallsBroadcast` target's `PRODUCT_BUNDLE_IDENTIFIER:
  works.oficina.meet.broadcast` (the app finds its extension as its own
  identifier plus `.broadcast`, so this one is not a free choice)

and, in the Apple Developer portal, the App Group `group.works.oficina.meet`
enabled on both `works.oficina.meet` and `works.oficina.meet.broadcast`, with
distribution profiles for both. The development values above stay as they are
on main.

## Verification

```sh
xcrun simctl list devices available
# Choose an iPhone simulator UUID:
CALLS_IOS_SIMULATOR=YOUR_SIMULATOR_UUID make iphone-browser
```

The Go harness creates isolated databases, meetings, and captured magic-link
emails. iPhone UI tests use the real Safari handoff for members and guests and
compare the authorization codes before returning to the app. No production
credentials are used and no real email is sent. The UI tests launch with
`-CallsForgetSignIn YES`, which signs the app out before anything else: the
simulator's Keychain outlives reinstalls, and a session kept by an earlier run
against a harness server long gone would otherwise decide what the next run sees.

The native media test creates two iOS WebRTC peers with a synthetic camera,
checks decoded video in both directions, continuity through three-second stream
renewals, and removal of the departed peer. Hardware audio is disabled in this
simulator-only test: it neither records the host microphone nor claims to prove
physical audio quality. Synthetic capture is compiled only into test targets.
Xcode logs and result bundles are retained under `native/build/`.
