fastlane 2.147.0 → 2.150.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +82 -82
  3. data/cert/lib/cert/commands_generator.rb +1 -0
  4. data/credentials_manager/lib/credentials_manager/cli.rb +2 -0
  5. data/deliver/lib/deliver.rb +0 -1
  6. data/deliver/lib/deliver/app_screenshot.rb +28 -27
  7. data/deliver/lib/deliver/commands_generator.rb +1 -0
  8. data/deliver/lib/deliver/html_generator.rb +2 -2
  9. data/deliver/lib/deliver/options.rb +6 -11
  10. data/deliver/lib/deliver/runner.rb +7 -4
  11. data/deliver/lib/deliver/setup.rb +5 -30
  12. data/deliver/lib/deliver/submit_for_review.rb +124 -83
  13. data/deliver/lib/deliver/upload_metadata.rb +284 -143
  14. data/deliver/lib/deliver/upload_price_tier.rb +15 -8
  15. data/deliver/lib/deliver/upload_screenshots.rb +86 -37
  16. data/fastlane/lib/assets/s3_html_template.erb +1 -1
  17. data/fastlane/lib/fastlane/actions/crashlytics.rb +0 -4
  18. data/fastlane/lib/fastlane/actions/docs/build_app.md +1 -1
  19. data/fastlane/lib/fastlane/actions/docs/capture_android_screenshots.md +42 -2
  20. data/fastlane/lib/fastlane/actions/docs/capture_ios_screenshots.md +1 -1
  21. data/fastlane/lib/fastlane/actions/docs/sync_code_signing.md +2 -2
  22. data/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb +3 -11
  23. data/fastlane/lib/fastlane/actions/docs/upload_to_play_store.md +15 -2
  24. data/fastlane/lib/fastlane/actions/download_dsyms.rb +7 -1
  25. data/fastlane/lib/fastlane/actions/get_managed_play_store_publishing_rights.rb +1 -1
  26. data/fastlane/lib/fastlane/actions/google_play_track_release_names.rb +74 -0
  27. data/fastlane/lib/fastlane/actions/hipchat.rb +1 -1
  28. data/fastlane/lib/fastlane/actions/push_to_git_remote.rb +1 -1
  29. data/fastlane/lib/fastlane/actions/slack.rb +2 -2
  30. data/fastlane/lib/fastlane/actions/slather.rb +8 -1
  31. data/fastlane/lib/fastlane/actions/spm.rb +7 -0
  32. data/fastlane/lib/fastlane/actions/swiftlint.rb +14 -0
  33. data/fastlane/lib/fastlane/actions/sync_code_signing.rb +1 -1
  34. data/fastlane/lib/fastlane/actions/upload_symbols_to_crashlytics.rb +3 -34
  35. data/fastlane/lib/fastlane/actions/xcodebuild.rb +4 -4
  36. data/fastlane/lib/fastlane/cli_tools_distributor.rb +28 -6
  37. data/fastlane/lib/fastlane/commands_generator.rb +1 -1
  38. data/fastlane/lib/fastlane/documentation/actions_list.rb +1 -1
  39. data/fastlane/lib/fastlane/lane.rb +3 -3
  40. data/fastlane/lib/fastlane/lane_manager.rb +0 -10
  41. data/fastlane/lib/fastlane/plugins/plugin_manager.rb +3 -3
  42. data/fastlane/lib/fastlane/plugins/template/.github/workflows/test.yml +29 -0
  43. data/fastlane/lib/fastlane/swift_fastlane_function.rb +22 -5
  44. data/fastlane/lib/fastlane/swift_lane_manager.rb +0 -8
  45. data/fastlane/lib/fastlane/version.rb +1 -1
  46. data/fastlane/swift/ControlCommand.swift +1 -0
  47. data/fastlane/swift/Deliverfile.swift +1 -1
  48. data/fastlane/swift/Fastlane.swift +140 -30
  49. data/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/xcshareddata/xcschemes/FastlaneRunner.xcscheme +3 -9
  50. data/fastlane/swift/Gymfile.swift +1 -1
  51. data/fastlane/swift/GymfileProtocol.swift +1 -1
  52. data/fastlane/swift/Matchfile.swift +1 -1
  53. data/fastlane/swift/MatchfileProtocol.swift +5 -1
  54. data/fastlane/swift/Precheckfile.swift +1 -1
  55. data/fastlane/swift/RubyCommand.swift +29 -6
  56. data/fastlane/swift/RubyCommandable.swift +1 -0
  57. data/fastlane/swift/Runner.swift +85 -13
  58. data/fastlane/swift/Scanfile.swift +1 -1
  59. data/fastlane/swift/ScanfileProtocol.swift +10 -2
  60. data/fastlane/swift/Screengrabfile.swift +1 -1
  61. data/fastlane/swift/Snapshotfile.swift +1 -1
  62. data/fastlane/swift/SnapshotfileProtocol.swift +17 -1
  63. data/fastlane/swift/SocketClient.swift +76 -45
  64. data/fastlane/swift/SocketClientDelegateProtocol.swift +1 -1
  65. data/fastlane/swift/SocketResponse.swift +1 -0
  66. data/fastlane_core/lib/fastlane_core/configuration/config_item.rb +1 -3
  67. data/fastlane_core/lib/fastlane_core/pkg_file_analyser.rb +7 -0
  68. data/frameit/lib/frameit/commands_generator.rb +1 -0
  69. data/frameit/lib/frameit/device_types.rb +100 -100
  70. data/gym/lib/gym/generators/package_command_generator.rb +4 -0
  71. data/gym/lib/gym/generators/package_command_generator_xcode7.rb +4 -0
  72. data/gym/lib/gym/options.rb +1 -1
  73. data/gym/lib/gym/runner.rb +14 -0
  74. data/match/lib/match/commands_generator.rb +1 -0
  75. data/match/lib/match/generator.rb +2 -1
  76. data/match/lib/match/nuke.rb +21 -16
  77. data/match/lib/match/options.rb +18 -1
  78. data/match/lib/match/storage/git_storage.rb +4 -0
  79. data/match/lib/match/storage/google_cloud_storage.rb +4 -0
  80. data/match/lib/match/storage/interface.rb +4 -0
  81. data/match/lib/match/storage/s3_storage.rb +4 -0
  82. data/pem/lib/pem/commands_generator.rb +1 -0
  83. data/pilot/lib/pilot/build_manager.rb +23 -7
  84. data/pilot/lib/pilot/options.rb +5 -0
  85. data/produce/lib/produce/commands_generator.rb +1 -0
  86. data/produce/lib/produce/itunes_connect.rb +20 -20
  87. data/produce/lib/produce/options.rb +3 -3
  88. data/scan/lib/scan/detect_values.rb +3 -0
  89. data/scan/lib/scan/options.rb +20 -2
  90. data/scan/lib/scan/test_command_generator.rb +6 -1
  91. data/scan/lib/scan/test_result_parser.rb +9 -2
  92. data/screengrab/lib/screengrab/runner.rb +10 -9
  93. data/sigh/lib/assets/resign.sh +7 -7
  94. data/sigh/lib/sigh/commands_generator.rb +1 -0
  95. data/sigh/lib/sigh/options.rb +7 -1
  96. data/sigh/lib/sigh/runner.rb +2 -1
  97. data/snapshot/lib/assets/SnapshotHelper.swift +16 -37
  98. data/snapshot/lib/assets/SnapshotHelperXcode8.swift +3 -3
  99. data/snapshot/lib/snapshot/detect_values.rb +15 -0
  100. data/snapshot/lib/snapshot/options.rb +31 -0
  101. data/snapshot/lib/snapshot/reports_generator.rb +8 -1
  102. data/snapshot/lib/snapshot/simulator_launchers/simulator_launcher_base.rb +3 -1
  103. data/snapshot/lib/snapshot/test_command_generator.rb +8 -3
  104. data/snapshot/lib/snapshot/test_command_generator_base.rb +7 -1
  105. data/spaceship/lib/spaceship/.DS_Store +0 -0
  106. data/spaceship/lib/spaceship/client.rb +9 -1
  107. data/spaceship/lib/spaceship/commands_generator.rb +1 -0
  108. data/spaceship/lib/spaceship/connect_api.rb +21 -2
  109. data/spaceship/lib/spaceship/connect_api/client.rb +47 -11
  110. data/spaceship/lib/spaceship/connect_api/model.rb +1 -1
  111. data/spaceship/lib/spaceship/connect_api/models/age_rating_declaration.rb +109 -0
  112. data/spaceship/lib/spaceship/connect_api/models/app.rb +113 -3
  113. data/spaceship/lib/spaceship/connect_api/models/app_category.rb +94 -0
  114. data/spaceship/lib/spaceship/connect_api/models/app_info.rb +74 -0
  115. data/spaceship/lib/spaceship/connect_api/models/app_info_localization.rb +38 -0
  116. data/spaceship/lib/spaceship/connect_api/models/app_price.rb +22 -0
  117. data/spaceship/lib/spaceship/connect_api/models/app_price_tier.rb +12 -0
  118. data/spaceship/lib/spaceship/connect_api/models/app_review_attachment.rb +81 -0
  119. data/spaceship/lib/spaceship/connect_api/models/app_screenshot.rb +117 -0
  120. data/spaceship/lib/spaceship/connect_api/models/app_screenshot_set.rb +101 -0
  121. data/spaceship/lib/spaceship/connect_api/models/app_store_review_detail.rb +51 -0
  122. data/spaceship/lib/spaceship/connect_api/models/app_store_version.rb +182 -0
  123. data/spaceship/lib/spaceship/connect_api/models/app_store_version_localization.rb +70 -0
  124. data/spaceship/lib/spaceship/connect_api/models/app_store_version_phased_release.rb +36 -0
  125. data/spaceship/lib/spaceship/connect_api/models/app_store_version_submission.rb +26 -0
  126. data/spaceship/lib/spaceship/connect_api/models/build.rb +8 -0
  127. data/spaceship/lib/spaceship/connect_api/models/idfa_declaration.rb +40 -0
  128. data/spaceship/lib/spaceship/connect_api/models/reset_ratings_request.rb +26 -0
  129. data/spaceship/lib/spaceship/connect_api/testflight/testflight.rb +10 -3
  130. data/spaceship/lib/spaceship/connect_api/tunes/client.rb +33 -0
  131. data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +703 -0
  132. data/spaceship/lib/spaceship/errors.rb +3 -0
  133. data/spaceship/lib/spaceship/spaceauth_runner.rb +2 -2
  134. data/supply/lib/supply/client.rb +19 -0
  135. data/supply/lib/supply/commands_generator.rb +1 -0
  136. data/supply/lib/supply/options.rb +9 -0
  137. data/supply/lib/supply/reader.rb +16 -0
  138. data/supply/lib/supply/uploader.rb +4 -0
  139. metadata +52 -47
  140. data/deliver/lib/deliver/upload_assets.rb +0 -27
  141. data/supply/lib/supply/.client.rb.swp +0 -0
