deliver 0.5.0 → 0.6.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.
@@ -42,6 +42,8 @@ module Deliver
42
42
 
43
43
  verify_pdf_file
44
44
 
45
+ additional_itc_information # e.g. copyright, age rating
46
+
45
47
  trigger_metadata_upload
46
48
  trigger_ipa_upload
47
49
 
@@ -173,8 +175,11 @@ module Deliver
173
175
  @app.metadata.update_privacy_url(@deploy_information[Deliverer::ValKey::PRIVACY_URL]) if @deploy_information[Deliverer::ValKey::PRIVACY_URL]
174
176
 
175
177
  @app.metadata.update_keywords(@deploy_information[Deliverer::ValKey::KEYWORDS]) if @deploy_information[Deliverer::ValKey::KEYWORDS]
178
+
179
+ @app.metadata.update_price_tier(@deploy_information[Deliverer::ValKey::PRICE_TIER]) if @deploy_information[Deliverer::ValKey::PRICE_TIER]
176
180
  end
177
181
 
182
+
178
183
  def set_screenshots
179
184
  screens_path = @deploy_information[Deliverer::ValKey::SCREENSHOTS_PATH]
180
185
 
@@ -226,6 +231,25 @@ module Deliver
226
231
  raise "Error uploading app metadata".red unless result == true
227
232
  end
228
233
 
234
+ def additional_itc_information
235
+ # e.g. rating or copyright
236
+ itc = ItunesConnect.new
237
+ itc.set_copyright!(@app, @deploy_information[Deliverer::ValKey::COPYRIGHT]) if @deploy_information[Deliverer::ValKey::COPYRIGHT]
238
+ itc.set_app_review_information!(@app, @deploy_information[Deliverer::ValKey::APP_REVIEW_INFORMATION]) if @deploy_information[Deliverer::ValKey::APP_REVIEW_INFORMATION]
239
+ itc.set_release_after_approval!(@app, @deploy_information[Deliverer::ValKey::AUTOMATIC_RELEASE]) if @deploy_information[Deliverer::ValKey::AUTOMATIC_RELEASE]
240
+
241
+ # Categories
242
+ primary = @deploy_information[Deliverer::ValKey::PRIMARY_CATEGORY]
243
+ secondary = @deploy_information[Deliverer::ValKey::SECONDARY_CATEGORY]
244
+ itc.set_categories!(@app, primary, secondary) if (primary or secondary)
245
+
246
+ # App Rating
247
+ itc.set_app_rating!(@app, @deploy_information[Deliverer::ValKey::RATINGS_CONFIG_PATH]) if @deploy_information[Deliverer::ValKey::RATINGS_CONFIG_PATH]
248
+
249
+ # App Icon
250
+ itc.upload_app_icon!(@app, @deploy_information[Deliverer::ValKey::APP_ICON]) if @deploy_information[Deliverer::ValKey::APP_ICON]
251
+ end
252
+
229
253
  def trigger_ipa_upload
230
254
  if @ipa
231
255
  @ipa.app = @app # we now have the resulting app
@@ -33,6 +33,25 @@ module Deliver
33
33
  CONFIG_JSON_FOLDER = :config_json_folder # Path to a folder containing a configuration file and including screenshots
34
34
  SKIP_PDF = :skip_pdf
35
35
  SUBMIT_FURTHER_INFORMATION = :submit_further_information # export compliance, content rights and advertising identifier
36
+ PRICE_TIER = :price_tier
37
+ APP_ICON = :app_icon
38
+
39
+ COPYRIGHT = :copyright
40
+ PRIMARY_CATEGORY = :primary_category
41
+ SECONDARY_CATEGORY = :secondary_category
42
+
43
+ AUTOMATIC_RELEASE = :automatic_release # should the update go live after approval
44
+ RATINGS_CONFIG_PATH = :ratings_config_path # a path to the configuration for the app's ratings
45
+
46
+ APP_REVIEW_INFORMATION = :app_review_information
47
+ # Supported
48
+ # first_name
49
+ # last_name
50
+ # phone_number
51
+ # email_address
52
+ # demo_user
53
+ # demo_password
54
+ # notes
36
55
  end
