fastlane 2.233.1 → 2.235.0

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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +105 -104
  3. data/bin/fastlane +2 -2
  4. data/deliver/lib/deliver/app_clip_header_image.rb +18 -0
  5. data/deliver/lib/deliver/commands_generator.rb +17 -0
  6. data/deliver/lib/deliver/download_app_clip_header_images.rb +64 -0
  7. data/deliver/lib/deliver/languages.rb +36 -3
  8. data/deliver/lib/deliver/loader.rb +41 -0
  9. data/deliver/lib/deliver/options.rb +23 -0
  10. data/deliver/lib/deliver/runner.rb +8 -0
  11. data/deliver/lib/deliver/upload_app_clip_default_experience_header_images.rb +188 -0
  12. data/deliver/lib/deliver/upload_app_clip_default_experience_metadata.rb +229 -0
  13. data/deliver/lib/deliver/upload_metadata.rb +71 -7
  14. data/deliver/lib/deliver/upload_price_tier.rb +4 -2
  15. data/deliver/lib/deliver/upload_screenshots.rb +1 -1
  16. data/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb +2 -2
  17. data/fastlane/lib/fastlane/actions/appium.rb +1 -1
  18. data/fastlane/lib/fastlane/actions/appledoc.rb +1 -1
  19. data/fastlane/lib/fastlane/actions/create_app_on_managed_play_store.rb +2 -2
  20. data/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb +100 -0
  21. data/fastlane/lib/fastlane/actions/gcovr.rb +1 -1
  22. data/fastlane/lib/fastlane/actions/get_version_number.rb +2 -12
  23. data/fastlane/lib/fastlane/actions/increment_version_number.rb +33 -0
  24. data/fastlane/lib/fastlane/actions/install_on_device.rb +1 -1
  25. data/fastlane/lib/fastlane/actions/ipa.rb +2 -2
  26. data/fastlane/lib/fastlane/actions/lcov.rb +1 -1
  27. data/fastlane/lib/fastlane/actions/notarize.rb +1 -148
  28. data/fastlane/lib/fastlane/actions/oclint.rb +1 -1
  29. data/fastlane/lib/fastlane/actions/slack.rb +9 -3
  30. data/fastlane/lib/fastlane/actions/sonar.rb +1 -1
  31. data/fastlane/lib/fastlane/actions/sourcedocs.rb +1 -1
  32. data/fastlane/lib/fastlane/actions/swiftlint.rb +1 -1
  33. data/fastlane/lib/fastlane/actions/testfairy.rb +1 -3
  34. data/fastlane/lib/fastlane/actions/validate_play_store_json_key.rb +4 -4
  35. data/fastlane/lib/fastlane/actions/xcodebuild.rb +1 -1
  36. data/fastlane/lib/fastlane/actions/xctool.rb +1 -1
  37. data/fastlane/lib/fastlane/documentation/markdown_docs_generator.rb +3 -1
  38. data/fastlane/lib/fastlane/helper/xcodebuild_formatter_helper.rb +1 -1
  39. data/fastlane/lib/fastlane/helper/xcodeproj_helper.rb +155 -0
  40. data/fastlane/lib/fastlane/helper/xcodes_helper.rb +1 -1
  41. data/fastlane/lib/fastlane/notification/slack.rb +9 -4
  42. data/fastlane/lib/fastlane/plugins/template/%gem_name%.gemspec.erb +1 -1
  43. data/fastlane/lib/fastlane/plugins/template/.rubocop.yml +2 -1
  44. data/fastlane/lib/fastlane/swift_runner_upgrader.rb +1 -1
  45. data/fastlane/lib/fastlane/version.rb +2 -2
  46. data/fastlane/swift/Deliverfile.swift +1 -1
  47. data/fastlane/swift/DeliverfileProtocol.swift +36 -1
  48. data/fastlane/swift/Fastlane.swift +109 -29
  49. data/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/project.pbxproj +4 -4
  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 +1 -1
  54. data/fastlane/swift/Precheckfile.swift +1 -1
  55. data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
  56. data/fastlane/swift/Scanfile.swift +1 -1
  57. data/fastlane/swift/ScanfileProtocol.swift +1 -1
  58. data/fastlane/swift/Screengrabfile.swift +1 -1
  59. data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
  60. data/fastlane/swift/Snapshotfile.swift +1 -1
  61. data/fastlane/swift/SnapshotfileProtocol.swift +1 -1
  62. data/fastlane_core/lib/fastlane_core/clipboard.rb +1 -1
  63. data/fastlane_core/lib/fastlane_core/command_executor.rb +13 -5
  64. data/fastlane_core/lib/fastlane_core/helper.rb +14 -1
  65. data/fastlane_core/lib/fastlane_core/languages.rb +1 -1
  66. data/frameit/lib/frameit/dependency_checker.rb +1 -1
  67. data/frameit/lib/frameit/device_types.rb +18 -0
  68. data/frameit/lib/frameit/editor.rb +2 -2
  69. data/frameit/lib/frameit/offsets.rb +3 -2
  70. data/internal/README.md +11 -0
  71. data/match/lib/match/nuke.rb +60 -40
  72. data/match/lib/match/storage/git_storage.rb +1 -1
  73. data/pilot/lib/pilot/build_manager.rb +69 -2
  74. data/pilot/lib/pilot/options.rb +23 -0
  75. data/produce/lib/produce/available_default_languages.rb +12 -1
  76. data/scan/lib/scan/detect_values.rb +5 -0
  77. data/screengrab/lib/screengrab/reports_generator.rb +2 -2
  78. data/screengrab/lib/screengrab/runner.rb +14 -15
  79. data/sigh/lib/assets/resign.sh +6 -10
  80. data/sigh/lib/sigh/resign.rb +2 -2
  81. data/snapshot/lib/snapshot/reports_generator.rb +9 -2
  82. data/snapshot/lib/snapshot/simulator_launchers/simulator_launcher.rb +1 -1
  83. data/snapshot/lib/snapshot/simulator_launchers/simulator_launcher_base.rb +9 -9
  84. data/spaceship/lib/assets/languageMapping.json +66 -0
  85. data/spaceship/lib/spaceship/connect_api/models/app.rb +14 -1
  86. data/spaceship/lib/spaceship/connect_api/models/app_clip.rb +18 -0
  87. data/spaceship/lib/spaceship/connect_api/models/app_clip_app_store_review_detail.rb +34 -0
  88. data/spaceship/lib/spaceship/connect_api/models/app_clip_default_experience.rb +43 -0
  89. data/spaceship/lib/spaceship/connect_api/models/app_clip_default_experience_localization.rb +49 -0
  90. data/spaceship/lib/spaceship/connect_api/models/app_clip_header_image.rb +159 -0
  91. data/spaceship/lib/spaceship/connect_api/models/app_store_version.rb +5 -1
  92. data/spaceship/lib/spaceship/connect_api/models/beta_app_clip_invocation.rb +44 -0
  93. data/spaceship/lib/spaceship/connect_api/models/beta_app_clip_invocation_localization.rb +35 -0
  94. data/spaceship/lib/spaceship/connect_api/models/build_bundle.rb +1 -1
  95. data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +291 -0
  96. data/spaceship/lib/spaceship/connect_api.rb +7 -0
  97. data/spaceship/lib/spaceship/portal/portal_client.rb +1 -1
  98. data/spaceship/lib/spaceship/portal/provisioning_profile.rb +63 -25
  99. data/spaceship/lib/spaceship/two_step_or_factor_client.rb +2 -1
  100. data/supply/lib/supply/client.rb +75 -62
  101. data/supply/lib/supply/options.rb +2 -2
  102. data/supply/lib/supply/reader.rb +16 -0
  103. data/supply/lib/supply/uploader.rb +1 -1
  104. metadata +53 -41
