fastlane 2.149.0 → 2.150.0.rc4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +82 -82
  3. data/deliver/lib/deliver.rb +0 -1
  4. data/deliver/lib/deliver/app_screenshot.rb +26 -25
  5. data/deliver/lib/deliver/options.rb +6 -11
  6. data/deliver/lib/deliver/runner.rb +7 -21
  7. data/deliver/lib/deliver/setup.rb +5 -30
  8. data/deliver/lib/deliver/submit_for_review.rb +155 -90
  9. data/deliver/lib/deliver/upload_metadata.rb +355 -143
  10. data/deliver/lib/deliver/upload_price_tier.rb +22 -8
  11. data/deliver/lib/deliver/upload_screenshots.rb +134 -39
  12. data/fastlane/lib/fastlane/actions/docs/capture_ios_screenshots.md +1 -1
  13. data/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb +77 -96
  14. data/fastlane/lib/fastlane/actions/docs/upload_to_play_store.md +3 -2
  15. data/fastlane/lib/fastlane/actions/download_dsyms.rb +7 -1
  16. data/fastlane/lib/fastlane/actions/google_play_track_release_names.rb +74 -0
  17. data/fastlane/lib/fastlane/actions/set_changelog.rb +23 -20
  18. data/fastlane/lib/fastlane/actions/slack.rb +1 -1
  19. data/fastlane/lib/fastlane/actions/spm.rb +7 -0
  20. data/fastlane/lib/fastlane/actions/upload_symbols_to_crashlytics.rb +1 -32
  21. data/fastlane/lib/fastlane/lane.rb +3 -3
  22. data/fastlane/lib/fastlane/swift_fastlane_function.rb +8 -4
  23. data/fastlane/lib/fastlane/version.rb +1 -1
  24. data/fastlane/swift/ControlCommand.swift +1 -0
  25. data/fastlane/swift/Deliverfile.swift +1 -1
  26. data/fastlane/swift/Fastlane.swift +48 -12
  27. data/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/xcshareddata/xcschemes/FastlaneRunner.xcscheme +3 -9
  28. data/fastlane/swift/Gymfile.swift +1 -1
  29. data/fastlane/swift/LaneFileProtocol.swift +2 -5
  30. data/fastlane/swift/Matchfile.swift +1 -1
  31. data/fastlane/swift/MatchfileProtocol.swift +1 -1
  32. data/fastlane/swift/Precheckfile.swift +1 -1
  33. data/fastlane/swift/RubyCommand.swift +29 -6
  34. data/fastlane/swift/RubyCommandable.swift +1 -0
  35. data/fastlane/swift/Runner.swift +85 -13
  36. data/fastlane/swift/Scanfile.swift +1 -1
  37. data/fastlane/swift/ScanfileProtocol.swift +1 -1
  38. data/fastlane/swift/Screengrabfile.swift +1 -1
  39. data/fastlane/swift/Snapshotfile.swift +1 -1
  40. data/fastlane/swift/SnapshotfileProtocol.swift +3 -3
  41. data/fastlane/swift/SocketClient.swift +76 -45
  42. data/fastlane/swift/SocketClientDelegateProtocol.swift +1 -1
  43. data/fastlane/swift/SocketResponse.swift +1 -0
  44. data/fastlane_core/lib/fastlane_core/configuration/config_item.rb +1 -3
  45. data/fastlane_core/lib/fastlane_core/itunes_transporter.rb +89 -52
  46. data/fastlane_core/lib/fastlane_core/pkg_file_analyser.rb +7 -0
  47. data/fastlane_core/lib/fastlane_core/project.rb +0 -1
  48. data/frameit/lib/frameit/device_types.rb +100 -100
  49. data/produce/lib/produce/itunes_connect.rb +32 -21
  50. data/produce/lib/produce/options.rb +3 -3
  51. data/sigh/lib/assets/resign.sh +7 -7
  52. data/snapshot/lib/assets/SnapshotHelper.swift +5 -5
  53. data/snapshot/lib/assets/SnapshotHelperXcode8.swift +3 -3
  54. data/snapshot/lib/snapshot/options.rb +0 -1
  55. data/snapshot/lib/snapshot/reports_generator.rb +8 -1
  56. data/spaceship/lib/spaceship/.DS_Store +0 -0
  57. data/spaceship/lib/spaceship/client.rb +4 -3
  58. data/spaceship/lib/spaceship/connect_api.rb +25 -2
  59. data/spaceship/lib/spaceship/connect_api/client.rb +97 -31
  60. data/spaceship/lib/spaceship/connect_api/file_uploader.rb +98 -0
  61. data/spaceship/lib/spaceship/connect_api/model.rb +1 -1
  62. data/spaceship/lib/spaceship/connect_api/models/age_rating_declaration.rb +113 -0
  63. data/spaceship/lib/spaceship/connect_api/models/app.rb +120 -3
  64. data/spaceship/lib/spaceship/connect_api/models/app_category.rb +94 -0
  65. data/spaceship/lib/spaceship/connect_api/models/app_info.rb +67 -0
  66. data/spaceship/lib/spaceship/connect_api/models/app_info_localization.rb +38 -0
  67. data/spaceship/lib/spaceship/connect_api/models/app_preview.rb +129 -0
  68. data/spaceship/lib/spaceship/connect_api/models/app_preview_set.rb +71 -0
  69. data/spaceship/lib/spaceship/connect_api/models/app_price.rb +22 -0
  70. data/spaceship/lib/spaceship/connect_api/models/app_price_tier.rb +12 -0
  71. data/spaceship/lib/spaceship/connect_api/models/app_review_attachment.rb +71 -0
  72. data/spaceship/lib/spaceship/connect_api/models/app_screenshot.rb +93 -0
  73. data/spaceship/lib/spaceship/connect_api/models/app_screenshot_set.rb +101 -0
  74. data/spaceship/lib/spaceship/connect_api/models/app_store_review_detail.rb +51 -0
  75. data/spaceship/lib/spaceship/connect_api/models/app_store_version.rb +183 -0
  76. data/spaceship/lib/spaceship/connect_api/models/app_store_version_localization.rb +86 -0
  77. data/spaceship/lib/spaceship/connect_api/models/app_store_version_phased_release.rb +36 -0
  78. data/spaceship/lib/spaceship/connect_api/models/app_store_version_submission.rb +26 -0
  79. data/spaceship/lib/spaceship/connect_api/models/build.rb +4 -0
  80. data/spaceship/lib/spaceship/connect_api/models/idfa_declaration.rb +40 -0
  81. data/spaceship/lib/spaceship/connect_api/models/reset_ratings_request.rb +26 -0
  82. data/spaceship/lib/spaceship/connect_api/models/territory.rb +27 -0
  83. data/spaceship/lib/spaceship/connect_api/testflight/testflight.rb +10 -3
  84. data/spaceship/lib/spaceship/connect_api/tunes/client.rb +33 -0
  85. data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +841 -0
  86. data/spaceship/lib/spaceship/spaceauth_runner.rb +2 -2
  87. data/supply/lib/supply/client.rb +19 -0
  88. data/supply/lib/supply/reader.rb +16 -0
  89. metadata +44 -39
  90. data/deliver/lib/deliver/upload_assets.rb +0 -27
  91. data/fastlane/lib/assets/.s3_html_template.erb.swp +0 -0
  92. data/fastlane/lib/fastlane/actions/.hipchat.rb.swp +0 -0
  93. data/fastlane/lib/fastlane/actions/.slack.rb.swp +0 -0
  94. data/scan/lib/scan/.options.rb.swp +0 -0