@@ -1,128 +1,169 @@
1
1
  require_relative 'module'
2
2
 
3
+ require 'fastlane_core/build_watcher'
4
+ require 'fastlane_core/ipa_file_analyser'
5
+ require 'fastlane_core/pkg_file_analyser'
6
+
3
7
  module Deliver
4
8
  class SubmitForReview
5
9
  def submit!(options)
6
- app = options[:app]
7
- select_build(options)
8
-
9
- UI.message("Submitting the app for review...")
10
- submission = app.create_submission(platform: options[:platform])
11
-
12
- # Set app submission information
13
- # Default Values
14
- submission.content_rights_contains_third_party_content = false
15
- submission.content_rights_has_rights = true
16
- submission.add_id_info_uses_idfa = false
17
-
18
- # User Values
19
- if options[:submission_information]
20
- options[:submission_information].each do |key, value|
21
- UI.message("Setting '#{key}' to '#{value}'...")
22
- submission.send("#{key}=", value)
23
- end
10
+ legacy_app = options[:app]
11
+ app_id = legacy_app.apple_id
12
+ app = Spaceship::ConnectAPI::App.get(app_id: app_id)
13
+
14
+ platform = Spaceship::ConnectAPI::Platform.map(options[:platform])
15
+ version = app.get_edit_app_store_version(platform: platform)
16
+
17
+ unless version
18
+ UI.user_error!("Cannot submit for review - could not find an editable version for '#{platform}'")
19
+ return
24
20
  end
