diff --git a/apps/ios/LocalSigning.xcconfig.example b/apps/ios/LocalSigning.xcconfig.example index bfa610fb350..64e8f119dec 100644 --- a/apps/ios/LocalSigning.xcconfig.example +++ b/apps/ios/LocalSigning.xcconfig.example @@ -2,12 +2,13 @@ // This file is only an example and should stay committed. OPENCLAW_CODE_SIGN_STYLE = Automatic -OPENCLAW_DEVELOPMENT_TEAM = P5Z8X89DJL +OPENCLAW_DEVELOPMENT_TEAM = YOUR_TEAM_ID -OPENCLAW_APP_BUNDLE_ID = ai.openclaw.ios.test.mariano -OPENCLAW_SHARE_BUNDLE_ID = ai.openclaw.ios.test.mariano.share -OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.ios.test.mariano.watchkitapp -OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.ios.test.mariano.watchkitapp.extension +OPENCLAW_APP_BUNDLE_ID = ai.openclaw.client +OPENCLAW_SHARE_BUNDLE_ID = ai.openclaw.client.share +OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclaw.client.activitywidget +OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.client.watchkitapp +OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.client.watchkitapp.extension // Leave empty with automatic signing. OPENCLAW_APP_PROFILE = diff --git a/apps/ios/Signing.xcconfig b/apps/ios/Signing.xcconfig index f942fc0224f..5966d6e2c2f 100644 --- a/apps/ios/Signing.xcconfig +++ b/apps/ios/Signing.xcconfig @@ -5,11 +5,14 @@ OPENCLAW_CODE_SIGN_STYLE = Manual OPENCLAW_DEVELOPMENT_TEAM = Y5PE65HELJ -OPENCLAW_APP_BUNDLE_ID = ai.openclaw.ios -OPENCLAW_SHARE_BUNDLE_ID = ai.openclaw.ios.share +OPENCLAW_APP_BUNDLE_ID = ai.openclaw.client +OPENCLAW_SHARE_BUNDLE_ID = ai.openclaw.client.share +OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.client.watchkitapp +OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.client.watchkitapp.extension +OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclaw.client.activitywidget -OPENCLAW_APP_PROFILE = ai.openclaw.ios Development -OPENCLAW_SHARE_PROFILE = ai.openclaw.ios.share Development +OPENCLAW_APP_PROFILE = ai.openclaw.client Development +OPENCLAW_SHARE_PROFILE = ai.openclaw.client.share Development // Keep local includes after defaults: xcconfig is evaluated top-to-bottom, // so later assignments in local files override the defaults above. diff --git a/apps/ios/Sources/Gateway/GatewaySettingsStore.swift b/apps/ios/Sources/Gateway/GatewaySettingsStore.swift index d91d2217741..37c039d69d1 100644 --- a/apps/ios/Sources/Gateway/GatewaySettingsStore.swift +++ b/apps/ios/Sources/Gateway/GatewaySettingsStore.swift @@ -412,11 +412,11 @@ enum GatewayDiagnostics { private static let keepLogBytes: Int64 = 256 * 1024 private static let logSizeCheckEveryWrites = 50 private static let logWritesSinceCheck = OSAllocatedUnfairLock(initialState: 0) - private static let isoFormatter: ISO8601DateFormatter = { - let f = ISO8601DateFormatter() - f.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - return f - }() + private static func isoTimestamp() -> String { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter.string(from: Date()) + } private static var fileURL: URL? { FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first? @@ -476,7 +476,7 @@ enum GatewayDiagnostics { guard let url = fileURL else { return } queue.async { self.truncateLogIfNeeded(url: url) - let timestamp = self.isoFormatter.string(from: Date()) + let timestamp = self.isoTimestamp() let line = "[\(timestamp)] gateway diagnostics started\n" if let data = line.data(using: .utf8) { self.appendToLog(url: url, data: data) @@ -486,7 +486,7 @@ enum GatewayDiagnostics { } static func log(_ message: String) { - let timestamp = self.isoFormatter.string(from: Date()) + let timestamp = self.isoTimestamp() let line = "[\(timestamp)] \(message)" logger.info("\(line, privacy: .public)") diff --git a/apps/ios/Sources/Info.plist b/apps/ios/Sources/Info.plist index 00f7f48029f..ea65f194a8d 100644 --- a/apps/ios/Sources/Info.plist +++ b/apps/ios/Sources/Info.plist @@ -2,6 +2,10 @@ + BGTaskSchedulerPermittedIdentifiers + + ai.openclaw.ios.bgrefresh + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -33,6 +37,8 @@ CFBundleVersion 20260307 + ITSAppUsesNonExemptEncryption + NSAppTransportSecurity NSAllowsArbitraryLoadsInWebContent @@ -52,6 +58,10 @@ OpenClaw uses your location when you allow location sharing. NSMicrophoneUsageDescription OpenClaw needs microphone access for voice wake. + NSMotionUsageDescription + OpenClaw may use motion data to support device-aware interactions and automations. + NSPhotoLibraryUsageDescription + OpenClaw needs photo library access when you choose existing photos to share with your assistant. NSSpeechRecognitionUsageDescription OpenClaw uses on-device speech recognition for voice wake. NSSupportsLiveActivities @@ -66,10 +76,6 @@ audio remote-notification - BGTaskSchedulerPermittedIdentifiers - - ai.openclaw.ios.bgrefresh - UILaunchScreen UISupportedInterfaceOrientations diff --git a/apps/ios/Sources/RootCanvas.swift b/apps/ios/Sources/RootCanvas.swift index 3fc62d7e859..4d6efa3fa71 100644 --- a/apps/ios/Sources/RootCanvas.swift +++ b/apps/ios/Sources/RootCanvas.swift @@ -264,61 +264,65 @@ private struct CanvasContent: View { var openSettings: () -> Void private var brightenButtons: Bool { self.systemColorScheme == .light } + private var talkActive: Bool { self.appModel.talkMode.isEnabled || self.talkEnabled } var body: some View { - ZStack(alignment: .topTrailing) { + ZStack { ScreenTab() - - VStack(spacing: 10) { - OverlayButton(systemImage: "text.bubble.fill", brighten: self.brightenButtons) { - self.openChat() - } - .accessibilityLabel("Chat") - - if self.talkButtonEnabled { - // Talk mode lives on a side bubble so it doesn't get buried in settings. - OverlayButton( - systemImage: self.appModel.talkMode.isEnabled ? "waveform.circle.fill" : "waveform.circle", - brighten: self.brightenButtons, - tint: self.appModel.seamColor, - isActive: self.appModel.talkMode.isEnabled) - { - let next = !self.appModel.talkMode.isEnabled - self.talkEnabled = next - self.appModel.setTalkEnabled(next) - } - .accessibilityLabel("Talk Mode") - } - - OverlayButton(systemImage: "gearshape.fill", brighten: self.brightenButtons) { - self.openSettings() - } - .accessibilityLabel("Settings") - } - .padding(.top, 10) - .padding(.trailing, 10) } .overlay(alignment: .center) { - if self.appModel.talkMode.isEnabled { + if self.talkActive { TalkOrbOverlay() .transition(.opacity) } } .overlay(alignment: .topLeading) { - StatusPill( - gateway: self.gatewayStatus, - voiceWakeEnabled: self.voiceWakeEnabled, - activity: self.statusActivity, - brighten: self.brightenButtons, - onTap: { - if self.gatewayStatus == .connected { - self.showGatewayActions = true - } else { + HStack(alignment: .top, spacing: 8) { + StatusPill( + gateway: self.gatewayStatus, + voiceWakeEnabled: self.voiceWakeEnabled, + activity: self.statusActivity, + brighten: self.brightenButtons, + onTap: { + if self.gatewayStatus == .connected { + self.showGatewayActions = true + } else { + self.openSettings() + } + }) + .layoutPriority(1) + + Spacer(minLength: 8) + + HStack(spacing: 8) { + OverlayButton(systemImage: "text.bubble.fill", brighten: self.brightenButtons) { + self.openChat() + } + .accessibilityLabel("Chat") + + if self.talkButtonEnabled { + // Keep Talk mode near status controls while freeing right-side screen real estate. + OverlayButton( + systemImage: self.talkActive ? "waveform.circle.fill" : "waveform.circle", + brighten: self.brightenButtons, + tint: self.appModel.seamColor, + isActive: self.talkActive) + { + let next = !self.talkActive + self.talkEnabled = next + self.appModel.setTalkEnabled(next) + } + .accessibilityLabel("Talk Mode") + } + + OverlayButton(systemImage: "gearshape.fill", brighten: self.brightenButtons) { self.openSettings() } - }) - .padding(.leading, 10) - .safeAreaPadding(.top, 10) + .accessibilityLabel("Settings") + } + } + .padding(.horizontal, 10) + .safeAreaPadding(.top, 10) } .overlay(alignment: .topLeading) { if let voiceWakeToastText, !voiceWakeToastText.isEmpty { @@ -334,6 +338,12 @@ private struct CanvasContent: View { isPresented: self.$showGatewayActions, onDisconnect: { self.appModel.disconnectGateway() }, onOpenSettings: { self.openSettings() }) + .onAppear { + // Keep the runtime talk state aligned with persisted toggle state on cold launch. + if self.talkEnabled != self.appModel.talkMode.isEnabled { + self.appModel.setTalkEnabled(self.talkEnabled) + } + } } private var statusActivity: StatusPill.Activity? { diff --git a/apps/ios/Tests/Info.plist b/apps/ios/Tests/Info.plist index a2cb4ee4ef3..0840e60efb0 100644 --- a/apps/ios/Tests/Info.plist +++ b/apps/ios/Tests/Info.plist @@ -15,10 +15,10 @@ CFBundleName $(PRODUCT_NAME) CFBundlePackageType - BNDL - CFBundleShortVersionString - 2026.3.7 - CFBundleVersion - 20260307 - - + BNDL + CFBundleShortVersionString + 2026.3.7 + CFBundleVersion + 20260307 + + diff --git a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-38@2x.png b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-38@2x.png index 82829afb947..fa192bff24d 100644 Binary files a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-38@2x.png and b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-38@2x.png differ diff --git a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-40@2x.png b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-40@2x.png index 114d4606420..7f7774e81df 100644 Binary files a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-40@2x.png and b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-40@2x.png differ diff --git a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-41@2x.png b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-41@2x.png index 5f9578b1b97..96da7b53503 100644 Binary files a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-41@2x.png and b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-41@2x.png differ diff --git a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-44@2x.png b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-44@2x.png index fe022ac7720..7fc6b49eebf 100644 Binary files a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-44@2x.png and b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-44@2x.png differ diff --git a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-45@2x.png b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-45@2x.png index 55977b8f6e7..3594312a6a0 100644 Binary files a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-45@2x.png and b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-45@2x.png differ diff --git a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-companion-29@2x.png b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-companion-29@2x.png index f8be7d06911..be6c01e95d3 100644 Binary files a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-companion-29@2x.png and b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-companion-29@2x.png differ diff --git a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-companion-29@3x.png b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-companion-29@3x.png index cce412d2452..5101bebfd3b 100644 Binary files a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-companion-29@3x.png and b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-companion-29@3x.png differ diff --git a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-marketing-1024.png b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-marketing-1024.png index 005486f2ee1..420828f1d80 100644 Binary files a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-marketing-1024.png and b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-marketing-1024.png differ diff --git a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-notification-38@2x.png b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-notification-38@2x.png index 7b7a0ee0b65..53e410a4422 100644 Binary files a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-notification-38@2x.png and b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-notification-38@2x.png differ diff --git a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-notification-42@2x.png b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-notification-42@2x.png index f13c9cdddda..3d4e3642a75 100644 Binary files a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-notification-42@2x.png and b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-notification-42@2x.png differ diff --git a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-38@2x.png b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-38@2x.png index aac0859b44c..83df80e34d8 100644 Binary files a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-38@2x.png and b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-38@2x.png differ diff --git a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-42@2x.png b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-42@2x.png index d09be6e98a6..37e1a554ea7 100644 Binary files a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-42@2x.png and b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-42@2x.png differ diff --git a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-44@2x.png b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-44@2x.png index 5b06a48744b..7c036f86624 100644 Binary files a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-44@2x.png and b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-44@2x.png differ diff --git a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-45@2x.png b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-45@2x.png index 72ba51ebb1d..9a37688f0c1 100644 Binary files a/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-45@2x.png and b/apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-45@2x.png differ diff --git a/apps/ios/fastlane/Appfile b/apps/ios/fastlane/Appfile index 8dbb75a8c26..b0374fbd716 100644 --- a/apps/ios/fastlane/Appfile +++ b/apps/ios/fastlane/Appfile @@ -1,7 +1,15 @@ -app_identifier("ai.openclaw.ios") +app_identifier("ai.openclaw.client") # Auth is expected via App Store Connect API key. # Provide either: # - APP_STORE_CONNECT_API_KEY_PATH=/path/to/AuthKey_XXXXXX.p8.json (recommended) # or: +# - ASC_KEY_PATH=/path/to/AuthKey_XXXXXX.p8 with ASC_KEY_ID and ASC_ISSUER_ID # - ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_CONTENT (base64 or raw p8 content) +# - ASC_KEY_ID and ASC_ISSUER_ID plus Keychain fallback: +# ASC_KEYCHAIN_SERVICE (default: openclaw-asc-key) +# ASC_KEYCHAIN_ACCOUNT (default: USER/LOGNAME) +# +# Optional deliver app lookup overrides: +# - ASC_APP_IDENTIFIER (bundle ID) +# - ASC_APP_ID (numeric App Store Connect app ID) diff --git a/apps/ios/fastlane/Fastfile b/apps/ios/fastlane/Fastfile index f1dbf6df18c..942939bb591 100644 --- a/apps/ios/fastlane/Fastfile +++ b/apps/ios/fastlane/Fastfile @@ -1,4 +1,5 @@ require "shellwords" +require "open3" default_platform(:ios) @@ -16,33 +17,104 @@ def load_env_file(path) end end +def env_present?(value) + !value.nil? && !value.strip.empty? +end + +def clear_empty_env_var(key) + return unless ENV.key?(key) + ENV.delete(key) unless env_present?(ENV[key]) +end + +def maybe_decode_hex_keychain_secret(value) + return value unless env_present?(value) + + candidate = value.strip + return candidate unless candidate.match?(/\A[0-9a-fA-F]+\z/) && candidate.length.even? + + begin + decoded = [candidate].pack("H*") + return candidate unless decoded.valid_encoding? + + # `security find-generic-password -w` can return hex when the stored secret + # includes newlines/non-printable bytes (like PEM files). + if decoded.include?("BEGIN PRIVATE KEY") || decoded.include?("END PRIVATE KEY") + UI.message("Decoded hex-encoded ASC key content from Keychain.") + return decoded + end + rescue StandardError + return candidate + end + + candidate +end + +def read_asc_key_content_from_keychain + service = ENV["ASC_KEYCHAIN_SERVICE"] + service = "openclaw-asc-key" unless env_present?(service) + + account = ENV["ASC_KEYCHAIN_ACCOUNT"] + account = ENV["USER"] unless env_present?(account) + account = ENV["LOGNAME"] unless env_present?(account) + return nil unless env_present?(account) + + begin + stdout, _stderr, status = Open3.capture3( + "security", + "find-generic-password", + "-s", + service, + "-a", + account, + "-w" + ) + + return nil unless status.success? + + key_content = stdout.to_s.strip + key_content = maybe_decode_hex_keychain_secret(key_content) + return nil unless env_present?(key_content) + + UI.message("Loaded ASC key content from Keychain service '#{service}' (account '#{account}').") + key_content + rescue Errno::ENOENT + nil + end +end + platform :ios do private_lane :asc_api_key do load_env_file(File.join(__dir__, ".env")) + clear_empty_env_var("APP_STORE_CONNECT_API_KEY_PATH") + clear_empty_env_var("ASC_KEY_PATH") + clear_empty_env_var("ASC_KEY_CONTENT") api_key = nil key_path = ENV["APP_STORE_CONNECT_API_KEY_PATH"] - if key_path && !key_path.strip.empty? + if env_present?(key_path) api_key = app_store_connect_api_key(path: key_path) else p8_path = ENV["ASC_KEY_PATH"] - if p8_path && !p8_path.strip.empty? - key_id = ENV["ASC_KEY_ID"] - issuer_id = ENV["ASC_ISSUER_ID"] - UI.user_error!("Missing ASC_KEY_ID or ASC_ISSUER_ID for ASC_KEY_PATH auth.") if [key_id, issuer_id].any? { |v| v.nil? || v.strip.empty? } + if env_present?(p8_path) + key_id = ENV["ASC_KEY_ID"] + issuer_id = ENV["ASC_ISSUER_ID"] + UI.user_error!("Missing ASC_KEY_ID or ASC_ISSUER_ID for ASC_KEY_PATH auth.") if [key_id, issuer_id].any? { |v| !env_present?(v) } api_key = app_store_connect_api_key( - key_id: key_id, - issuer_id: issuer_id, - key_filepath: p8_path - ) + key_id: key_id, + issuer_id: issuer_id, + key_filepath: p8_path + ) else key_id = ENV["ASC_KEY_ID"] issuer_id = ENV["ASC_ISSUER_ID"] key_content = ENV["ASC_KEY_CONTENT"] + key_content = read_asc_key_content_from_keychain unless env_present?(key_content) - UI.user_error!("Missing App Store Connect API key. Set APP_STORE_CONNECT_API_KEY_PATH (json) or ASC_KEY_PATH (p8) or ASC_KEY_ID/ASC_ISSUER_ID/ASC_KEY_CONTENT.") if [key_id, issuer_id, key_content].any? { |v| v.nil? || v.strip.empty? } + UI.user_error!( + "Missing App Store Connect API key. Set APP_STORE_CONNECT_API_KEY_PATH (json), ASC_KEY_PATH (p8), or ASC_KEY_ID/ASC_ISSUER_ID with ASC_KEY_CONTENT (or Keychain via ASC_KEYCHAIN_SERVICE/ASC_KEYCHAIN_ACCOUNT)." + ) if [key_id, issuer_id, key_content].any? { |v| !env_present?(v) } is_base64 = key_content.include?("BEGIN PRIVATE KEY") ? false : true @@ -64,7 +136,7 @@ platform :ios do team_id = ENV["IOS_DEVELOPMENT_TEAM"] if team_id.nil? || team_id.strip.empty? - helper_path = File.expand_path("../../scripts/ios-team-id.sh", __dir__) + helper_path = File.expand_path("../../../scripts/ios-team-id.sh", __dir__) if File.exist?(helper_path) # Keep CI/local compatibility where teams are present in keychain but not Xcode account metadata. team_id = sh("IOS_ALLOW_KEYCHAIN_TEAM_FALLBACK=1 bash #{helper_path.shellescape}").strip @@ -77,6 +149,7 @@ platform :ios do scheme: "OpenClaw", export_method: "app-store", clean: true, + skip_profile_detection: true, xcargs: "DEVELOPMENT_TEAM=#{team_id} -allowProvisioningUpdates", export_xcargs: "-allowProvisioningUpdates", export_options: { @@ -86,19 +159,40 @@ platform :ios do upload_to_testflight( api_key: api_key, - skip_waiting_for_build_processing: true + skip_waiting_for_build_processing: true, + uses_non_exempt_encryption: false ) end desc "Upload App Store metadata (and optionally screenshots)" lane :metadata do api_key = asc_api_key + clear_empty_env_var("APP_STORE_CONNECT_API_KEY_PATH") + app_identifier = ENV["ASC_APP_IDENTIFIER"] + app_id = ENV["ASC_APP_ID"] + app_identifier = nil unless env_present?(app_identifier) + app_id = nil unless env_present?(app_id) - deliver( + deliver_options = { api_key: api_key, force: true, skip_screenshots: ENV["DELIVER_SCREENSHOTS"] != "1", - skip_metadata: ENV["DELIVER_METADATA"] != "1" - ) + skip_metadata: ENV["DELIVER_METADATA"] != "1", + run_precheck_before_submit: false + } + deliver_options[:app_identifier] = app_identifier if app_identifier + if app_id && app_identifier.nil? + # `deliver` prefers app_identifier from Appfile unless explicitly blanked. + deliver_options[:app_identifier] = "" + deliver_options[:app] = app_id + end + + deliver(**deliver_options) + end + + desc "Validate App Store Connect API auth" + lane :auth_check do + asc_api_key + UI.success("App Store Connect API auth loaded successfully.") end end diff --git a/apps/ios/fastlane/SETUP.md b/apps/ios/fastlane/SETUP.md index 930258fcc79..8dccf264b41 100644 --- a/apps/ios/fastlane/SETUP.md +++ b/apps/ios/fastlane/SETUP.md @@ -11,18 +11,54 @@ Create an App Store Connect API key: - App Store Connect → Users and Access → Keys → App Store Connect API → Generate API Key - Download the `.p8`, note the **Issuer ID** and **Key ID** -Create `apps/ios/fastlane/.env` (gitignored): +Recommended (macOS): store the private key in Keychain and write non-secret vars: + +```bash +scripts/ios-asc-keychain-setup.sh \ + --key-path /absolute/path/to/AuthKey_XXXXXXXXXX.p8 \ + --issuer-id YOUR_ISSUER_ID \ + --write-env +``` + +This writes these auth variables in `apps/ios/fastlane/.env`: + +```bash +ASC_KEY_ID=YOUR_KEY_ID +ASC_ISSUER_ID=YOUR_ISSUER_ID +ASC_KEYCHAIN_SERVICE=openclaw-asc-key +ASC_KEYCHAIN_ACCOUNT=YOUR_MAC_USERNAME +``` + +Optional app targeting variables (helpful if Fastlane cannot auto-resolve app by bundle): + +```bash +ASC_APP_IDENTIFIER=ai.openclaw.ios +# or +ASC_APP_ID=6760218713 +``` + +File-based fallback (CI/non-macOS): ```bash ASC_KEY_ID=YOUR_KEY_ID ASC_ISSUER_ID=YOUR_ISSUER_ID ASC_KEY_PATH=/absolute/path/to/AuthKey_XXXXXXXXXX.p8 +``` -# Code signing (Apple Team ID / App ID Prefix) +Code signing variable (optional in `.env`): + +```bash IOS_DEVELOPMENT_TEAM=YOUR_TEAM_ID ``` -Tip: run `scripts/ios-team-id.sh` from the repo root to print a Team ID to paste into `.env`. The helper prefers the canonical OpenClaw team (`Y5PE65HELJ`) when present locally; otherwise it prefers the first non-personal team from your Xcode account (then personal team if needed). Fastlane uses this helper automatically if `IOS_DEVELOPMENT_TEAM` is missing. +Tip: run `scripts/ios-team-id.sh` from repo root to print a Team ID for `.env`. The helper prefers the canonical OpenClaw team (`Y5PE65HELJ`) when present locally; otherwise it prefers the first non-personal team from your Xcode account (then personal team if needed). Fastlane uses this helper automatically if `IOS_DEVELOPMENT_TEAM` is missing. + +Validate auth: + +```bash +cd apps/ios +fastlane ios auth_check +``` Run: diff --git a/apps/ios/fastlane/metadata/README.md b/apps/ios/fastlane/metadata/README.md new file mode 100644 index 00000000000..74eb7df87d3 --- /dev/null +++ b/apps/ios/fastlane/metadata/README.md @@ -0,0 +1,47 @@ +# App Store metadata (Fastlane deliver) + +This directory is used by `fastlane deliver` for App Store Connect text metadata. + +## Upload metadata only + +```bash +cd apps/ios +ASC_APP_ID=6760218713 \ +DELIVER_METADATA=1 fastlane ios metadata +``` + +## Optional: include screenshots + +```bash +cd apps/ios +DELIVER_METADATA=1 DELIVER_SCREENSHOTS=1 fastlane ios metadata +``` + +## Auth + +The `ios metadata` lane uses App Store Connect API key auth from `apps/ios/fastlane/.env`: + +- Keychain-backed (recommended on macOS): + - `ASC_KEY_ID` + - `ASC_ISSUER_ID` + - `ASC_KEYCHAIN_SERVICE` (default: `openclaw-asc-key`) + - `ASC_KEYCHAIN_ACCOUNT` (default: current user) +- File/path fallback: + - `ASC_KEY_ID` + - `ASC_ISSUER_ID` + - `ASC_KEY_PATH` + +Or set `APP_STORE_CONNECT_API_KEY_PATH`. + +## Notes + +- Locale files live under `metadata/en-US/`. +- `privacy_url.txt` is set to `https://openclaw.ai/privacy`. +- If app lookup fails in `deliver`, set one of: + - `ASC_APP_IDENTIFIER` (bundle ID) + - `ASC_APP_ID` (numeric App Store Connect app ID, e.g. from `/apps//...` URL) +- For first app versions, include review contact files under `metadata/review_information/`: + - `first_name.txt` + - `last_name.txt` + - `email_address.txt` + - `phone_number.txt` (E.164-ish, e.g. `+1 415 555 0100`) diff --git a/apps/ios/fastlane/metadata/en-US/description.txt b/apps/ios/fastlane/metadata/en-US/description.txt new file mode 100644 index 00000000000..466de5d8fa1 --- /dev/null +++ b/apps/ios/fastlane/metadata/en-US/description.txt @@ -0,0 +1,18 @@ +OpenClaw is a personal AI assistant you run on your own devices. + +Pair this iPhone app with your OpenClaw Gateway to connect your phone as a secure node for voice, camera, and device automation. + +What you can do: +- Chat with your assistant from iPhone +- Use voice wake and push-to-talk +- Capture photos and short clips on request +- Record screen snippets for troubleshooting and workflows +- Share text, links, and media directly from iOS into OpenClaw +- Run location-aware and device-aware automations + +OpenClaw is local-first: you control your gateway, keys, and configuration. + +Getting started: +1) Set up your OpenClaw Gateway +2) Open the iOS app and pair with your gateway +3) Start using commands and automations from your phone diff --git a/apps/ios/fastlane/metadata/en-US/keywords.txt b/apps/ios/fastlane/metadata/en-US/keywords.txt new file mode 100644 index 00000000000..b524ae74493 --- /dev/null +++ b/apps/ios/fastlane/metadata/en-US/keywords.txt @@ -0,0 +1 @@ +openclaw,ai assistant,local ai,voice assistant,automation,gateway,chat,agent,node diff --git a/apps/ios/fastlane/metadata/en-US/marketing_url.txt b/apps/ios/fastlane/metadata/en-US/marketing_url.txt new file mode 100644 index 00000000000..5760de806f8 --- /dev/null +++ b/apps/ios/fastlane/metadata/en-US/marketing_url.txt @@ -0,0 +1 @@ +https://openclaw.ai diff --git a/apps/ios/fastlane/metadata/en-US/name.txt b/apps/ios/fastlane/metadata/en-US/name.txt new file mode 100644 index 00000000000..12bd1d59377 --- /dev/null +++ b/apps/ios/fastlane/metadata/en-US/name.txt @@ -0,0 +1 @@ +OpenClaw - iOS Client diff --git a/apps/ios/fastlane/metadata/en-US/privacy_url.txt b/apps/ios/fastlane/metadata/en-US/privacy_url.txt new file mode 100644 index 00000000000..44207346064 --- /dev/null +++ b/apps/ios/fastlane/metadata/en-US/privacy_url.txt @@ -0,0 +1 @@ +https://openclaw.ai/privacy diff --git a/apps/ios/fastlane/metadata/en-US/promotional_text.txt b/apps/ios/fastlane/metadata/en-US/promotional_text.txt new file mode 100644 index 00000000000..16beaa2a39b --- /dev/null +++ b/apps/ios/fastlane/metadata/en-US/promotional_text.txt @@ -0,0 +1 @@ +Run OpenClaw from your iPhone: pair with your own gateway, trigger automations, and use voice, camera, and share actions. diff --git a/apps/ios/fastlane/metadata/en-US/release_notes.txt b/apps/ios/fastlane/metadata/en-US/release_notes.txt new file mode 100644 index 00000000000..53059d9cbc3 --- /dev/null +++ b/apps/ios/fastlane/metadata/en-US/release_notes.txt @@ -0,0 +1 @@ +First App Store release of OpenClaw for iPhone. Pair with your OpenClaw Gateway to use chat, voice, sharing, and device actions from iOS. diff --git a/apps/ios/fastlane/metadata/en-US/subtitle.txt b/apps/ios/fastlane/metadata/en-US/subtitle.txt new file mode 100644 index 00000000000..f0796fb024f --- /dev/null +++ b/apps/ios/fastlane/metadata/en-US/subtitle.txt @@ -0,0 +1 @@ +Personal AI on your devices diff --git a/apps/ios/fastlane/metadata/en-US/support_url.txt b/apps/ios/fastlane/metadata/en-US/support_url.txt new file mode 100644 index 00000000000..d9b96750003 --- /dev/null +++ b/apps/ios/fastlane/metadata/en-US/support_url.txt @@ -0,0 +1 @@ +https://docs.openclaw.ai/platforms/ios diff --git a/apps/ios/fastlane/metadata/review_information/email_address.txt b/apps/ios/fastlane/metadata/review_information/email_address.txt new file mode 100644 index 00000000000..5dbbc8730ff --- /dev/null +++ b/apps/ios/fastlane/metadata/review_information/email_address.txt @@ -0,0 +1 @@ +support@openclaw.ai diff --git a/apps/ios/fastlane/metadata/review_information/first_name.txt b/apps/ios/fastlane/metadata/review_information/first_name.txt new file mode 100644 index 00000000000..9a5b1392dc5 --- /dev/null +++ b/apps/ios/fastlane/metadata/review_information/first_name.txt @@ -0,0 +1 @@ +OpenClaw diff --git a/apps/ios/fastlane/metadata/review_information/last_name.txt b/apps/ios/fastlane/metadata/review_information/last_name.txt new file mode 100644 index 00000000000..ce1e10deda0 --- /dev/null +++ b/apps/ios/fastlane/metadata/review_information/last_name.txt @@ -0,0 +1 @@ +Team diff --git a/apps/ios/fastlane/metadata/review_information/notes.txt b/apps/ios/fastlane/metadata/review_information/notes.txt new file mode 100644 index 00000000000..22a99b207ce --- /dev/null +++ b/apps/ios/fastlane/metadata/review_information/notes.txt @@ -0,0 +1 @@ +OpenClaw iOS client for gateway-connected workflows. Reviewers can follow the standard onboarding and pairing flow in-app. diff --git a/apps/ios/fastlane/metadata/review_information/phone_number.txt b/apps/ios/fastlane/metadata/review_information/phone_number.txt new file mode 100644 index 00000000000..4d31de695e8 --- /dev/null +++ b/apps/ios/fastlane/metadata/review_information/phone_number.txt @@ -0,0 +1 @@ ++1 415 555 0100 diff --git a/apps/ios/screenshots/session-2026-03-07/canvas-cool.png b/apps/ios/screenshots/session-2026-03-07/canvas-cool.png new file mode 100644 index 00000000000..965e3cb0fa1 Binary files /dev/null and b/apps/ios/screenshots/session-2026-03-07/canvas-cool.png differ diff --git a/apps/ios/screenshots/session-2026-03-07/onboarding.png b/apps/ios/screenshots/session-2026-03-07/onboarding.png new file mode 100644 index 00000000000..5a440308501 Binary files /dev/null and b/apps/ios/screenshots/session-2026-03-07/onboarding.png differ diff --git a/apps/ios/screenshots/session-2026-03-07/settings.png b/apps/ios/screenshots/session-2026-03-07/settings.png new file mode 100644 index 00000000000..8870e525948 Binary files /dev/null and b/apps/ios/screenshots/session-2026-03-07/settings.png differ diff --git a/apps/ios/screenshots/session-2026-03-07/talk-mode.png b/apps/ios/screenshots/session-2026-03-07/talk-mode.png new file mode 100644 index 00000000000..d49f49cba12 Binary files /dev/null and b/apps/ios/screenshots/session-2026-03-07/talk-mode.png differ diff --git a/scripts/ios-asc-keychain-setup.sh b/scripts/ios-asc-keychain-setup.sh new file mode 100755 index 00000000000..125a3c54b82 --- /dev/null +++ b/scripts/ios-asc-keychain-setup.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + scripts/ios-asc-keychain-setup.sh --key-path /path/to/AuthKey_XXXXXX.p8 --issuer-id [options] + +Required: + --key-path Path to App Store Connect API key (.p8) + --issuer-id App Store Connect issuer ID + +Optional: + --key-id API key ID (auto-detected from AuthKey_.p8 if omitted) + --service Keychain service name (default: openclaw-asc-key) + --account Keychain account name (default: $USER or $LOGNAME) + --write-env Upsert non-secret env vars into apps/ios/fastlane/.env + --env-file Override env file path used with --write-env + -h, --help Show this help + +Example: + scripts/ios-asc-keychain-setup.sh \ + --key-path "$HOME/keys/AuthKey_ABC1234567.p8" \ + --issuer-id "00000000-1111-2222-3333-444444444444" \ + --write-env +EOF +} + +upsert_env_line() { + local file="$1" + local key="$2" + local value="$3" + local tmp + tmp="$(mktemp)" + + if [[ -f "$file" ]]; then + awk -v key="$key" -v value="$value" ' + BEGIN { updated = 0 } + $0 ~ ("^" key "=") { print key "=" value; updated = 1; next } + { print } + END { if (!updated) print key "=" value } + ' "$file" >"$tmp" + else + printf "%s=%s\n" "$key" "$value" >"$tmp" + fi + + mv "$tmp" "$file" +} + +delete_env_line() { + local file="$1" + local key="$2" + local tmp + tmp="$(mktemp)" + + if [[ ! -f "$file" ]]; then + rm -f "$tmp" + return + fi + + awk -v key="$key" ' + $0 ~ ("^" key "=") { next } + { print } + ' "$file" >"$tmp" + + mv "$tmp" "$file" +} + +KEY_PATH="" +KEY_ID="" +ISSUER_ID="" +SERVICE="openclaw-asc-key" +ACCOUNT="${USER:-${LOGNAME:-}}" +WRITE_ENV=0 +ENV_FILE="" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DEFAULT_ENV_FILE="$REPO_ROOT/apps/ios/fastlane/.env" + +while [[ $# -gt 0 ]]; do + case "$1" in + --key-path) + KEY_PATH="${2:-}" + shift 2 + ;; + --key-id) + KEY_ID="${2:-}" + shift 2 + ;; + --issuer-id) + ISSUER_ID="${2:-}" + shift 2 + ;; + --service) + SERVICE="${2:-}" + shift 2 + ;; + --account) + ACCOUNT="${2:-}" + shift 2 + ;; + --write-env) + WRITE_ENV=1 + shift + ;; + --env-file) + ENV_FILE="${2:-}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage + exit 1 + ;; + esac +done + +if [[ -z "$KEY_PATH" || -z "$ISSUER_ID" ]]; then + echo "Missing required arguments." >&2 + usage + exit 1 +fi + +if [[ ! -f "$KEY_PATH" ]]; then + echo "Key file not found: $KEY_PATH" >&2 + exit 1 +fi + +if [[ -z "$KEY_ID" ]]; then + key_filename="$(basename "$KEY_PATH")" + if [[ "$key_filename" =~ ^AuthKey_([A-Za-z0-9]+)\.p8$ ]]; then + KEY_ID="${BASH_REMATCH[1]}" + else + echo "Could not infer --key-id from filename '$key_filename'. Pass --key-id explicitly." >&2 + exit 1 + fi +fi + +if [[ -z "$ACCOUNT" ]]; then + echo "Could not determine Keychain account. Pass --account explicitly." >&2 + exit 1 +fi + +KEY_CONTENT="$(cat "$KEY_PATH")" +if [[ -z "$KEY_CONTENT" ]]; then + echo "Key file is empty: $KEY_PATH" >&2 + exit 1 +fi + +security add-generic-password \ + -a "$ACCOUNT" \ + -s "$SERVICE" \ + -w "$KEY_CONTENT" \ + -U >/dev/null + +echo "Stored ASC API private key in macOS Keychain (service='$SERVICE', account='$ACCOUNT')." +echo +echo "Export these vars for Fastlane:" +echo "ASC_KEY_ID=$KEY_ID" +echo "ASC_ISSUER_ID=$ISSUER_ID" +echo "ASC_KEYCHAIN_SERVICE=$SERVICE" +echo "ASC_KEYCHAIN_ACCOUNT=$ACCOUNT" + +if [[ "$WRITE_ENV" -eq 1 ]]; then + if [[ -z "$ENV_FILE" ]]; then + ENV_FILE="$DEFAULT_ENV_FILE" + fi + + mkdir -p "$(dirname "$ENV_FILE")" + touch "$ENV_FILE" + + upsert_env_line "$ENV_FILE" "ASC_KEY_ID" "$KEY_ID" + upsert_env_line "$ENV_FILE" "ASC_ISSUER_ID" "$ISSUER_ID" + upsert_env_line "$ENV_FILE" "ASC_KEYCHAIN_SERVICE" "$SERVICE" + upsert_env_line "$ENV_FILE" "ASC_KEYCHAIN_ACCOUNT" "$ACCOUNT" + # Remove file/path based keys so Keychain is used by default. + delete_env_line "$ENV_FILE" "ASC_KEY_PATH" + delete_env_line "$ENV_FILE" "ASC_KEY_CONTENT" + delete_env_line "$ENV_FILE" "APP_STORE_CONNECT_API_KEY_PATH" + + echo + echo "Updated env file: $ENV_FILE" +fi diff --git a/scripts/ios-configure-signing.sh b/scripts/ios-configure-signing.sh index 99219725fe7..da534c6d0a5 100755 --- a/scripts/ios-configure-signing.sh +++ b/scripts/ios-configure-signing.sh @@ -63,6 +63,7 @@ fi bundle_base="$(normalize_bundle_id "${bundle_base}")" share_bundle_id="${OPENCLAW_IOS_SHARE_BUNDLE_ID:-${bundle_base}.share}" +activity_widget_bundle_id="${OPENCLAW_IOS_ACTIVITY_WIDGET_BUNDLE_ID:-${bundle_base}.activitywidget}" watch_app_bundle_id="${OPENCLAW_IOS_WATCH_APP_BUNDLE_ID:-${bundle_base}.watchkitapp}" watch_extension_bundle_id="${OPENCLAW_IOS_WATCH_EXTENSION_BUNDLE_ID:-${watch_app_bundle_id}.extension}" @@ -76,7 +77,8 @@ cat >"${tmp_file}" <