@@ -1,132 +1,197 @@
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
- if app_version
40
- build = v.candidate_builds.detect { |a| a.build_version == options[:build_number] && a.train_version == app_version }
41
- else
42
- build = v.candidate_builds.detect { |a| a.build_version == options[:build_number] }
43
- end
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
+
44
44
  unless build
45
45
  UI.user_error!("Build number: #{options[:build_number]} does not exist")
46
46
  end
47
47
  else
48
48
  UI.message("Selecting the latest build...")
49
- build = wait_for_build(app, app_version)
49
+ build = wait_for_build_processing_to_be_complete(app: app, platform: platform, options: options)
50
50
  end
51
- UI.message("Selecting build #{app_version} (#{build.build_version})...")
51
+ UI.message("Selecting build #{build.app_version} (#{build.version})...")
52
52
 
53
- v.select_build(build)
54
- v.save!
53
+ version.select_build(build_id: build.id)
55
54
 
56
55
  UI.success("Successfully selected build")
56
+
57
+ return build
57
58
  end
58
59
 
59
- def wait_for_build(app, app_version)
60
- UI.user_error!("Could not find app with app identifier") unless app
60
+ def update_export_compliance(options, app, build)
61
+ submission_information = options[:submission_information] || {}
62
+ uses_encryption = submission_information[:export_compliance_uses_encryption]
63
+
64
+ if build.uses_non_exempt_encryption.nil?
65
+ UI.verbose("Updating build for export compliance status of '#{uses_encryption}'")
66
+
67
+ if uses_encryption.to_s.empty?
68
+ message = [
69
+ "Export compliance is required to submit",
70
+ "Add information to the :submission_information option...",
71
+ " Docs: http://docs.fastlane.tools/actions/deliver/#compliance-and-idfa-settings",
72
+ " Example: submission_information: { export_compliance_uses_encryption: false }",
73
+ "This can also be set in your Info.plist with key 'ITSAppUsesNonExemptEncryption'"
74
+ ].join("\n")
75
+ UI.user_error!(message)
76
+ end
77
+
78
+ build = build.update(attributes: {
79
+ usesNonExemptEncryption: uses_encryption
80
+ })
61
81
 
