deliver 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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