@@ -0,0 +1,188 @@
1
+ require 'fastlane_core'
2
+ require 'spaceship/tunes/tunes'
3
+ require 'digest/md5'
4
+
5
+ require_relative 'module'
6
+ require_relative 'loader'
7
+
8
+ module Deliver
9
+ class UploadAppClipDefaultExperienceHeaderImages
10
+ UploadAppClipHeaderImageJob = Struct.new(:path, :localization)
11
+
12
+ def find_and_upload(options)
13
+ return if options[:edit_live] || options[:app_clip_header_images_path].nil?
14
+
15
+ app_clip_header_images = collect_app_clip_header_images(options)
16
+
17
+ app = Deliver.cache[:app]
18
+
19
+ platform = Spaceship::ConnectAPI::Platform.map(options[:platform])
20
+ version = app.get_edit_app_store_version(platform: platform, includes: Spaceship::ConnectAPI::AppStoreVersion::ESSENTIAL_INCLUDES + ",appClipDefaultExperience")
21
+ UI.user_error!("Could not find a version to edit for app '#{app.name}' for '#{platform}'") unless version
22
+
23
+ app_clip_default_experience = version.app_clip_default_experience
24
+ UI.user_error!("Could not find a default app clip experience for version '#{version}'. Use the :app_clip_default_experience_subtitle and :app_clip_default_experience_action options to create and add metadata to the app clip default experience for this version.") unless app_clip_default_experience
25
+
26
+ UI.important("Will begin uploading app clip default experience header images for '#{version.version_string}' on App Store Connect")
27
+ UI.message("Starting with the upload of app clip header images...")
28
+
29
+ upload(app_clip_default_experience, app_clip_header_images)
30
+
31
+ UI.success("Successfully uploaded app clip default experience header images to App Store Connect")
32
+ end
33
+
34
+ def upload(app_clip_default_experience, app_clip_header_images)
35
+ # get the existing localizations and their header images
36
+ app_clip_default_experience_localizations = Spaceship::ConnectAPI::AppClipDefaultExperienceLocalization.find_all(app_clip_default_experience_id: app_clip_default_experience.id, includes: 'appClipHeaderImage')
37
+
38
+ # Create missing localizations for languages that have header images but no localization
39
+ app_clip_header_images.each do |header_image|
40
+ # Skip if language is nil or empty
41
+ next if header_image.language.nil? || header_image.language.empty?
42
+
43
+ existing_localization = app_clip_default_experience_localizations.find { |l| l.locale.eql?(header_image.language) }
44
+ next if existing_localization
45
+
46
+ UI.message("Creating app clip default experience localization for '#{header_image.language}'")
47
+ new_localization = Spaceship::ConnectAPI::AppClipDefaultExperienceLocalization.create(
48
+ default_experience_id: app_clip_default_experience.id,
49
+ attributes: { locale: header_image.language }
50
+ )
51
+ app_clip_default_experience_localizations << new_localization
52
+ end
53
+
54
+ # Upload app clip header images
55
+ worker = FastlaneCore::QueueWorker.new do |job|
56
+ begin
57
+ localization = job.localization
58
+
59
+ # if there's an existing header image, it must be deleted before uploading the new one
60
+ unless localization.app_clip_header_image.nil?
61
+ localization.app_clip_header_image.delete!
62
+ UI.verbose("[#{localization.locale}] Removed existing header image")
63
+ end
64
+
65
+ UI.verbose("[#{localization.locale}] Uploading '#{job.path}'...")
66
+ start_time = Time.now
67
+ Spaceship::ConnectAPI::AppClipHeaderImage.create(app_clip_default_experience_localization_id: localization.id, path: job.path, wait_for_processing: false)
68
+ UI.message("Uploaded '#{job.path}'... (#{Time.now - start_time} secs)")
69
+ rescue => error
70
+ UI.error(error)
71
+ end
72
+ end
73
+
74
+ app_clip_header_images.each do |header_image|
75
+ localization = app_clip_default_experience_localizations.find { |l| l.locale.eql?(header_image.language) }
76
+ unless localization
77
+ UI.error("Could not find or create localization for #{header_image.language}")
78
+ next
79
+ end
80
+
81
+ # check to see if it's already uploaded
82
+ checksum = UploadAppClipDefaultExperienceHeaderImages.calculate_checksum(header_image.path)
83
+ if !localization.app_clip_header_image.nil? && checksum.eql?(localization.app_clip_header_image.source_file_checksum)
84
+ UI.message("Skipping '#{header_image.path}' as it is already uploaded")
85
+ next
86
+ end
87
+
88
+ # upload
89
+ worker.enqueue(UploadAppClipHeaderImageJob.new(header_image.path, localization))
90
+ end
91
+
92
+ worker.start
93
+
94
+ UI.verbose('Uploading jobs are completed')
95
+
96
+ Helper.show_loading_indicator("Waiting for all the app clip header images to finish being processed...")
97
+ wait_for_complete(app_clip_default_experience.id)
98
+ Helper.hide_loading_indicator
99
+
100
+ UI.message("Successfully uploaded all app clip header images")
101
+ end
102
+
103
+ # Verify all screenshots have been processed
104
+ # Functionality copied and modified from upload_screenshots.rb
105
+ def wait_for_complete(app_clip_default_experience_id)
106
+ loop do
107
+ # fetch
108
+ app_clip_default_experience_localizations = Spaceship::ConnectAPI::AppClipDefaultExperienceLocalization.find_all(app_clip_default_experience_id: app_clip_default_experience_id, includes: 'appClipHeaderImage')
109
+ header_images = app_clip_default_experience_localizations.map(&:app_clip_header_image)
110
+
111
+ # group states
112
+ states = header_images.each_with_object({}) do |header_image, hash|
113
+ next unless header_image
114
+
115
+ state = header_image.asset_delivery_state['state']
116
+ hash[state] ||= 0
117
+ hash[state] += 1
118
+ end
119
+
120
+ is_processing = states.fetch('UPLOAD_COMPLETE', 0) > 0
121
+ return states unless is_processing
122
+
123
+ UI.verbose("There are still incomplete app clip header images - #{states}")
124
+ sleep(5)
125
+ end
126
+ end
127
+
128
+ def collect_app_clip_header_images(options)
129
+ app_clip_header_images = Loader.load_app_clip_header_images(options[:app_clip_header_images_path], options[:ignore_language_directory_validation])
130
+
131
+ # Apply default folder logic similar to metadata
132
+ assign_default_images(options, app_clip_header_images)
133
+
134
+ return app_clip_header_images
135
+ end
136
+
137
+ # If the user has a 'default' language folder, assign those images to languages that don't have images
138
+ def assign_default_images(options, app_clip_header_images)
139
+ # Build a complete list of the required languages
140
+ enabled_languages = detect_languages(options, app_clip_header_images)
141
+
142
+ # Check if there's a default image (from 'default' folder)
143
+ default_image = app_clip_header_images.find do |img|
144
+ folder_name = File.basename(File.dirname(img.path))
145
+ folder_name.casecmp?("default")
146
+ end
147
+ return unless default_image
148
+
149
+ # For each enabled language, if there's no image, use the default
150
+ enabled_languages.each do |language|
151
+ next if language&.casecmp?("default")
152
+
153
+ existing_image = app_clip_header_images.find { |img| img.language.eql?(language) }
154
+ unless existing_image
155
+ UI.message("Using default folder image for language '#{language}'")
156
+ app_clip_header_images << Deliver::AppClipHeaderImage.new(default_image.path, language)
157
+ end
158
+ end
159
+
160
+ # Remove the default image from the list (language is nil for default folder)
161
+ app_clip_header_images.reject! { |img| img.language.nil? }
162
+ end
163
+
164
+ def detect_languages(options, app_clip_header_images)
165
+ # Start with languages from the common detection method
166
+ enabled_languages = Languages.detect_languages(
167
+ options: options,
168
+ metadata_path: options[:app_clip_header_images_path],
169
+ ignore_validation: options[:ignore_language_directory_validation]
170
+ )
171
+
172
+ # Also add languages from existing header images
173
+ app_clip_header_images.each do |header_image|
174
+ language = header_image.language
175
+ next if language.nil? || language.empty?
176
+ enabled_languages << language unless enabled_languages.include?(language)
177
+ end
178
+
179
+ enabled_languages.uniq
180
+ end
181
+
182
+ # helper method to mock this step in tests
183
+ def self.calculate_checksum(path)
184
+ bytes = File.binread(path)
185
+ Digest::MD5.hexdigest(bytes)
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,229 @@
1
+ require 'fastlane_core'
2
+ require 'spaceship'
3
+
4
+ require_relative 'module'
5
+
6
+ module Deliver
7
+ # rubocop:disable Metrics/ClassLength
8
+ class UploadAppClipDefaultExperienceMetadata
9
+ LOCALISED_APP_CLIP_DEFAULT_EXPERIENCE_VALUES = {
10
+ app_clip_default_experience_subtitle: "app_clip_default_experience_subtitle"
11
+ }
12
+
13
+ NON_LOCALISED_APP_CLIP_DEFAULT_EXPERIENCE_VALUES = {
14
+ app_clip_default_experience_action: "app_clip_default_experience_action"
15
+ }
16
+
17
+ require_relative 'loader'
18
+
19
+ def upload_metadata(options)
20
+ # app clip default experience metadata is not editable in a live version
21
+ return if options[:edit_live] || options[:app_clip_default_experience_metadata_path].nil?
22
+
23
+ # load the metadata from the filesystem before uploading
24
+ load_from_filesystem(options)
25
+
26
+ # Assign default values to all languages
27
+ assign_defaults(options)
28
+
29
+ app = Deliver.cache[:app]
30
+ platform = Spaceship::ConnectAPI::Platform.map(options[:platform])
31
+ version = fetch_edit_app_store_version(app, platform)
32
+
33
+ UI.important("Will begin uploading app clip default experience metadata for '#{version.version_string}' on App Store Connect")
34
+
35
+ # Currently only one app clip target per app is supported
36
+ app_clips = app.get_app_clips
37
+ app_clip = app_clips.first
38
+
39
+ # Validate options
40
+ subtitle_localized = options[:app_clip_default_experience_subtitle]
41
+ action = options[:app_clip_default_experience_action]
42
+ has_options_specified = !subtitle_localized.nil? || !action.nil?
43
+
44
+ if app_clip.nil?
45
+ UI.user_error!("A build with an app clip must be uploaded to App Store Connect before uploading the default experience metadata") if has_options_specified
46
+ # Nothing to do if the app clip is nil and no app clip options specified
47
+ return
48
+ end
49
+
50
+ unless has_options_specified
51
+ # Handle the default case where there is an app clip, but no options specified.
52
+ return copy_live_version_app_clip_default_experience_metadata(app: app, platform: platform, edit_version: version, app_clip: app_clip)
53
+ end
54
+
55
+ UI.user_error!("You must provide at least the subtitle and action for an app clip default experience") if subtitle_localized.nil? || action.nil?
56
+
57
+ # see if there's an existing experience for this version
58
+ default_experience = version.app_clip_default_experience
59
+ if default_experience
60
+ # update the existing default experience
61
+ default_experience.update(attributes: { action: action })
62
+ UI.message("Updated app clip default experience")
63
+ else
64
+ # create a new default experience
65
+ default_experience = Spaceship::ConnectAPI::AppClipDefaultExperience.create(app_clip_id: app_clip.id, app_store_version_id: version.id, attributes: { action: action })
66
+ UI.important("Created default experience for version '#{version.version_string}'")
67
+ end
68
+
69
+ # update the subtitle localizations
70
+ upload_subtitle_localizations(app_clip_default_experience: default_experience, subtitle_localizations: subtitle_localized)
71
+ end
72
+
73
+ # Loads the app clip default experience metadata files and stores them into the options object
74
+ def load_from_filesystem(options)
75
+ metadata_path = options[:app_clip_default_experience_metadata_path]
76
+
77
+ # Load localised data
78
+ ignore_validation = options[:ignore_language_directory_validation]
79
+ Loader.language_folders(metadata_path, ignore_validation).each do |lang_folder|
80
+ LOCALISED_APP_CLIP_DEFAULT_EXPERIENCE_VALUES.keys.each do |key|
81
+ path = File.join(lang_folder.path, "#{key}.txt")
82
+ next unless File.exist?(path)
83
+
84
+ UI.message("Loading '#{path}'...")
85
+ options[key] ||= {}
86
+ options[key][lang_folder.basename] ||= File.read(path).strip
87
+ end
88
+ end
89
+
90
+ # Load non localised data
91
+ NON_LOCALISED_APP_CLIP_DEFAULT_EXPERIENCE_VALUES.keys.each do |key|
92
+ path = File.join(metadata_path, "#{key}.txt")
93
+ next unless File.exist?(path)
94
+
95
+ UI.message("Loading '#{path}'...")
96
+ options[key] ||= File.read(path).strip
97
+ end
98
+ end
99
+
100
+ # If the user is using the 'default' language, then assign values where they are needed
101
+ def assign_defaults(options)
102
+ # Build a complete list of the required languages
103
+ enabled_languages = detect_languages(options)
104
+
105
+ return unless enabled_languages.include?("default")
106
+ UI.message("Detected languages for app clip default experience: " + enabled_languages.to_s)
107
+
108
+ LOCALISED_APP_CLIP_DEFAULT_EXPERIENCE_VALUES.keys.each do |key|
109
+ current = options[key]
110
+ next unless current && current.kind_of?(Hash)
111
+
112
+ default = current["default"]
113
+ next if default.nil?
114
+
115
+ enabled_languages.each do |language|
116
+ value = current[language]
117
+ next unless value.nil?
118
+
119
+ current[language] = default
120
+ end
121
+ current.delete("default")
122
+ end
123
+ end
124
+
125
+ def detect_languages(options)
126
+ Languages.detect_languages(
127
+ options: options,
128
+ localized_values_keys: LOCALISED_APP_CLIP_DEFAULT_EXPERIENCE_VALUES.keys,
129
+ metadata_path: options[:app_clip_default_experience_metadata_path],
130
+ ignore_validation: options[:ignore_language_directory_validation]
131
+ )
132
+ end
133
+
134
+ # from upload_metadata.rb
135
+ def fetch_edit_app_store_version(app, platform, wait_time: 10)
136
+ retry_if_nil("Cannot find edit app store version", wait_time: wait_time) do
137
+ app.get_edit_app_store_version(platform: platform, includes: Spaceship::ConnectAPI::AppStoreVersion::ESSENTIAL_INCLUDES + ",appClipDefaultExperience")
138
+ end
139
+ end
140
+
141
+ def fetch_live_app_store_version(app, platform, wait_time: 10)
142
+ retry_if_nil("Cannot find live app store version", wait_time: wait_time) do
143
+ app.get_live_app_store_version(platform: platform, includes: Spaceship::ConnectAPI::AppStoreVersion::ESSENTIAL_INCLUDES + ",appClipDefaultExperience")
144
+ end
145
+ end
146
+
147
+ # from upload_metadata.rb
148
+ def retry_if_nil(message, tries: 5, wait_time: 10)
149
+ loop do
150
+ tries -= 1
151
+
152
+ value = yield
153
+ return value if value
154
+
155
+ UI.message("#{message}... Retrying after #{wait_time} seconds (remaining: #{tries})")
156
+ sleep(wait_time)
157
+
158
+ return nil if tries.zero?
159
+ end
160
+ end
161
+
162
+ def upload_subtitle_localizations(app_clip_default_experience:, subtitle_localizations:)
163
+ localized_subtitle_attributes_by_locale = {}
164
+ subtitle_localizations.keys.each do |key|
165
+ localized_subtitle_attributes_by_locale[key] = {}
166
+ localized_subtitle_attributes_by_locale[key][:create_attributes] = { subtitle: subtitle_localizations[key], locale: key }
167
+ localized_subtitle_attributes_by_locale[key][:update_attributes] = { subtitle: subtitle_localizations[key] }
168
+ end
169
+
170
+ # update the subtitle
171
+ existing_localizations = Spaceship::ConnectAPI::AppClipDefaultExperienceLocalization.find_all(app_clip_default_experience_id: app_clip_default_experience.id)
172
+
173
+ # from upload_metadata.rb
174
+ app_info_worker = FastlaneCore::QueueWorker.new do |locale|
175
+ UI.message("Uploading app clip default experience metadata to App Store Connect for localized subtitle '#{locale}'")
176
+
177
+ # find an existing localization
178
+ existing_localization = existing_localizations.find { |l| locale.to_s.eql?(l.locale) }
179
+
180
+ if existing_localization
181
+ # update existing
182
+ attributes = localized_subtitle_attributes_by_locale[locale][:update_attributes]
183
+ existing_localization.update(attributes: attributes)
184
+ UI.verbose("[#{locale}] Updated existing to #{attributes}")
185
+ else
186
+ # create new
187
+ attributes = localized_subtitle_attributes_by_locale[locale][:create_attributes]
188
+ Spaceship::ConnectAPI::AppClipDefaultExperienceLocalization.create(default_experience_id: app_clip_default_experience.id, attributes: attributes)
189
+ UI.verbose("[#{locale}] Created new with #{attributes}")
190
+ end
191
+ end
192
+ app_info_worker.batch_enqueue(localized_subtitle_attributes_by_locale.keys)
193
+ app_info_worker.start
194
+ end
195
+
196
+ # Handle the case where an app clip exists, but the user did not specify any app clip default
197
+ # experience metadata options. We check if the current editable version already has the app clip
198
+ # default experience metadata. If not, copy over the live version's app clip default
199
+ # experience metadata. This mimics the default behavior of creating an App Store version from
200
+ # the ASC UI.
201
+ #
202
+ # This function will also produce warnings, but not outright fail, when it finds missing app
203
+ # clip metadata.
204
+ #
205
+ # As of 2022-05-18, App Store versions created by the ASC API do not "carry over" the live
206
+ # version's app clip default experience metadata, so we handle this for the user by default.
207
+ #
208
+ def copy_live_version_app_clip_default_experience_metadata(app:, platform:, edit_version:, app_clip:)
209
+ edit_version_default_experience = edit_version.app_clip_default_experience
210
+ if !edit_version_default_experience
211
+ live_version = fetch_live_app_store_version(app, platform)
212
+ # no live version to carry over metadata from
213
+ return if live_version.nil?
214
+
215
+ live_version_default_experience = live_version.app_clip_default_experience
216
+ # no live version default experience to carry over metadata from
217
+ return if live_version_default_experience.nil?
218
+
219
+ # create a default experience and use the live version default experience as a "template"
220
+ Spaceship::ConnectAPI::AppClipDefaultExperience.create(app_clip_id: app_clip.id, app_store_version_id: edit_version.id, template_default_experience_id: live_version_default_experience.id)
221
+
222
+ UI.message("Created the app clip default experience using the live version's app clip metadata as a template.")
223
+ else
224
+ # if the edit version app clip default experience already exists, just check it's values
225
+ UI.important("ASC requires the app clip default experience to contain a valid `action` value. To specify this value in fastlane use deliver's `app_clip_default_experience_action` option.") if edit_version_default_experience.action.nil?
226
+ end
227
+ end
228
+ end
229
+ end
@@ -59,6 +59,9 @@ module Deliver
59
59
  demo_password: "demo_account_password",