62
- start = Time.now
63
- build = nil
82
+ UI.verbose("Successfully updated build for export compliance status of '#{build.uses_non_exempt_encryption}' on App Store Connect")
83
+ end
84
+ end
64
85
 
65
- use_latest_version = app_version.nil?
86
+ def update_idfa(options, app, version)
87
+ submission_information = options[:submission_information] || {}
88
+ uses_idfa = submission_information[:add_id_info_uses_idfa]
89
+
90
+ idfa_declaration = begin
91
+ version.fetch_idfa_declaration
92
+ rescue
93
+ nil
94
+ end
95
+
96
+ # Set IDFA on version
97
+ UI.verbose("Updating app store version for IDFA status of '#{uses_idfa}'")
98
+ unless uses_idfa.nil?
99
+ version = version.update(attributes: {
100
+ usesIdfa: uses_idfa
101
+ })
102
+ end
103
+ UI.verbose("Updated app store version for IDFA status of '#{version.uses_idfa}'")
104
+
105
+ # Error if uses_idfa not set
106
+ if version.uses_idfa.nil?
107
+ message = [
108
+ "Use of Advertising Identifier (IDFA) is required to submit",
109
+ "Add information to the :submission_information option...",
110
+ " Docs: http://docs.fastlane.tools/actions/deliver/#compliance-and-idfa-settings",
111
+ " Example: submission_information: { add_id_info_uses_idfa: false }",
112
+ " Example: submission_information: {",
113
+ " add_id_info_uses_idfa: true,",
114
+ " add_id_info_limits_tracking: false,",
115
+ " add_id_info_serves_ads: false,",
116
+ " add_id_info_uses_idfa: false,",
117
+ " add_id_info_tracks_install: false",
118
+ " }"
119
+ ].join("\n")
120
+ UI.user_error!(message)
121
+ end
66
122
 
67
- loop do
68
- # Sometimes candidate_builds don't appear immediately after submission
69
- # Wait for candidate_builds to appear on App Store Connect
70
- # Issue https://github.com/fastlane/fastlane/issues/10411
71
- if use_latest_version
72
- candidate_builds = app.latest_version.candidate_builds
73
- else
74
- candidate_builds = app.tunes_all_builds_for_train(train: app_version)
123
+ # Create, update, or delete IDFA declaration
124
+ if uses_idfa == false
125
+ if idfa_declaration
126
+ UI.verbose("Deleting IDFA delcaration")
127
+ idfa_declaration.delete!
128
+ UI.verbose("Deleted IDFA delcaration")
75
129
  end
