Class: Pindo::BundleIdHelper

Inherits:
Object
  • Object
show all
Defined in:
lib/pindo/module/appstore/bundleid_helper.rb

Overview

Bundle ID 管理辅助类负责在 Apple 开发者中心创建和配置 Bundle ID

Class Method Summary collapse

Class Method Details

.build_settings_for(settings_key:, options_key:) ⇒ Array

构建设置

Parameters:

  • settings_key (String)

    设置键

  • options_key (String)

    选项键

Returns:

  • (Array)

    设置数组



304
305
306
307
308
309
310
311
# File 'lib/pindo/module/appstore/bundleid_helper.rb', line 304

def self.build_settings_for(settings_key:, options_key:)
  [{
    key: settings_key,
    options: [{
      key: options_key
    }]
  }]
end

.create_bundleid(bundle_id:, app_group: nil, app_icloud: nil, setting_array: []) ⇒ Object

创建 Bundle ID

Parameters:

  • bundle_id (String)

    Bundle ID

  • app_group (Object) (defaults to: nil)

    App Group 对象

  • app_icloud (Object) (defaults to: nil)

    iCloud Container 对象

  • setting_array (Array) (defaults to: [])

    配置数组

Returns:

  • (Object)

    创建的 App 对象



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/pindo/module/appstore/bundleid_helper.rb', line 160

def self.create_bundleid(bundle_id:, app_group: nil, app_icloud: nil, setting_array: [])
  return nil if bundle_id.nil?

  # 查找或创建 Bundle ID
  app = Spaceship::Portal.app.find(bundle_id)
  puts

  display_name = generate_bundleid_name(bundle_id)
  if app.nil?
    puts "Create bundle id #{bundle_id} (#{display_name}) in apple developer center..."
    app = Spaceship::Portal.app.create!(bundle_id: bundle_id, name: display_name)
  else
    puts "Find bundle id #{bundle_id} in apple developer center..."
    # 更新名称
    if app.name != display_name
      puts "Update bundle id name: #{app.name} -> #{display_name}"
      begin
        app.update_name!(display_name)
      rescue => e
        puts "Portal 更新名称失败: #{e.message},尝试通过 API Key 更新..."
        begin
          update_bundleid_name(bundle_id: bundle_id, new_name: display_name)
        rescue => e2
          puts "更新名称失败: #{e2.message}"
        end
      end
    end
  end

  bundle_id_obj = Spaceship::ConnectAPI::BundleId.find(bundle_id)

  # 配置 Game Center(通配符 App ID 不支持 GAME_CENTER)
  is_wildcard = bundle_id.include?("*")
  if !bundle_id_obj.nil? && !is_wildcard
    if !setting_array.nil? && setting_array.to_s.include?("game_center")
      puts "Enable #{bundle_id} game_center on"
      bundle_id_obj.update_capability("GAME_CENTER", enabled: true)
    else
      puts "Enable #{bundle_id} game_center off"
      bundle_id_obj.update_capability("GAME_CENTER", enabled: false)
    end
  elsif is_wildcard
    puts "Skip #{bundle_id} game_center (wildcard App ID not supported)"
  end

  return nil if setting_array.nil? || setting_array.length == 0

  # 配置 Push Notification
  if !app.nil? && setting_array.to_s.include?("push_notification")
    puts "Enable #{bundle_id} push on"
    app.update_service(Spaceship::Portal.app_service.push_notification.on)
  else
    app.update_service(Spaceship::Portal.app_service.push_notification.off)
    puts "Enable #{bundle_id} push off"
  end

  # 配置 Siri
  if !app.nil? && setting_array.to_s.include?("siri")
    puts "Enable #{bundle_id} siri on"
    app.update_service(Spaceship::Portal.app_service.siri_kit.on)
  else
    app.update_service(Spaceship::Portal.app_service.siri_kit.off)
    puts "Enable #{bundle_id} siri off"
  end

  # 配置 Sign with Apple(通配符 App ID 不支持)
  if !is_wildcard && !bundle_id_obj.nil?
    if setting_array.to_s.include?("apple_signin")
      puts "Enable #{bundle_id} Sign with Apple on"
      settings = build_settings_for(settings_key: "APPLE_ID_AUTH_APP_CONSENT", options_key: "PRIMARY_APP_CONSENT")
      bundle_id_obj.update_capability("APPLE_ID_AUTH", enabled: true, settings: settings)
    else
      puts "Enable #{bundle_id} Sign with Apple off"
      bundle_id_obj.update_capability("APPLE_ID_AUTH", enabled: false)
    end
  elsif is_wildcard
    puts "Skip #{bundle_id} Sign with Apple (wildcard App ID not supported)"
  end

  # 配置 App Group
  if !app_group.nil? && setting_array.to_s.include?("app_group")
    puts "Enable #{bundle_id} group on"
    app.update_service(Spaceship::Portal.app_service.app_group.on)
    app.associate_groups([app_group])
  else
    puts "Enable #{bundle_id} group off"
    app.update_service(Spaceship::Portal.app_service.app_group.off)
  end

  # 配置 iCloud
  if !app_icloud.nil? && setting_array.to_s.include?("iclound")
    puts "Enable #{bundle_id} icloud on"
    app.update_service(Spaceship::Portal.app_service.cloud.on)
    app.update_service(Spaceship::Portal.app_service.cloud_kit.cloud_kit)
    app.associate_cloud_containers([app_icloud])
  else
    puts "Enable #{bundle_id} icloud off"
    app.update_service(Spaceship::Portal.app_service.cloud.off)
  end

  app