37
56
 
38
57
  module AllBlocks
@@ -9,7 +9,7 @@ module Deliver
9
9
  # @param deliver_path (String) The path in which the Deliverfile should be created
10
10
  # @param project_name (String) The default name of the project, which is used in the generated Deliverfile
11
11
  def self.create(deliver_path, project_name = nil)
12
- deliver_file_path = [deliver_path, Deliver::Deliverfile::Deliverfile::FILE_NAME].join("/")
12
+ deliver_file_path = File.join(deliver_path, Deliver::Deliverfile::Deliverfile::FILE_NAME)
13
13
  raise "Deliverfile already exists at path '#{deliver_file_path}'. Run 'deliver' to use Deliver.".red if File.exists?(deliver_file_path)
14
14
 
15
15
  project_name ||= Dir.pwd.split("/").last
@@ -18,12 +18,16 @@ module Deliver
18
18
  # Setting all the metadata
19
19
  def method_missing(method_sym, *arguments, &block)
20
20
  allowed = Deliver::Deliverer.all_available_keys_to_set
21
- not_translated = [:ipa, :app_identifier, :apple_id, :screenshots_path, :config_json_folder, :submit_further_information]
21
+ not_translated = [:ipa, :app_identifier, :apple_id, :screenshots_path, :config_json_folder,
22
+ :submit_further_information, :copyright, :primary_category, :secondary_category,
23
+ :automatic_release, :app_review_information, :ratings_config_path, :price_tier,
24
+ :app_icon]
22
25
 
23
26
  if allowed.include?(method_sym)
24
- value = arguments.first || block.call
27
+ value = arguments.first
28
+ value = block.call if (value == nil and block != nil)
25
29
 
26
- unless value
30
+ if value == nil
27
31
  Helper.log.error(caller)
28
32
  Helper.log.fatal("No value or block passed to method '#{method_sym}'")
29
33
  raise DeliverfileDSLError.new(MISSING_VALUE_ERROR_MESSAGE.red)