76
- if (candidate_builds || []).count == 0
77
- UI.message("Waiting for candidate builds to appear...")
78
- if (Time.now - start) > (60 * 5)
79
- UI.user_error!("Could not find any available candidate builds on App Store Connect to submit")
80
- else
81
- sleep(30)
82
- next
83
- end
130
+ elsif uses_idfa == true
131
+ attributes = {
132
+ honorsLimitedAdTracking: !!submission_information[:add_id_info_limits_tracking],
133
+ servesAds: !!submission_information[:add_id_info_serves_ads],
134
+ attributesAppInstallationToPreviousAd: !!submission_information[:add_id_info_tracks_install],
135
+ attributesActionWithPreviousAd: !!submission_information[:add_id_info_tracks_action]
136
+ }
137
+
138
+ if idfa_declaration
139
+ UI.verbose("Updating IDFA delcaration")
140
+ idfa_declaration.update(attributes: attributes)
141
+ UI.verbose("Updated IDFA delcaration")
142
+ else
143
+ UI.verbose("Creating IDFA delcaration")
144
+ version.create_idfa_declaration(attributes: attributes)
145
+ UI.verbose("Created IDFA delcaration")
84
146
  end
147
+ end
85
148
 
86
- latest_build = find_build(candidate_builds)
149
+ UI.success("Successfully updated IDFA delcarations on App Store Connect")
150
+ end
87
151
 
88
- # if the app version isn't present in the hash (could happen if we are waiting for submission, but didn't provide
89
- # it explicitly and no ipa was passed to grab it from), then fall back to the best guess, which is the train_version
90
- # of the most recently uploaded build
91
- app_version ||= latest_build.train_version
152
+ def update_submission_information(options, app)
153
+ submission_information = options[:submission_information] || {}
92
154
 
93
- # Sometimes latest build will disappear and a different build would get selected
94
- # Only set build if no latest build found or if same build versions as previously fetched build
95
- # Issue: https://github.com/fastlane/fastlane/issues/10945
96
- if build.nil? || (latest_build && latest_build.build_version == build.build_version && latest_build.train_version == app_version)
97
- build = latest_build
98
- end
155
+ content_rights = submission_information[:content_rights_contains_third_party_content]
99
156
 
100
- return build if build && build.processing == false
157
+ unless content_rights.nil?
158
+ value = if content_rights
159
+ Spaceship::ConnectAPI::App::ContentRightsDeclaration::USES_THIRD_PARTY_CONTENT
160
+ else
161
+ Spaceship::ConnectAPI::App::ContentRightsDeclaration::DOES_NOT_USE_THIRD_PARTY_CONTENT
162
+ end
101
163
 
102
- if build
103
- UI.message("Waiting App Store Connect processing for build #{app_version} (#{build.build_version})... this might take a while...")
104
- else
105
- UI.message("Waiting App Store Connect processing for build... this might take a while...")
106
- end
107
-
108
- if (Time.now - start) > (60 * 5)
109
- UI.message("")
110
- UI.message("You can tweet: \"App Store Connect #iosprocessingtime #{((Time.now - start) / 60).round} minutes\"")
111
- end
112
- sleep(30)
164
+ UI.verbose("Updating contents rights declaration on App Store Connect")
165
+ app.update(attributes: {
166
+ contentRightsDeclaration: value
167
+ })
168
+ UI.success("Successfully updated contents rights declaration on App Store Connect")
113
169
  end
114
- nil
115
170
  end
116
171
 