60
60
  notes: "notes"
61
61
  }
62
+ APP_CLIP_REVIEW_INFORMATION_VALUES = {
63
+ invocation_urls: "invocation_urls"
64
+ }
62
65
 
63
66
  # Localized app details values, that are editable in live state
64
67
  LOCALISED_LIVE_VALUES = [:description, :release_notes, :support_url, :marketing_url, :promotional_text, :privacy_url]
@@ -72,7 +75,10 @@ module Deliver
72
75
  # Directory name it contains review information
73
76
  REVIEW_INFORMATION_DIR = "review_information"
74
77
 
75
- ALL_META_SUB_DIRS = [TRADE_REPRESENTATIVE_CONTACT_INFORMATION_DIR, REVIEW_INFORMATION_DIR]
78
+ # Directory name it contains app clip review information
79
+ APP_CLIP_REVIEW_INFORMATION_DIR = "app_clip_review_information"
80
+
81
+ ALL_META_SUB_DIRS = [TRADE_REPRESENTATIVE_CONTACT_INFORMATION_DIR, REVIEW_INFORMATION_DIR, APP_CLIP_REVIEW_INFORMATION_DIR]
76
82
 
77
83
  # rubocop:disable Metrics/PerceivedComplexity
78
84
 
@@ -100,7 +106,7 @@ module Deliver
100
106
 
