比较提交
没有共同的提交。b413573178d3926ea8887dd663c4163890443bb0 和 027937db1b4a831b999f3d4ee91ac770c6813b6f 的历史完全不同。
b413573178
...
027937db1b
21
README.md
21
README.md
@ -20,7 +20,7 @@ XuqmGroup-iOSSDK/
|
|||||||
├── Push/
|
├── Push/
|
||||||
│ └── PushSDK.swift # APNs Token 注册
|
│ └── PushSDK.swift # APNs Token 注册
|
||||||
└── Update/
|
└── Update/
|
||||||
└── UpdateSDK.swift # 版本检查 / App 更新
|
└── UpdateSDK.swift # 版本检查 / Bundle 下载
|
||||||
```
|
```
|
||||||
|
|
||||||
## 集成
|
## 集成
|
||||||
@ -174,7 +174,6 @@ public struct ImMessage: Decodable {
|
|||||||
public let extra: String?
|
public let extra: String?
|
||||||
public let revoked: Bool
|
public let revoked: Bool
|
||||||
public let createdAt: String
|
public let createdAt: String
|
||||||
public let editedAt: Int64?
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -227,7 +226,23 @@ if result.needsUpdate, let info = result.info {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`UpdateSDK` 这里只负责 iOS App 版本更新。RN 热更新如果需要,走独立的 RN SDK / RN 更新模块,不并入统一发版能力。
|
### 检查 RN Bundle 更新
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let rnResult = try await UpdateSDK.checkRnUpdate(
|
||||||
|
appId: "ak_xxx",
|
||||||
|
moduleId: "main",
|
||||||
|
currentVersion: "1.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
if rnResult.needsUpdate, let info = rnResult.info {
|
||||||
|
let filePath = try await UpdateSDK.downloadBundle(
|
||||||
|
from: info.downloadUrl,
|
||||||
|
filename: "main.ios.bundle"
|
||||||
|
)
|
||||||
|
// 校验 MD5,通知 RN 引擎加载
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -84,7 +84,6 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
status: .sending,
|
status: .sending,
|
||||||
mentionedUserIds: mentionedUserIds?.isEmpty == false ? mentionedUserIds : nil,
|
mentionedUserIds: mentionedUserIds?.isEmpty == false ? mentionedUserIds : nil,
|
||||||
groupReadCount: nil,
|
groupReadCount: nil,
|
||||||
revoked: false,
|
|
||||||
createdAt: now
|
createdAt: now
|
||||||
)
|
)
|
||||||
guard let activeAppId else {
|
guard let activeAppId else {
|
||||||
@ -200,9 +199,6 @@ public final class ImClient: NSObject, URLSessionWebSocketDelegate, @unchecked S
|
|||||||
let msg = try? JSONDecoder().decode(ImMessage.self, from: messageData) else {
|
let msg = try? JSONDecoder().decode(ImMessage.self, from: messageData) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (msg.revoked ?? false) || msg.status == .revoked || msg.msgType == .revoked {
|
|
||||||
delegate?.imClientDidReceiveRevokedMessage(msg)
|
|
||||||
}
|
|
||||||
if msg.chatType == .group {
|
if msg.chatType == .group {
|
||||||
delegate?.imClientDidReceiveGroupMessage(msg)
|
delegate?.imClientDidReceiveGroupMessage(msg)
|
||||||
} else {
|
} else {
|
||||||
@ -302,7 +298,6 @@ private extension ImMessage {
|
|||||||
status: .failed,
|
status: .failed,
|
||||||
mentionedUserIds: mentionedUserIds,
|
mentionedUserIds: mentionedUserIds,
|
||||||
groupReadCount: groupReadCount,
|
groupReadCount: groupReadCount,
|
||||||
revoked: false,
|
|
||||||
createdAt: createdAt
|
createdAt: createdAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -244,23 +244,8 @@ public final class ImSDK {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func revokeMessage(messageId: String) async throws -> ImMessage {
|
public func revokeMessage(messageId: String) {
|
||||||
let config = XuqmSDK.shared.requireConfig()
|
client?.revoke(messageId: messageId)
|
||||||
return try await ApiClient.shared.request(
|
|
||||||
path: "/api/im/messages/\(messageId)/revoke",
|
|
||||||
method: "POST",
|
|
||||||
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func editMessage(messageId: String, content: String) async throws -> ImMessage {
|
|
||||||
let config = XuqmSDK.shared.requireConfig()
|
|
||||||
return try await ApiClient.shared.request(
|
|
||||||
path: "/api/im/messages/\(messageId)",
|
|
||||||
method: "PUT",
|
|
||||||
queryItems: [URLQueryItem(name: "appId", value: config.appId)],
|
|
||||||
body: EditMessageRequest(content: content)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func subscribeGroup(_ groupId: String) {
|
public func subscribeGroup(_ groupId: String) {
|
||||||
@ -289,7 +274,6 @@ public final class ImSDK {
|
|||||||
status: .failed,
|
status: .failed,
|
||||||
mentionedUserIds: mentionedUserIds,
|
mentionedUserIds: mentionedUserIds,
|
||||||
groupReadCount: nil,
|
groupReadCount: nil,
|
||||||
revoked: false,
|
|
||||||
createdAt: Int64(Date().timeIntervalSince1970 * 1000)
|
createdAt: Int64(Date().timeIntervalSince1970 * 1000)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -335,57 +319,6 @@ public final class ImSDK {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func locateHistoryPage(
|
|
||||||
toId: String,
|
|
||||||
messageId: String,
|
|
||||||
pageSize: Int = 20,
|
|
||||||
maxPages: Int = 20
|
|
||||||
) async throws -> [ImMessage]? {
|
|
||||||
try await locatePage(
|
|
||||||
messageId: messageId,
|
|
||||||
maxPages: maxPages,
|
|
||||||
pageSize: pageSize,
|
|
||||||
loadPage: { page in
|
|
||||||
try await self.fetchHistory(toId: toId, page: page, size: pageSize)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func locateGroupHistoryPage(
|
|
||||||
groupId: String,
|
|
||||||
messageId: String,
|
|
||||||
pageSize: Int = 20,
|
|
||||||
maxPages: Int = 20
|
|
||||||
) async throws -> [ImMessage]? {
|
|
||||||
try await locatePage(
|
|
||||||
messageId: messageId,
|
|
||||||
maxPages: maxPages,
|
|
||||||
pageSize: pageSize,
|
|
||||||
loadPage: { page in
|
|
||||||
try await self.fetchGroupHistory(groupId: groupId, page: page, size: pageSize)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func locatePage(
|
|
||||||
messageId: String,
|
|
||||||
maxPages: Int,
|
|
||||||
pageSize: Int,
|
|
||||||
loadPage: @escaping (Int) async throws -> [ImMessage]
|
|
||||||
) async throws -> [ImMessage]? {
|
|
||||||
let pageCount = max(maxPages, 1)
|
|
||||||
for page in 0..<pageCount {
|
|
||||||
let messages = try await loadPage(page)
|
|
||||||
if messages.contains(where: { $0.id == messageId }) {
|
|
||||||
return messages
|
|
||||||
}
|
|
||||||
if messages.count < pageSize {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
public func listGroups() async throws -> [ImGroup] {
|
public func listGroups() async throws -> [ImGroup] {
|
||||||
let config = XuqmSDK.shared.requireConfig()
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
let groups: [ImGroup] = try await ApiClient.shared.request(
|
let groups: [ImGroup] = try await ApiClient.shared.request(
|
||||||
@ -406,56 +339,6 @@ public final class ImSDK {
|
|||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
public func searchUsers(keyword: String, size: Int = 20) async throws -> [UserProfile] {
|
|
||||||
let config = XuqmSDK.shared.requireConfig()
|
|
||||||
return try await ApiClient.shared.request(
|
|
||||||
path: "/api/im/admin/users/search",
|
|
||||||
queryItems: [
|
|
||||||
URLQueryItem(name: "appId", value: config.appId),
|
|
||||||
URLQueryItem(name: "keyword", value: keyword),
|
|
||||||
URLQueryItem(name: "size", value: String(size)),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func searchGroups(keyword: String, size: Int = 20) async throws -> [ImGroup] {
|
|
||||||
let config = XuqmSDK.shared.requireConfig()
|
|
||||||
return try await ApiClient.shared.request(
|
|
||||||
path: "/api/im/admin/groups/search",
|
|
||||||
queryItems: [
|
|
||||||
URLQueryItem(name: "appId", value: config.appId),
|
|
||||||
URLQueryItem(name: "keyword", value: keyword),
|
|
||||||
URLQueryItem(name: "size", value: String(size)),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func searchMessages(
|
|
||||||
keyword: String? = nil,
|
|
||||||
chatType: String? = nil,
|
|
||||||
msgType: String? = nil,
|
|
||||||
startTime: Date? = nil,
|
|
||||||
endTime: Date? = nil,
|
|
||||||
page: Int = 0,
|
|
||||||
size: Int = 20
|
|
||||||
) async throws -> PageResult<ImMessage> {
|
|
||||||
let config = XuqmSDK.shared.requireConfig()
|
|
||||||
var items: [URLQueryItem] = [
|
|
||||||
URLQueryItem(name: "appId", value: config.appId),
|
|
||||||
URLQueryItem(name: "page", value: String(page)),
|
|
||||||
URLQueryItem(name: "size", value: String(size)),
|
|
||||||
]
|
|
||||||
if let keyword, !keyword.isEmpty { items.append(URLQueryItem(name: "keyword", value: keyword)) }
|
|
||||||
if let chatType, !chatType.isEmpty { items.append(URLQueryItem(name: "chatType", value: chatType)) }
|
|
||||||
if let msgType, !msgType.isEmpty { items.append(URLQueryItem(name: "msgType", value: msgType)) }
|
|
||||||
if let startTime { items.append(URLQueryItem(name: "startTime", value: isoLocalDateTime(startTime))) }
|
|
||||||
if let endTime { items.append(URLQueryItem(name: "endTime", value: isoLocalDateTime(endTime))) }
|
|
||||||
return try await ApiClient.shared.request(
|
|
||||||
path: "/api/im/admin/messages/search",
|
|
||||||
queryItems: items
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func createGroup(name: String, memberIds: [String], groupType: String = "WORK") async throws -> ImGroup {
|
public func createGroup(name: String, memberIds: [String], groupType: String = "WORK") async throws -> ImGroup {
|
||||||
let config = XuqmSDK.shared.requireConfig()
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
return try await ApiClient.shared.request(
|
return try await ApiClient.shared.request(
|
||||||
@ -470,26 +353,6 @@ public final class ImSDK {
|
|||||||
try await ApiClient.shared.request(path: "/api/im/groups/\(groupId)")
|
try await ApiClient.shared.request(path: "/api/im/groups/\(groupId)")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func listGroupMembers(groupId: String) async throws -> [UserProfile] {
|
|
||||||
let config = XuqmSDK.shared.requireConfig()
|
|
||||||
return try await ApiClient.shared.request(
|
|
||||||
path: "/api/im/groups/\(groupId)/members",
|
|
||||||
queryItems: [URLQueryItem(name: "appId", value: config.appId)]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func searchGroupMembers(groupId: String, keyword: String, size: Int = 20) async throws -> [UserProfile] {
|
|
||||||
let config = XuqmSDK.shared.requireConfig()
|
|
||||||
return try await ApiClient.shared.request(
|
|
||||||
path: "/api/im/groups/\(groupId)/members/search",
|
|
||||||
queryItems: [
|
|
||||||
URLQueryItem(name: "appId", value: config.appId),
|
|
||||||
URLQueryItem(name: "keyword", value: keyword),
|
|
||||||
URLQueryItem(name: "size", value: String(size))
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func updateGroupInfo(groupId: String, name: String? = nil, announcement: String? = nil) async throws -> ImGroup {
|
public func updateGroupInfo(groupId: String, name: String? = nil, announcement: String? = nil) async throws -> ImGroup {
|
||||||
try await ApiClient.shared.request(
|
try await ApiClient.shared.request(
|
||||||
path: "/api/im/groups/\(groupId)",
|
path: "/api/im/groups/\(groupId)",
|
||||||
|
|||||||
@ -43,35 +43,17 @@ public struct ImMessage: Codable, Sendable {
|
|||||||
public let status: MsgStatus
|
public let status: MsgStatus
|
||||||
public let mentionedUserIds: String?
|
public let mentionedUserIds: String?
|
||||||
public let groupReadCount: Int?
|
public let groupReadCount: Int?
|
||||||
public let revoked: Bool?
|
|
||||||
public let createdAt: Int64
|
public let createdAt: Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct PageResult<T: Decodable & Sendable>: Decodable, Sendable {
|
|
||||||
public let content: [T]
|
|
||||||
public let totalElements: Int64
|
|
||||||
public let totalPages: Int
|
|
||||||
public let size: Int
|
|
||||||
public let number: Int
|
|
||||||
public let numberOfElements: Int
|
|
||||||
public let first: Bool
|
|
||||||
public let last: Bool
|
|
||||||
public let empty: Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
public protocol ImEventDelegate: AnyObject {
|
public protocol ImEventDelegate: AnyObject {
|
||||||
func imClientDidConnect()
|
func imClientDidConnect()
|
||||||
func imClientDidDisconnect(reason: String?)
|
func imClientDidDisconnect(reason: String?)
|
||||||
func imClientDidReceiveMessage(_ message: ImMessage)
|
func imClientDidReceiveMessage(_ message: ImMessage)
|
||||||
func imClientDidReceiveGroupMessage(_ message: ImMessage)
|
func imClientDidReceiveGroupMessage(_ message: ImMessage)
|
||||||
func imClientDidReceiveRevokedMessage(_ message: ImMessage)
|
|
||||||
func imClientDidError(_ error: String)
|
func imClientDidError(_ error: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension ImEventDelegate {
|
|
||||||
func imClientDidReceiveRevokedMessage(_ message: ImMessage) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct ImGroup: Codable, Sendable {
|
public struct ImGroup: Codable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let appId: String
|
public let appId: String
|
||||||
@ -145,10 +127,6 @@ public struct ImSendMessageRequest: Encodable, Sendable {
|
|||||||
public let mentionedUserIds: String?
|
public let mentionedUserIds: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct EditMessageRequest: Encodable, Sendable {
|
|
||||||
public let content: String
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct ImLoginResponse: Decodable, Sendable {
|
public struct ImLoginResponse: Decodable, Sendable {
|
||||||
public let token: String
|
public let token: String
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,15 @@ public struct AppUpdateInfo: Decodable, Sendable {
|
|||||||
public let appStoreUrl: String?
|
public let appStoreUrl: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct RnUpdateInfo: Decodable, Sendable {
|
||||||
|
public let needsUpdate: Bool
|
||||||
|
public let latestVersion: String
|
||||||
|
public let downloadUrl: String
|
||||||
|
public let md5: String
|
||||||
|
public let minCommonVersion: String
|
||||||
|
public let note: String
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public final class UpdateSDK {
|
public final class UpdateSDK {
|
||||||
|
|
||||||
@ -41,4 +50,17 @@ public final class UpdateSDK {
|
|||||||
NSWorkspace.shared.open(storeURL)
|
NSWorkspace.shared.open(storeURL)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func checkRnUpdate(moduleId: String, currentVersion: String) async throws -> RnUpdateInfo {
|
||||||
|
let config = XuqmSDK.shared.requireConfig()
|
||||||
|
return try await ApiClient.shared.request(
|
||||||
|
path: "/api/v1/rn/update/check",
|
||||||
|
queryItems: [
|
||||||
|
URLQueryItem(name: "appId", value: config.appId),
|
||||||
|
URLQueryItem(name: "moduleId", value: moduleId),
|
||||||
|
URLQueryItem(name: "platform", value: "IOS"),
|
||||||
|
URLQueryItem(name: "currentVersion", value: currentVersion),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
187
scripts/Fastfile
187
scripts/Fastfile
@ -1,187 +0,0 @@
|
|||||||
# XuqmGroup Update Service — iOS Fastlane Release Lane
|
|
||||||
#
|
|
||||||
# Requires: fastlane (gem install fastlane) — standard iOS platform toolchain
|
|
||||||
# Usage (from your iOS project root):
|
|
||||||
# fastlane xuqm_release
|
|
||||||
#
|
|
||||||
# Config: add a .env.xuqm file to your project root with:
|
|
||||||
# XUQM_SERVER_URL=https://update.dev.xuqinmin.com
|
|
||||||
# XUQM_APP_ID=your-app-id
|
|
||||||
# XUQM_API_TOKEN=your-api-token
|
|
||||||
# XUQM_SCHEME=MyApp
|
|
||||||
# XUQM_WORKSPACE=MyApp.xcworkspace
|
|
||||||
# XUQM_BUNDLE_ID=com.example.myapp
|
|
||||||
# XUQM_STORE_TARGETS=APP_STORE # comma-separated, e.g. "APP_STORE"
|
|
||||||
# XUQM_AUTO_PUBLISH_AFTER_REVIEW=false
|
|
||||||
# XUQM_WEBHOOK_URL= # optional
|
|
||||||
# XUQM_SCHEDULED_PUBLISH_AT= # optional, ISO datetime
|
|
||||||
# # For App Store Connect API (used by upload_to_app_store):
|
|
||||||
# APP_STORE_CONNECT_API_KEY_ID=XXXXXXXXXX
|
|
||||||
# APP_STORE_CONNECT_API_ISSUER_ID=xxxx-xxxx-xxxx-xxxx
|
|
||||||
# APP_STORE_CONNECT_API_KEY_FILEPATH=~/.appstoreconnect/AuthKey_XXXXXXXXXX.p8
|
|
||||||
|
|
||||||
require 'net/http'
|
|
||||||
require 'json'
|
|
||||||
require 'uri'
|
|
||||||
|
|
||||||
default_platform(:ios)
|
|
||||||
|
|
||||||
platform :ios do
|
|
||||||
|
|
||||||
desc "Build, upload to XuqmGroup update service, and submit to App Store"
|
|
||||||
lane :xuqm_release do
|
|
||||||
server_url = ENV.fetch("XUQM_SERVER_URL")
|
|
||||||
app_id = ENV.fetch("XUQM_APP_ID")
|
|
||||||
api_token = ENV.fetch("XUQM_API_TOKEN")
|
|
||||||
scheme = ENV.fetch("XUQM_SCHEME")
|
|
||||||
workspace = ENV.fetch("XUQM_WORKSPACE")
|
|
||||||
bundle_id = ENV.fetch("XUQM_BUNDLE_ID", "")
|
|
||||||
store_targets = ENV.fetch("XUQM_STORE_TARGETS", "")
|
|
||||||
auto_publish = ENV.fetch("XUQM_AUTO_PUBLISH_AFTER_REVIEW", "false") == "true"
|
|
||||||
webhook_url = ENV.fetch("XUQM_WEBHOOK_URL", "")
|
|
||||||
scheduled_at = ENV.fetch("XUQM_SCHEDULED_PUBLISH_AT", "")
|
|
||||||
|
|
||||||
# ── 1. Read local version ────────────────────────────────────────────
|
|
||||||
version_name = get_version_number(target: scheme)
|
|
||||||
version_code = get_build_number.to_i
|
|
||||||
UI.message("Local version: #{version_name} (#{version_code})")
|
|
||||||
|
|
||||||
# ── 2. Check server latest ───────────────────────────────────────────
|
|
||||||
server_version_code = xuqm_get_latest_version_code(server_url, app_id, api_token, "IOS")
|
|
||||||
UI.message("Server latest versionCode: #{server_version_code}")
|
|
||||||
|
|
||||||
if version_code <= server_version_code
|
|
||||||
UI.user_error!(
|
|
||||||
"Local versionCode (#{version_code}) must be greater than server (#{server_version_code}). " \
|
|
||||||
"Please bump CFBundleVersion before releasing."
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# ── 3. Archive & export IPA ──────────────────────────────────────────
|
|
||||||
archive_path = File.join(Dir.tmpdir, "xuqm_#{scheme}.xcarchive")
|
|
||||||
ipa_output = File.join(Dir.tmpdir, "xuqm_#{scheme}_ipa")
|
|
||||||
|
|
||||||
build_app(
|
|
||||||
scheme: scheme,
|
|
||||||
workspace: workspace,
|
|
||||||
archive_path: archive_path,
|
|
||||||
output_directory: ipa_output,
|
|
||||||
export_method: "app-store",
|
|
||||||
silent: false,
|
|
||||||
)
|
|
||||||
|
|
||||||
ipa_path = Dir.glob("#{ipa_output}/*.ipa").first
|
|
||||||
UI.user_error!("IPA not found in #{ipa_output}") unless ipa_path
|
|
||||||
UI.success("IPA: #{ipa_path}")
|
|
||||||
|
|
||||||
# ── 4. Upload to XuqmGroup update service ────────────────────────────
|
|
||||||
change_log = prompt(text: "Release notes: ", ci_input: "")
|
|
||||||
version_id = xuqm_upload_ipa(
|
|
||||||
server_url: server_url,
|
|
||||||
app_id: app_id,
|
|
||||||
api_token: api_token,
|
|
||||||
ipa_path: ipa_path,
|
|
||||||
version_name: version_name,
|
|
||||||
version_code: version_code,
|
|
||||||
change_log: change_log,
|
|
||||||
package_name: bundle_id,
|
|
||||||
store_targets: store_targets.empty? ? [] : store_targets.split(",").map(&:strip),
|
|
||||||
auto_publish_after_review: auto_publish,
|
|
||||||
scheduled_publish_at: scheduled_at,
|
|
||||||
webhook_url: webhook_url,
|
|
||||||
)
|
|
||||||
UI.success("Uploaded, version ID: #{version_id}")
|
|
||||||
|
|
||||||
# ── 5. Submit to App Store via App Store Connect API ─────────────────
|
|
||||||
if store_targets.include?("APP_STORE")
|
|
||||||
UI.message("Submitting to App Store Connect...")
|
|
||||||
|
|
||||||
# Use Fastlane's built-in action — handles JWT auth, upload, and submission
|
|
||||||
# Requires APP_STORE_CONNECT_API_KEY_* env vars
|
|
||||||
api_key = app_store_connect_api_key(
|
|
||||||
key_id: ENV.fetch("APP_STORE_CONNECT_API_KEY_ID"),
|
|
||||||
issuer_id: ENV.fetch("APP_STORE_CONNECT_API_ISSUER_ID"),
|
|
||||||
key_filepath: ENV.fetch("APP_STORE_CONNECT_API_KEY_FILEPATH"),
|
|
||||||
)
|
|
||||||
|
|
||||||
upload_to_app_store(
|
|
||||||
api_key: api_key,
|
|
||||||
ipa: ipa_path,
|
|
||||||
submit_for_review: true,
|
|
||||||
force: true,
|
|
||||||
automatic_release: auto_publish,
|
|
||||||
skip_metadata: true,
|
|
||||||
skip_screenshots: true,
|
|
||||||
run_precheck_before_submit: false,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Notify update service that we've submitted
|
|
||||||
xuqm_post(
|
|
||||||
url: "#{server_url}/api/v1/updates/store/app/#{version_id}/submit",
|
|
||||||
token: api_token,
|
|
||||||
body: { storeTypes: ["APP_STORE"] }.to_json,
|
|
||||||
)
|
|
||||||
UI.success("App Store submission done. Review status will update via webhook or manually.")
|
|
||||||
end
|
|
||||||
|
|
||||||
# ── 6. Trigger server-side store submission for other stores ──────────
|
|
||||||
other_stores = store_targets.split(",").map(&:strip).reject { |s| s == "APP_STORE" }
|
|
||||||
unless other_stores.empty?
|
|
||||||
xuqm_post(
|
|
||||||
url: "#{server_url}/api/v1/updates/store/app/#{version_id}/execute-submit",
|
|
||||||
token: api_token,
|
|
||||||
body: { storeTypes: other_stores }.to_json,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
UI.success("Release complete. Version ID: #{version_id}")
|
|
||||||
UI.message(scheduled_at.empty? ? "Publish manually or wait for auto-publish." : "Will publish at #{scheduled_at}")
|
|
||||||
end
|
|
||||||
|
|
||||||
# ── Private helpers ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private_lane :xuqm_get_latest_version_code do |options|
|
|
||||||
uri = URI("#{options[:server_url]}/api/v1/updates/app/list?appId=#{options[:app_id]}&platform=#{options[:platform]}")
|
|
||||||
req = Net::HTTP::Get.new(uri)
|
|
||||||
req["Authorization"] = "Bearer #{options[:api_token]}"
|
|
||||||
resp = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |h| h.request(req) }
|
|
||||||
data = JSON.parse(resp.body)
|
|
||||||
codes = (data.dig("data") || []).filter_map { |v| v["versionCode"] }
|
|
||||||
codes.max || 0
|
|
||||||
end
|
|
||||||
|
|
||||||
private_lane :xuqm_upload_ipa do |options|
|
|
||||||
require 'net/http/post/multipart'
|
|
||||||
|
|
||||||
uri = URI("#{options[:server_url]}/api/v1/updates/app/upload")
|
|
||||||
request = Net::HTTP::Post::Multipart.new(uri.path,
|
|
||||||
"appId" => options[:app_id],
|
|
||||||
"platform" => "IOS",
|
|
||||||
"versionName" => options[:version_name],
|
|
||||||
"versionCode" => options[:version_code].to_s,
|
|
||||||
"changeLog" => options[:change_log].to_s,
|
|
||||||
"forceUpdate" => "false",
|
|
||||||
"packageName" => options[:package_name].to_s,
|
|
||||||
"autoPublishAfterReview" => options[:auto_publish_after_review].to_s,
|
|
||||||
"storeSubmitTargets" => options[:store_targets].empty? ? "" : options[:store_targets].to_json,
|
|
||||||
"scheduledPublishAt" => options[:scheduled_publish_at].to_s,
|
|
||||||
"webhookUrl" => options[:webhook_url].to_s,
|
|
||||||
"apkFile" => UploadIO.new(File.open(options[:ipa_path]), "application/octet-stream", File.basename(options[:ipa_path])),
|
|
||||||
)
|
|
||||||
request["Authorization"] = "Bearer #{options[:api_token]}"
|
|
||||||
|
|
||||||
resp = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |h| h.request(request) }
|
|
||||||
data = JSON.parse(resp.body)
|
|
||||||
data.dig("data", "id") || UI.user_error!("Upload failed: #{resp.body}")
|
|
||||||
end
|
|
||||||
|
|
||||||
private_lane :xuqm_post do |options|
|
|
||||||
uri = URI(options[:url])
|
|
||||||
req = Net::HTTP::Post.new(uri)
|
|
||||||
req["Authorization"] = "Bearer #{options[:token]}"
|
|
||||||
req["Content-Type"] = "application/json"
|
|
||||||
req.body = options[:body]
|
|
||||||
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |h| h.request(req) }
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户