feat(ios): prepare app store connect release assets

This commit is contained in:
Nimrod Gutman
2026-03-07 16:39:53 +02:00
committed by Nimrod Gutman
parent 4c0b873a4d
commit 43ab4f33ad
44 changed files with 516 additions and 90 deletions

View File

@@ -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 =

View File

@@ -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.

View File

@@ -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)")

View File

@@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>ai.openclaw.ios.bgrefresh</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -33,6 +37,8 @@
</array>
<key>CFBundleVersion</key>
<string>20260307</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
@@ -52,6 +58,10 @@
<string>OpenClaw uses your location when you allow location sharing.</string>
<key>NSMicrophoneUsageDescription</key>
<string>OpenClaw needs microphone access for voice wake.</string>
<key>NSMotionUsageDescription</key>
<string>OpenClaw may use motion data to support device-aware interactions and automations.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>OpenClaw needs photo library access when you choose existing photos to share with your assistant.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>OpenClaw uses on-device speech recognition for voice wake.</string>
<key>NSSupportsLiveActivities</key>
@@ -66,10 +76,6 @@
<string>audio</string>
<string>remote-notification</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>ai.openclaw.ios.bgrefresh</string>
</array>
<key>UILaunchScreen</key>
<dict/>
<key>UISupportedInterfaceOrientations</key>

View File

@@ -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? {

View File

@@ -15,10 +15,10 @@
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>2026.3.7</string>
<key>CFBundleVersion</key>
<string>20260307</string>
</dict>
</plist>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>2026.3.7</string>
<key>CFBundleVersion</key>
<string>20260307</string>
</dict>
</plist>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -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)

View File

@@ -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

View File

@@ -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:

View File

@@ -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/<id>/...` 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`)

View File

@@ -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

View File

@@ -0,0 +1 @@
openclaw,ai assistant,local ai,voice assistant,automation,gateway,chat,agent,node

View File

@@ -0,0 +1 @@
https://openclaw.ai

View File

@@ -0,0 +1 @@
OpenClaw - iOS Client

View File

@@ -0,0 +1 @@
https://openclaw.ai/privacy

View File

@@ -0,0 +1 @@
Run OpenClaw from your iPhone: pair with your own gateway, trigger automations, and use voice, camera, and share actions.

View File

@@ -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.

View File

@@ -0,0 +1 @@
Personal AI on your devices

View File

@@ -0,0 +1 @@
https://docs.openclaw.ai/platforms/ios

View File

@@ -0,0 +1 @@
support@openclaw.ai

View File

@@ -0,0 +1 @@
OpenClaw

View File

@@ -0,0 +1 @@
Team

View File

@@ -0,0 +1 @@
OpenClaw iOS client for gateway-connected workflows. Reviewers can follow the standard onboarding and pairing flow in-app.

View File

@@ -0,0 +1 @@
+1 415 555 0100

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

187
scripts/ios-asc-keychain-setup.sh Executable file
View File

@@ -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 <issuer-uuid> [options]
Required:
--key-path <path> Path to App Store Connect API key (.p8)
--issuer-id <uuid> App Store Connect issuer ID
Optional:
--key-id <id> API key ID (auto-detected from AuthKey_<id>.p8 if omitted)
--service <name> Keychain service name (default: openclaw-asc-key)
--account <name> Keychain account name (default: $USER or $LOGNAME)
--write-env Upsert non-secret env vars into apps/ios/fastlane/.env
--env-file <path> 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

View File

@@ -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}" <<EOF
// This file is local-only and should not be committed.
// Override values with env vars if needed:
// OPENCLAW_IOS_APP_BUNDLE_ID / OPENCLAW_IOS_BUNDLE_ID_BASE
// OPENCLAW_IOS_SHARE_BUNDLE_ID / OPENCLAW_IOS_WATCH_APP_BUNDLE_ID / OPENCLAW_IOS_WATCH_EXTENSION_BUNDLE_ID
// OPENCLAW_IOS_SHARE_BUNDLE_ID / OPENCLAW_IOS_ACTIVITY_WIDGET_BUNDLE_ID
// OPENCLAW_IOS_WATCH_APP_BUNDLE_ID / OPENCLAW_IOS_WATCH_EXTENSION_BUNDLE_ID
// OPENCLAW_IOS_CODE_SIGN_STYLE / OPENCLAW_IOS_APP_PROFILE / OPENCLAW_IOS_SHARE_PROFILE
OPENCLAW_CODE_SIGN_STYLE = ${code_sign_style}
OPENCLAW_DEVELOPMENT_TEAM = ${team_id}
@@ -84,6 +86,7 @@ OPENCLAW_DEVELOPMENT_TEAM = ${team_id}
OPENCLAW_IOS_SELECTED_TEAM = ${team_id}
OPENCLAW_APP_BUNDLE_ID = ${bundle_base}
OPENCLAW_SHARE_BUNDLE_ID = ${share_bundle_id}
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ${activity_widget_bundle_id}
OPENCLAW_WATCH_APP_BUNDLE_ID = ${watch_app_bundle_id}
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ${watch_extension_bundle_id}
OPENCLAW_APP_PROFILE = ${app_profile}