101
107
  if options[:edit_live]
102
108
  # not all values are editable when using live_version
103
- version = app.get_live_app_store_version(platform: platform)
109
+ version = app.get_live_app_store_version(platform: platform, includes: Spaceship::ConnectAPI::AppStoreVersion::ESSENTIAL_INCLUDES + ",appClipDefaultExperience")
104
110
  localised_options = LOCALISED_LIVE_VALUES
105
111
  non_localised_options = NON_LOCALISED_LIVE_VALUES
106
112
 
@@ -349,6 +355,7 @@ module Deliver
349
355
  end
350
356
 
351
357
  review_information(version)
358
+ app_clip_review_information(version)
352
359
  review_attachment_file(version)
353
360
  app_rating(app_info)
354
361
  end
@@ -433,9 +440,9 @@ module Deliver
433
440
  .uniq
434
441
  end
435
442
 
436
- def fetch_edit_app_store_version(app, platform)
437
- retry_if_nil("Cannot find edit app store version") do
438
- app.get_edit_app_store_version(platform: platform)
443
+ def fetch_edit_app_store_version(app, platform, wait_time: 10)
444
+ retry_if_nil("Cannot find edit app store version", wait_time: wait_time) do
445
+ app.get_edit_app_store_version(platform: platform, includes: Spaceship::ConnectAPI::AppStoreVersion::ESSENTIAL_INCLUDES + ",appClipDefaultExperience")
439
446
  end