end

.create_extension_bundleids(app, app_group, app_icloud, config_info) ⇒ Object

创建扩展的 Bundle ID

Parameters:

  • app (Object)

    主应用对象

  • app_group (Object)

    App Group 对象

  • app_icloud (Object)

    iCloud Container 对象

  • config_info (Hash)

    配置信息



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/pindo/module/appstore/bundleid_helper.rb', line 318

def self.create_extension_bundleids(app, app_group, app_icloud, config_info)
  return if app.nil?

  # Push Content Extension
  if !config_info[:bundle_id_pushcontent].nil?
    create_bundleid(
      bundle_id: config_info[:bundle_id_pushcontent],
      app_group: app_group,
      app_icloud: app_icloud,
      setting_array: ["push_notification"]
    )
  end

  # Push Service Extension
  if !config_info[:bundle_id_pushservice].nil?
    create_bundleid(
      bundle_id: config_info[:bundle_id_pushservice],
      app_group: app_group,
      app_icloud: app_icloud,
      setting_array: ["push_notification"]
    )
  end

  # Keyboard Extension
  if !config_info[:bundle_id_keyboard].nil?
    create_bundleid(
      bundle_id: config_info[:bundle_id_keyboard],
      app_group: app_group,
      app_icloud: app_icloud,
      setting_array: ["app_group"]
    )
  end

  # iMessage Extension
  if !config_info[:bundle_id_imessage].nil?
    create_bundleid(
      bundle_id: config_info[:bundle_id_imessage],
      app_group: app_group,
      app_icloud: app_icloud,
      setting_array: ["app_group"]
    )
  end

  # Siri Extension
  if !config_info[:bundle_id_siri].nil?
    create_bundleid(
      bundle_id: config_info[:bundle_id_siri],
      app_group: app_group,
      app_icloud: app_icloud,
      setting_array: ["app_group", "siri"]
    )
  end

  # Siri UI Extension
  if !config_info[:bundle_id_siriui].nil?
    create_bundleid(
      bundle_id: config_info[:bundle_id_siriui],
      app_group: app_group,
      app_icloud: app_icloud,
      setting_array: ["app_group", "siri"]
    )
  end

  # Widget Extension
  if !config_info[:bundle_id_widget].nil?
    create_bundleid(
      bundle_id: config_info[:bundle_id_widget],
      app_group: app_group,
      app_icloud: app_icloud,
      setting_array: ["app_group"]
    )
  end

  # General Extension
  if !config_info[:bundle_id_extension].nil?
    create_bundleid(
      bundle_id: config_info[:bundle_id_extension],
      app_group: app_group,
      app_icloud: app_icloud,
      setting_array: ["app_group"]
    )
  end

  # Ad Extension
  if !config_info[:bundle_id_extensionad].nil?
    create_bundleid(
      bundle_id: config_info[:bundle_id_extensionad],
      app_group: app_group,
      app_icloud: app_icloud,
      setting_array: ["app_group"]
    )
  end

  # Watch App
  if !config_info[:bundle_id_watchapp].nil?
    create_bundleid(
      bundle_id: config_info[:bundle_id_watchapp],
      app_group: nil,
      app_icloud: app_icloud,
      setting_array: ["push_notification"]
    )
  end

  # Watch App Extension
  if !config_info[:bundle_id_watchapp_extension].nil?
    create_bundleid(
      bundle_id: config_info[:bundle_id_watchapp_extension],
      app_group: nil,
      app_icloud: app_icloud,
      setting_array: ["push_notification"]
    )
  end