117
- def find_build(candidate_builds)
118
- if (candidate_builds || []).count == 0
119
- UI.user_error!("Could not find any available candidate builds on App Store Connect to submit")
120
- end
121
-
122
- build = candidate_builds.first
123
- candidate_builds.each do |b|
124
- if b.upload_date > build.upload_date
125
- build = b
126
- end
172
+ def wait_for_build_processing_to_be_complete(app: nil, platform: nil, options: nil)
173
+ app_version = options[:app_version]
174
+ app_version ||= FastlaneCore::IpaFileAnalyser.fetch_app_version(options[:ipa]) if options[:ipa]
175
+ app_version ||= FastlaneCore::PkgFileAnalyser.fetch_app_version(options[:pkg]) if options[:pkg]
176
+
177
+ app_build ||= FastlaneCore::IpaFileAnalyser.fetch_app_build(options[:ipa]) if options[:ipa]
178
+ app_build ||= FastlaneCore::PkgFileAnalyser.fetch_app_build(options[:pkg]) if options[:pkg]
179
+
180
+ latest_build = FastlaneCore::BuildWatcher.wait_for_build_processing_to_be_complete(
181
+ app_id: app.id,
182
+ platform: platform,
183
+ app_version: app_version,
184
+ build_version: app_build,
185
+ poll_interval: 15,
186
+ return_when_build_appears: false,
187
+ return_spaceship_testflight_build: false
188
+ )
189
+
190
+ unless latest_build.app_version == app_version && latest_build.version == app_build
191
+ UI.important("Uploaded app #{app_version} - #{app_build}, but received build #{latest_build.app_version} - #{latest_build.version}.")
127
192
  end
128
193
 
129
- return build
194
+ return latest_build
130
195
  end
131
196
  end
132
197
  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,195 @@ 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)
136
191
 
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
192
+ # Update categories
193
+ app_info = app.fetch_edit_app_info
194
+ if app_info
195
+ category_id_map = {}
196
+
197
+ primary_category = options[:primary_category].to_s.strip
198
+ secondary_category = options[:secondary_category].to_s.strip
199
+ primary_first_sub_category = options[:primary_first_sub_category].to_s.strip
200
+ primary_second_sub_category = options[:primary_second_sub_category].to_s.strip
201
+ secondary_first_sub_category = options[:secondary_first_sub_category].to_s.strip
202
+ secondary_second_sub_category = options[:secondary_second_sub_category].to_s.strip
203
+
204
+ mapped_values = {}
205
+
206
+ # Only update primary and secondar category if explicitly set
207
+ unless primary_category.empty?
208
+ mapped = Spaceship::ConnectAPI::AppCategory.map_category_from_itc(
209
+ primary_category
210
+ )
211
+
212
+ mapped_values[primary_category] = mapped
213
+ category_id_map[:primary_category_id] = mapped
214
+ end
215
+ unless secondary_category.empty?
216
+ mapped = Spaceship::ConnectAPI::AppCategory.map_category_from_itc(
217
+ secondary_category
218
+ )
219
+
220
+ mapped_values[secondary_category] = mapped
221
+ category_id_map[:secondary_category_id] = mapped
152
222
  end
223
+
224
+ # Only set if primary category is going to be set
225
+ unless primary_category.empty?
226
+ mapped = Spaceship::ConnectAPI::AppCategory.map_subcategory_from_itc(
227
+ primary_first_sub_category
228
+ )
229
+
230
+ mapped_values[primary_first_sub_category] = mapped
231
+ category_id_map[:primary_subcategory_one_id] = mapped
232
+ end
233
+ unless primary_category.empty?
234
+ mapped = Spaceship::ConnectAPI::AppCategory.map_subcategory_from_itc(
235
+ primary_second_sub_category
236
+ )
237
+
238
+ mapped_values[primary_second_sub_category] = mapped
239
+ category_id_map[:primary_subcategory_two_id] = mapped
240
+ end
241
+
242
+ # Only set if secondary category is going to be set
243
+ unless secondary_category.empty?
244
+ mapped = Spaceship::ConnectAPI::AppCategory.map_subcategory_from_itc(
245
+ secondary_first_sub_category
246
+ )
247
+
248
+ mapped_values[secondary_first_sub_category] = mapped
249
+ category_id_map[:secondary_subcategory_one_id] = mapped
250
+ end
251
+ unless secondary_category.empty?
252
+ mapped = Spaceship::ConnectAPI::AppCategory.map_subcategory_from_itc(
253
+ secondary_second_sub_category
254
+ )
255
+
256
+ mapped_values[secondary_second_sub_category] = mapped
257
+ category_id_map[:secondary_subcategory_two_id] = mapped
258
+ end
259
+
260
+ # Print deprecation warnings if category was mapped
261
+ has_mapped_values = false
262
+ mapped_values.each do |k, v|
263
+ next if k.nil? || v.nil?
264
+ next if k == v
265
+ has_mapped_values = true
266
+ UI.deprecated("Category '#{k}' from iTunesConnect as been deprecated. Please replace with '#{v}'")
267
+ end
268
+ UI.deprecated("You can find more info at https://docs.fastlane.tools/actions/deliver/#reference") if has_mapped_values
269
+
270
+ app_info.update_categories(category_id_map: category_id_map)
153
271
  end