440
447
  end
441
448
 
@@ -452,9 +459,8 @@ module Deliver
452
459
  end
453
460
 
454
461
  # Retries a block of code if the return value is nil, with an exponential backoff.
455
- def retry_if_nil(message)
462
+ def retry_if_nil(message, wait_time: 10)
456
463
  tries = options[:version_check_wait_retry_limit]
457
- wait_time = 10
458
464
  loop do
459
465
  tries -= 1
460
466
 
@@ -633,6 +639,22 @@ module Deliver
633
639
  next if path.nil?
634
640
  options[:app_review_information][option_name] ||= File.read(path)
635
641
  end
642
+
643
+ # Load app clip review information
644
+ options[:app_clip_review_information] ||= {}
645
+ resolve_app_clip_review_info_path = lambda do |option_name|
646
+ path = File.join(options[:metadata_path], APP_CLIP_REVIEW_INFORMATION_DIR, "#{option_name}.txt")
647
+ return nil unless File.exist?(path)
648
+ return nil if options[:app_clip_review_information][option_name].to_s.length > 0
649
+ return path
650
+ end
651
+
652
+ # Then app clip load review information from new App Store Connect filenames
653
+ APP_CLIP_REVIEW_INFORMATION_VALUES.keys.each do |option_name|
654
+ path = resolve_app_clip_review_info_path.call(option_name)
655
+ next if path.nil?
656
+ options[:app_clip_review_information][option_name] ||= File.read(path)
657
+ end
636
658
  end
