Class: Deliver::UploadMetadata

Inherits:
Object
  • Object
show all
Defined in:
deliver/lib/deliver/upload_metadata.rb

Overview

upload description, rating, etc. rubocop:disable Metrics/ClassLength

Constant Summary collapse

LOCALISED_VERSION_VALUES =

All the localised values attached to the version

{
  description: "description",
  keywords: "keywords",
  release_notes: "whats_new",
  support_url: "support_url",
  marketing_url: "marketing_url",
  promotional_text: "promotional_text"
}
NON_LOCALISED_VERSION_VALUES =

Everything attached to the version but not being localised

{
  copyright: "copyright"
}
LOCALISED_APP_VALUES =

Localised app details values

{
  name: "name",
  subtitle: "subtitle",
  privacy_url: "privacy_policy_url",
  apple_tv_privacy_policy: "privacy_policy_text"
}
NON_LOCALISED_APP_VALUES =

Non localized app details values

{
  primary_category: :primary_category,
  secondary_category: :secondary_category,
  primary_first_sub_category: :primary_subcategory_one,
  primary_second_sub_category: :primary_subcategory_two,
  secondary_first_sub_category: :secondary_subcategory_one,
  secondary_second_sub_category: :secondary_subcategory_two
}
REVIEW_INFORMATION_VALUES_LEGACY =

Review information values

{
  review_first_name: :first_name,
  review_last_name: :last_name,
  review_phone_number: :phone_number,
  review_email: :email_address,
  review_demo_user: :demo_user,
  review_demo_password: :demo_password,
  review_notes: :notes
}
REVIEW_INFORMATION_VALUES =
{
  first_name: "contact_first_name",
  last_name: "contact_last_name",
  phone_number: "contact_phone",
  email_address: "contact_email",
  demo_user: "demo_account_name",
  demo_password: "demo_account_password",
  notes: "notes"
}
LOCALISED_LIVE_VALUES =

Localized app details values, that are editable in live state

[:description, :release_notes, :support_url, :marketing_url, :promotional_text, :privacy_url]
NON_LOCALISED_LIVE_VALUES =

Non localized app details values, that are editable in live state

[:copyright]
TRADE_REPRESENTATIVE_CONTACT_INFORMATION_DIR =

Directory name it contains trade representative contact information

"trade_representative_contact_information"
REVIEW_INFORMATION_DIR =

Directory name it contains review information

"review_information"
ALL_META_SUB_DIRS =
[TRADE_REPRESENTATIVE_CONTACT_INFORMATION_DIR, REVIEW_INFORMATION_DIR]

Instance Method Summary collapse

Instance Method Details

#assign_defaults(options) ⇒ Object

If the user is using the ‘default’ language, then assign values where they are needed



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'deliver/lib/deliver/upload_metadata.rb', line 341

