deliver 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +37 -10
- data/.rspec +1 -0
- data/.travis.yml +8 -0
- data/.yardopts +1 -0
- data/Gemfile.lock +106 -0
- data/LICENSE +21 -0
- data/README.md +268 -17
- data/Rakefile +3 -0
- data/assets/PDFExample.png +0 -0
- data/assets/SubmitForReviewInformation.png +0 -0
- data/assets/deliver.png +0 -0
- data/assets/deliver.pxm +0 -0
- data/assets/deliverFullSize.png +0 -0
- data/bin/deliver +32 -0
- data/deliver.gemspec +15 -9
- data/lib/assets/DeliverfileDefault +47 -0
- data/lib/assets/DeliverfileExample +62 -0
- data/lib/assets/ScreenshotsHelp +4 -0
- data/lib/deliver.rb +20 -1
- data/lib/deliver/app.rb +146 -0
- data/lib/deliver/app_metadata.rb +487 -0
- data/lib/deliver/app_screenshot.rb +119 -0
- data/lib/deliver/commands.rb +4 -0
- data/lib/deliver/commands/init.rb +12 -0
- data/lib/deliver/commands/run.rb +20 -0
- data/lib/deliver/deliver_process.rb +241 -0
- data/lib/deliver/deliverer.rb +112 -0
- data/lib/deliver/deliverfile/deliverfile.rb +35 -0
- data/lib/deliver/deliverfile/deliverfile_creator.rb +115 -0
- data/lib/deliver/deliverfile/dsl.rb +124 -0
- data/lib/deliver/dependency_checker.rb +32 -0
- data/lib/deliver/helper.rb +55 -0
- data/lib/deliver/ipa_uploader.rb +160 -0
- data/lib/deliver/itunes_connect.rb +514 -0
- data/lib/deliver/itunes_search_api.rb +48 -0
- data/lib/deliver/itunes_transporter.rb +154 -0
- data/lib/deliver/languages.rb +6 -0
- data/lib/deliver/metadata_item.rb +94 -0
- data/lib/deliver/password_manager.rb +86 -0
- data/lib/deliver/pdf_generator.rb +131 -0
- data/lib/deliver/version.rb +1 -1
- data/spec/app_metadata_spec.rb +350 -0
- data/spec/app_screenshot_spec.rb +88 -0
- data/spec/app_spec.rb +85 -0
- data/spec/deliverer_spec.rb +48 -0
- data/spec/deliverfile_creator_spec.rb +73 -0
- data/spec/example_deliver_files_spec.rb +227 -0
- data/spec/fixtures/Deliverfiles/DeliverfileCallbacks +14 -0
- data/spec/fixtures/Deliverfiles/DeliverfileCallbacksFailingTests +14 -0
- data/spec/fixtures/Deliverfiles/DeliverfileCallbacksNoErrorBlock +6 -0
- data/spec/fixtures/Deliverfiles/DeliverfileDefaultLanguageNotOnTop +8 -0
- data/spec/fixtures/Deliverfiles/DeliverfileDuplicateIpa +2 -0
- data/spec/fixtures/Deliverfiles/DeliverfileLocales +16 -0
- data/spec/fixtures/Deliverfiles/DeliverfileMetadataJson +6 -0
- data/spec/fixtures/Deliverfiles/DeliverfileMissingAppVersion +1 -0
- data/spec/fixtures/Deliverfiles/DeliverfileMissingBlockForTests +7 -0
- data/spec/fixtures/Deliverfiles/DeliverfileMissingIdentifier +1 -0
- data/spec/fixtures/Deliverfiles/DeliverfileMissingLanguage +1 -0
- data/spec/fixtures/Deliverfiles/DeliverfileMissingValue +3 -0
- data/spec/fixtures/Deliverfiles/DeliverfileMixed +37 -0
- data/spec/fixtures/Deliverfiles/DeliverfileNoVersion +3 -0
- data/spec/fixtures/Deliverfiles/DeliverfileScreenshots +6 -0
- data/spec/fixtures/Deliverfiles/DeliverfileScreenshotsFallbackDefaultLanguage +7 -0
- data/spec/fixtures/Deliverfiles/DeliverfileSimple +8 -0
- data/spec/fixtures/Deliverfiles/DeliverfileVersionMismatchPackage +8 -0
- data/spec/fixtures/Deliverfiles/DeliverfileWrongIdentifier +5 -0
- data/spec/fixtures/Deliverfiles/DeliverfileWrongVersion +5 -0
- data/spec/fixtures/Deliverfiles/metadata.json +24 -0
- data/spec/fixtures/example1.itmsp/metadata.xml +121 -0
- data/spec/fixtures/example2.itmsp/metadata.xml +54 -0
- data/spec/fixtures/ipas/Example1.ipa +0 -0
- data/spec/fixtures/metadata/ipa_result.xml +12 -0
- data/spec/fixtures/metadata/ipa_result2.xml +12 -0
- data/spec/fixtures/packages/464686641.itmsp/metadata.xml +104 -0
- data/spec/fixtures/packages/794902327.itmsp/metadata.xml +107 -0
- data/spec/fixtures/packages/878567776.itmsp/metadata.xml +104 -0
- data/spec/fixtures/screenshots/de-DE/iPhone4.png +0 -0
- data/spec/fixtures/screenshots/de-DE/iPhone6.png +0 -0
- data/spec/fixtures/screenshots/de-DE/iPhone6Plus1.png +0 -0
- data/spec/fixtures/screenshots/de-DE/iPhone6Plus2.png +0 -0
- data/spec/fixtures/screenshots/de-DE/screenshot1.png +0 -0
- data/spec/fixtures/screenshots/de-DE/screenshot2.png +0 -0
- data/spec/fixtures/screenshots/de-DE/screenshot3.png +0 -0
- data/spec/fixtures/screenshots/de-DE/screenshot5.png +0 -0
- data/spec/fixtures/screenshots/en-US/english.png +0 -0
- data/spec/fixtures/screenshots/iPhone4.png +0 -0
- data/spec/fixtures/screenshots/invalidImage.png +0 -0
- data/spec/fixtures/screenshots/screenshot1.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 2.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 2.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 3.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 4.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 5.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 6.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4.png +0 -0
- data/spec/helper_spec.rb +16 -0
- data/spec/ipa_uploader_spec.rb +61 -0
- data/spec/itunes_connect_spec.rb +12 -0
- data/spec/itunes_search_api_spec.rb +24 -0
- data/spec/itunes_transporter_spec.rb +52 -0
- data/spec/languages_spec.rb +7 -0
- data/spec/metadata_item_spec.rb +36 -0
- data/spec/mocking/transporter_mocking.rb +40 -0
- data/spec/mocking/webmocking.rb +31 -0
- data/spec/password_manager_spec.rb +27 -0
- data/spec/responses/itunesLookup-.json +4 -0
- data/spec/responses/itunesLookup-0.json +4 -0
- data/spec/responses/itunesLookup-284882215.json +106 -0
- data/spec/responses/itunesLookup-at.felixkrause.iTanky.json +8 -0
- data/spec/responses/itunesLookup-com.facebook.Facebook.json +106 -0
- data/spec/responses/itunesLookup-invalid.json +4 -0
- data/spec/responses/itunesLookup-net.sunapps.invalid.json +4 -0
- data/spec/responses/transporter/download_invalid_apple_id.txt +35 -0
- data/spec/responses/transporter/download_valid_apple_id.txt +32 -0
- data/spec/responses/transporter/upload_invalid.txt +174 -0
- data/spec/responses/transporter/upload_valid.txt +290 -0
- data/spec/spec_helper.rb +23 -0
- data/tasks/rspec.rake +3 -0
- metadata +242 -8
- 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
|