@@ -0,0 +1,72 @@
1
+ require 'capybara'
2
+ require 'capybara/poltergeist'
3
+ require 'fastimage'
4
+ require 'credentials_manager/password_manager'
5
+
6
+ # Import all the actions
7
+ require 'deliver/itunes_connect/itunes_connect_submission'
8
+ require 'deliver/itunes_connect/itunes_connect_reader'
9
+ require 'deliver/itunes_connect/itunes_connect_helper'
10
+ require 'deliver/itunes_connect/itunes_connect_new_version'
11
+ require 'deliver/itunes_connect/itunes_connect_login'
12
+ require 'deliver/itunes_connect/itunes_connect_app_icon'
13
+ require 'deliver/itunes_connect/itunes_connect_app_rating'
14
+ require 'deliver/itunes_connect/itunes_connect_additional'
15
+
16
+ module Deliver
17
+ # Everything that can't be achived using the {Deliver::ItunesTransporter}
18
+ # will be scripted using the iTunesConnect frontend.
19
+ #
20
+ # Every method you call here, might take a time
21
+ class ItunesConnect
22
+ # This error occurs only if there is something wrong with the given login data
23
+ class ItunesConnectLoginError < StandardError
24
+ end
25
+
26
+ # This error can occur for many reaons. It is
27
+ # usually raised when a UI element could not be found
28
+ class ItunesConnectGeneralError < StandardError
29
+ end
30
+
31
+ include Capybara::DSL
32
+
33
+ ITUNESCONNECT_URL = "https://itunesconnect.apple.com/"
34
+ APP_DETAILS_URL = "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/ra/ng/app/[[app_id]]"
35
+
36
+ BUTTON_STRING_NEW_VERSION = "New Version"
37
+ BUTTON_STRING_SUBMIT_FOR_REVIEW = "Submit for Review"
38
+ BUTTON_ADD_NEW_BUILD = 'Click + to add a build before you submit your app.'
39
+
40
+ WAITING_FOR_REVIEW = "Waiting For Review"
41
+ PROCESSING_TEXT = "Processing"
42
+
43
+ def initialize
44
+ super
45
+
46
+ return if Helper.is_test?
47
+
48
+ DependencyChecker.check_dependencies
49
+
50
+ Capybara.run_server = false
51
+ Capybara.default_driver = :poltergeist
52
+ Capybara.javascript_driver = :poltergeist
53
+ Capybara.current_driver = :poltergeist
54
+ Capybara.app_host = ITUNESCONNECT_URL
55
+
56
+ # Since Apple has some SSL errors, we have to configure the client properly:
57
+ # https://github.com/ariya/phantomjs/issues/11239
58
+ Capybara.register_driver :poltergeist do |a|
59
+ conf = ['--debug=no', '--ignore-ssl-errors=yes', '--ssl-protocol=TLSv1']
60
+ Capybara::Poltergeist::Driver.new(a, {
61
+ phantomjs_options: conf,
62
+ phantomjs_logger: File.open("/tmp/poltergeist_log.txt", "a"),
63
+ js_errors: false
64
+ })
65
+ end
66
+
67
+ page.driver.headers = { "Accept-Language" => "en" }
68
+
69
+ login
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,90 @@
1
+ module Deliver
2
+ class ItunesConnect
3
+ # This file sets additional information like copyright and age rating
4
+
5
+ def set_copyright!(app, text)
6
+ verify_app(app)
7
+ open_app_page(app)
8
+
9
+ Helper.log.info "Setting copyright to '#{text}'".green
10
+
11
+ first("input[ng-model='versionInfo.copyright.value']").set text
12
+
13
+ (click_on "Save" rescue nil) # if nothing has changed, there is no back button and we don't care
14
+ rescue => ex
15
+ error_occured(ex)
16
+ end
17
+
18
+ def set_app_review_information!(app, hash)
19
+ verify_app(app)
20
+ open_app_page(app)
21
+
22
+ Helper.log.info "Setting review information: #{hash}"
23
+
24
+ first("input[ng-model='versionInfo.appReviewInfo.firstName.value']").set hash[:first_name]
25
+ first("input[ng-model='versionInfo.appReviewInfo.lastName.value']").set hash[:last_name]
26
+ first("input[ng-model='versionInfo.appReviewInfo.phoneNumber.value']").set hash[:phone_number]
27
+ first("input[ng-model='versionInfo.appReviewInfo.emailAddress.value']").set hash[:email_address]
28
+ first("input[ng-model='versionInfo.appReviewInfo.userName.value']").set hash[:demo_user]
29
+ first("input[ng-model='versionInfo.appReviewInfo.password.value']").set hash[:demo_password]
30
+ first("span[ng-show='versionInfo.appReviewInfo.reviewNotes.isEditable'] > * > textarea").set hash[:notes]
31
+
32
+ (click_on "Save" rescue nil) # if nothing has changed, there is no back button and we don't care
33
+
34
+ Helper.log.info "Successfully saved review information".green
35
+ rescue => ex
36
+ error_occured(ex)
37
+ end
38
+
39
+ def set_release_after_approval!(app, automatic_release)
40
+ verify_app(app)
41
+ open_app_page(app)
42
+
43
+ Helper.log.info "Setting automatic release to '#{automatic_release}'".green
44
+
45
+ # Find the correct radio button
46
+ first("div[itc-radio='versionInfo.releaseOnApproval.value'][radio-value='#{automatic_release.to_s}'] > * > a").click
47
+
48
+ (click_on "Save" rescue nil) # if nothing has changed, there is no back button and we don't care
49
+ rescue => ex
50
+ error_occured(ex)
51
+ end
52
+
53
+ def set_categories!(app, primary, secondary)
54
+ verify_app(app)
55
+ open_app_page(app)
56
+
57
+ Helper.log.info "Setting primary/secondary category.'".green
58
+ if primary
59
+ all("select[ng-model='versionInfo.primaryCategory.value'] > option").each do |category|
60
+ if category.text.to_s == primary.to_s
61
+ category.select_option
62
+ primary = nil
63
+ break
64
+ end
65
+ end
66
+ if primary
67
+ Helper.log.info "Could not find category '#{primary}'. Make sure it's available on iTC".red
68
+ end
69
+ end
70
+
71
+ if secondary
72
+ all("select[ng-model='versionInfo.secondaryCategory.value'] > option").each do |category|
73
+ if category.text.to_s == secondary.to_s
74
+ category.select_option
75
+ secondary = nil
76
+ break
77
+ end
78
+ end
79
+ if secondary
80
+ Helper.log.info "Could not find category '#{secondary}'. Make sure it's available on iTC".red
81
+ end
82
+ end
83
+
84
+
85
+ (click_on "Save" rescue nil) # if nothing has changed, there is no back button and we don't care
86
+ rescue => ex
87
+ error_occured(ex)
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,35 @@
1
+ require 'fastimage'
2
+
3
+ module Deliver
4
+ class ItunesConnect
5
+ # Uploading a new full size app icon
6
+
7
+ def upload_app_icon!(app, path)
8
+ path = File.expand_path(path)
9
+ raise "Could not find app icon at path '#{path}'".red unless File.exists?path
10
+
11
+ size = FastImage.size(path)
12
+ raise "App icon must have the resolution of 1024x1024px".red unless (size[0] == 1024 and size[1] == 1024)
13
+
14
+ begin
15
+ verify_app(app)
16
+ open_app_page(app)
17
+
18
+ Helper.log.info "Starting upload of new app icon".green
19
+
20
+ evaluate_script("$('.appversionicon > .ios7-style-icon').prev().click()") # delete button
21
+ evaluate_script("$('[style-class=\"appversionicon rounded\"] [itc-launch-filechooser] + input').attr('id', 'deliverFileUploadInput')") # set div
22
+ evaluate_script("URL = webkitURL; URL.createObjectURL = function(){return 'blob:abc'}"); # shim URL
23
+ page.attach_file("deliverFileUploadInput", path) # add file
24
+
25
+ sleep 10
26
+
27
+ click_on "Save"
28
+
29
+ Helper.log.info "Finished uploading the new app icon".green
30
+ rescue => ex
31
+ error_occured(ex)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,68 @@
1
+ require 'json'
2
+
3
+ module Deliver
4
+ class ItunesConnect
5
+ # Setting the app's age restrictions
6
+
7
+ def set_app_rating!(app, path_to_json)
8
+ path_to_json = File.expand_path(path_to_json)
9
+ raise "Could not find app rating JSON file" unless File.exists?(path_to_json)
10
+
11
+ config = JSON.parse(File.read(path_to_json))
12
+
13
+ verify_app(app)
14
+ open_app_page(app)
15
+
16
+ Helper.log.info "Updating the app's rating".green
17
+
18
+ first("a[ng-show='versionInfo.ratings.isEditable']").click # open the ratings screen
19
+
20
+ rows = wait_for_elements(".defaultTable.ratingsTable > tbody > tr.ng-scope") # .ng-scope, since there is one empty row
21
+
22
+ if rows.count != config.count
23
+ raise "The number of options passed in the config file does not match the number of options available on iTC!".red
24
+ end
25
+
26
+
27
+
28
+ # Setting all the values based on config file
29
+ rows.each_with_index do |row, index|
30
+ current = config[index]
31
+
32
+ level = name_for_level(current['level'], current['type'] == 'boolean')
33
+
34
+ Helper.log.info "Setting '#{current['comment']}' to #{level}.".green
35
+
36
+ radio_value = "ITC.apps.ratings.level.#{level}"
37
+
38
+ row.first("td > div[radio-value='#{radio_value}']").click
39
+ end
40
+
41
+ # Check if there is a warning or error message because of this rating
42
+ error_message = first("p[ng-show='tempPageContent.ratingDialog.showErrorMessage']")
43
+ Helper.log.error error_message.text if error_message
44
+
45
+ Helper.log.info "Finished setting updated app rating"
46
+
47
+ (click_on "Done" rescue nil)
48
+
49
+ (click_on "Save" rescue nil) # if nothing has changed, there is no back button and we don't care
50
+ rescue => ex
51
+ error_occured(ex)
52
+ end
53
+
54
+ private
55
+ def name_for_level(level, is_boolean)
56
+ if is_boolean
57
+ return "NO" if level == 0
58
+ return "YES" if level == 1
59
+ else
60
+ return "NONE" if level == 0
61
+ return "INFREQUENT_MILD" if level == 1
62
+ return "FREQUENT_INTENSE" if level == 2
63
+ end
64
+
65
+ raise "Unknown level '#{level}' - must be 0, 1 or 2".red
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,83 @@
1
+ module Deliver
2
+ class ItunesConnect
3
+ # All the private helpers
4
+ private
5
+ # Opens the app details page of the given app.
6
+ # @param app (Deliver::App) the app that should be opened
7
+ # @return (bool) true if everything worked fine
8
+ # @raise [ItunesConnectGeneralError] General error while executing
9
+ # this action
10
+ # @raise [ItunesConnectLoginError] Login data is wrong
11
+ def open_app_page(app)
12
+ verify_app(app)
13
+
14
+ Helper.log.info "Opening detail page for app #{app}"
15
+
16
+ visit APP_DETAILS_URL.gsub("[[app_id]]", app.apple_id.to_s)
17
+
18
+ wait_for_elements('.page-subnav')
19
+ sleep 5
20
+
21
+ if current_url.include?"wa/defaultError" # app could not be found
22
+ raise "Could not open app details for app '#{app}'. Make sure you're using the correct Apple ID and the correct Apple developer account (#{CredentialsManager::PasswordManager.shared_manager.username}).".red
23
+ end
24
+
25
+ true
26
+ rescue => ex
27
+ error_occured(ex)
28
+ end
29
+
30
+
31
+ def verify_app(app)
32
+ raise ItunesConnectGeneralError.new("No valid Deliver::App given") unless app.kind_of?Deliver::App
33
+ raise ItunesConnectGeneralError.new("App is missing information (apple_id not given)") unless (app.apple_id || '').to_s.length > 5
34
+ end
35
+
36
+ def error_occured(ex)
37
+ snap
38
+ raise ex # re-raise the error after saving the snapshot
39
+ end
40
+
41
+ def snap
42
+ path = "Error#{Time.now.to_i}.png"
43
+ save_screenshot(path, :full => true)
44
+ system("open '#{path}'")
45
+ end
46
+
47
+ # Since Apple takes for ages, after the upload is properly processed, we have to wait here
48
+ def wait_for_preprocessing
49
+ started = Time.now
50
+
51
+ # Wait, while iTunesConnect is processing the uploaded file
52
+ while page.has_content?"Uploaded"
53
+ # iTunesConnect is super slow... so we have to wait...
54
+ Helper.log.info("Sorry, we have to wait for iTunesConnect, since it's still processing the uploaded ipa file\n" +
55
+ "If this takes longer than 45 minutes, you have to re-upload the ipa file again.\n" +
56
+ "You can always open the browser page yourself: '#{current_url}'\n" +
57
+ "Passed time: ~#{((Time.now - started) / 60.0).to_i} minute(s)")
58
+ sleep 60
59
+ visit current_url
60
+ sleep 10
61
+ end
62
+ end
63
+
64
+ def wait_for_elements(name)
65
+ counter = 0
66
+ results = all(name)
67
+ while results.count == 0
68
+ # Helper.log.debug "Waiting for #{name}"
69
+ sleep 0.2
70
+
71
+ results = all(name)
72
+
73
+ counter += 1
74
+ if counter > 100
75
+ Helper.log.debug page.html
76
+ Helper.log.debug caller
77
+ raise ItunesConnectGeneralError.new("Couldn't find element '#{name}' after waiting for quite some time")
78
+ end
79
+ end
80
+ return results
81
+ end
82
+ end
83
+ end