#!/usr/bin/env bash # Patch the freshly-generated Capacitor iOS project (mobile/ios/App) for App Store submission. # Runs on the Codemagic macOS instance after `npx cap add ios`. Idempotent — safe to re-run. set -euo pipefail PLIST="mobile/ios/App/App/Info.plist" PB=/usr/libexec/PlistBuddy set_str() { # set_str — add the key if missing, else overwrite "$PB" -c "Add :$1 string $2" "$PLIST" 2>/dev/null || "$PB" -c "Set :$1 $2" "$PLIST" } echo "Patching $PLIST" # ── Privacy usage strings (Apple REJECTS the build if a used capability has no purpose string) ── set_str NSCameraUsageDescription "Biz Connect uses the camera for video calls and to share photos and your screen." set_str NSMicrophoneUsageDescription "Biz Connect uses the microphone for voice and video calls." set_str NSPhotoLibraryUsageDescription "Biz Connect needs photo access so you can send images in chat." set_str NSPhotoLibraryAddUsageDescription "Biz Connect saves images and recordings you download to your photos." # Human-readable display name on the home screen. set_str CFBundleDisplayName "Biz Connect" # CFBundleVersion (build number) MUST be unique AND higher than every previous App Store Connect upload, # or publishing fails with "The bundle version must be higher than the previously uploaded version". # Capacitor ships "1" by default, so every build collided. Use Codemagic's monotonically-increasing # BUILD_NUMBER (this is build index 6+, already > the "1" that's on TestFlight). Fall back to an epoch # timestamp if it's somehow unset, which is also strictly increasing. BUILD_NO="${BUILD_NUMBER:-$(date +%s)}" echo "Setting CFBundleVersion = $BUILD_NO" set_str CFBundleVersion "$BUILD_NO" # ── Make the app's Documents folder visible in the Files app ─────────────────────────────────────── # Downloads are saved to Documents/{Images,Videos,Files}. Without these two keys that folder is private # and the user has no way to reach what they saved. With them, Files shows # Files → Browse → On My iPhone → Biz Connect → Images / Videos / Files # UIFileSharingEnabled exposes the folder; LSSupportsOpeningDocumentsInPlace lets other apps open those # files in place rather than silently working on a copy. set_bool() { "$PB" -c "Add :$1 bool $2" "$PLIST" 2>/dev/null || "$PB" -c "Set :$1 $2" "$PLIST"; } set_bool UIFileSharingEnabled true set_bool LSSupportsOpeningDocumentsInPlace true # ── Keep CALL AUDIO alive when the app is BACKGROUNDED (minimised / screen locked) ────────────────── # Without the 'audio' background mode, iOS suspends the WebView a few seconds after it backgrounds, which # freezes the WebRTC mic + audio pipeline — so participants can't hear each other once the app is minimised # or the phone locks. Declaring background audio keeps the audio session (and the app) running so a voice # call continues in the background. (Video RENDERING still pauses while backgrounded — unavoidable in a # WebView — but audio keeps flowing, which is what matters for a call.) Idempotent: rebuild the array each run. "$PB" -c "Delete :UIBackgroundModes" "$PLIST" 2>/dev/null || true "$PB" -c "Add :UIBackgroundModes array" "$PLIST" "$PB" -c "Add :UIBackgroundModes:0 string audio" "$PLIST" echo "UIBackgroundModes: audio (calls keep audio when minimised)" # ── Custom URL scheme so the Share Extension can bounce the user back into the app ────────────────── # The extension stages the shared files into the App Group, then opens bizconnect://share; the app reads # the staged files and shows "Send to…". Registering the scheme is what makes that openURL succeed. if ! "$PB" -c "Print :CFBundleURLTypes" "$PLIST" >/dev/null 2>&1; then "$PB" -c "Add :CFBundleURLTypes array" "$PLIST" "$PB" -c "Add :CFBundleURLTypes:0 dict" "$PLIST" "$PB" -c "Add :CFBundleURLTypes:0:CFBundleURLName string com.bizgaze.connect" "$PLIST" "$PB" -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes array" "$PLIST" "$PB" -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string bizconnect" "$PLIST" fi # ── Push Notifications entitlement (aps-environment) ──────────────────────────────────────────────── # The @capacitor/push-notifications plugin does NOT add the Push Notifications capability to the generated # Xcode project — in Xcode that's a manual "Signing & Capabilities → + Push Notifications" click, which # never happens on a fresh CI checkout. Without the aps-environment entitlement, PushNotifications.register() # fails on device ("no valid 'aps-environment' entitlement string found") and NO APNs token is ever # obtained, so device_tokens stays empty and the server has nothing to push to. Create the entitlements # file with aps-environment HERE; add-share-extension.rb (runs after this) MERGES the App Group into the # same file, preserving this key. REQUIRES: the App ID com.bizgaze.connect must have the Push Notifications # capability enabled in the Apple Developer portal, so the fetched provisioning profile carries # aps-environment — otherwise the archive fails code-signing. "production" is correct for App Store + # TestFlight (pair it with APNS_PRODUCTION=1 on the server). ENT="mobile/ios/App/App/App.entitlements" if [ ! -f "$ENT" ]; then cat > "$ENT" <<'PLIST' PLIST fi "$PB" -c "Add :aps-environment string production" "$ENT" 2>/dev/null || "$PB" -c "Set :aps-environment production" "$ENT" echo "Entitlements: aps-environment=production ensured in $ENT" # We use only standard encryption (HTTPS/TLS), which is exempt — declaring this up front stops App Store # Connect from asking the "export compliance" question on every single build/TestFlight upload. "$PB" -c "Add :ITSAppUsesNonExemptEncryption bool false" "$PLIST" 2>/dev/null \ || "$PB" -c "Set :ITSAppUsesNonExemptEncryption false" "$PLIST" # Allow the webview to load our HTTPS origin (we do NOT enable arbitrary cleartext). "$PB" -c "Delete :NSAppTransportSecurity" "$PLIST" 2>/dev/null || true # ── Default in-call audio to the LOUDSPEAKER (AVAudioSession) ────────────────────────────────────── # Without this, iOS routes WebRTC call audio to the quiet EARPIECE. A Node helper (Node 20 is already set # up for this build) injects an AVAudioSession category into the generated AppDelegate so calls default to # the speaker (headphones/Bluetooth still win when connected). The helper is tolerant and exits 0 even if # the template differs, so it NEVER fails the build. First-pass fix — if WebRTC re-grabs the session # mid-call on device, we follow up with a plugin that re-asserts .overrideOutputAudioPort(.speaker). AD="mobile/ios/App/App/AppDelegate.swift" if [ -f "$AD" ]; then echo "Patching AppDelegate audio session" node "$(dirname "$0")/inject-audio.js" "$AD" || echo " (AVAudioSession patch skipped — non-fatal)" # Capacitor 7's AppDelegate template omits the APNs registration callbacks, so the push-notifications # plugin never receives the device token (register() fires no event at all). Inject the forwarding methods. echo "Patching AppDelegate APNs registration forwarding" node "$(dirname "$0")/inject-push.js" "$AD" || echo " (push forwarding patch skipped — non-fatal)" fi echo "Info.plist patched:" "$PB" -c "Print :NSCameraUsageDescription" "$PLIST" "$PB" -c "Print :NSMicrophoneUsageDescription" "$PLIST"