637
659
 
638
660
  private
@@ -684,6 +706,48 @@ module Deliver
684
706
  end
685
707
  end
686
708
 
709
+ def app_clip_review_information(version)
710
+ info = options[:app_clip_review_information]
711
+ return if info.nil? || info.empty?
712
+
713
+ UI.user_error!("`app_clip_review_information` must be a hash", show_github_issues: true) unless info.kind_of?(Hash)
714
+ info = info.transform_keys(&:to_sym)
715
+ attributes = {}
716
+ APP_CLIP_REVIEW_INFORMATION_VALUES.each do |key, attribute_name|
717
+ if info[key].kind_of?(Array)
718
+ attributes[attribute_name] = info[key].map { |value| value.to_s.strip } unless info[key].empty?
719
+ else
720
+ strip_value = info[key].to_s.strip
721
+ attributes[attribute_name] = strip_value unless strip_value.empty?
722
+ end
723
+ end
724
+
725
+ if attributes["invocation_urls"].kind_of?(String)
726
+ attributes["invocation_urls"] = attributes["invocation_urls"].split(", ")
727
+ end
728
+
729
+ UI.message("Uploading app clip review information to App Store Connect")
730
+ default_experience = version.app_clip_default_experience
731
+ if default_experience.nil?
732
+ # By this point the upload app clip default experience metadata step should have run and
733
+ # created a default experience, if not, we shouldn't create the default experience here.
734
+ UI.important("Could not upload app clip review information due to the app clip default experience missing.")
735
+ return
736
+ end
737
+
738
+ app_clip_app_store_review_detail = begin
739
+ Spaceship::ConnectAPI::AppClipDefaultExperience.get(app_clip_default_experience_id: default_experience.id, includes: "appClipAppStoreReviewDetail").app_clip_app_store_review_detail
740
+ rescue => error
741
+ UI.error("Error fetching app clip app store review detail - #{error.message}")
742
+ nil
743
+ end # errors if doesn't exist
744
+ if app_clip_app_store_review_detail
745
+ app_clip_app_store_review_detail.update(attributes: attributes)
746
+ else
747
+ Spaceship::ConnectAPI::AppClipAppStoreReviewDetail.create(app_clip_default_experience_id: default_experience.id, attributes: attributes)
748
+ end
749
+ end
750
+
687
751
  def review_attachment_file(version)