25
21
 
26
- # Finalize app submission
27
- submission.complete!
22
+ build = select_build(options, app, version, platform)
23
+
24
+ update_export_compliance(options, app, build)
25
+ update_idfa(options, app, version)
26
+ update_submission_information(options, app)
27
+
28
+ version.create_app_store_version_submission
28
29
 
29
30
  UI.success("Successfully submitted the app for review!")
30
31
  end
31
32
 
32
- private def select_build(options)
33
- app = options[:app]
34
- app_version = options[:app_version]
35
- v = app.edit_version(platform: options[:platform])
36
-
33
+ private def select_build(options, app, version, platform)
37
34
  if options[:build_number] && options[:build_number] != "latest"
38
35
  UI.message("Selecting existing build-number: #{options[:build_number]}")
39
- build = v.candidate_builds.detect { |a| a.build_version == options[:build_number] }
36
+
37
+ build = Spaceship::ConnectAPI::Build.all(
38
+ app_id: app.id,
39
+ version: options[:app_version],
40
+ build_number: options[:build_number],
41
+ platform: platform
42
+ ).first
43
+
40
44
  unless build
41
45
  UI.user_error!("Build number: #{options[:build_number]} does not exist")
42
46
  end
43
47
  else
44
48
  UI.message("Selecting the latest build...")
45
- build = wait_for_build(app, app_version)
49
+ build = wait_for_build_processing_to_be_complete(app: app, platform: platform, options: options)
46
50
  end
47
- UI.message("Selecting build #{app_version} (#{build.build_version})...")
51
+ UI.message("Selecting build #{build.app_version} (#{build.version})...")
48
52
 
49
- v.select_build(build)
50
- v.save!
53
+ version.select_build(build_id: build.id)
51
54
 
52
55
  UI.success("Successfully selected build")
53
- end
54
56
 
55
- def wait_for_build(app, app_version)
56
- UI.user_error!("Could not find app with app identifier") unless app
57
+ return build
58
+ end
57
59
 
58
- start = Time.now
59
- build = nil
60
+ def update_export_compliance(options, app, build)
61
+ submission_information = options[:submission_information] || {}
62
+ uses_encryption = submission_information[:export_compliance_uses_encryption]
60
63
 
61
- use_latest_version = app_version.nil?
64
+ UI.verbose("Updating build for export compliance status of '#{uses_encryption}'")
65
+ if build.uses_non_exempt_encryption.nil?
66
+ build = build.update(attributes: {
67
+ usesNonExemptEncryption: uses_encryption
68
+ })
69
+ end
70
+ UI.verbose("Updated build for export compliance status of '#{build.uses_non_exempt_encryption}'")
71
+ end
62
72
 
63
- loop do
64
- # Sometimes candidate_builds don't appear immediately after submission
65
- # Wait for candidate_builds to appear on App Store Connect
66
- # Issue https://github.com/fastlane/fastlane/issues/10411
67
- if use_latest_version
68
- candidate_builds = app.latest_version.candidate_builds
69
- else
70
- candidate_builds = app.tunes_all_builds_for_train(train: app_version)
73
+ def update_idfa(options, app, version)
74
+ submission_information = options[:submission_information] || {}
75
+ return unless submission_information.include?(:add_id_info_uses_idfa)
76
+
77
+ uses_idfa = submission_information[:add_id_info_uses_idfa]
78
+ idfa_declaration = begin
79
+ version.fetch_idfa_declaration
80
+ rescue
81
+ nil
82
+ end
83
+
84
+ UI.verbose("Updating app store version for IDFA status of '#{uses_idfa}'")
85
+ version = version.update(attributes: {
86
+ usesIdfa: uses_idfa
87
+ })
88
+ UI.verbose("Updated app store version for IDFA status of '#{version.uses_idfa}'")
89
+
90
+ if uses_idfa == false
91
+ if idfa_declaration
92
+ UI.verbose("Deleting IDFA delcaration")
93
+ idfa_declaration.delete!
94
+ UI.verbose("Deleted IDFA delcaration")
71
95
  end
72
- if (candidate_builds || []).count == 0
73
- UI.message("Waiting for candidate builds to appear...")
74
- if (Time.now - start) > (60 * 5)
75
- UI.user_error!("Could not find any available candidate builds on App Store Connect to submit")
76
- else
77
- sleep(30)
78
- next
79
- end
96
+ else
97
+ attributes = {}
98
+ if submission_information.include?(:add_id_info_limits_tracking)
99
+ attributes[:honorsLimitedAdTracking] = submission_information[:add_id_info_limits_tracking]
80
100
  end
81
101
 
82
- latest_build = find_build(candidate_builds)
83
-
84
- # if the app version isn't present in the hash (could happen if we are waiting for submission, but didn't provide
85
- # it explicitly and no ipa was passed to grab it from), then fall back to the best guess, which is the train_version
86
- # of the most recently uploaded build
87
- app_version ||= latest_build.train_version
88
-
89
- # Sometimes latest build will disappear and a different build would get selected
90
- # Only set build if no latest build found or if same build versions as previously fetched build
91
- # Issue: https://github.com/fastlane/fastlane/issues/10945
92
- if build.nil? || (latest_build && latest_build.build_version == build.build_version && latest_build.train_version == app_version)
93
- build = latest_build
102
+ if submission_information.include?(:add_id_info_serves_ads)
103
+ attributes[:servesAds] = submission_information[:add_id_info_serves_ads]
94
104
  end
95
105
 
96
- return build if build && build.processing == false
106
+ if submission_information.include?(:add_id_info_tracks_install)
107
+ attributes[:attributesAppInstallationToPreviousAd] = submission_information[:add_id_info_tracks_install]
108
+ end
97
109
 
98
- if build
99
- UI.message("Waiting App Store Connect processing for build #{app_version} (#{build.build_version})... this might take a while...")
100
- else
101
- UI.message("Waiting App Store Connect processing for build... this might take a while...")
110
+ if submission_information.include?(:add_id_info_tracks_action)
111
+ attributes[:attributesActionWithPreviousAd] = submission_information[:add_id_info_tracks_action]
102
112
  end
