Lets a native-call user share their iPhone screen (whole device, works
backgrounded). LiveKit 2.15.3 ships the broadcast stack (BroadcastManager +
LKSampleHandler + IPC), so:
- New Broadcast Upload Extension target "BroadcastExtension"
(com.bizgaze.connect.broadcast): SampleHandler.swift subclasses
LKSampleHandler; injected by mobile/scripts/add-broadcast-extension.rb
which also LINKS the LiveKit SPM product into the extension + sets the
App Group. Sources in mobile/ios-broadcast/.
- Plugin: Room created with ScreenShareCaptureOptions(useBroadcastExtension:
true); startScreenShare -> BroadcastManager.requestActivation() (system
picker); stopScreenShare -> requestStop(); BroadcastManagerDelegate ->
fires screenShareState to the web. LiveKit auto-publishes the track.
- ios-patch.sh: RTCScreenSharingExtension + RTCAppGroupIdentifier keys.
- codemagic.yaml: run the injector + sign the 3rd bundle id (.broadcast).
- home.html: toggleScreen native -> start/stop; screenShareState listener
reflects state + broadcasts meeting-screen so peers' stage shows it.
REQUIRES a one-time manual Apple portal step: enable the App Group on the
com.bizgaze.connect.broadcast App ID (see mobile/IOS_SETUP.md) or the
archive fails code-signing. SPM-linked extension is new on our CI — expect
build iteration. Web deployed (no-ops on builds without startScreenShare).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move off CocoaPods (sunsetting; trunk goes read-only Dec 2026) to SPM, the Cap 8
default. This also ends the LiveKit git-pin hack — LiveKit becomes a real SPM dep.
- Each local plugin gets a Package.swift (swift-tools 5.9, iOS 15, capacitor-swift-pm
from 8.0.0, source path ios/Sources/<Plugin>). native-call additionally declares
LiveKit: .package(client-sdk-swift, exact 2.15.3) + product "LiveKit".
- native-call Swift: import LiveKitClient -> import LiveKit (SPM product name).
- codemagic.yaml: `cap add ios --packagemanager SPM`; removed the "Install CocoaPods"
(pod install) step; XCODE_WORKSPACE -> XCODE_PROJECT (App.xcodeproj); build-ipa
--workspace -> --project. Xcode resolves the Swift packages during archive.
- ios-patch.sh: removed the Podfile LiveKit git-pin injection + Podfile.lock deletion
(no Podfile under SPM). Info.plist/entitlements/AppDelegate/notif.wav patches stay.
- add-share-extension.rb is SPM-safe (operates on App.xcodeproj, no workspace/Pods refs).
Authored blind (no local Xcode) — expect build iterations on the first SPM build
(cap-add SPM layout, CapApp-SPM plugin wiring, LiveKit SPM resolution, signing).
Fully revertible: git revert -> back to Cap 8 + CocoaPods (working).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LiveKitClient + LiveKitUniFFI now pre-download OK; the last unresolved dep is
LiveKitWebRTC (= 144.7559.11), also SPM-only (CDN only has 125.x). Its repo ships
a podspec at that tag with NO further deps, but its source is an :http release-zip
(not in the git tree), so we pin via :podspec => <podspec URL> (not :git, which
would clone a repo with no xcframework). SwiftProtobuf resolves from the CDN. This
should complete resolution for LiveKit 2.15.3 on CocoaPods.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
--repo-update still failed: "Unable to find a specification for LiveKitUniFFI
(= 0.0.6)". LiveKitUniFFI is genuinely NOT on the CocoaPods CDN (trunk returns
"No pod found") — it's SPM-only — but its repo ships a podspec at tag 0.0.6 with
no further deps (it just downloads its prebuilt XCFramework). So pin it to its
git tag too, alongside LiveKitClient. LiveKitWebRTC 144.x + SwiftProtobuf resolve
from the CDN. This should complete resolution and keep us on CocoaPods (no SPM
migration).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The git-tag pin worked, but the build failed: "could not find compatible versions
… In snapshot (Podfile.lock): LiveKitClient (= 2.0.18) … In Podfile: LiveKitClient
(from git, tag 2.15.3)". Cause: `npx cap sync` runs `pod install` internally with
the PRE-pin Podfile, creating a Podfile.lock pinned to 2.0.18; our injected git-tag
source then conflicts with that lock. Fix: rm the Podfile.lock right after injecting
the pin so the real "Install CocoaPods" step re-resolves against tag 2.15.3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The LiveKitClient CocoaPod on trunk caps at 2.0.18 (2.1+ is SPM-only), so the
2.15 CallKit audio API was unreachable. BUT the repo still ships a valid podspec
at tag 2.15.3, and its deps (LiveKitWebRTC 144.7559.11, LiveKitUniFFI 0.0.6,
SwiftProtobuf) ARE on trunk. So instead of a risky SPM migration:
- ios-patch.sh injects `pod 'LiveKitClient', :git => <repo>, :tag => '2.15.3'`
into the generated Podfile (after cap sync, before pod install). The NativeCall
podspec's '~> 2.0' is satisfied by 2.15.3. Idempotent; hard-fails if it can't
find the App target so we never silently fall back to 2.0.18.
- Plugin re-adds the CallKit<->LiveKit audio-session coordination, now compilable:
auto-config OFF + engine OFF at load; configure session + enable engine in
didActivate; disable in didDeactivate; request mic permission on connect.
Core Room APIs (connect/disconnect/setMicrophone) verified compatible between
2.0.18 and 2.15.3 against the real source.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The native call feature (iOS). Backward-compatible: without the plugin (current
builds) nativeCallOn() is false and every CallKit branch is skipped, so web/older
builds behave exactly as before.
Native (mobile/plugins/native-call, a local Capacitor plugin like audio-route):
- PushKit: registers for VoIP pushes, reports the VoIP token to JS (-> /api/v1/devices
'ios-voip'). On an incoming VoIP push, reports a CallKit incoming call (full-screen
ring, works when the app is force-killed).
- CallKit: answer/decline/end -> events to JS; configures the call AVAudioSession on
didActivate so the WebView's WebRTC audio rides a call-priority session (background).
- Outgoing calls register with CallKit too (reportOutgoingCall) so they get the same
active-call background-audio context.
- NativeCall.podspec (frameworks CallKit/PushKit/AVFoundation); added to mobile deps;
ios-patch.sh now sets UIBackgroundModes = [audio, voip] (voip required for PushKit).
Web bridge (home.html): setupNativeCall() registers the VoIP token, joins on CallKit
answer, leaves/declines on CallKit end; on CallKit devices the in-app call-invite popup
+ WebAudio ring are suppressed (the system rings instead); outgoing calls are reported
to CallKit; call-end events dismiss the CallKit call. calls.js threads a stable call
uuid through the dm-call/group-call WS events + start responses so both sides can match
the CallKit call.
Needs a Codemagic build to compile the plugin; first on-device iteration expected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Notifications were silent despite the payload requesting sound:'default' and correct
device settings. Ship an explicit tone: a generated PCM .wav is bundled into the app
(ios-patch.sh copies it in; add-share-extension.rb adds it to the App target's Copy
Bundle Resources, tolerantly) and the server now sends sound:'notif.wav'. Part of the
consolidated iOS build alongside the background-audio + call-push + AppDelegate fixes.
Only affects iOS-native-app tokens (currently just the one test device); web push
ignores the field. Needs a fresh Codemagic build for the bundled file to exist.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Capacitor/WebView apps get suspended by iOS a few seconds after backgrounding, which
freezes the WebRTC mic + audio pipeline — so when the user minimised the app or locked
the phone during a call, no one could hear anyone. Add UIBackgroundModes=[audio] to
Info.plist (via ios-patch.sh) so iOS keeps the audio session (and the app) alive while
a call is actively playing/recording. Video rendering still pauses in the background
(unavoidable in a WebView) but voice continues. Needs a fresh Codemagic build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ROOT CAUSE of iOS push not working: Capacitor 7's default AppDelegate.swift
template does NOT implement application(_:didRegisterForRemoteNotificationsWithDeviceToken:)
or ...didFailToRegisterForRemoteNotificationsWithError:. So when @capacitor/push-
notifications calls registerForRemoteNotifications(), iOS fetches the APNs token
and calls the AppDelegate, but nothing posts .capacitorDidRegisterForRemoteNotifications,
so the plugin never delivers the token to JS. register() 'succeeds' yet neither the
registration nor registrationError event fires — proven by server push telemetry
(register-called logged; no token, no error; device_tokens stayed empty).
inject-push.js adds the two forwarding methods to the CI-generated AppDelegate
(idempotent, tolerant — never fails the build), wired into ios-patch.sh after the
audio patch. Verified against the real Capacitor 7 template: methods land inside
the class, braces balance, both listeners present.
This is the missing piece alongside the earlier aps-environment entitlement fix and
the server APNs config. Needs a fresh Codemagic build to take effect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The @capacitor/push-notifications plugin does not add the Push Notifications
capability to the CI-generated Xcode project (that's a manual Xcode step), and
add-share-extension.rb only merged the App Group into App.entitlements, assuming
aps-environment was already there. It never was — so on device PushNotifications.
register() failed with 'no valid aps-environment entitlement', no APNs token was
obtained, and device_tokens stayed empty (server had nothing to push to).
ios-patch.sh now creates App/App.entitlements with aps-environment=production
before the share-extension script merges the App Group in. Still requires the App
ID to have Push Notifications enabled (so the profile carries the entitlement) and
the server APNS_* key set (Step 5) for end-to-end delivery.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the reverse direction: share FROM Photos/Files/Safari INTO a Biz Connect
conversation. An app can only appear in the iOS share sheet as an app-extension
target, so this is real native work, not a web change.
Pieces:
- mobile/ios-share/ShareViewController.swift: a UI-less Share Extension. It stages
the shared items into the App Group container and opens bizconnect://share. It
deliberately does NOT reimplement the chat picker — that lives in the app, which
already has the chat list, search and upload progress. Appends to the manifest
(never overwrites), so sharing twice before opening the app loses nothing.
- mobile/scripts/add-share-extension.rb: injects the extension target into the
Capacitor-generated Xcode project on every CI build (Codemagic checks out fresh),
using the xcodeproj gem that ships with CocoaPods. Embeds it, sets the bundle id
<app>.share, and MERGES the App Group into the app's entitlements rather than
clobbering them (push's aps-environment must survive). Idempotent.
- mobile/plugins/share-inbox: getPending()/clear() to read that manifest — the App
Group container isn't one of Filesystem's known directories, so it needs a bridge.
- home.html: on bizconnect://share (and every resume, and cold-launch), read the
inbox and show a "Send to…" picker over the chat list; chosen files run the SAME
upload + /api/messages send as an in-app attachment. Reuses convertFileSrc to read
the staged bytes with no base64 marshalling.
- ios-patch.sh registers the bizconnect URL scheme; codemagic.yaml fetches a profile
for the .share bundle id too.
One-time manual gate (CI cannot toggle App capabilities): the App Group
group.com.bizgaze.connect must be created and enabled on both App IDs in the Apple
portal — documented in mobile/IOS_SETUP.md. Without it the two processes can't see
each other's files and sharing silently no-ops; everything else still works.
Validated cross-file: pod-name/jsName/method wiring for all three plugins, App
Group id identical in all 4 files, URL scheme consistent across extension/plist/web,
entitlement-merge preserves push. Needs a new iOS build (new targets + plugins).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three connected pieces, so downloads stop being write-only.
1. A folder the user can actually find. ios-patch.sh now sets UIFileSharingEnabled
and LSSupportsOpeningDocumentsInPlace, and downloads go to the app's Documents
folder in typed subfolders. iOS shows it as:
Files -> On My iPhone -> Biz Connect -> Images | Videos | Files
Previously everything was written to CACHE (private, and iOS purges it whenever
it likes) and pushed straight at the share sheet, so nothing was ever really
"kept" by the app.
2. Manage storage (Settings -> Storage). Lists what this device has downloaded,
grouped by type with per-group and total sizes; each row can be shared to the OS
sheet (this is where "Save to Photos" now lives) or deleted. Plus Delete all.
Deleting removes ONLY the local copy — the attachment stays on the server, so
anything deleted can be downloaded again from the chat.
3. A downloaded video never downloads twice. Images and files have their own
download link, but a video's tile IS the player, so it had no control at all and
re-streamed on every play. It now carries a download button; once saved, the
button becomes a tick and the tile plays from the local file — no network.
The index is treated as a cache of the filesystem, never as truth, because the user
can delete these from the Files app behind our back: every listing re-stats and
forgets what is gone, the library is reconciled at startup, and a local file that
has vanished by play time falls straight back to streaming instead of showing a
broken player.
Unit-checked the path allocator: collisions between different attachments with the
same filename resolve to "name (2)", re-downloading the SAME attachment reuses its
path, and path traversal / illegal characters are neutralised.
Note: the folder and the save location need a new iOS build to take effect. The web
side degrades cleanly — none of this UI appears outside the native app.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Calls were routing to the quiet earpiece. ios-patch.sh now runs a Node helper (Node 20 is
already in the build env) that injects an AVAudioSession .playAndRecord/.voiceChat category with
.defaultToSpeaker + Bluetooth into the generated AppDelegate. Tolerant: exits 0 and no-ops if the
template differs, so it can never fail the Codemagic build. Verified locally against the Cap 7
AppDelegate template — injects correctly and is idempotent. First pass; if WebRTC re-grabs the
session mid-call on device, a follow-up plugin will re-assert .overrideOutputAudioPort(.speaker).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
App Store Connect rejected the upload: 'The bundle version must be higher than the
previously uploaded version: 1'. Capacitor defaults CFBundleVersion to 1, so every
build collided with the first upload. Patch it to the monotonic BUILD_NUMBER.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The IPA now builds, signs, uploads and processes on App Store Connect. Set
submit_to_testflight=false so the build stays green via internal testing; external
beta review (which needs Test Information + a demo login) is opt-in later. Also set
ITSAppUsesNonExemptEncryption=false in Info.plist so ASC stops prompting for export
compliance on every upload.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The iOS app is a Capacitor shell over the live web UI — the web/server side is already fully
Capacitor-ready (nativePlatform() detects Capacitor; setupNativePush registers APNs tokens via
/api/v1/devices; the APNs sender is built into server/push.js, config-gated). So this adds only
the build/sign/upload path:
- codemagic.yaml: macOS-cloud workflow that generates the iOS project, patches Info.plist,
generates icons/splash, signs via an App Store Connect API key (automatic signing), archives,
and uploads to TestFlight. No Mac required.
- mobile/scripts/ios-patch.sh: adds the App-Review privacy usage strings (camera/mic/photos) +
display name to the generated Info.plist.
- mobile/IOS_SETUP.md: click-by-click runbook — ASC app record, API key, Codemagic integration,
first build, APNs key → server .env, and the public-submission checklist.
Bundle id com.bizgaze.connect. No secrets committed — Apple keys live in Codemagic + server .env.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>