end

.create_group_id(group_id:) ⇒ Object

创建 App Group

Parameters:

  • group_id (String)

    Group ID

Returns:

  • (Object)

    App Group 对象



266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/pindo/module/appstore/bundleid_helper.rb', line 266

def self.create_group_id(group_id:)
  return nil if group_id.nil?

  app_group = Spaceship::Portal.app_group.find(group_id)

  if app_group.nil?
    puts "Create group_id #{group_id} in apple developer center..."
    app_group = Spaceship::Portal.app_group.create!(group_id: group_id, name: generate_bundleid_name(group_id))
  else
    puts "Group_id #{group_id} is existed in apple developer center !!!"
  end

  app_group
end

.create_icloud_id(icloud_id:) ⇒ Object

创建 iCloud Container

Parameters:

  • icloud_id (String)

    iCloud ID

Returns:

  • (Object)

    iCloud Container 对象



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/pindo/module/appstore/bundleid_helper.rb', line 284

def self.create_icloud_id(icloud_id:)
  return nil if icloud_id.nil?

  puts "icloud_id +++++ #{icloud_id}"
  app_icloud = Spaceship::Portal.cloud_container.find(icloud_id)

  if app_icloud.nil?
    puts "Create icloud_id #{icloud_id} in apple developer center..."
    app_icloud = Spaceship::Portal.cloud_container.create!(identifier: icloud_id, name: generate_bundleid_name(icloud_id))
  else
    puts "icloud_id #{icloud_id} is existed in apple developer center !!!"
  end

  app_icloud
end

.execute_bundleid_creation(config_json:, sign_flag: false) ⇒ Object

执行 Bundle ID 创建流程

Parameters:

  • config_json (Hash)

    配置 JSON 对象

  • sign_flag (Boolean) (defaults to: false)

    签名标志



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/pindo/module/appstore/bundleid_helper.rb', line 112

def self.execute_bundleid_creation(config_json:, sign_flag: false)
  # 提取配置信息
  config_info = extract_config_info(config_json)

  # 验证必需字段
  raise "配置文件中缺少 Apple ID" if config_info[:apple_id].nil? || config_info[:apple_id].empty?
  raise "配置文件中缺少 Bundle ID" if config_info[:bundle_id].nil? || config_info[:bundle_id].empty?

  # 登录 Apple 开发者中心
  puts config_info[:apple_id]
  puts "Login #{config_info[:apple_id]}..."
  Spaceship.(config_info[:apple_id].to_s)
  Spaceship.select_team

  # 创建 App Group
  app_group = nil
  if !config_info[:group_id].nil?
    app_group = create_group_id(group_id: config_info[:group_id])
  end

  # 配置参数
  parms = ["push_notification", "app_group"]

  # 创建 iCloud ID
  app_icloud = nil
  if !config_info[:icloud_id].nil?
    app_icloud = create_icloud_id(icloud_id: config_info[:icloud_id])
    parms << "iclound"
  end

  # 创建主应用 Bundle ID
  app = create_bundleid(
    bundle_id: config_info[:bundle_id],
    app_group: app_group,
    app_icloud: app_icloud,
    setting_array: parms
  )

  # 创建各种扩展的 Bundle ID
  create_extension_bundleids(app, app_group, app_icloud, config_info)
end

.extract_config_info(config_json) ⇒ Hash

从配置文件中提取所有必要的信息

Parameters:

  • config_json (Hash)

    配置 JSON 对象

Returns:

  • (Hash)

    提取的配置信息



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/pindo/module/appstore/bundleid_helper.rb', line 70