103
113
 
104
- if (Time.now - start) > (60 * 5)
105
- UI.message("")
106
- UI.message("You can tweet: \"App Store Connect #iosprocessingtime #{((Time.now - start) / 60).round} minutes\"")
114
+ if idfa_declaration
115
+ UI.verbose("Updating IDFA delcaration")
116
+ idfa_declaration.update(attributes: attributes)
117
+ UI.verbose("Updated IDFA delcaration")
118
+ else
119
+ UI.verbose("Creating IDFA delcaration")
120
+ version.create_idfa_declaration(attributes: attributes)
121
+ UI.verbose("Created IDFA delcaration")
107
122
  end
108
- sleep(30)
109
123
  end
110
- nil
124
+
125
+ UI.success("Successfully updated IDFA delcarations")
111
126
  end
112
127
 
113
- def find_build(candidate_builds)
114
- if (candidate_builds || []).count == 0
115
- UI.user_error!("Could not find any available candidate builds on App Store Connect to submit")
128
+ def update_submission_information(options, app)
129
+ submission_information = options[:submission_information] || {}
130
+ if submission_information.include?(:content_rights_contains_third_party_content)
131
+ value = if submission_information[:content_rights_contains_third_party_content]
132
+ Spaceship::ConnectAPI::App::ContentRightsDeclaration::USES_THIRD_PARTY_CONTENT
133
+ else
134
+ Spaceship::ConnectAPI::App::ContentRightsDeclaration::DOES_NOT_USE_THIRD_PARTY_CONTENT
135
+ end
136
+
137
+ UI.success("Updating contents rights declaration on App Store Connect")
138
+ app.update(attributes: {
139
+ contentRightsDeclaration: value
140
+ })
116
141
  end
142
+ end
117
143
 
118
- build = candidate_builds.first
119
- candidate_builds.each do |b|
120
- if b.upload_date > build.upload_date
121
- build = b
122
- end
144
+ def wait_for_build_processing_to_be_complete(app: nil, platform: nil, options: nil)
145
+ app_version = options[:app_version]
146
+ app_version ||= FastlaneCore::IpaFileAnalyser.fetch_app_version(options[:ipa]) if options[:ipa]
147
+ app_version ||= FastlaneCore::PkgFileAnalyser.fetch_app_version(options[:pkg]) if options[:pkg]
148
+
149
+ app_build ||= FastlaneCore::IpaFileAnalyser.fetch_app_build(options[:ipa]) if options[:ipa]
150
+ app_build ||= FastlaneCore::PkgFileAnalyser.fetch_app_build(options[:pkg]) if options[:pkg]
151
+
152
+ latest_build = FastlaneCore::BuildWatcher.wait_for_build_processing_to_be_complete(
153
+ app_id: app.id,
154
+ platform: platform,
155
+ app_version: app_version,
156
+ build_version: app_build,
157
+ poll_interval: 15,
158
+ return_when_build_appears: false,
159
+ return_spaceship_testflight_build: false
160
+ )
161
+
162
+ unless latest_build.app_version == app_version && latest_build.version == app_build
163
+ UI.important("Uploaded app #{app_version} - #{app_build}, but received build #{latest_build.app_version} - #{latest_build.version}.")
123
164
  end
124
165
 
125
- return build
166
+ return latest_build
126
167
  end
127
168
  end
128
169
  end
@@ -2,40 +2,38 @@ require_relative 'module'
2
2
 
3
3
  module Deliver
4
4
  # upload description, rating, etc.
5
+ # rubocop:disable Metrics/ClassLength
5
6
  class UploadMetadata
6
7
  # All the localised values attached to the version
7
- LOCALISED_VERSION_VALUES = [:description, :keywords, :release_notes, :support_url, :marketing_url, :promotional_text]
8
+ LOCALISED_VERSION_VALUES = {
9
+ description: "description",
10
+ keywords: "keywords",
11
+ release_notes: "whatsNew",
12
+ support_url: "supportUrl",
13
+ marketing_url: "marketingUrl",
14
+ promotional_text: "promotionalText"
15
+ }
8
16
 
9
17
  # Everything attached to the version but not being localised
10
- NON_LOCALISED_VERSION_VALUES = [:copyright]
18
+ NON_LOCALISED_VERSION_VALUES = {
19
+ copyright: "copyright"
20
+ }
11
21
 
12
22
  # Localised app details values
13
- LOCALISED_APP_VALUES = [:name, :subtitle, :privacy_url, :apple_tv_privacy_policy]
23
+ LOCALISED_APP_VALUES = {
24
+ name: "name",
25
+ subtitle: "subtitle",
26
+ privacy_url: "privacyPolicyUrl",
27
+ apple_tv_privacy_policy: "privacyPolicyText"
28
+ }
14
29
 
15
30
  # Non localized app details values
16
31
  NON_LOCALISED_APP_VALUES = [:primary_category, :secondary_category,
17
32
  :primary_first_sub_category, :primary_second_sub_category,
18
33
  :secondary_first_sub_category, :secondary_second_sub_category]
19
34
 
20
- # Trade Representative Contact Information values
21
- TRADE_REPRESENTATIVE_CONTACT_INFORMATION_VALUES = {
22
- trade_representative_trade_name: :trade_name,
23
- trade_representative_first_name: :first_name,
24
- trade_representative_last_name: :last_name,
25
- trade_representative_address_line_1: :address_line1,
26
- trade_representative_address_line_2: :address_line2,
27
- trade_representative_address_line_3: :address_line3,
28
- trade_representative_city_name: :city_name,
29
- trade_representative_state: :state,
30
- trade_representative_country: :country,
31
- trade_representative_postal_code: :postal_code,
32
- trade_representative_phone_number: :phone_number,
33
- trade_representative_email: :email_address,
34
- trade_representative_is_displayed_on_app_store: :is_displayed_on_app_store
35
- }
36
-
37
35
  # Review information values
