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}" <