def self.extract_config_info(config_json)
  config_info = {}

  # 提取 Apple ID
  if config_json['account_info'] && config_json['account_info']['apple_acount_id']
    config_info[:apple_id] = config_json['account_info']['apple_acount_id']
  end

  # 提取 Bundle ID 信息
  if config_json['app_info']
    app_info = config_json['app_info']
    config_info[:bundle_id] = app_info['app_identifier']
    config_info[:bundle_id_pushcontent] = app_info['app_identifier_pushcontent']
    config_info[:bundle_id_pushservice] = app_info['app_identifier_pushservice']
    config_info[:bundle_id_keyboard] = app_info['app_identifier_keyboard']
    config_info[:bundle_id_imessage] = app_info['app_identifier_imessage']
    config_info[:bundle_id_extension] = app_info['app_identifier_extension']
    config_info[:bundle_id_siri] = app_info['app_identifier_siri']
    config_info[:bundle_id_siriui] = app_info['app_identifier_siriui']
    config_info[:bundle_id_widget] = app_info['app_identifier_widget']
    config_info[:bundle_id_extensionad] = app_info['app_identifier_extensionad']
    config_info[:bundle_id_watchapp] = app_info['app_identifier_watchapp']
    config_info[:bundle_id_watchapp_extension] = app_info['app_identifier_watchapp_extension']

    # 从 app_info 提取 app_group_id(优先)
    config_info[:group_id] = app_info['app_group_id']
    config_info[:icloud_id] = app_info['app_icloud_id']
  end

  # 从 app_setting 提取(如果 app_info 中没有,则使用这里的配置)
  if config_json['app_setting']
    app_setting = config_json['app_setting']
    config_info[:group_id] ||= app_setting['app_group_id']
    config_info[:icloud_id] ||= app_setting['app_icloud_id']
  end

  config_info
end

.fetch_capabilities_from_portal(bundle_id:) ⇒ Hash?

从 Apple Developer Portal 读取指定 Bundle ID 的 Capability 状态

Parameters:

  • bundle_id (String)

    Bundle ID 字符串

Returns:

  • (Hash, nil)

    capabilities 对象,Bundle ID 不存在时返回 nil



435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
# File 'lib/pindo/module/appstore/bundleid_helper.rb', line 435

def self.fetch_capabilities_from_portal(bundle_id:)
  return nil if bundle_id.nil? || bundle_id.empty?

  # 通过 Spaceship Portal 查找 Bundle ID
  app = Spaceship::Portal.app.find(bundle_id)
  if app.nil?
    puts "Bundle ID #{bundle_id} 在 Apple Developer Portal 中未找到"
    return nil
  end

  # 更新 Bundle ID 名称
  expected_name = generate_bundleid_name(bundle_id)
  if app.name != expected_name
    puts "Update bundle id name: #{app.name} -> #{expected_name}"
    begin
      app.update_name!(expected_name)
    rescue => e
      puts "Portal 更新名称失败: #{e.message},尝试通过 API Key 更新..."
      begin
        update_bundleid_name(bundle_id: bundle_id, new_name: expected_name)
      rescue => e2
        puts "更新名称失败: #{e2.message}"
      end
    end
  end

  capabilities = {}

  # --- Spaceship Portal 侧:读取 features/services ---
  begin
    app_details = app.details
    features = app_details.features

    # Push Notification
    push_enabled = features["push"] == true || features["push"].to_s == "true"
    capabilities["push_notification"] = push_enabled

    # App Group
    app_group_enabled = features["APG3427HIY"] == true || features["appGroup"] == true ||
                        features["APG3427HIY"].to_s == "true" || features["appGroup"].to_s == "true"
    if app_group_enabled
      groups = app_details.associated_groups rescue []
      if groups && !groups.empty?
        capabilities["app_group"] = groups.first.group_id
      else
        capabilities["app_group"] = true
      end
    else
      capabilities["app_group"] = false
    end

    # iCloud
    icloud_enabled = features["cloudKitVersion"] == 2 || features["iCloud"] == true ||
                     features["cloudKitVersion"].to_i > 0 || features["iCloud"].to_s == "true"
    if icloud_enabled
      containers = app_details.associated_cloud_containers rescue []
      if containers && !containers.empty?
        capabilities["icloud"] = containers.first.identifier
      else
        capabilities["icloud"] = true
      end
    else
      capabilities["icloud"] = false
    end

    # Siri
    siri_enabled = features["SI015DKUHP"] == true || features["siriKit"] == true ||
                   features["SI015DKUHP"].to_s == "true" || features["siriKit"].to_s == "true"
    capabilities["siri"] = siri_enabled

    # In-App Purchase
    iap_enabled = features["inAppPurchase"] == true || features["inAppPurchase"].to_s == "true"
    capabilities["in_app_purchase"] = iap_enabled

  rescue => e
    puts "读取 Portal features 失败: #{e.message},尝试通过 ConnectAPI 读取..."
  end

  # --- ConnectAPI 侧:补充 game_center 和 apple_signin ---
  begin
    bundle_id_obj = Spaceship::ConnectAPI::BundleId.find(bundle_id)
    if bundle_id_obj
      api_capabilities = bundle_id_obj.get_capabilities

      capabilities["game_center"] = api_capabilities.any? { |c| c.is_type?("GAME_CENTER") } rescue false
      capabilities["apple_signin"] = api_capabilities.any? { |c| c.is_type?("APPLE_ID_AUTH") } rescue false

      # 如果 Portal 侧读取失败,用 ConnectAPI 补充
      if !capabilities.key?("push_notification")
        capabilities["push_notification"] = api_capabilities.any? { |c| c.is_type?("PUSH_NOTIFICATIONS") } rescue false
      end
      if !capabilities.key?("in_app_purchase")
        capabilities["in_app_purchase"] = api_capabilities.any? { |c| c.is_type?("IN_APP_PURCHASE") } rescue false
      end
      if !capabilities.key?("siri")
        capabilities["siri"] = api_capabilities.any? { |c| c.is_type?("SIRIKIT") } rescue false
      end
    end
  rescue => e
    puts "读取 ConnectAPI capabilities 失败: #{e.message}"
    capabilities["game_center"] ||= false
    capabilities["apple_signin"] ||= false
  end

  capabilities