154
- end
155
272
 
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
273
+ # Update phased release
274
+ unless options[:phased_release].nil?
275
+ phased_release = begin
276
+ version.fetch_app_store_version_phased_release
277
+ rescue
278
+ nil
279
+ end # returns no data error so need to rescue
280
+ if !!options[:phased_release]
281
+ unless phased_release
282
+ UI.message("Creating phased release on App Store Connect")
283
+ version.create_app_store_version_phased_release(attributes: {
284
+ phasedReleaseState: Spaceship::ConnectAPI::AppStoreVersionPhasedRelease::PhasedReleaseState::INACTIVE
285
+ })
286
+ end
287
+ elsif phased_release
288
+ UI.message("Removing phased release on App Store Connect")
289
+ phased_release.delete!
290
+ end
170
291
  end
292
+
293
+ # Update rating reset
294
+ unless options[:reset_ratings].nil?
295
+ reset_rating_request = begin
296
+ version.fetch_reset_ratings_request
297
+ rescue
298
+ nil
299
+ end # returns no data error so need to rescue
300
+ if !!options[:reset_ratings]
301
+ unless reset_rating_request
302
+ UI.message("Creating reset ratings request on App Store Connect")
303
+ version.create_reset_ratings_request
304
+ end
305
+ elsif reset_rating_request
306
+ UI.message("Removing reset ratings request on App Store Connect")
307
+ reset_rating_request.delete!
308
+ end
309
+ end
310
+
311
+ set_review_information(version, options)
312
+ set_review_attachment_file(version, options)
313
+ set_app_rating(version, options)
171
314
  end
172
315
 
173
316
  # rubocop:enable Metrics/PerceivedComplexity
@@ -181,7 +324,7 @@ module Deliver
181
324
  enabled_languages = detect_languages(options)
182
325
 
183
326
  # Get all languages used in existing settings