def assign_defaults(options)
  # Normalizes languages keys from symbols to strings
  normalize_language_keys(options)

  # Build a complete list of the required languages
  enabled_languages = detect_languages(options)

  # Get all languages used in existing settings
  (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
    current = options[key]
    next unless current && current.kind_of?(Hash)
    current.each do |language, value|
      enabled_languages << language unless enabled_languages.include?(language)
    end
  end

  # Check folder list (an empty folder signifies a language is required)
  ignore_validation = options[:ignore_language_directory_validation]
  Loader.language_folders(options[:metadata_path], ignore_validation).each do |lang_folder|
    next unless File.directory?(lang_folder) # We don't want to read txt as they are non localised
    language = File.basename(lang_folder)
    enabled_languages << language unless enabled_languages.include?(language)
  end

  return unless enabled_languages.include?("default")
  UI.message("Detected languages: " + enabled_languages.to_s)

  (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
    current = options[key]
    next unless current && current.kind_of?(Hash)

    default = current["default"]
    next if default.nil?

    enabled_languages.each do |language|
      value = current[language]
      next unless value.nil?

      current[language] = default
    end
    current.delete("default")
  end
end

#convert_ms_to_iso8601(time_in_ms) ⇒ Object

rubocop:enable Metrics/PerceivedComplexity



330
331
332
333
334
335
336
337
338
# File 'deliver/lib/deliver/upload_metadata.rb', line 330

def convert_ms_to_iso8601(time_in_ms)
  time_in_s = time_in_ms / 1000

  # Remove minutes and seconds (whole hour)
  seconds_in_hour = 60 * 60
  time_in_s_to_hour = (time_in_s / seconds_in_hour).to_i * seconds_in_hour

  return Time.at(time_in_s_to_hour).utc.strftime("%Y-%m-%dT%H:%M:%S%:z")
end

#detect_languages(options) ⇒ Object



385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'deliver/lib/deliver/upload_metadata.rb', line 385

def detect_languages(options)
  # Build a complete list of the required languages
  enabled_languages = options[:languages] || []

  # Get all languages used in existing settings
  (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
    current = options[key]
    next unless current && current.kind_of?(Hash)
    current.each do |language, value|
      enabled_languages << language unless enabled_languages.include?(language)
    end
  end

  # Check folder list (an empty folder signifies a language is required)
  ignore_validation = options[:ignore_language_directory_validation]
  Loader.language_folders(options[:metadata_path], ignore_validation).each do |lang_folder|
    next unless File.directory?(lang_folder) # We don't want to read txt as they are non localised

    language = File.basename(lang_folder)
    enabled_languages << language unless enabled_languages.include?(language)
  end

  # Mapping to strings because :default symbol can be passed in
  enabled_languages
    .map(&:to_s)
    .uniq
end

#load_from_filesystem(options) ⇒ Object

Loads the metadata files and stores them into the options object



483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
# File 'deliver/lib/deliver/upload_metadata.rb', line 483

def load_from_filesystem(options)
  return if options[:skip_metadata]

  # Load localised data
  ignore_validation = options[:ignore_language_directory_validation]
  Loader.language_folders(options[:metadata_path], ignore_validation).each do |lang_folder|
    language = File.basename(lang_folder)
    (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
      path = File.join(lang_folder, "#{key}.txt")
      next unless File.exist?(path)

      UI.message("Loading '#{path}'...")
      options[key] ||= {}
      options[key][language] ||= File.read(path)
    end
  end

  # Load non localised data
  (NON_LOCALISED_VERSION_VALUES.keys + NON_LOCALISED_APP_VALUES.keys).each do |key|
    path = File.join(options[:metadata_path], "#{key}.txt")
    next unless File.exist?(path)

    UI.message("Loading '#{path}'...")
    options[key] ||= File.read(path)
  end

  # Load review information
  # This is used to find the file path for both new and legacy review information filenames
  resolve_review_info_path = lambda do |option_name|
    path = File.join(options[:metadata_path], REVIEW_INFORMATION_DIR, "#{option_name}.txt")
    return nil unless File.exist?(path)
    return nil if options[:app_review_information][option_name].to_s.length > 0

    UI.message("Loading '#{path}'...")
    return path
  end

  # First try and load review information from legacy filenames
  options[:app_review_information] ||= {}
  REVIEW_INFORMATION_VALUES_LEGACY.each do |legacy_option_name, option_name|
    path = resolve_review_info_path.call(legacy_option_name)
    next if path.nil?
    options[:app_review_information][option_name] ||= File.read(path)

    UI.deprecated("Review rating option '#{legacy_option_name}' from iTunesConnect has been deprecated. Please replace with '#{option_name}'")
  end

  # Then load review information from new App Store Connect filenames
  REVIEW_INFORMATION_VALUES.keys.each do |option_name|
    path = resolve_review_info_path.call(option_name)
    next if path.nil?
    options[:app_review_information][option_name] ||= File.read(path)
  end
end

#upload(options) ⇒ Object

Make sure to call ‘load_from_filesystem` before calling upload



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'deliver/lib/deliver/upload_metadata.rb', line 79

def upload(options)
  return if options[:skip_metadata]

  app = options[:app]

  platform = Spaceship::ConnectAPI::Platform.map(options[:platform])

  enabled_languages = detect_languages(options)

  app_store_version_localizations = verify_available_version_languages!(options, app, enabled_languages) unless options[:edit_live]
  app_info_localizations = verify_available_info_languages!(options, app, enabled_languages) unless options[:edit_live]

  if options[:edit_live]
    # not all values are editable when using live_version
    version = app.get_live_app_store_version(platform: platform)
    localised_options = LOCALISED_LIVE_VALUES
    non_localised_options = NON_LOCALISED_LIVE_VALUES

    if v.nil?
      UI.message("Couldn't find live version, editing the current version on App Store Connect instead")
      version = app.get_edit_app_store_version(platform: platform)
      # we don't want to update the localised_options and non_localised_options
      # as we also check for `options[:edit_live]` at other areas in the code
      # by not touching those 2 variables, deliver is more consistent with what the option says
      # in the documentation
    else
      UI.message("Found live version")
    end
  else
    version = app.get_edit_app_store_version(platform: platform)
    localised_options = (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys)
    non_localised_options = NON_LOCALISED_VERSION_VALUES.keys
  end

  # Needed for to filter out release notes from being sent up
  number_of_versions = app.get_app_store_versions(filter: { platform: platform }, limit: 2).size
  is_first_version = number_of_versions == 1
  UI.verbose("Version '#{version.version_string}' is the first version on App Store Connect") if is_first_version

  UI.important("Will begin uploading metadata for '#{version.version_string}' on App Store Connect")

  localized_version_attributes_by_locale = {}
  localized_info_attributes_by_locale = {}

  localised_options.each do |key|
    current = options[key]
    next unless current

    unless current.kind_of?(Hash)
      UI.error("Error with provided '#{key}'. Must be a hash, the key being the language.")
      next
    end

    if key == :release_notes && is_first_version
      UI.error("Skipping 'release_notes'... this is the first version of the app")
      next
    end

    current.each do |language, value|
      next unless value.to_s.length > 0
      strip_value = value.to_s.strip

      if LOCALISED_VERSION_VALUES.include?(key) && !strip_value.empty?
        attribute_name = LOCALISED_VERSION_VALUES[key]

        localized_version_attributes_by_locale[language] ||= {}
        localized_version_attributes_by_locale[language][attribute_name] = strip_value
      end

      next unless LOCALISED_APP_VALUES.include?(key) && !strip_value.empty?
      attribute_name = LOCALISED_APP_VALUES[key]

      localized_info_attributes_by_locale[language] ||= {}
      localized_info_attributes_by_locale[language][attribute_name] = strip_value
    end
  end

  non_localized_version_attributes = {}
  non_localised_options.each do |key|
    strip_value = options[key].to_s.strip
    next unless strip_value.to_s.length > 0

    if NON_LOCALISED_VERSION_VALUES.include?(key) && !strip_value.empty?
      attribute_name = NON_LOCALISED_VERSION_VALUES[key]
      non_localized_version_attributes[attribute_name] = strip_value
    end
  end

  release_type = if options[:auto_release_date]
                   # Convert time format to 2020-06-17T12:00:00-07:00
                   time_in_ms = options[:auto_release_date]
                   date = convert_ms_to_iso8601(time_in_ms)

                   non_localized_version_attributes['earliestReleaseDate'] = date
                   Spaceship::ConnectAPI::AppStoreVersion::ReleaseType::SCHEDULED
                 elsif options[:automatic_release]
                   Spaceship::ConnectAPI::AppStoreVersion::ReleaseType::AFTER_APPROVAL
                 else
                   Spaceship::ConnectAPI::AppStoreVersion::ReleaseType::MANUAL
                 end
  non_localized_version_attributes['releaseType'] = release_type

  # Update app store version
  # This needs to happen before updating localizations (https://openradar.appspot.com/radar?id=4925914991296512)
  UI.message("Uploading metadata to App Store Connect for version")
  version.update(attributes: non_localized_version_attributes)

  # Update app store version localizations
  app_store_version_localizations.each do |app_store_version_localization|
    attributes = localized_version_attributes_by_locale[app_store_version_localization.locale]
    if attributes
      UI.message("Uploading metadata to App Store Connect for localized version '#{app_store_version_localization.locale}'")
      app_store_version_localization.update(attributes: attributes)
    end
  end

  # Update app info localizations
  app_info_localizations.each do |app_info_localization|
    attributes = localized_info_attributes_by_locale[app_info_localization.locale]
    if attributes
      UI.message("Uploading metadata to App Store Connect for localized info '#{app_info_localization.locale}'")
      app_info_localization.update(attributes: attributes)
    end
  end

  # Update categories
  app_info = app.fetch_edit_app_info
  if app_info
    category_id_map = {}

    primary_category = options[:primary_category].to_s.strip
    secondary_category = options[:secondary_category].to_s.strip
    primary_first_sub_category = options[:primary_first_sub_category].to_s.strip
    primary_second_sub_category = options[:primary_second_sub_category].to_s.strip
    secondary_first_sub_category = options[:secondary_first_sub_category].to_s.strip
    secondary_second_sub_category = options[:secondary_second_sub_category].to_s.strip

    mapped_values = {}

    # Only update primary and secondar category if explicitly set
    unless primary_category.empty?
      mapped = Spaceship::ConnectAPI::AppCategory.map_category_from_itc(
        primary_category
      )

      mapped_values[primary_category] = mapped
      category_id_map[:primary_category_id] = mapped
    end
    unless secondary_category.empty?
      mapped = Spaceship::ConnectAPI::AppCategory.map_category_from_itc(
        secondary_category
      )

      mapped_values[secondary_category] = mapped
      category_id_map[:secondary_category_id] = mapped
    end

    # Only set if primary category is going to be set
    unless primary_category.empty?
      mapped = Spaceship::ConnectAPI::AppCategory.map_subcategory_from_itc(
        primary_first_sub_category
      )

      mapped_values[primary_first_sub_category] = mapped
      category_id_map[:primary_subcategory_one_id] = mapped
    end
    unless primary_category.empty?
      mapped = Spaceship::ConnectAPI::AppCategory.map_subcategory_from_itc(
        primary_second_sub_category
      )

      mapped_values[primary_second_sub_category] = mapped
      category_id_map[:primary_subcategory_two_id] = mapped
    end

    # Only set if secondary category is going to be set
    unless secondary_category.empty?
      mapped = Spaceship::ConnectAPI::AppCategory.map_subcategory_from_itc(
        secondary_first_sub_category
      )

      mapped_values[secondary_first_sub_category] = mapped
      category_id_map[:secondary_subcategory_one_id] = mapped
    end
    unless secondary_category.empty?
      mapped = Spaceship::ConnectAPI::AppCategory.map_subcategory_from_itc(
        secondary_second_sub_category
      )

      mapped_values[secondary_second_sub_category] = mapped
      category_id_map[:secondary_subcategory_two_id] = mapped
    end

    # Print deprecation warnings if category was mapped
    has_mapped_values = false
    mapped_values.each do |k, v|
      next if k.nil? || v.nil?
      next if k == v
      has_mapped_values = true
      UI.deprecated("Category '#{k}' from iTunesConnect has been deprecated. Please replace with '#{v}'")
    end
    UI.deprecated("You can find more info at https://docs.fastlane.tools/actions/deliver/#reference") if has_mapped_values

    app_info.update_categories(category_id_map: category_id_map)
  end

  # Update phased release
  unless options[:phased_release].nil?
    phased_release = begin
                       version.fetch_app_store_version_phased_release
                     rescue
                       nil
                     end # returns no data error so need to rescue
    if !!options[:phased_release]
      unless phased_release
        UI.message("Creating phased release on App Store Connect")
        version.create_app_store_version_phased_release(attributes: {
          phasedReleaseState: Spaceship::ConnectAPI::AppStoreVersionPhasedRelease::PhasedReleaseState::INACTIVE
        })
      end
    elsif phased_release
      UI.message("Removing phased release on App Store Connect")
      phased_release.delete!
    end
  end

  # Update rating reset
  unless options[:reset_ratings].nil?
    reset_rating_request = begin
                             version.fetch_reset_ratings_request
                           rescue
                             nil
                           end # returns no data error so need to rescue
    if !!options[:reset_ratings]
      unless reset_rating_request
        UI.message("Creating reset ratings request on App Store Connect")
        version.create_reset_ratings_request
      end
    elsif reset_rating_request
      UI.message("Removing reset ratings request on App Store Connect")
      reset_rating_request.delete!
    end
  end

  set_review_information(version, options)
  set_review_attachment_file(version, options)
  set_app_rating(version, options)
end

#verify_available_info_languages!(options, app, languages) ⇒ Object

Finding languages to enable



414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'deliver/lib/deliver/upload_metadata.rb', line 414

def verify_available_info_languages!(options, app, languages)
  app_info = app.fetch_edit_app_info

  unless app_info
    UI.user_error!("Cannot update languages - could not find an editable info")
    return
  end

  localizations = app_info.get_app_info_localizations

  languages = (languages || []).reject { |lang| lang == "default" }
  locales_to_enable = languages - localizations.map(&:locale)

  if locales_to_enable.count > 0
    lng_text = "language"
    lng_text += "s" if locales_to_enable.count != 1
    Helper.show_loading_indicator("Activating info #{lng_text} #{locales_to_enable.join(', ')}...")

    locales_to_enable.each do |locale|
      app_info.create_app_info_localization(attributes: {
        locale: locale
      })
    end

    Helper.hide_loading_indicator

    # Refresh version localizations
    localizations = app_info.get_app_info_localizations
  end

  return localizations
end

#verify_available_version_languages!(options, app, languages) ⇒ Object

Finding languages to enable



448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# File 'deliver/lib/deliver/upload_metadata.rb', line 448

def verify_available_version_languages!(options, app, languages)
  platform = Spaceship::ConnectAPI::Platform.map(options[:platform])
  version = app.get_edit_app_store_version(platform: platform)

  unless version
    UI.user_error!("Cannot update languages - could not find an editable version for '#{platform}'")
    return
  end

  localizations = version.get_app_store_version_localizations

  languages = (languages || []).reject { |lang| lang == "default" }
  locales_to_enable = languages - localizations.map(&:locale)

  if locales_to_enable.count > 0
    lng_text = "language"
    lng_text += "s" if locales_to_enable.count != 1
    Helper.show_loading_indicator("Activating version #{lng_text} #{locales_to_enable.join(', ')}...")

    locales_to_enable.each do |locale|
      version.create_app_store_version_localization(attributes: {
        locale: locale
      })
    end

    Helper.hide_loading_indicator

    # Refresh version localizations
    localizations = version.get_app_store_version_localizations
  end

  return localizations
end