688
752
  app_store_review_detail = version.fetch_app_store_review_detail
689
753
  app_store_review_attachments = app_store_review_detail.app_store_review_attachments || []
@@ -13,8 +13,10 @@ module Deliver
13
13
 
14
14
  attributes = {}
15
15
 
16
- # Check App update method to understand how to use territory_ids.
17
- territory_ids = nil # nil won't update app's territory_ids, empty array would remove app from sale.
16
+ # nil leaves existing territory availability unchanged.
17
+ # [] intentionally removes the app from sale in all territories.
18
+ # Pass explicit territory IDs to make the app available only in those territories.
19
+ territory_ids = nil
18
20
 
19
21
  # As of 2020-09-14:
20
22
  # Official App Store Connect does not have an endpoint to get app prices for an app
@@ -201,7 +201,7 @@ module Deliver
201
201
  iterator.each_app_screenshot.select { |_, _, app_screenshot| app_screenshot.error? }.each do |localization, _, app_screenshot|
202
202
  UI.error("#{app_screenshot.file_name} for #{localization.locale} has error(s) - #{app_screenshot.error_messages.join(', ')}")
203
203
  end
204
- incomplete_screenshot_count = states.reject { |k, v| k == 'COMPLETE' }.reduce(0) { |sum, (k, v)| sum + v }
204
+ incomplete_screenshot_count = states.except('COMPLETE').reduce(0) { |sum, (k, v)| sum + v }
205
205
  UI.user_error!("Failed verification of all screenshots uploaded... #{incomplete_screenshot_count} incomplete screenshot(s) still exist")