184
- (LOCALISED_VERSION_VALUES + LOCALISED_APP_VALUES).each do |key|
327
+ (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
185
328
  current = options[key]
186
329
  next unless current && current.kind_of?(Hash)
187
330
  current.each do |language, value|
@@ -200,7 +343,7 @@ module Deliver
200
343
  return unless enabled_languages.include?("default")
201
344
  UI.message("Detected languages: " + enabled_languages.to_s)
202
345
 
203
- (LOCALISED_VERSION_VALUES + LOCALISED_APP_VALUES).each do |key|
346
+ (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
204
347
  current = options[key]
205
348
  next unless current && current.kind_of?(Hash)
206
349
 
@@ -222,7 +365,7 @@ module Deliver
222
365
  enabled_languages = options[:languages] || []
223
366
 
224
367
  # Get all languages used in existing settings
225
- (LOCALISED_VERSION_VALUES + LOCALISED_APP_VALUES).each do |key|
368
+ (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
226
369
  current = options[key]
227
370
  next unless current && current.kind_of?(Hash)
228
371
  current.each do |language, value|
@@ -245,41 +388,73 @@ module Deliver
245
388
  .uniq
246
389
  end
247
390
 
248
- # Makes sure all languages we need are actually created
249
- def verify_available_languages!(options)
250
- return if options[:skip_metadata]
391
+ # Finding languages to enable
392
+ def verify_available_info_languages!(options, app)
393
+ app_info = app.fetch_edit_app_info
251
394
 
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
395
+ unless app_info
396
+ UI.user_error!("Cannot update languages - could not find an editable info")
397
+ return
398
+ end
257
399
 
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)
400
+ localizations = app_info.get_app_info_localizations
401
+
402
+ languages = (options[:languages] || []).reject { |lang| lang == "default" }
403
+ locales_to_enable = languages - localizations.map(&:locale)
404
+
405
+ if locales_to_enable.count > 0
406
+ lng_text = "language"
407
+ lng_text += "s" if locales_to_enable.count != 1
408
+ Helper.show_loading_indicator("Activating info #{lng_text} #{locales_to_enable.join(', ')}...")
409
+
410
+ locales_to_enable.each do |locale|
411
+ app_info.create_app_info_localization(attributes: {
412
+ locale: locale
413
+ })
265
414
  end
415
+
416
+ Helper.hide_loading_indicator
417
+
418
+ # Refresh version localizations
419
+ localizations = app_info.get_app_info_localizations
420
+ end
421
+
422
+ return localizations
423
+ end
424
+
425
+ # Finding languages to enable
426
+ def verify_available_version_languages!(options, app)
427
+ platform = Spaceship::ConnectAPI::Platform.map(options[:platform])
428
+ version = app.get_edit_app_store_version(platform: platform)
429
+
430
+ unless version
431
+ UI.user_error!("Cannot update languages - could not find an editable version for '#{platform}'")
432
+ return
266
433
  end
267
434
 
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
435
+ localizations = version.get_app_store_version_localizations
273
436
 
274
- if enabled_languages.count > 0
275
- v.create_languages(enabled_languages)
437
+ languages = (options[:languages] || []).reject { |lang| lang == "default" }
438
+ locales_to_enable = languages - localizations.map(&:locale)
439
+
440
+ if locales_to_enable.count > 0
276
441
  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!
442
+ lng_text += "s" if locales_to_enable.count != 1
443
+ Helper.show_loading_indicator("Activating version #{lng_text} #{locales_to_enable.join(', ')}...")
444
+
445
+ locales_to_enable.each do |locale|
446
+ version.create_app_store_version_localization(attributes: {
447
+ locale: locale
448
+ })
449
+ end
450
+
280
451
  Helper.hide_loading_indicator
452
+
453
+ # Refresh version localizations
454
+ localizations = version.get_app_store_version_localizations
281
455
  end
282
- true
456
+
457
+ return localizations
283
458
  end
284
459
 
285
460
  # Loads the metadata files and stores them into the options object
@@ -290,7 +465,7 @@ module Deliver
290
465
  ignore_validation = options[:ignore_language_directory_validation]
291
466
  Loader.language_folders(options[:metadata_path], ignore_validation).each do |lang_folder|
292
467
  language = File.basename(lang_folder)
293
- (LOCALISED_VERSION_VALUES + LOCALISED_APP_VALUES).each do |key|
468
+ (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
294
469
  path = File.join(lang_folder, "#{key}.txt")
295
470
  next unless File.exist?(path)
296
471
 
@@ -301,7 +476,7 @@ module Deliver
301
476
  end
302
477
 
303
478
  # Load non localised data
304
- (NON_LOCALISED_VERSION_VALUES + NON_LOCALISED_APP_VALUES).each do |key|
479
+ (NON_LOCALISED_VERSION_VALUES.keys + NON_LOCALISED_APP_VALUES).each do |key|
305
480
  path = File.join(options[:metadata_path], "#{key}.txt")
306
481
  next unless File.exist?(path)
307
482
 
@@ -309,20 +484,9 @@ module Deliver
309
484
  options[key] ||= File.read(path)
310
485
  end
311
486
 
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
487
  # Load review information
324
488
  options[:app_review_information] ||= {}
325
- REVIEW_INFORMATION_VALUES.values.each do |option_name|
489
+ REVIEW_INFORMATION_VALUES.keys.each do |option_name|
326
490
  path = File.join(options[:metadata_path], REVIEW_INFORMATION_DIR, "#{option_name}.txt")
327
491
  next unless File.exist?(path)
328
492
  next if options[:app_review_information][option_name].to_s.length > 0
@@ -336,7 +500,7 @@ module Deliver
336
500
 
337
501
  # Normalizes languages keys from symbols to strings
338
502
  def normalize_language_keys(options)
339
- (LOCALISED_VERSION_VALUES + LOCALISED_APP_VALUES).each do |key|
503
+ (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
340
504
  current = options[key]
341
505
  next unless current && current.kind_of?(Hash)
342
506
 
@@ -348,33 +512,55 @@ module Deliver
348
512
  options
349
513
  end
350
514
 
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)
515
+ def set_review_information(version, options)
362
516
  return unless options[:app_review_information]
363
517
  info = options[:app_review_information]
364
518
  UI.user_error!("`app_review_information` must be a hash", show_github_issues: true) unless info.kind_of?(Hash)
365
519
 
366
- REVIEW_INFORMATION_VALUES.each do |key, option_name|
367
- v.send("#{key}=", info[option_name].to_s.chomp) if info[option_name]
520
+ attributes = {}
521
+ REVIEW_INFORMATION_VALUES.each do |key, attribute_name|
522
+ strip_value = info[key].to_s.strip
523
+ attributes[attribute_name] = strip_value unless strip_value.empty?
524
+ end
525
+
526
+ if !attributes["demoAccountName"].to_s.empty? && !attributes["demoAccountPassword"].to_s.empty?
527
+ attributes["demoAccountRequired"] = true
528
+ else
529
+ attributes["demoAccountRequired"] = false
530
+ end
531
+
532
+ UI.message("Uploading app review information to App Store Connect")
533
+ app_store_review_detail = begin
534
+ version.fetch_app_store_review_detail
535
+ rescue
536
+ nil
537
+ end # errors if doesn't exist
538
+ if app_store_review_detail
539
+ app_store_review_detail.update(attributes: attributes)
540
+ else
541
+ version.create_app_store_review_detail(attributes: attributes)
368
542
  end
369
- v.review_user_needed = (v.review_demo_user.to_s.chomp + v.review_demo_password.to_s.chomp).length > 0
370
543
  end
371
544
 
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])
545
+ def set_review_attachment_file(version, options)
546
+ app_store_review_detail = version.fetch_app_store_review_detail
547
+ app_review_attachments = app_store_review_detail.fetch_app_review_attachments
548
+
549
+ if options[:app_review_attachment_file]
550
+ app_review_attachments.each do |app_review_attachment|
551
+ UI.message("Removing previous review attachment file from App Store Connect")
552
+ app_review_attachment.delete!
553
+ end
554
+
555
+ UI.message("Uploading review attachment file to App Store Connect")
556
+ app_store_review_detail.upload_attachment(path: options[:app_review_attachment_file])
557
+ else
558
+ app_review_attachments.each(&:delete!)
559
+ UI.message("Removing review attachment file to App Store Connect") unless app_review_attachments.empty?
560
+ end
375
561
  end
376
562
 
377
- def set_app_rating(v, options)
563
+ def set_app_rating(version, options)
378
564
  return unless options[:app_rating_config_path]
379
565
 
380
566
  require 'json'
@@ -385,7 +571,33 @@ module Deliver
385
571
  UI.user_error!("Error parsing JSON file at path '#{options[:app_rating_config_path]}'")
386
572
  end
387
573
  UI.message("Setting the app's age rating...")
388
- v.update_rating(json)
574
+
575
+ # Maping from legacy ITC values to App Store Connect Values
576
+ mapped_values = {}
577
+ attributes = {}
578
+ json.each do |k, v|
579
+ new_key = Spaceship::ConnectAPI::AgeRatingDeclaration.map_key_from_itc(k)
580
+ new_value = Spaceship::ConnectAPI::AgeRatingDeclaration.map_value_from_itc(new_key, v)
581
+
582
+ mapped_values[k] = new_key
583
+ mapped_values[v] = new_value
584
+
585
+ attributes[new_key] = new_value
586
+ end
587
+
588
+ # Print deprecation warnings if category was mapped
589
+ has_mapped_values = false
590
+ mapped_values.each do |k, v|
591
+ next if k.nil? || v.nil?
592
+ next if k == v
593
+ has_mapped_values = true
594
+ UI.deprecated("Age rating '#{k}' from iTunesConnect as been deprecated. Please replace with '#{v}'")
595
+ end
596
+ UI.deprecated("You can find more info at https://docs.fastlane.tools/actions/deliver/#reference") if has_mapped_values
597
+
598
+ age_rating_delcaration = version.fetch_age_rating_declaration
599
+ age_rating_delcaration.update(attributes: attributes)
389
600
  end
390
601
  end
602
+ # rubocop:enable Metrics/ClassLength
391
603
  end