end

.generate_bundleid_name(bundle_id) ⇒ String

根据 Bundle ID 生成显示名称规则:去掉前两段(如 com.heroneverdie101),剩余部分去掉点号拼接示例:

"com.heroneverdie101.fancyapptest"          => "fancyapptest"
"com.heroneverdie101.fancyapptest.content"  => "fancyapptestcontent"
"com.heroneverdie101.fancyapptest.service"  => "fancyapptestservice"
"com.heroneverdie101.*"                     => "wildcarddevelopmentid"

Parameters:

  • bundle_id (String)

    Bundle ID 字符串

Returns:

  • (String)

    显示名称



20
21
22
23
24
25
26
# File 'lib/pindo/module/appstore/bundleid_helper.rb', line 20

def self.generate_bundleid_name(bundle_id)
  return "wildcarddevelopmentid" if bundle_id.nil? || bundle_id.include?("*")

  parts = bundle_id.split('.')
  # 去掉前两段(如 com.heroneverdie101),剩余部分拼接
  parts[2..].join('')
end

.load_api_key_configHash?

加载 API Key 配置

Returns:

  • (Hash, nil)

    API Key 配置,包含 issuer_id、key_id、private_key



52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/pindo/module/appstore/bundleid_helper.rb', line 52

def self.load_api_key_config
  pindo_dir = File.expand_path("~/.pindo")
  api_key_file = File.join(pindo_dir, "api_key.json")
  return nil unless File.exist?(api_key_file)

  api_key_json = JSON.parse(File.read(api_key_file))
  # 取第一个有效的 API Key 配置
  api_key_json.each do |_apple_id, config|
    if config.is_a?(Hash) && config["issuer_id"] && config["key_id"] && config["private_key"]
      return config
    end
  end
  nil
end

.update_bundleid_name(bundle_id:, new_name:) ⇒ Object

通过 App Store Connect API 更新 Bundle ID 名称

Parameters:

  • bundle_id (String)

    Bundle ID 字符串

  • new_name (String)

    新名称



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/pindo/module/appstore/bundleid_helper.rb', line 31

def self.update_bundleid_name(bundle_id:, new_name:)
  bundle_id_obj = Spaceship::ConnectAPI::BundleId.find(bundle_id)
  return if bundle_id_obj.nil?

  api_key_json = load_api_key_config
  if api_key_json.nil?
    puts "未配置 API Key,跳过名称更新"
    return
  end

  client = AppStoreDevApi::Client.new(
    issuer_id: api_key_json["issuer_id"],
    key_id: api_key_json["key_id"],
    private_key: api_key_json["private_key"]
  )

  client.update_bundle_id(id: bundle_id_obj.id, name: new_name)
end