38
- REVIEW_INFORMATION_VALUES = {
36
+ REVIEW_INFORMATION_VALUES_LEGACY = {
39
37
  review_first_name: :first_name,
40
38
  review_last_name: :last_name,
41
39
  review_phone_number: :phone_number,
@@ -44,6 +42,15 @@ module Deliver
44
42
  review_demo_password: :demo_password,
45
43
  review_notes: :notes
46
44
  }
45
+ REVIEW_INFORMATION_VALUES = {
46
+ first_name: "contactFirstName",
47
+ last_name: "contactLastName",
48
+ phone_number: "contactPhone",
49
+ email_address: "contactEmail",
50
+ demo_user: "demoAccountName",
51
+ demo_password: "demoAccountPassword",
52
+ notes: "notes"
53
+ }
47
54
 
48
55
  # Localized app details values, that are editable in live state
49
56
  LOCALISED_LIVE_VALUES = [:description, :release_notes, :support_url, :marketing_url, :promotional_text, :privacy_url]
@@ -66,35 +73,46 @@ module Deliver
66
73
  # Make sure to call `load_from_filesystem` before calling upload
67
74
  def upload(options)
68
75
  return if options[:skip_metadata]
69
- # it is not possible to create new languages, because
70
- # :keywords is not write-able on published versions
71
- # therefore skip it.
72
- verify_available_languages!(options) unless options[:edit_live]
73
76
 
74
- app = options[:app]
77
+ legacy_app = options[:app]
78
+ app_id = legacy_app.apple_id
79
+ app = Spaceship::ConnectAPI::App.get(app_id: app_id)
80
+
81
+ platform = Spaceship::ConnectAPI::Platform.map(options[:platform])
82
+
83
+ app_store_version_localizations = verify_available_version_languages!(options, app) unless options[:edit_live]
84
+ app_info_localizations = verify_available_info_languages!(options, app) unless options[:edit_live]
75
85
 
76
- details = app.details
77
86
  if options[:edit_live]
78
87
  # not all values are editable when using live_version
79
- v = app.live_version(platform: options[:platform])
88
+ version = app.get_live_app_store_version(platform: platform)
80
89
  localised_options = LOCALISED_LIVE_VALUES
81
90
  non_localised_options = NON_LOCALISED_LIVE_VALUES
82
91
 
83
92
  if v.nil?
84
93
  UI.message("Couldn't find live version, editing the current version on App Store Connect instead")
85
- v = app.edit_version(platform: options[:platform])
94
+ version = app.get_edit_app_store_version(platform: platform)
86
95
  # we don't want to update the localised_options and non_localised_options
87
96
  # as we also check for `options[:edit_live]` at other areas in the code
88
97
  # by not touching those 2 variables, deliver is more consistent with what the option says
89
98
  # in the documentation
99
+ else
100
+ UI.message("Found live version")
90
101
  end
91
102
  else
92
- v = app.edit_version(platform: options[:platform])
93
- localised_options = (LOCALISED_VERSION_VALUES + LOCALISED_APP_VALUES)
94
- non_localised_options = (NON_LOCALISED_VERSION_VALUES + NON_LOCALISED_APP_VALUES)
103
+ version = app.get_edit_app_store_version(platform: platform)
104
+ localised_options = (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys)
105
+ non_localised_options = NON_LOCALISED_VERSION_VALUES.keys
95
106
  end
96
107
 
97
- individual = options[:individual_metadata_items] || []
108
+ # Needed for to filter out release notes from being sent up
109
+ is_first_version = app.get_live_app_store_version(platform: platform).nil?
110
+
111
+ UI.important("Will begin uploading metadata for '#{version.version_string}' on App Store Connect")
112
+
113
+ localized_version_attributes_by_locale = {}
114
+ localized_info_attributes_by_locale = {}
115
+
98
116
  localised_options.each do |key|
99
117
  current = options[key]
100
118
  next unless current
@@ -104,70 +122,139 @@ module Deliver
104
122
  next
105
123
  end
106
124
 
125
+ if key == :release_notes && is_first_version
126
+ UI.error("Skipping 'release_notes'... this is the first version of the app")
127
+ next
128
+ end
129
+
107
130
  current.each do |language, value|
108
131
  next unless value.to_s.length > 0
109
132
  strip_value = value.to_s.strip
110
- if individual.include?(key.to_s)
111
- upload_individual_item(app, v, language, key, strip_value)
112
- else
113
- v.send(key)[language] = strip_value if LOCALISED_VERSION_VALUES.include?(key)
114
- details.send(key)[language] = strip_value if LOCALISED_APP_VALUES.include?(key)
133
+
134
+ if LOCALISED_VERSION_VALUES.include?(key) && !strip_value.empty?
135
+ attribute_name = LOCALISED_VERSION_VALUES[key]
136
+
137
+ localized_version_attributes_by_locale[language] ||= {}
138
+ localized_version_attributes_by_locale[language][attribute_name] = strip_value
115
139
  end
140
+
141
+ next unless LOCALISED_APP_VALUES.include?(key) && !strip_value.empty?
142
+ attribute_name = LOCALISED_APP_VALUES[key]
143
+
144
+ localized_info_attributes_by_locale[language] ||= {}
145
+ localized_info_attributes_by_locale[language][attribute_name] = strip_value
116
146
  end
117
147
  end
118
148
 
149
+ non_localized_version_attributes = {}
119
150
  non_localised_options.each do |key|
120
- current = options[key].to_s.strip
121
- next unless current.to_s.length > 0
122
- v.send("#{key}=", current) if NON_LOCALISED_VERSION_VALUES.include?(key)
123
- details.send("#{key}=", current) if NON_LOCALISED_APP_VALUES.include?(key)
151
+ strip_value = options[key].to_s.strip
152
+ next unless strip_value.to_s.length > 0
153
+
154
+ if NON_LOCALISED_VERSION_VALUES.include?(key) && !strip_value.empty?
155
+ attribute_name = NON_LOCALISED_VERSION_VALUES[key]
156
+ non_localized_version_attributes[attribute_name] = strip_value
157
+ end
124
158
  end
125
159
 
126
- v.release_on_approval = options[:automatic_release]
127
- v.auto_release_date = options[:auto_release_date] unless options[:auto_release_date].nil?
128
- v.toggle_phased_release(enabled: !!options[:phased_release]) unless options[:phased_release].nil?
160
+ release_type = if options[:auto_release_date]
161
+ non_localized_version_attributes['earliestReleaseDate'] = options[:auto_release_date]
162
+ Spaceship::ConnectAPI::AppStoreVersion::ReleaseType::SCHEDULED
163
+ elsif options[:automatic_release]
164
+ Spaceship::ConnectAPI::AppStoreVersion::ReleaseType::AFTER_APPROVAL
165
+ else
166
+ Spaceship::ConnectAPI::AppStoreVersion::ReleaseType::MANUAL
167
+ end
168
+ non_localized_version_attributes['releaseType'] = release_type
169
+
170
+ # Update app store version localizations
171
+ app_store_version_localizations.each do |app_store_version_localization|
172
+ attributes = localized_version_attributes_by_locale[app_store_version_localization.locale]
173
+ if attributes
174
+ UI.message("Uploading metadata to App Store Connect for localized version '#{app_store_version_localization.locale}'")
175
+ app_store_version_localization.update(attributes: attributes)
176
+ end
177
+ end
129
178
 
130
- set_trade_representative_contact_information(v, options)
131
- set_review_information(v, options)
132
- set_app_rating(v, options)
133
- v.ratings_reset = options[:reset_ratings] unless options[:reset_ratings].nil?
179
+ # Update app info localizations
180
+ app_info_localizations.each do |app_info_localization|
181
+ attributes = localized_info_attributes_by_locale[app_info_localization.locale]
182
+ if attributes
183
+ UI.message("Uploading metadata to App Store Connect for localized info '#{app_info_localization.locale}'")
184
+ app_info_localization.update(attributes: attributes)
185
+ end
186
+ end
134
187
 
135
- set_review_attachment_file(v, options)
188
+ # Update app store version
189
+ UI.message("Uploading metadata to App Store Connect for version")
190
+ version.update(attributes: non_localized_version_attributes)
191
+
192
+ # Update categories
193
+ app_info = app.fetch_edit_app_info
194
+ if app_info
195
+ app_info.update_categories(
196
+ primary_category_id: Spaceship::ConnectAPI::AppCategory.map_category_from_itc(
197
+ options[:primary_category].to_s.strip
198
+ ),
199
+ secondary_category_id: Spaceship::ConnectAPI::AppCategory.map_category_from_itc(
200
+ options[:secondary_category].to_s.strip
201
+ ),
202
+ primary_subcategory_one_id: Spaceship::ConnectAPI::AppCategory.map_subcategory_from_itc(
203
+ options[:primary_first_sub_category].to_s.strip
204
+ ),
205
+ primary_subcategory_two_id: Spaceship::ConnectAPI::AppCategory.map_subcategory_from_itc(
206
+ options[:primary_second_sub_category].to_s.strip
207
+ ),
208
+ secondary_subcategory_one_id: Spaceship::ConnectAPI::AppCategory.map_subcategory_from_itc(
209
+ options[:secondary_first_sub_category].to_s.strip
210
+ ),
211
+ secondary_subcategory_two_id: Spaceship::ConnectAPI::AppCategory.map_subcategory_from_itc(
212
+ options[:secondary_second_sub_category].to_s.strip
213
+ )
214
+ )
215
+ end
136
216
 
137
- Helper.show_loading_indicator("Uploading metadata to App Store Connect")
138
- v.save!
139
- Helper.hide_loading_indicator
140
- begin
141
- details.save!
142
- UI.success("Successfully uploaded set of metadata to App Store Connect")
143
- rescue Spaceship::TunesClient::ITunesConnectError => e
144
- # This makes sure that we log invalid app names as user errors
145
- # If another string needs to be checked here we should
146
- # figure out a more generic way to handle these cases.
147
- if e.message.include?('App Name cannot be longer than 50 characters') || e.message.include?('The app name you entered is already being used')
148
- UI.error("Error in app name. Try using 'individual_metadata_items' to identify the problem language.")
149
- UI.user_error!(e.message)
150
- else
151
- raise e
217
+ # Update phased release
218
+ unless options[:phased_release].nil?
219
+ phased_release = begin
220
+ version.fetch_app_store_version_phased_release
221
+ rescue
222
+ nil
223
+ end # returns no data error so need to rescue
224
+ if !!options[:phased_release]
225
+ unless phased_release
226
+ UI.message("Creating phased release on App Store Connect")
227
+ version.create_app_store_version_phased_release(attributes: {
228
+ phasedReleaseState: Spaceship::ConnectAPI::AppStoreVersionPhasedRelease::PhasedReleaseState::INACTIVE
229
+ })
230
+ end
231
+ elsif phased_release
232
+ UI.message("Removing phased release on App Store Connect")
233
+ phased_release.delete!
152
234
  end
153
235
  end
154
- end
155
236
 
156
- # Uploads metadata individually by language to help identify exactly which items have issues
157
- def upload_individual_item(app, version, language, key, value)
158
- details = app.details
159
- version.send(key)[language] = value if LOCALISED_VERSION_VALUES.include?(key)
160
- details.send(key)[language] = value if LOCALISED_APP_VALUES.include?(key)
161
- Helper.show_loading_indicator("Uploading #{language} #{key} to App Store Connect")
162
- version.save!
163
- Helper.hide_loading_indicator
164
- begin
165
- details.save!
166
- UI.success("Successfully uploaded #{language} #{key} to App Store Connect")
167
- rescue Spaceship::TunesClient::ITunesConnectError => e
168
- UI.error("Error in #{language} #{key}: \n#{value}")
169
- UI.error(e.message) # Don't use user_error to allow all values to get checked
237
+ # Update rating reset
238
+ unless options[:reset_ratings].nil?
239
+ reset_rating_request = begin
240
+ version.fetch_reset_ratings_request
241
+ rescue
242
+ nil
243
+ end # returns no data error so need to rescue
244
+ if !!options[:reset_ratings]
245
+ unless reset_rating_request
246
+ UI.message("Creating reset ratings request on App Store Connect")
247
+ version.create_reset_ratings_request
248
+ end
249
+ elsif reset_rating_request
250
+ UI.message("Removing reset ratings request on App Store Connect")
251
+ reset_rating_request.delete!
252
+ end
170
253
  end
254
+
255
+ set_review_information(version, options)
256
+ set_review_attachment_file(version, options)
257
+ set_app_rating(version, options)
171
258
  end
172
259
 
173
260
  # rubocop:enable Metrics/PerceivedComplexity
@@ -181,7 +268,7 @@ module Deliver
181
268
  enabled_languages = detect_languages(options)
182
269
 
183
270
  # Get all languages used in existing settings
184
- (LOCALISED_VERSION_VALUES + LOCALISED_APP_VALUES).each do |key|
271
+ (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
185
272
  current = options[key]
186
273
  next unless current && current.kind_of?(Hash)
187
274
  current.each do |language, value|
@@ -200,7 +287,7 @@ module Deliver
200
287
  return unless enabled_languages.include?("default")
201
288
  UI.message("Detected languages: " + enabled_languages.to_s)
202
289
 
203
- (LOCALISED_VERSION_VALUES + LOCALISED_APP_VALUES).each do |key|
290
+ (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
204
291
  current = options[key]
205
292
  next unless current && current.kind_of?(Hash)
206
293
 
@@ -222,7 +309,7 @@ module Deliver
222
309
  enabled_languages = options[:languages] || []
223
310
 
224
311
  # Get all languages used in existing settings
225
- (LOCALISED_VERSION_VALUES + LOCALISED_APP_VALUES).each do |key|
312
+ (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
226
313
  current = options[key]
227
314
  next unless current && current.kind_of?(Hash)
228
315
  current.each do |language, value|
@@ -245,41 +332,73 @@ module Deliver
245
332
  .uniq
246
333
  end
247
334
 
248
- # Makes sure all languages we need are actually created
249
- def verify_available_languages!(options)
250
- return if options[:skip_metadata]
335
+ # Finding languages to enable
336
+ def verify_available_info_languages!(options, app)
337
+ app_info = app.fetch_edit_app_info
251
338
 
252
- # Collect all languages we need
253
- # We only care about languages from user provided values
254
- # as the other languages are on iTC already anyway
255
- v = options[:app].edit_version(platform: options[:platform])
256
- UI.user_error!("Could not find a version to edit for app '#{options[:app].name}', the app metadata is read-only currently") unless v
339
+ unless app_info
340
+ UI.user_error!("Cannot update languages - could not find an editable info")
341
+ return
342
+ end
257
343
 
258
- enabled_languages = options[:languages] || []
259
- LOCALISED_VERSION_VALUES.each do |key|
260
- current = options[key]
261
- next unless current && current.kind_of?(Hash)
262
- current.each do |language, value|
263
- language = language.to_s
264
- enabled_languages << language unless enabled_languages.include?(language)
344
+ localizations = app_info.get_app_info_localizations
345
+
346
+ languages = options[:languages] || []
347
+ locales_to_enable = languages - localizations.map(&:locale)
348
+
349
+ if locales_to_enable.count > 0
350
+ lng_text = "language"
351
+ lng_text += "s" if locales_to_enable.count != 1
352
+ Helper.show_loading_indicator("Activating info #{lng_text} #{locales_to_enable.join(', ')}...")
353
+
354
+ locales_to_enable.each do |locale|
355
+ app_info.create_app_info_localization(attributes: {
356
+ locale: locale
357
+ })
265
358
  end
359
+
360
+ Helper.hide_loading_indicator
361
+
362
+ # Refresh version localizations
363
+ localizations = app_info.get_app_info_localizations
266
364
  end
267
365
 
268
- # Reject "default" language from getting enabled
269
- # because "default" is not an iTC language
270
- enabled_languages = enabled_languages.reject do |lang|
271
- lang == "default"
272
- end.uniq
366
+ return localizations
367
+ end
273
368
 
274
- if enabled_languages.count > 0
275
- v.create_languages(enabled_languages)
369
+ # Finding languages to enable
370
+ def verify_available_version_languages!(options, app)
371
+ platform = Spaceship::ConnectAPI::Platform.map(options[:platform])
372
+ version = app.get_edit_app_store_version(platform: platform)
373
+
374
+ unless version
375
+ UI.user_error!("Cannot update languages - could not find an editable version for '#{platform}'")
376
+ return
377
+ end
378
+
379
+ localizations = version.get_app_store_version_localizations
380
+
381
+ languages = options[:languages] || []
382
+ locales_to_enable = languages - localizations.map(&:locale)
383
+
384
+ if locales_to_enable.count > 0
276
385
  lng_text = "language"
277
- lng_text += "s" if enabled_languages.count != 1
278
- Helper.show_loading_indicator("Activating #{lng_text} #{enabled_languages.join(', ')}...")
279
- v.save!
386
+ lng_text += "s" if locales_to_enable.count != 1
387
+ Helper.show_loading_indicator("Activating version #{lng_text} #{locales_to_enable.join(', ')}...")
388
+
389
+ locales_to_enable.each do |locale|
390
+ version.create_app_store_version_localization(attributes: {
391
+ locale: locale
392
+ })
393
+ end
394
+
280
395
  Helper.hide_loading_indicator
396
+
397
+ # Refresh version localizations
398
+ localizations = version.get_app_store_version_localizations
281
399
  end
282
- true
400
+
401
+ return localizations
283
402
  end
284
403
 
285
404
  # Loads the metadata files and stores them into the options object
@@ -290,7 +409,7 @@ module Deliver
290
409
  ignore_validation = options[:ignore_language_directory_validation]
291
410
  Loader.language_folders(options[:metadata_path], ignore_validation).each do |lang_folder|
292
411
  language = File.basename(lang_folder)
293
- (LOCALISED_VERSION_VALUES + LOCALISED_APP_VALUES).each do |key|
412
+ (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
294
413
  path = File.join(lang_folder, "#{key}.txt")
295
414
  next unless File.exist?(path)
296
415
 
@@ -301,7 +420,7 @@ module Deliver
301
420
  end
302
421
 
303
422
  # Load non localised data
304
- (NON_LOCALISED_VERSION_VALUES + NON_LOCALISED_APP_VALUES).each do |key|
423
+ (NON_LOCALISED_VERSION_VALUES.keys + NON_LOCALISED_APP_VALUES).each do |key|
305
424
  path = File.join(options[:metadata_path], "#{key}.txt")
306
425
  next unless File.exist?(path)
307
426
 
@@ -309,20 +428,9 @@ module Deliver
309
428
  options[key] ||= File.read(path)
310
429
  end
311
430
 
312
- # Load trade representative contact information
313
- options[:trade_representative_contact_information] ||= {}
314
- TRADE_REPRESENTATIVE_CONTACT_INFORMATION_VALUES.values.each do |option_name|
315
- path = File.join(options[:metadata_path], TRADE_REPRESENTATIVE_CONTACT_INFORMATION_DIR, "#{option_name}.txt")
316
- next unless File.exist?(path)
317
- next if options[:trade_representative_contact_information][option_name].to_s.length > 0
318
-
319
- UI.message("Loading '#{path}'...")
320
- options[:trade_representative_contact_information][option_name] ||= File.read(path)
321
- end
322
-
323
431
  # Load review information
324
432
  options[:app_review_information] ||= {}
325
- REVIEW_INFORMATION_VALUES.values.each do |option_name|
433
+ REVIEW_INFORMATION_VALUES.keys.each do |option_name|
326
434
  path = File.join(options[:metadata_path], REVIEW_INFORMATION_DIR, "#{option_name}.txt")
327
435
  next unless File.exist?(path)
328
436
  next if options[:app_review_information][option_name].to_s.length > 0
@@ -336,7 +444,7 @@ module Deliver
336
444
 
337
445
  # Normalizes languages keys from symbols to strings
338
446
  def normalize_language_keys(options)
339
- (LOCALISED_VERSION_VALUES + LOCALISED_APP_VALUES).each do |key|
447
+ (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
340
448
  current = options[key]
341
449
  next unless current && current.kind_of?(Hash)
342
450
 
@@ -348,33 +456,55 @@ module Deliver
348
456
  options
349
457
  end
350
458
 
351
- def set_trade_representative_contact_information(v, options)
352
- return unless options[:trade_representative_contact_information]
353
- info = options[:trade_representative_contact_information]
354
- UI.user_error!("`trade_representative_contact_information` must be a hash", show_github_issues: true) unless info.kind_of?(Hash)
355
-
356
- TRADE_REPRESENTATIVE_CONTACT_INFORMATION_VALUES.each do |key, option_name|
357
- v.send("#{key}=", info[option_name].to_s.chomp) if info[option_name]
358
- end
359
- end
360
-
361
- def set_review_information(v, options)
459
+ def set_review_information(version, options)
362
460
  return unless options[:app_review_information]
363
461
  info = options[:app_review_information]
364
462
  UI.user_error!("`app_review_information` must be a hash", show_github_issues: true) unless info.kind_of?(Hash)
365
463
 
366
- REVIEW_INFORMATION_VALUES.each do |key, option_name|
367
- v.send("#{key}=", info[option_name].to_s.chomp) if info[option_name]
464
+ attributes = {}
465
+ REVIEW_INFORMATION_VALUES.each do |key, attribute_name|
466
+ strip_value = info[key].to_s.strip
467
+ attributes[attribute_name] = strip_value unless strip_value.empty?
468
+ end
469
+
470
+ if !attributes["demoAccountName"].to_s.empty? && !attributes["demoAccountPassword"].to_s.empty?
471
+ attributes["demoAccountRequired"] = true
472
+ else
473
+ attributes["demoAccountRequired"] = false
474
+ end
475
+
476
+ UI.message("Uploading app review information to App Store Connect")
477
+ app_store_review_detail = begin
478
+ version.fetch_app_store_review_detail
479
+ rescue
480
+ nil
481
+ end # errors if doesn't exist
482
+ if app_store_review_detail
483
+ app_store_review_detail.update(attributes: attributes)
484
+ else
485
+ version.create_app_store_review_detail(attributes: attributes)
368
486
  end
369
- v.review_user_needed = (v.review_demo_user.to_s.chomp + v.review_demo_password.to_s.chomp).length > 0
370
487
  end
371
488
 
372
- def set_review_attachment_file(v, options)
373
- return unless options[:app_review_attachment_file]
374
- v.upload_review_attachment!(options[:app_review_attachment_file])
489
+ def set_review_attachment_file(version, options)
490
+ app_store_review_detail = version.fetch_app_store_review_detail
491
+ app_review_attachments = app_store_review_detail.fetch_app_review_attachments
492
+
493
+ if options[:app_review_attachment_file]
494
+ app_review_attachments.each do |app_review_attachment|
495
+ UI.message("Removing previous review attachment file from App Store Connect")
496
+ app_review_attachment.delete!
497
+ end
498
+
499
+ UI.message("Uploading review attachment file to App Store Connect")
500
+ app_store_review_detail.upload_attachment(path: options[:app_review_attachment_file])
501
+ else
502
+ app_review_attachments.each(&:delete!)
503
+ UI.message("Removing review attachment file to App Store Connect") unless app_review_attachments.empty?
504
+ end
375
505
  end
376
506
 
377
- def set_app_rating(v, options)
507
+ def set_app_rating(version, options)
378
508
  return unless options[:app_rating_config_path]
379
509
 
380
510
  require 'json'
@@ -385,7 +515,18 @@ module Deliver
385
515
  UI.user_error!("Error parsing JSON file at path '#{options[:app_rating_config_path]}'")
386
516
  end
387
517
  UI.message("Setting the app's age rating...")
388
- v.update_rating(json)
518
+
519
+ # Maping from legacy ITC values to App Store Connect Values
520
+ attributes = {}
521
+ json.each do |k, v|
522
+ new_key = Spaceship::ConnectAPI::AgeRatingDeclaration.map_key_from_itc(k)
523
+ new_value = Spaceship::ConnectAPI::AgeRatingDeclaration.map_value_from_itc(new_key, v)
524
+ attributes[new_key] = new_value
525
+ end
526
+
527
+ age_rating_delcaration = version.fetch_age_rating_declaration
528
+ age_rating_delcaration.update(attributes: attributes)
389
529
  end
390
530
  end
531
+ # rubocop:enable Metrics/ClassLength
391
532
  end