deliver 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +37 -10
  3. data/.rspec +1 -0
  4. data/.travis.yml +8 -0
  5. data/.yardopts +1 -0
  6. data/Gemfile.lock +106 -0
  7. data/LICENSE +21 -0
  8. data/README.md +268 -17
  9. data/Rakefile +3 -0
  10. data/assets/PDFExample.png +0 -0
  11. data/assets/SubmitForReviewInformation.png +0 -0
  12. data/assets/deliver.png +0 -0
  13. data/assets/deliver.pxm +0 -0
  14. data/assets/deliverFullSize.png +0 -0
  15. data/bin/deliver +32 -0
  16. data/deliver.gemspec +15 -9
  17. data/lib/assets/DeliverfileDefault +47 -0
  18. data/lib/assets/DeliverfileExample +62 -0
  19. data/lib/assets/ScreenshotsHelp +4 -0
  20. data/lib/deliver.rb +20 -1
  21. data/lib/deliver/app.rb +146 -0
  22. data/lib/deliver/app_metadata.rb +487 -0
  23. data/lib/deliver/app_screenshot.rb +119 -0
  24. data/lib/deliver/commands.rb +4 -0
  25. data/lib/deliver/commands/init.rb +12 -0
  26. data/lib/deliver/commands/run.rb +20 -0
  27. data/lib/deliver/deliver_process.rb +241 -0
  28. data/lib/deliver/deliverer.rb +112 -0
  29. data/lib/deliver/deliverfile/deliverfile.rb +35 -0
  30. data/lib/deliver/deliverfile/deliverfile_creator.rb +115 -0
  31. data/lib/deliver/deliverfile/dsl.rb +124 -0
  32. data/lib/deliver/dependency_checker.rb +32 -0
  33. data/lib/deliver/helper.rb +55 -0
  34. data/lib/deliver/ipa_uploader.rb +160 -0
  35. data/lib/deliver/itunes_connect.rb +514 -0
  36. data/lib/deliver/itunes_search_api.rb +48 -0
  37. data/lib/deliver/itunes_transporter.rb +154 -0
  38. data/lib/deliver/languages.rb +6 -0
  39. data/lib/deliver/metadata_item.rb +94 -0
  40. data/lib/deliver/password_manager.rb +86 -0
  41. data/lib/deliver/pdf_generator.rb +131 -0
  42. data/lib/deliver/version.rb +1 -1
  43. data/spec/app_metadata_spec.rb +350 -0
  44. data/spec/app_screenshot_spec.rb +88 -0
  45. data/spec/app_spec.rb +85 -0
  46. data/spec/deliverer_spec.rb +48 -0
  47. data/spec/deliverfile_creator_spec.rb +73 -0
  48. data/spec/example_deliver_files_spec.rb +227 -0
  49. data/spec/fixtures/Deliverfiles/DeliverfileCallbacks +14 -0
  50. data/spec/fixtures/Deliverfiles/DeliverfileCallbacksFailingTests +14 -0
  51. data/spec/fixtures/Deliverfiles/DeliverfileCallbacksNoErrorBlock +6 -0
  52. data/spec/fixtures/Deliverfiles/DeliverfileDefaultLanguageNotOnTop +8 -0
  53. data/spec/fixtures/Deliverfiles/DeliverfileDuplicateIpa +2 -0
  54. data/spec/fixtures/Deliverfiles/DeliverfileLocales +16 -0
  55. data/spec/fixtures/Deliverfiles/DeliverfileMetadataJson +6 -0
  56. data/spec/fixtures/Deliverfiles/DeliverfileMissingAppVersion +1 -0
  57. data/spec/fixtures/Deliverfiles/DeliverfileMissingBlockForTests +7 -0
  58. data/spec/fixtures/Deliverfiles/DeliverfileMissingIdentifier +1 -0
  59. data/spec/fixtures/Deliverfiles/DeliverfileMissingLanguage +1 -0
  60. data/spec/fixtures/Deliverfiles/DeliverfileMissingValue +3 -0
  61. data/spec/fixtures/Deliverfiles/DeliverfileMixed +37 -0
  62. data/spec/fixtures/Deliverfiles/DeliverfileNoVersion +3 -0
  63. data/spec/fixtures/Deliverfiles/DeliverfileScreenshots +6 -0
  64. data/spec/fixtures/Deliverfiles/DeliverfileScreenshotsFallbackDefaultLanguage +7 -0
  65. data/spec/fixtures/Deliverfiles/DeliverfileSimple +8 -0
  66. data/spec/fixtures/Deliverfiles/DeliverfileVersionMismatchPackage +8 -0
  67. data/spec/fixtures/Deliverfiles/DeliverfileWrongIdentifier +5 -0
  68. data/spec/fixtures/Deliverfiles/DeliverfileWrongVersion +5 -0
  69. data/spec/fixtures/Deliverfiles/metadata.json +24 -0
  70. data/spec/fixtures/example1.itmsp/metadata.xml +121 -0
  71. data/spec/fixtures/example2.itmsp/metadata.xml +54 -0
  72. data/spec/fixtures/ipas/Example1.ipa +0 -0
  73. data/spec/fixtures/metadata/ipa_result.xml +12 -0
  74. data/spec/fixtures/metadata/ipa_result2.xml +12 -0
  75. data/spec/fixtures/packages/464686641.itmsp/metadata.xml +104 -0
  76. data/spec/fixtures/packages/794902327.itmsp/metadata.xml +107 -0
  77. data/spec/fixtures/packages/878567776.itmsp/metadata.xml +104 -0
  78. data/spec/fixtures/screenshots/de-DE/iPhone4.png +0 -0
  79. data/spec/fixtures/screenshots/de-DE/iPhone6.png +0 -0
  80. data/spec/fixtures/screenshots/de-DE/iPhone6Plus1.png +0 -0
  81. data/spec/fixtures/screenshots/de-DE/iPhone6Plus2.png +0 -0
  82. data/spec/fixtures/screenshots/de-DE/screenshot1.png +0 -0
  83. data/spec/fixtures/screenshots/de-DE/screenshot2.png +0 -0
  84. data/spec/fixtures/screenshots/de-DE/screenshot3.png +0 -0
  85. data/spec/fixtures/screenshots/de-DE/screenshot5.png +0 -0
  86. data/spec/fixtures/screenshots/en-US/english.png +0 -0
  87. data/spec/fixtures/screenshots/iPhone4.png +0 -0
  88. data/spec/fixtures/screenshots/invalidImage.png +0 -0
  89. data/spec/fixtures/screenshots/screenshot1.png +0 -0
  90. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 2.png +0 -0
  91. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 2.png +0 -0
  92. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 3.png +0 -0
  93. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 4.png +0 -0
  94. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 5.png +0 -0
  95. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 6.png +0 -0
  96. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy.png +0 -0
  97. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4.png +0 -0
  98. data/spec/helper_spec.rb +16 -0
  99. data/spec/ipa_uploader_spec.rb +61 -0
  100. data/spec/itunes_connect_spec.rb +12 -0
  101. data/spec/itunes_search_api_spec.rb +24 -0
  102. data/spec/itunes_transporter_spec.rb +52 -0
  103. data/spec/languages_spec.rb +7 -0
  104. data/spec/metadata_item_spec.rb +36 -0
  105. data/spec/mocking/transporter_mocking.rb +40 -0
  106. data/spec/mocking/webmocking.rb +31 -0
  107. data/spec/password_manager_spec.rb +27 -0
  108. data/spec/responses/itunesLookup-.json +4 -0
  109. data/spec/responses/itunesLookup-0.json +4 -0
  110. data/spec/responses/itunesLookup-284882215.json +106 -0
  111. data/spec/responses/itunesLookup-at.felixkrause.iTanky.json +8 -0
  112. data/spec/responses/itunesLookup-com.facebook.Facebook.json +106 -0
  113. data/spec/responses/itunesLookup-invalid.json +4 -0
  114. data/spec/responses/itunesLookup-net.sunapps.invalid.json +4 -0
  115. data/spec/responses/transporter/download_invalid_apple_id.txt +35 -0
  116. data/spec/responses/transporter/download_valid_apple_id.txt +32 -0
  117. data/spec/responses/transporter/upload_invalid.txt +174 -0
  118. data/spec/responses/transporter/upload_valid.txt +290 -0
  119. data/spec/spec_helper.rb +23 -0
  120. data/tasks/rspec.rake +3 -0
  121. metadata +242 -8
  122. data/LICENSE.txt +0 -22
