feat(ios): prepare app store connect release assets
@@ -2,12 +2,13 @@
|
|||||||
// This file is only an example and should stay committed.
|
// This file is only an example and should stay committed.
|
||||||
|
|
||||||
OPENCLAW_CODE_SIGN_STYLE = Automatic
|
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_APP_BUNDLE_ID = ai.openclaw.client
|
||||||
OPENCLAW_SHARE_BUNDLE_ID = ai.openclaw.ios.test.mariano.share
|
OPENCLAW_SHARE_BUNDLE_ID = ai.openclaw.client.share
|
||||||
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.ios.test.mariano.watchkitapp
|
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclaw.client.activitywidget
|
||||||
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.ios.test.mariano.watchkitapp.extension
|
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.client.watchkitapp
|
||||||
|
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.client.watchkitapp.extension
|
||||||
|
|
||||||
// Leave empty with automatic signing.
|
// Leave empty with automatic signing.
|
||||||
OPENCLAW_APP_PROFILE =
|
OPENCLAW_APP_PROFILE =
|
||||||
|
|||||||
@@ -5,11 +5,14 @@
|
|||||||
OPENCLAW_CODE_SIGN_STYLE = Manual
|
OPENCLAW_CODE_SIGN_STYLE = Manual
|
||||||
OPENCLAW_DEVELOPMENT_TEAM = Y5PE65HELJ
|
OPENCLAW_DEVELOPMENT_TEAM = Y5PE65HELJ
|
||||||
|
|
||||||
OPENCLAW_APP_BUNDLE_ID = ai.openclaw.ios
|
OPENCLAW_APP_BUNDLE_ID = ai.openclaw.client
|
||||||
OPENCLAW_SHARE_BUNDLE_ID = ai.openclaw.ios.share
|
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_APP_PROFILE = ai.openclaw.client Development
|
||||||
OPENCLAW_SHARE_PROFILE = ai.openclaw.ios.share Development
|
OPENCLAW_SHARE_PROFILE = ai.openclaw.client.share Development
|
||||||
|
|
||||||
// Keep local includes after defaults: xcconfig is evaluated top-to-bottom,
|
// Keep local includes after defaults: xcconfig is evaluated top-to-bottom,
|
||||||
// so later assignments in local files override the defaults above.
|
// so later assignments in local files override the defaults above.
|
||||||
|
|||||||
@@ -412,11 +412,11 @@ enum GatewayDiagnostics {
|
|||||||
private static let keepLogBytes: Int64 = 256 * 1024
|
private static let keepLogBytes: Int64 = 256 * 1024
|
||||||
private static let logSizeCheckEveryWrites = 50
|
private static let logSizeCheckEveryWrites = 50
|
||||||
private static let logWritesSinceCheck = OSAllocatedUnfairLock(initialState: 0)
|
private static let logWritesSinceCheck = OSAllocatedUnfairLock(initialState: 0)
|
||||||
private static let isoFormatter: ISO8601DateFormatter = {
|
private static func isoTimestamp() -> String {
|
||||||
let f = ISO8601DateFormatter()
|
let formatter = ISO8601DateFormatter()
|
||||||
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||||
return f
|
return formatter.string(from: Date())
|
||||||
}()
|
}
|
||||||
|
|
||||||
private static var fileURL: URL? {
|
private static var fileURL: URL? {
|
||||||
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?
|
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?
|
||||||
@@ -476,7 +476,7 @@ enum GatewayDiagnostics {
|
|||||||
guard let url = fileURL else { return }
|
guard let url = fileURL else { return }
|
||||||
queue.async {
|
queue.async {
|
||||||
self.truncateLogIfNeeded(url: url)
|
self.truncateLogIfNeeded(url: url)
|
||||||
let timestamp = self.isoFormatter.string(from: Date())
|
let timestamp = self.isoTimestamp()
|
||||||
let line = "[\(timestamp)] gateway diagnostics started\n"
|
let line = "[\(timestamp)] gateway diagnostics started\n"
|
||||||
if let data = line.data(using: .utf8) {
|
if let data = line.data(using: .utf8) {
|
||||||
self.appendToLog(url: url, data: data)
|
self.appendToLog(url: url, data: data)
|
||||||
@@ -486,7 +486,7 @@ enum GatewayDiagnostics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func log(_ message: String) {
|
static func log(_ message: String) {
|
||||||
let timestamp = self.isoFormatter.string(from: Date())
|
let timestamp = self.isoTimestamp()
|
||||||
let line = "[\(timestamp)] \(message)"
|
let line = "[\(timestamp)] \(message)"
|
||||||
logger.info("\(line, privacy: .public)")
|
logger.info("\(line, privacy: .public)")
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
|
<array>
|
||||||
|
<string>ai.openclaw.ios.bgrefresh</string>
|
||||||
|
</array>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
@@ -33,6 +37,8 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>20260307</string>
|
<string>20260307</string>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||||
@@ -52,6 +58,10 @@
|
|||||||
<string>OpenClaw uses your location when you allow location sharing.</string>
|
<string>OpenClaw uses your location when you allow location sharing.</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>OpenClaw needs microphone access for voice wake.</string>
|
<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>
|
<key>NSSpeechRecognitionUsageDescription</key>
|
||||||
<string>OpenClaw uses on-device speech recognition for voice wake.</string>
|
<string>OpenClaw uses on-device speech recognition for voice wake.</string>
|
||||||
<key>NSSupportsLiveActivities</key>
|
<key>NSSupportsLiveActivities</key>
|
||||||
@@ -66,10 +76,6 @@
|
|||||||
<string>audio</string>
|
<string>audio</string>
|
||||||
<string>remote-notification</string>
|
<string>remote-notification</string>
|
||||||
</array>
|
</array>
|
||||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
|
||||||
<array>
|
|
||||||
<string>ai.openclaw.ios.bgrefresh</string>
|
|
||||||
</array>
|
|
||||||
<key>UILaunchScreen</key>
|
<key>UILaunchScreen</key>
|
||||||
<dict/>
|
<dict/>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
|||||||
@@ -264,61 +264,65 @@ private struct CanvasContent: View {
|
|||||||
var openSettings: () -> Void
|
var openSettings: () -> Void
|
||||||
|
|
||||||
private var brightenButtons: Bool { self.systemColorScheme == .light }
|
private var brightenButtons: Bool { self.systemColorScheme == .light }
|
||||||
|
private var talkActive: Bool { self.appModel.talkMode.isEnabled || self.talkEnabled }
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .topTrailing) {
|
ZStack {
|
||||||
ScreenTab()
|
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) {
|
.overlay(alignment: .center) {
|
||||||
if self.appModel.talkMode.isEnabled {
|
if self.talkActive {
|
||||||
TalkOrbOverlay()
|
TalkOrbOverlay()
|
||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.overlay(alignment: .topLeading) {
|
.overlay(alignment: .topLeading) {
|
||||||
StatusPill(
|
HStack(alignment: .top, spacing: 8) {
|
||||||
gateway: self.gatewayStatus,
|
StatusPill(
|
||||||
voiceWakeEnabled: self.voiceWakeEnabled,
|
gateway: self.gatewayStatus,
|
||||||
activity: self.statusActivity,
|
voiceWakeEnabled: self.voiceWakeEnabled,
|
||||||
brighten: self.brightenButtons,
|
activity: self.statusActivity,
|
||||||
onTap: {
|
brighten: self.brightenButtons,
|
||||||
if self.gatewayStatus == .connected {
|
onTap: {
|
||||||
self.showGatewayActions = true
|
if self.gatewayStatus == .connected {
|
||||||
} else {
|
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()
|
self.openSettings()
|
||||||
}
|
}
|
||||||
})
|
.accessibilityLabel("Settings")
|
||||||
.padding(.leading, 10)
|
}
|
||||||
.safeAreaPadding(.top, 10)
|
}
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.safeAreaPadding(.top, 10)
|
||||||
}
|
}
|
||||||
.overlay(alignment: .topLeading) {
|
.overlay(alignment: .topLeading) {
|
||||||
if let voiceWakeToastText, !voiceWakeToastText.isEmpty {
|
if let voiceWakeToastText, !voiceWakeToastText.isEmpty {
|
||||||
@@ -334,6 +338,12 @@ private struct CanvasContent: View {
|
|||||||
isPresented: self.$showGatewayActions,
|
isPresented: self.$showGatewayActions,
|
||||||
onDisconnect: { self.appModel.disconnectGateway() },
|
onDisconnect: { self.appModel.disconnectGateway() },
|
||||||
onOpenSettings: { self.openSettings() })
|
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? {
|
private var statusActivity: StatusPill.Activity? {
|
||||||
|
|||||||
@@ -15,10 +15,10 @@
|
|||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>$(PRODUCT_NAME)</string>
|
<string>$(PRODUCT_NAME)</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>BNDL</string>
|
<string>BNDL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2026.3.7</string>
|
<string>2026.3.7</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>20260307</string>
|
<string>20260307</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 504 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 32 KiB |
@@ -1,7 +1,15 @@
|
|||||||
app_identifier("ai.openclaw.ios")
|
app_identifier("ai.openclaw.client")
|
||||||
|
|
||||||
# Auth is expected via App Store Connect API key.
|
# Auth is expected via App Store Connect API key.
|
||||||
# Provide either:
|
# Provide either:
|
||||||
# - APP_STORE_CONNECT_API_KEY_PATH=/path/to/AuthKey_XXXXXX.p8.json (recommended)
|
# - APP_STORE_CONNECT_API_KEY_PATH=/path/to/AuthKey_XXXXXX.p8.json (recommended)
|
||||||
# or:
|
# 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, 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)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
require "shellwords"
|
require "shellwords"
|
||||||
|
require "open3"
|
||||||
|
|
||||||
default_platform(:ios)
|
default_platform(:ios)
|
||||||
|
|
||||||
@@ -16,33 +17,104 @@ def load_env_file(path)
|
|||||||
end
|
end
|
||||||
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
|
platform :ios do
|
||||||
private_lane :asc_api_key do
|
private_lane :asc_api_key do
|
||||||
load_env_file(File.join(__dir__, ".env"))
|
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
|
api_key = nil
|
||||||
|
|
||||||
key_path = ENV["APP_STORE_CONNECT_API_KEY_PATH"]
|
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)
|
api_key = app_store_connect_api_key(path: key_path)
|
||||||
else
|
else
|
||||||
p8_path = ENV["ASC_KEY_PATH"]
|
p8_path = ENV["ASC_KEY_PATH"]
|
||||||
if p8_path && !p8_path.strip.empty?
|
if env_present?(p8_path)
|
||||||
key_id = ENV["ASC_KEY_ID"]
|
key_id = ENV["ASC_KEY_ID"]
|
||||||
issuer_id = ENV["ASC_ISSUER_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? }
|
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(
|
api_key = app_store_connect_api_key(
|
||||||
key_id: key_id,
|
key_id: key_id,
|
||||||
issuer_id: issuer_id,
|
issuer_id: issuer_id,
|
||||||
key_filepath: p8_path
|
key_filepath: p8_path
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
key_id = ENV["ASC_KEY_ID"]
|
key_id = ENV["ASC_KEY_ID"]
|
||||||
issuer_id = ENV["ASC_ISSUER_ID"]
|
issuer_id = ENV["ASC_ISSUER_ID"]
|
||||||
key_content = ENV["ASC_KEY_CONTENT"]
|
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
|
is_base64 = key_content.include?("BEGIN PRIVATE KEY") ? false : true
|
||||||
|
|
||||||
@@ -64,7 +136,7 @@ platform :ios do
|
|||||||
|
|
||||||
team_id = ENV["IOS_DEVELOPMENT_TEAM"]
|
team_id = ENV["IOS_DEVELOPMENT_TEAM"]
|
||||||
if team_id.nil? || team_id.strip.empty?
|
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)
|
if File.exist?(helper_path)
|
||||||
# Keep CI/local compatibility where teams are present in keychain but not Xcode account metadata.
|
# 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
|
team_id = sh("IOS_ALLOW_KEYCHAIN_TEAM_FALLBACK=1 bash #{helper_path.shellescape}").strip
|
||||||
@@ -77,6 +149,7 @@ platform :ios do
|
|||||||
scheme: "OpenClaw",
|
scheme: "OpenClaw",
|
||||||
export_method: "app-store",
|
export_method: "app-store",
|
||||||
clean: true,
|
clean: true,
|
||||||
|
skip_profile_detection: true,
|
||||||
xcargs: "DEVELOPMENT_TEAM=#{team_id} -allowProvisioningUpdates",
|
xcargs: "DEVELOPMENT_TEAM=#{team_id} -allowProvisioningUpdates",
|
||||||
export_xcargs: "-allowProvisioningUpdates",
|
export_xcargs: "-allowProvisioningUpdates",
|
||||||
export_options: {
|
export_options: {
|
||||||
@@ -86,19 +159,40 @@ platform :ios do
|
|||||||
|
|
||||||
upload_to_testflight(
|
upload_to_testflight(
|
||||||
api_key: api_key,
|
api_key: api_key,
|
||||||
skip_waiting_for_build_processing: true
|
skip_waiting_for_build_processing: true,
|
||||||
|
uses_non_exempt_encryption: false
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Upload App Store metadata (and optionally screenshots)"
|
desc "Upload App Store metadata (and optionally screenshots)"
|
||||||
lane :metadata do
|
lane :metadata do
|
||||||
api_key = asc_api_key
|
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,
|
api_key: api_key,
|
||||||
force: true,
|
force: true,
|
||||||
skip_screenshots: ENV["DELIVER_SCREENSHOTS"] != "1",
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
- App Store Connect → Users and Access → Keys → App Store Connect API → Generate API Key
|
||||||
- Download the `.p8`, note the **Issuer ID** and **Key ID**
|
- 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
|
```bash
|
||||||
ASC_KEY_ID=YOUR_KEY_ID
|
ASC_KEY_ID=YOUR_KEY_ID
|
||||||
ASC_ISSUER_ID=YOUR_ISSUER_ID
|
ASC_ISSUER_ID=YOUR_ISSUER_ID
|
||||||
ASC_KEY_PATH=/absolute/path/to/AuthKey_XXXXXXXXXX.p8
|
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
|
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:
|
Run:
|
||||||
|
|
||||||
|
|||||||
47
apps/ios/fastlane/metadata/README.md
Normal 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`)
|
||||||
18
apps/ios/fastlane/metadata/en-US/description.txt
Normal 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
|
||||||
1
apps/ios/fastlane/metadata/en-US/keywords.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
openclaw,ai assistant,local ai,voice assistant,automation,gateway,chat,agent,node
|
||||||
1
apps/ios/fastlane/metadata/en-US/marketing_url.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://openclaw.ai
|
||||||
1
apps/ios/fastlane/metadata/en-US/name.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
OpenClaw - iOS Client
|
||||||
1
apps/ios/fastlane/metadata/en-US/privacy_url.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://openclaw.ai/privacy
|
||||||
1
apps/ios/fastlane/metadata/en-US/promotional_text.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Run OpenClaw from your iPhone: pair with your own gateway, trigger automations, and use voice, camera, and share actions.
|
||||||
1
apps/ios/fastlane/metadata/en-US/release_notes.txt
Normal 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.
|
||||||
1
apps/ios/fastlane/metadata/en-US/subtitle.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Personal AI on your devices
|
||||||
1
apps/ios/fastlane/metadata/en-US/support_url.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://docs.openclaw.ai/platforms/ios
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
support@openclaw.ai
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
OpenClaw
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Team
|
||||||
1
apps/ios/fastlane/metadata/review_information/notes.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
OpenClaw iOS client for gateway-connected workflows. Reviewers can follow the standard onboarding and pairing flow in-app.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
+1 415 555 0100
|
||||||
BIN
apps/ios/screenshots/session-2026-03-07/canvas-cool.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
apps/ios/screenshots/session-2026-03-07/onboarding.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
apps/ios/screenshots/session-2026-03-07/settings.png
Normal file
|
After Width: | Height: | Size: 300 KiB |
BIN
apps/ios/screenshots/session-2026-03-07/talk-mode.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
187
scripts/ios-asc-keychain-setup.sh
Executable 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
|
||||||
@@ -63,6 +63,7 @@ fi
|
|||||||
bundle_base="$(normalize_bundle_id "${bundle_base}")"
|
bundle_base="$(normalize_bundle_id "${bundle_base}")"
|
||||||
|
|
||||||
share_bundle_id="${OPENCLAW_IOS_SHARE_BUNDLE_ID:-${bundle_base}.share}"
|
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_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}"
|
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.
|
// This file is local-only and should not be committed.
|
||||||
// Override values with env vars if needed:
|
// Override values with env vars if needed:
|
||||||
// OPENCLAW_IOS_APP_BUNDLE_ID / OPENCLAW_IOS_BUNDLE_ID_BASE
|
// 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_IOS_CODE_SIGN_STYLE / OPENCLAW_IOS_APP_PROFILE / OPENCLAW_IOS_SHARE_PROFILE
|
||||||
OPENCLAW_CODE_SIGN_STYLE = ${code_sign_style}
|
OPENCLAW_CODE_SIGN_STYLE = ${code_sign_style}
|
||||||
OPENCLAW_DEVELOPMENT_TEAM = ${team_id}
|
OPENCLAW_DEVELOPMENT_TEAM = ${team_id}
|
||||||
@@ -84,6 +86,7 @@ OPENCLAW_DEVELOPMENT_TEAM = ${team_id}
|
|||||||
OPENCLAW_IOS_SELECTED_TEAM = ${team_id}
|
OPENCLAW_IOS_SELECTED_TEAM = ${team_id}
|
||||||
OPENCLAW_APP_BUNDLE_ID = ${bundle_base}
|
OPENCLAW_APP_BUNDLE_ID = ${bundle_base}
|
||||||
OPENCLAW_SHARE_BUNDLE_ID = ${share_bundle_id}
|
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_APP_BUNDLE_ID = ${watch_app_bundle_id}
|
||||||
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ${watch_extension_bundle_id}
|
OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ${watch_extension_bundle_id}
|
||||||
OPENCLAW_APP_PROFILE = ${app_profile}
|
OPENCLAW_APP_PROFILE = ${app_profile}
|
||||||
|
|||||||