206
206
  else
207
207
  UI.error("Failed to upload all screenshots... Tries remaining: #{tries}")
@@ -9,8 +9,8 @@ module Fastlane
9
9
 
10
10
  class AppStoreConnectApiKeyAction < Action
11
11
  def self.run(options)
12
- key_id = options[:key_id]
13
- issuer_id = options[:issuer_id]
12
+ key_id = options[:key_id]&.strip
13
+ issuer_id = options[:issuer_id]&.strip
14
14
  key_content = options[:key_content]
15
15
  is_key_content_base64 = options[:is_key_content_base64]
16
16
  key_filepath = options[:key_filepath]
@@ -41,7 +41,7 @@ module Fastlane
41
41
  end
42
42
 
43
43
  def self.detect_appium(params)
44
- appium_path = params[:appium_path] || `which appium`.to_s.strip
44
+ appium_path = params[:appium_path] || Helper.which('appium').to_s
45
45
 
46
46
  if appium_path.empty?
47
47
  if File.exist?(APPIUM_PATH_HOMEBREW)
@@ -56,7 +56,7 @@ module Fastlane
56
56
  def self.run(params)
57
57
  unless Helper.test?
58
58
  UI.message("Install using `brew install appledoc`")
59
- UI.user_error!("appledoc not installed") if `which appledoc`.length == 0
59
+ UI.user_error!("appledoc not installed") unless Helper.which('appledoc')
60
60
  end
61
61
 
62
62
  params_hash = params.values
@@ -57,7 +57,7 @@ module Fastlane
57
57
  short_option: "-j",
58
58
  conflicting_options: [:json_key_data],
59
59
  optional: true, # optional until it is possible specify either json_key OR json_key_data are required
60
- description: "The path to a file containing service account JSON, used to authenticate with Google",
60
+ description: "The path to a Google credentials JSON file (Application Default, Workload Identity, or Service Account), used to authenticate with Google",
61
61
  code_gen_sensitive: true,
62
62
  default_value: CredentialsManager::AppfileConfig.try_fetch_value(:json_key_file),
63
63
  default_value_dynamic: true,
@@ -70,7 +70,7 @@ module Fastlane
70
70
  short_option: "-c",
71
71
  conflicting_options: [:json_key],
72
72
  optional: true,
73
- description: "The raw service account JSON data used to authenticate with Google",
73
+ description: "The raw content of a Google credentials JSON file (Application Default, Workload Identity, or Service Account) used to authenticate with Google",
74
74
  code_gen_sensitive: true,
75
75
  default_value: CredentialsManager::AppfileConfig.try_fetch_value(:json_key_data_raw),
76
76
  default_value_dynamic: true,