@@ -0,0 +1,48 @@
1
+ require 'open-uri'
2
+
3
+ module Deliver
4
+ # A wrapper around the Apple iTunes Search API to access app information like
5
+ # the app identifier of an app.
6
+ class ItunesSearchApi
7
+
8
+ # Fetch all information you can get from a specific AppleID of an app
9
+ # @param id (int) The AppleID of the given app. This usually consists of 9 digits.
10
+ # @return (Hash) the response of the first result from Apple (https://itunes.apple.com/lookup?id=284882215)
11
+ # @example Response of Facebook App: https://itunes.apple.com/lookup?id=284882215
12
+ # {
13
+ # ...
14
+ # artistName: "Facebook, Inc.",
15
+ # price: 0,
16
+ # version: "14.9",
17
+ # ...
18
+ # }
19
+ def self.fetch(id)
20
+ # Example: https://itunes.apple.com/lookup?id=284882215
21
+ fetch_url("https://itunes.apple.com/lookup?id=#{id.to_s}")
22
+ end
23
+
24
+ def self.fetch_by_identifier(app_identifier)
25
+ # Example: http://itunes.apple.com/lookup?bundleId=net.sunapps.1
26
+ fetch_url("https://itunes.apple.com/lookup?bundleId=#{app_identifier}")
27
+ end
28
+
29
+
30
+ # This method only fetches the bundle identifier of a given app
31
+ # @param id (int) The AppleID of the given app. This usually consists of 9 digits.
32
+ # @return (String) the Bundle identifier of the app
33
+ def self.fetch_bundle_identifier(id)
34
+ self.fetch(id)['bundleId']
35
+ end
36
+
37
+ private
38
+ def self.fetch_url(url)
39
+ response = JSON.parse(open(url).read)
40
+ return nil if response['resultCount'] == 0
41
+
42
+ return response['results'].first
43
+ rescue
44
+ Helper.log.error "Could not find object '#{url}' using the iTunes API"
45
+ nil
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,154 @@
1
+ require 'pty'
2
+
3
+ require 'deliver/password_manager'
4
+
5
+
6
+ module Deliver
7
+ # The TransporterInputError occurs when you passed wrong inputs to the {Deliver::ItunesTransporter}
8
+ class TransporterInputError < StandardError
9
+ end
10
+ # The TransporterTransferError occurs when some error happens
11
+ # while uploading or downloading something from/to iTC
12
+ class TransporterTransferError < StandardError
13
+ end
14
+
15
+ class ItunesTransporter
16
+ ERROR_REGEX = />\s*ERROR:\s+(.+)/
17
+ WARNING_REGEX = />\s*WARN:\s+(.+)/
18
+ OUTPUT_REGEX = />\s+(.+)/
19
+
20
+ private_constant :ERROR_REGEX, :WARNING_REGEX, :OUTPUT_REGEX
21
+
22
+ # Returns a new instance of the iTunesTranspoter.
23
+ # If no username or password given, it will be taken from
24
+ # the #{Deliver::PasswordManager}
25
+ def initialize(user = nil, password = nil)
26
+ @user = (user || PasswordManager.new.username)
27
+ @password = (password || PasswordManager.new.password)
28
+ end
29
+
30
+ # Downloads the latest version of the app metadata package from iTC.
31
+ # @param app [Deliver::App] The app you want to download the data for
32
+ # @param dir [String] the path to the package file
33
+ # @return (Bool) True if everything worked fine
34
+ # @raise [Deliver::TransporterTransferError] when something went wrong
35
+ # when transfering
36
+ # @raise [Deliver::TransporterInputError] when passing wrong inputs
37
+ def download(app, dir = nil)
38
+ raise TransporterInputError.new("No valid Deliver::App given") unless app.kind_of?Deliver::App
39
+
40
+ Helper.log.info "Going to download app metadata from iTunesConnect"
41
+ dir ||= app.get_metadata_directory
42
+ command = build_download_command(@user, @password, app.apple_id, dir)
43
+
44
+ result = execute_transporter(command)
45
+
46
+ if result
47
+ Helper.log.info "Successfully downloaded the latest package from iTunesConnect.".green
48
+ end
49
+
50
+ result
51
+ end
52
+
53
+ # Uploads the modified package back to iTunesConnect
54
+ # @param app [Deliver::App] The app you want to download the data for
55
+ # @param dir [String] the path in which the package file is located
56
+ # @return (Bool) True if everything worked fine
57
+ # @raise [Deliver::TransporterTransferError] when something went wrong
58
+ # when transfering
59
+ # @raise [Deliver::TransporterInputError] when passing wrong inputs
60
+ def upload(app, dir)
61
+ raise TransporterInputError.new("No valid Deliver::App given") unless app.kind_of?Deliver::App
62
+
63
+ dir ||= app.get_metadata_directory
64
+ dir += "/#{app.apple_id}.itmsp"
65
+
66
+ Helper.log.info "Going to upload updated app to iTunesConnect"
67
+
68
+ command = build_upload_command(@user, @password, dir)
69
+ result = execute_transporter(command)
70
+
71
+ if result
72
+ Helper.log.info "Successfully uploaded package to iTunesConnect. It might take a few minutes until it's visible online.".green
73
+ end
74
+
75
+ result
76
+ end
77
+
78
+ private
79
+ def execute_transporter(command)
80
+ @errors = []
81
+ @warnings = []
82
+
83
+ begin
84
+ PTY.spawn(command) do |stdin, stdout, pid|
85
+ stdin.each do |line|
86
+ parse_line(line) # this is where the parsing happens
87
+ end
88
+ end
89
+ rescue Exception => ex
90
+ Helper.log.fatal(ex.to_s)
91
+ @errors << ex.to_s
92
+ end
93
+
94
+ if @errors.count > 0
95
+ raise TransporterTransferError.new(@errors.join("\n"))
96
+ end
97
+
98
+ if @warnings.count > 0
99
+ Helper.log.warn(@warnings.join("\n"))
100
+ end
101
+
102
+ true
103
+ end
104
+
105
+ def parse_line(line)
106
+ # Taken from https://github.com/sshaw/itunes_store_transporter/blob/master/lib/itunes/store/transporter/output_parser.rb
107
+
108
+ if line =~ ERROR_REGEX
109
+ @errors << $1
110
+
111
+ # Check if it's a login error
112
+ if $1.include?"Your Apple ID or password was entered incorrectly" or
113
+ $1.include?"This Apple ID has been locked for security reasons"
114
+
115
+ Deliver::PasswordManager.new.password_seems_wrong
116
+ end
117
+
118
+ elsif line =~ WARNING_REGEX
119
+ @warnings << $1
120
+ end
121
+
122
+ if line =~ OUTPUT_REGEX
123
+ # General logging for debug purposes
124
+ Helper.log.debug "[Transpoter Output]: #{$1}"
125
+ end
126
+ end
127
+
128
+ def build_download_command(username, password, apple_id, destination = "/tmp")
129
+ [
130
+ Helper.transporter_path,
131
+ "-m lookupMetadata",
132
+ "-u \"#{username}\"",
133
+ "-p '#{escaped_password(password)}'",
134
+ "-apple_id #{apple_id}",
135
+ "-destination '#{destination}'"
136
+ ].join(' ')
137
+ end
138
+
139
+ def build_upload_command(username, password, source = "/tmp")
140
+ [
141
+ Helper.transporter_path,
142
+ "-m upload",
143
+ "-u \"#{username}\"",
144
+ "-p '#{escaped_password(password)}'",
145
+ "-f '#{source}'"
146
+ ].join(' ')
147
+ end
148
+
149
+ def escaped_password(password)
150
+ password.gsub('$', '\\$')
151
+ end
152
+
153
+ end
154
+ end
@@ -0,0 +1,6 @@
1
+ module Deliver
2
+ module Languages
3
+ # These are all the languages which are available to use to upload app metadata and screenshots
4
+ ALL_LANGUAGES = ["da-DK", "de-DE", "el-GR", "en-AU", "en-CA", "en-GB", "en-US", "es-ES", "es-MX", "fi-FI", "fr-CA", "fr-FR", "id-ID", "it-IT", "ja-JP", "ko-KR", "ms-MY", "nl-NL", "no-NO", "pt-BR", "pt-PT", "ru-RU", "sv-SE", "th-TH", "tr-TR", "vi-VI", "cmn-Hans", "zh_CN", "cmn-Hant"]
5
+ end
6
+ end
@@ -0,0 +1,94 @@
1
+ require 'nokogiri'
2
+
3
+ module Deliver
4
+ # This class represents a file, included in the metadata.xml
5
+ #
6
+ # It takes care of calculating the file size and md5 value.
7
+ class MetadataItem
8
+ # @return [String] The path to this particular asset
9
+ attr_accessor :path
10
+
11
+ # Returns a new instance of MetadataItem
12
+ # @param path [String] the path to the real world file
13
+ # @param custom_node_name [String] You can set a custom name
14
+ # for the newly created node.
15
+ def initialize(path, custom_node_name = nil)
16
+ raise "File not found at path '#{path}'" unless File.exists?path
17
+
18
+ self.path = path
19
+ @custom_node_name = custom_node_name
20
+ end
21
+
22
+ # This method is called when storing this item into the metadata.xml file
23
+ #
24
+ # This method will calculate the md5 hash and exact file size
25
+ # Generates XML code that looks something like this
26
+ # +code+
27
+ # <data_file>
28
+ # <size>11463227</size>
29
+ # <file_name>myapp.54.56.ipa</file_name>
30
+ # <checksum type="md5">9d6b7b0e20bde9a3c831db89563e949f</checksum>
31
+ # </data_file>
32
+ # Take a look at the subclass {Deliver::AppScreenshot#create_xml_node} for a
33
+ # screenshot specific implementation
34
+ # @param doc [Nokogiri::XML::Document] The document this node
35
+ # should be added to
36
+ # @return [Nokogiri::XML::Node] the resulting XML node
37
+ def create_xml_node(doc)
38
+ screenshot = Nokogiri::XML::Node.new(name_for_xml_node, doc)
39
+
40
+ node_set = Nokogiri::XML::NodeSet.new(doc)
41
+
42
+ # File Size
43
+ size = Nokogiri::XML::Node.new('size', doc)
44
+ size.content = File.size(self.path)
45
+ node_set << size
46
+
47
+ # File Name
48
+ file_name = Nokogiri::XML::Node.new('file_name', doc)
49
+ file_name.content = resulting_file_name
50
+ node_set << file_name
51
+
52
+ # md5 Checksum
53
+ checksum = Nokogiri::XML::Node.new('checksum', doc)
54
+ checksum.content = md5_value
55
+ checksum['type'] = 'md5'
56
+ node_set << checksum
57
+
58
+
59
+ screenshot.children = node_set
60
+
61
+ return screenshot
62
+ end
63
+
64
+ # We also have to copy the file itself, since it has to be *inside* the package
65
+ # You don't have to call this method manually.
66
+ def store_file_inside_package(path_to_package)
67
+ # This will also rename the resulting file to not have any spaces or other
68
+ # illegal characters in the file name
69
+
70
+ FileUtils.cp(self.path, "#{path_to_package}/#{resulting_file_name}")
71
+ end
72
+
73
+ private
74
+ def name_for_xml_node
75
+ @custom_node_name || 'data_file'
76
+ end
77
+
78
+ # The file name which is used inside the package
79
+ def resulting_file_name
80
+ extension = File.extname(self.path)
81
+ "#{file_name_for_element}#{extension}"
82
+ end
83
+
84
+ def md5_value
85
+ Digest::MD5.hexdigest(File.read(self.path))
86
+ end
87
+
88
+ # This method will also take some other things into account to generate a truly unique
89
+ # file name. This will enable using the same screenshots multiple times
90
+ def file_name_for_element
91
+ Digest::MD5.hexdigest([File.read(self.path), self.path].join("-"))
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,86 @@
1
+ require 'security'
2
+ require 'highline/import' # to hide the entered password
3
+
4
+ module Deliver
5
+ # Handles reading out the password from the keychain or asking for login data
6
+ class PasswordManager
7
+ # @return [String] The username / email address of the currently logged in user
8
+ attr_accessor :username
9
+ # @return [String] The password of the currently logged in user
10
+ attr_accessor :password
11
+
12
+ HOST = "itunesconnect.apple.com"
13
+ private_constant :HOST
14
+
15
+ # A new instance of PasswordManager.
16
+ #
17
+ # This already check the Keychain if there is a username and password stored.
18
+ # If that's not the case, it will ask for login data via stdin
19
+ def initialize
20
+ self.username ||= ENV["DELIVER_USER"] || load_from_keychain[0]
21
+ self.password ||= ENV["DELIVER_PASSWORD"] || load_from_keychain[1]
22
+
23
+ if (self.username || '').length == 0 or (self.password || '').length == 0
24
+ puts "No username or password given. You can set environment variables:"
25
+ puts "DELIVER_USER, DELIVER_PASSWORD"
26
+
27
+ ask_for_login
28
+ end
29
+ end
30
+
31
+ # This method is called, when the iTunes backend returns that the login data is wrong
32
+ # This will ask the user, if he wants to re-enter the password
33
+ def password_seems_wrong
34
+ return false if Helper.is_test?
35
+
36
+ puts "It seems like the username or password for the account '#{self.username}' is wrong."
37
+ reenter = agree("Do you want to re-enter your username and password? (y/n)", true)
38
+ if reenter
39
+ @username = nil
40
+ @password = nil
41
+ remove_from_keychain
42
+
43
+ puts "You will have to re-run the recent command to use the new username/password."
44
+ return true
45
+ else
46
+ return false
47
+ end
48
+ end
49
+
50
+ private
51
+ def ask_for_login
52
+ puts "--------------------------------------------------------------------------".green
53
+ puts "The login information you enter now will be stored in your keychain ".green
54
+ puts "More information about that on GitHub: https://github.com/krausefx/deliver".green
55
+ puts "--------------------------------------------------------------------------".green
56
+
57
+ while (self.username || '').length == 0
58
+ self.username = ask("Username: ")
59
+ end
60
+
61
+ while (self.password || '').length == 0
62
+ self.password = ask("Password: ") { |q| q.echo = "*" }
63
+ end
64
+
65
+ # Now we store this information in the keychain
66
+ # Example usage taken from https://github.com/nomad/cupertino/blob/master/lib/cupertino/provisioning_portal/commands/login.rb
67
+ if Security::InternetPassword.add(HOST, self.username, self.password)
68
+ return true
69
+ else
70
+ Helper.log.error "Could not store password in keychain"
71
+ return false
72
+ end
73
+ end
74
+
75
+ def remove_from_keychain
76
+ Security::InternetPassword.delete(:server => HOST)
77
+ end
78
+
79
+ def load_from_keychain
80
+ pass = Security::InternetPassword.find(:server => HOST)
81
+
82
+ return [pass.attributes['acct'], pass.password] if pass
83
+ return [nil, nil]
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,131 @@
1
+ require 'prawn'
2
+
3
+ require 'pry'
4
+
5
+ module Deliver
6
+ class PdfGenerator
7
+
8
+ # Renders all data available in the Deliverer to quickly see if everything was correctly generated.
9
+ # @param deliverer [Deliver::Deliverer] The deliver process on which based the PDF file should be generated
10
+ # @param export_path (String) The path to a folder where the resulting PDF file should be stored.
11
+ def render(deliverer, export_path = nil)
12
+ export_path ||= '/tmp'
13
+
14
+ pdf = Prawn::Document.new(:margin => [0, 0, 0, 0])
15
+
16
+ resulting_path = "#{export_path}/#{Time.now.to_i}.pdf"
17
+ Prawn::Document.generate(resulting_path) do
18
+
19
+ counter = 0
20
+ deliverer.app.metadata.information.each do |language, content|
21
+ title = content[:title][:value] rescue ''
22
+
23
+ Helper.log.info("[PDF] Exporting locale '#{language}' for app with title '#{title}'")
24
+
25
+ font_size 20
26
+ text "#{language}: #{title}"
27
+ stroke_horizontal_rule
28
+ font_size 14
29
+
30
+ move_down 30
31
+
32
+
33
+
34
+ col1 = 200
35
+ modified_color = '0000AA'
36
+ standard_color = '000000'
37
+
38
+
39
+ prev_cursor = cursor.to_f
40
+ # Description on right side
41
+ bounding_box([col1, cursor], width: 340.0) do
42
+ if content[:description] and content[:description][:value]
43
+ text content[:description][:value], size: 6, color: (content[:description][:modified] ? modified_color : standard_color)
44
+ end
45
+ move_down 10
46
+ stroke_horizontal_rule
47
+ move_down 10
48
+ text "Changelog:", size: 8
49
+ move_down 5
50
+ if content[:version_whats_new] and content[:version_whats_new][:value]
51
+ text content[:version_whats_new][:value], size: 6, color: (content[:version_whats_new][:modified] ? modified_color : standard_color)
52
+ end
53
+ end
54
+ title_bottom = cursor.to_f
55
+
56
+ move_cursor_to prev_cursor
57
+
58
+
59
+ all_keys = [:support_url, :privacy_url, :software_url, :keywords]
60
+
61
+ all_keys.each_with_index do |key, index|
62
+ value = content[key][:value] rescue nil
63
+
64
+ color = (content[key][:modified] ? modified_color : standard_color rescue standard_color)
65
+
66
+ bounding_box([0, cursor], width: col1) do
67
+ key = key.to_s.gsub('_', ' ').capitalize
68
+
69
+ width = 200
70
+ size = 10
71
+
72
+ if value.kind_of?Array
73
+ # Keywords only
74
+ text "#{key}:", color: color, width: width, size: size
75
+ move_down 2
76
+
77
+ keywords_padding_left = 5
78
+ bounding_box([keywords_padding_left, cursor], width: (col1 - keywords_padding_left)) do
79
+ value.each do |item|
80
+ text "- #{item}", color: color, width: width, size: (size - 2)
81
+ end
82
+ end
83
+ else
84
+ # Everything else
85
+ next if value == nil or value.length == 0
86
+
87
+ text "#{key}: #{value}", color: color, width: width, size: size
88
+ end
89
+ end
90
+ end
91
+
92
+ image_width = 60
93
+ padding = 10
94
+ last_size = nil
95
+ top = [cursor, title_bottom].min - padding
96
+ index = 0
97
+ previous_image_height = 0
98
+ if (content[:screenshots] || []).count > 0
99
+ content[:screenshots].sort{ |a, b| a.screen_size <=> b.screen_size }.each do |screenshot|
100
+
101
+ if last_size and last_size != screenshot.screen_size
102
+ # Next row (other simulator size)
103
+ top -= (previous_image_height + padding)
104
+ index = 0
105
+ end
106
+
107
+ image screenshot.path, width: image_width,
108
+ at: [(index * (image_width + padding)), top]
109
+
110
+ original_size = FastImage.size(screenshot.path)
111
+ previous_image_height = (image_width.to_f / original_size[0].to_f) * original_size[1].to_f
112
+
113
+ last_size = screenshot.screen_size
114
+ index += 1
115
+ end
116
+ else
117
+ move_cursor_to top
118
+ text "No screenshots passed. Is this correct? They will get removed from iTunesConnect."
119
+ end
120
+
121
+ counter += 1
122
+ if counter < deliverer.app.metadata.information.count
123
+ start_new_page
124
+ end
125
+ end
126
+ end
127
+
128
+ return resulting_path
129
+ end
130
+ end
131
+ end