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
data/Rakefile CHANGED
@@ -1,2 +1,5 @@
1
1
  require "bundler/gem_tasks"
2
2
 
3
+ Dir.glob('tasks/**/*.rake').each(&method(:import))
4
+
5
+ task :default => :spec
Binary file
Binary file
Binary file
Binary file
data/bin/deliver ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.push File.expand_path("../../lib", __FILE__)
4
+
5
+ require 'deliver'
6
+ require 'commander/import'
7
+
8
+ HighLine.track_eof = false
9
+
10
+
11
+ # Commander
12
+ program :version, Deliver::VERSION
13
+ program :description, 'CLI for \'Deliver\' - Automate uploading of app metadata, screenshots and app updates to Apple'
14
+ program :help, 'Author', 'Felix Krause <krausefx@gmail.com>'
15
+ program :help, 'Website', 'http://felixkrause.at'
16
+ program :help, 'GitHub', 'https://github.com/krausefx/deliver'
17
+ program :help_formatter, :compact
18
+
19
+ global_option('--verbose') { $verbose = true }
20
+
21
+ default_command :run
22
+
23
+ require 'deliver/commands'
24
+
25
+ def deliver_path
26
+ [enclosed_directory, Deliver::Deliverfile::Deliverfile::FILE_NAME].join('/')
27
+ end
28
+
29
+ # The directoy in which the Deliverfile and metadata should be created
30
+ def enclosed_directory
31
+ "."
32
+ end
data/deliver.gemspec CHANGED
@@ -12,10 +12,10 @@ Gem::Specification.new do |spec|
12
12
  spec.description = %q{Using Deliver you can easily integrate a real continuous delivery
13
13
  solution for iOS applications. You can update the app metadata, upload screenshots
14
14
  in all languages for different screensizes to iTunesConnect and even publish a new
15
- ipa file to iTunesConnect. You define you prefered deployment information once in a so called
15
+ ipa file to iTunesConnect. You define your prefered deployment information once in a so called
16
16
  Deliverfile and store it in git, to easily deploy from every machine, even your Continuos Integration server}
17
17
  spec.homepage = ""
18
- # spec.license = ""
18
+ spec.license = "MIT"
19
19
 
20
20
  spec.required_ruby_version = '>= 1.9.3'
21
21
 
@@ -30,17 +30,23 @@ Gem::Specification.new do |spec|
30
30
  spec.add_dependency 'fastimage', '~> 1.6.3' # fetch the image sizes from the screenshots
31
31
  spec.add_dependency 'rubyzip', '~> 1.1.6' # needed for extracting the ipa file
32
32
  spec.add_dependency 'plist', '~> 3.1.0' # for reading the Info.plist of the ipa file
33
- spec.add_dependency 'colored'
33
+ spec.add_dependency 'colored' # coloured terminal output
34
+ spec.add_dependency 'commander', '~> 4.2.0' # CLI parser
35
+ spec.add_dependency 'prawn' # generating PDF file for the applied changes
34
36
 
35
37
  # Frontend Scripting
36
38
  spec.add_dependency 'capybara', '~> 2.4.3' # for controlling iTC
37
39
  spec.add_dependency 'poltergeist', '~> 1.5.1' # headless Javascript browser for controlling iTC
38
40
 
39
41
  # Development only
40
- spec.add_development_dependency "bundler"
41
- spec.add_development_dependency "rake"
42
- spec.add_development_dependency "rspec", "~> 3.1.0"
43
- spec.add_development_dependency "pry"
44
- spec.add_development_dependency "yard", "~> 0.8.7.4"
45
- spec.add_development_dependency "webmock", "~> 1.19.0"
42
+ spec.add_development_dependency 'bundler'
43
+ spec.add_development_dependency 'rake'
44
+ spec.add_development_dependency 'rspec', '~> 3.1.0'
45
+ spec.add_development_dependency 'pry'
46
+ spec.add_development_dependency 'yard', '~> 0.8.7.4'
47
+ spec.add_development_dependency 'webmock', '~> 1.19.0'
48
+ spec.add_development_dependency 'codeclimate-test-reporter'
49
+
50
+
51
+ spec.post_install_message = "This gem requires phantomjs. Install it using 'brew update && brew install phantomjs'"
46
52
  end
@@ -0,0 +1,47 @@
1
+ # For more information about each property, visit the GitHub documentation: https://github.com/krausefx/deliver
2
+ # Everything next to a # is a comment and will be ignored
3
+
4
+
5
+
6
+ ########################################
7
+ # App Metadata
8
+ ########################################
9
+
10
+ # The app identifier is required
11
+ app_identifier "[[APP_IDENTIFIER]]"
12
+
13
+
14
+ # This folder has to include one folder for each language
15
+ # More information about automatic screenshot upload:
16
+ screenshots_path "./deliver/screenshots/"
17
+
18
+
19
+ # version '1.2' # you can pass this if you want to verify the version number with the ipa file
20
+
21
+ config_json_folder './deliver'
22
+
23
+
24
+ ########################################
25
+ # Building and Testing
26
+ ########################################
27
+
28
+ # Dynamic generation of the ipa file
29
+ # I'm using Shenzhen by Mattt, but you can use any build tool you want
30
+ # Remove the whole block if you do not want to upload an ipa file
31
+ ipa do
32
+ # Add any code you want, like incrementing the build
33
+ # number or changing the app identifier
34
+
35
+ # system("ipa build") # build your project using Shenzhen
36
+ "./[[APP_NAME]].ipa" # Tell 'Deliver' where it can find the finished ipa file
37
+ end
38
+
39
+ # ipa "./latest.ipa" # this can be used, if you prefer manually building the ipa file
40
+
41
+ # unit_tests do
42
+ # system("xctool test")
43
+ # end
44
+
45
+ success do
46
+ system("say 'Successfully deployed a new version.'")
47
+ end
@@ -0,0 +1,62 @@
1
+ # This is the example Deliverfile
2
+ # For more information about each property, visit the GitHub documentation: https://github.com/krausefx/deliver
3
+ #
4
+ # You can remove those parts you don't need
5
+ #
6
+ # A list of available language codes can be found here: https://github.com/krausefx/deliver#available-language-codes
7
+ #
8
+ # Everything next to a # is a comment and will be ignored
9
+
10
+
11
+
12
+ ########################################
13
+ # App Metadata
14
+ ########################################
15
+
16
+
17
+
18
+ # The app identifier is required
19
+ app_identifier "[Your App Identifier, e.g. at.felixkrause.app_name]"
20
+
21
+
22
+ # This folder has to include one folder for each language
23
+ # More information about automatic screenshot upload:
24
+ screenshots_path "./screenshots"
25
+
26
+
27
+ # version '1.2' # you can pass this if you want to verify the version number with the ipa file
28
+ #
29
+ # title({
30
+ # "en-US" => "Your App Name"
31
+ # })
32
+ #
33
+ # changelog({
34
+ # "en-US" => "iPhone 6 (Plus) Support"
35
+ # })
36
+
37
+
38
+
39
+ ########################################
40
+ # Building and Testing
41
+ ########################################
42
+
43
+ # Dynamic generation of the ipa file
44
+ # I'm using Shenzhen by Mattt, but you can use any build tool you want
45
+ # Remove the whole block if you do not want to upload an ipa file
46
+ ipa do
47
+ # Add any code you want, like incrementing the build
48
+ # number or changing the app identifier
49
+
50
+ # system("ipa build") # build your project using Shenzhen
51
+ "./[[APP_NAME]].ipa" # Tell 'Deliver' where it can find the finished ipa file
52
+ end
53
+
54
+ # ipa "./latest.ipa" # this can be used, if you prefer manually building the ipa file
55
+
56
+ # unit_tests do
57
+ # system("xctool test")
58
+ # end
59
+
60
+ success do
61
+ system("say 'Successfully deployed a new version.'")
62
+ end
@@ -0,0 +1,4 @@
1
+ Put all screenshots you want to use inside the folder of its language (e.g. en-US).
2
+ The device type will automatically be recognized using the image resolution.
3
+
4
+ The name of the screenshots itself does not matter
data/lib/deliver.rb CHANGED
@@ -1,4 +1,23 @@
1
- require "deliver/version"
1
+ require 'deliver/version'
2
+ require 'deliver/helper'
3
+ require 'deliver/app'
4
+ require 'deliver/app_metadata'
5
+ require 'deliver/metadata_item'
6
+ require 'deliver/app_screenshot'
7
+ require 'deliver/itunes_connect'
8
+ require 'deliver/itunes_search_api'
9
+ require 'deliver/itunes_transporter'
10
+ require 'deliver/deliverfile/deliverfile'
11
+ require 'deliver/deliverfile/deliverfile_creator'
12
+ require 'deliver/deliverer'
13
+ require 'deliver/ipa_uploader'
14
+ require 'deliver/languages'
15
+ require 'deliver/pdf_generator'
16
+ require 'deliver/dependency_checker'
17
+ require 'deliver/deliver_process'
18
+
19
+ # Third Party code
20
+ require 'colored'
2
21
 
3
22
  module Deliver
4
23
  # Your code goes here...
@@ -0,0 +1,146 @@
1
+ module Deliver
2
+ class App
3
+ attr_accessor :apple_id, :app_identifier, :metadata
4
+
5
+
6
+ # Defines the different states of the app
7
+ #
8
+ # As specified by Apple: https://developer.apple.com/library/ios/documentation/LanguagesUtilities/Conceptual/iTunesConnect_Guide/Chapters/ChangingAppStatus.html
9
+ module AppStatus
10
+ PREPARE_FOR_SUBMISSION = "Prepare for Submission"
11
+ WAITING_FOR_REVIEW = "Waiting For Review"
12
+ IN_REVIEW = "In Review"
13
+ UPLOAD_RECEIVED = "Upload Received"
14
+ PENDING_DEVELOPER_RELEASE = "Pending Developer Release"
15
+ PROCESSING_FOR_APP_STORE = "Processing for App Store"
16
+ READY_FOR_SALE = "Ready for Sale"
17
+ REJECTED = "Rejected"
18
+
19
+
20
+ # Unused app states
21
+ # PENDING_APPLE_RELASE="Pending Apple Release"
22
+ # PENDING_CONTRACT = "Pending Contract"
23
+ # WAITING_FOR_EXPORT_COMPLIANCE = "Waiting For Export Compliance"
24
+ # METADATA_REJECTED = "Metadata Rejected"
25
+ # REMOVED_FROM_SALE = "Removed From Sale"
26
+ # DEVELOPER_REJECTED = "Developer Rejected" # equals PREPARE_FOR_SUBMISSION
27
+ # DEVELOPER_REMOVED_FROM_SALE = "Developer Removed From Sale"
28
+ # INVALID_BINARY = "Invalid Binary"
29
+ end
30
+
31
+ # @param apple_id The Apple ID of the app you want to modify or update. This ID has usually 9 digits
32
+ # @param app_identifier If you don't pass this, it will automatically be fetched from the Apple API
33
+ # which means it takes longer. If you **can** pass the app_identifier (e.g. com.facebook.Facebook) do it
34
+ def initialize(apple_id: nil, app_identifier: nil)
35
+ self.apple_id = (apple_id || '').to_s.gsub('id', '').to_i
36
+ self.app_identifier = app_identifier
37
+
38
+ if apple_id and not app_identifier
39
+ # Fetch the app identifier based on the given Apple ID
40
+ self.app_identifier = Deliver::ItunesSearchApi.fetch_bundle_identifier(apple_id)
41
+ elsif app_identifier and not apple_id
42
+ # Fetch the Apple ID based on the given app identifier
43
+ begin
44
+ self.apple_id = Deliver::ItunesSearchApi.fetch_by_identifier(app_identifier)['trackId']
45
+ rescue
46
+ Helper.log.fatal "Could not find Apple ID based on the app identifier '#{app_identifier}'. Maybe the app is not in the AppStore yet?"
47
+ raise "Please pass a valid Apple ID using 'apple_id'".red
48
+ end
49
+ end
50
+ end
51
+
52
+ def to_s
53
+ "#{apple_id} - #{app_identifier}"
54
+ end
55
+
56
+ #####################################################
57
+ # @!group Interacting with iTunesConnect
58
+ #####################################################
59
+
60
+ # The iTC handler which is used to interact with the iTunesConnect backend
61
+ def itc
62
+ @itc ||= Deliver::ItunesConnect.new
63
+ end
64
+
65
+ # This method fetches the current app status from iTunesConnect.
66
+ # This method may take some time to execute, since it uses frontend scripting under the hood.
67
+ # @return the current App Status defined at {Deliver::App::AppStatus}, like "Waiting For Review"
68
+ def get_app_status
69
+ itc.get_app_status(self)
70
+ end
71
+
72
+ # This method fetches the app version of the latest published version
73
+ # This method may take some time to execute, since it uses frontend scripting under the hood.
74
+ # @return the currently active app version, which in production
75
+ def get_live_version
76
+ itc.get_live_version(self)
77
+ end
78
+
79
+
80
+ #####################################################
81
+ # @!group Updating the App Metadata
82
+ #####################################################
83
+
84
+ # Use this method to change the default download location for the metadata packages
85
+ def set_metadata_directory(dir)
86
+ raise "Can not change metadata directory after accessing metadata of an app" if @metadata
87
+ @metadata_dir = dir
88
+ end
89
+
90
+ # @return the path to the directy in which the itmsp files will be downloaded
91
+ def get_metadata_directory
92
+ return @metadata_dir if @metadata_dir
93
+ return "./spec/fixtures/packages/" if Helper.is_test?
94
+ return "./"
95
+ end
96
+
97
+ # Access to update the metadata of this app
98
+ #
99
+ # The first time accessing this, will take some time, since it's downloading
100
+ # the latest version from iTC.
101
+ #
102
+ # Don't forget to call {#upload_metadata!} once you are finished
103
+ # @return [Deliver::AppMetadata] the latest metadata of this app
104
+ def metadata
105
+ @metadata ||= Deliver::AppMetadata.new(self, get_metadata_directory)
106
+ end
107
+
108
+ # Was the app metadata already downloaded?
109
+ def metadata_downloaded?
110
+ @metadata != nil
111
+ end
112
+
113
+
114
+ # # Uploads a new app icon to iTunesConnect. This uses a headless browser
115
+ # # which makes this command quite slow.
116
+ # # @param (path) a path to the new app icon. The image must have the resolution of 1024x1024
117
+ # def update_app_icon!(path)
118
+ # itc.update_app_icon!(self, path)
119
+ # end
120
+
121
+ #####################################################
122
+ # @!group Destructive/Constructive methods
123
+ #####################################################
124
+
125
+ # This method creates a new version of your app using the
126
+ # iTunesConnect frontend. This will happen directly after calling
127
+ # this method.
128
+ # @param version_number (String) the version number as string for
129
+ # the new version that should be created
130
+ def create_new_version!(version_number)
131
+ itc.create_new_version!(self, version_number)
132
+ end
133
+
134
+ # This method has to be called, after modifying the values of .metadata.
135
+ # It will take care of uploading all changes to Apple.
136
+ # This method might take a few minutes to run
137
+ # @return [bool] true on success
138
+ # @raise [Deliver::TransporterTransferError]
139
+ # @raise [Deliver::TransporterInputError]
140
+ def upload_metadata!
141
+ raise "You first have to modify the metadata using app.metadata.setDescription" unless @metadata
142
+
143
+ self.metadata.upload!
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,487 @@
1
+ require 'nokogiri'
2
+
3
+ module Deliver
4
+ class AppMetadataError < StandardError
5
+ end
6
+ class AppMetadataParameterError < StandardError
7
+ end
8
+
9
+ class AppMetadata
10
+ ITUNES_NAMESPACE = "http://apple.com/itunes/importer"
11
+ METADATA_FILE_NAME = "metadata.xml"
12
+ MAXIMUM_NUMBER_OF_SCREENSHOTS = 5
13
+
14
+ # @return Data contains all information for this app, including the unmodified one
15
+ attr_accessor :information
16
+ # data = {
17
+ # 'en-US' => {
18
+ # title: {
19
+ # value: "Something",
20
+ # modified: false
21
+ # },
22
+ # version_whats_new: {
23
+ # value: "Some text",
24
+ # modified: true
25
+ # }
26
+ # screenshots: {
27
+ # '45' => [
28
+ # Screenshot1
29
+ # ]
30
+ # }
31
+ # }
32
+ # }
33
+
34
+ private_constant :METADATA_FILE_NAME, :MAXIMUM_NUMBER_OF_SCREENSHOTS
35
+
36
+ INVALID_LANGUAGE_ERROR = "The specified language could not be found. Make sure it is available in Deliver::Languages::ALL_LANGUAGES"
37
+
38
+ # You don't have to manually create an AppMetadata object. It will
39
+ # be created when you access the app's metadata ({Deliver::App#metadata})
40
+ # @param app [Deliver::App] The app this metadata is from/for
41
+ # @param dir [String] The app this metadata is from/for
42
+ # @param redownload_package [bool] When true
43
+ # the current package will be downloaded from iTC before you can
44
+ # modify any values. This should only be false for unit tests
45
+ # @raise (AppMetadataParameterError) Is thrown when don't pass a correct app object
46
+ def initialize(app, dir, redownload_package = true)
47
+ raise AppMetadataParameterError.new("No valid Deliver::App given") unless app.kind_of?Deliver::App
48
+
49
+ @metadata_dir = dir
50
+ @app = app
51
+
52
+ if self.class == AppMetadata
53
+ if redownload_package
54
+ # Delete the one that may exists already
55
+ unless Helper.is_test?
56
+ `rm -fr #{dir}/*.itmsp`
57
+ end
58
+
59
+ # we want to update the metadata, so first we have to download the existing one
60
+ transporter.download(app, dir)
61
+
62
+ # Parse the downloaded package
63
+ parse_package(dir)
64
+ else
65
+ # use_data contains the data to be used. This is the case for unit tests
66
+ parse_package(dir)
67
+ end
68
+ end
69
+ end
70
+
71
+ def information
72
+ @information ||= {}
73
+ end
74
+
75
+ # Verifies the if the version of iTunesConnect matches the one you pass as parameter
76
+ def verify_version(version_number)
77
+ xml_version = self.fetch_value("//x:version").first['string']
78
+ raise "Version mismatch: on iTunesConnect the latest version is '#{xml_version}', you specified '#{version_number}'" if xml_version != version_number
79
+ true
80
+ end
81
+
82
+ # Adds a new locale (language) to the given app
83
+ # @param language (Deliver::Languages::ALL_LANGUAGES) the language you want to
84
+ # this app
85
+ # @raise (AppMetadataParameterError) Is thrown when don't pass a correct hash with correct language codes.
86
+ # @return (Bool) Is true, if the language was created. False, when the language alreade existed
87
+ def add_new_locale(language)
88
+ unless Deliver::Languages::ALL_LANGUAGES.include?language
89
+ raise "Language '#{language}' is invalid. It must be in #{Deliver::Languages::ALL_LANGUAGES}."
90
+ end
91
+
92
+ if information[language] != nil
93
+ Helper.log.info("Locale '#{language}' already exists. Can not create it again.")
94
+ return false
95
+ end
96
+
97
+
98
+ locales = fetch_value("//x:locales").first
99
+
100
+ new_locale = @data.create_element('locale')
101
+ new_locale['name'] = language
102
+ locales << new_locale
103
+
104
+ # Title is the only thing which is required by iTC
105
+ default_title = information.values.first[:title][:value]
106
+
107
+ title = @data.create_element('title')
108
+ title.content = default_title
109
+ new_locale << title
110
+
111
+ Helper.log.info("Successfully created the new locale '#{language}'. The default title '#{default_title}' was set, since it's required by iTunesConnect.")
112
+ Helper.log.info("You can update the title using 'app.metadata.update_title'")
113
+
114
+ information[language] ||= {}
115
+ information[language][:title] = { value: default_title, modified: true}
116
+
117
+ true
118
+ end
119
+
120
+
121
+ #####################################################
122
+ # @!group Updating metadata information
123
+ #####################################################
124
+
125
+ # Updates the app title
126
+ # @param (Hash) hash The hash should contain the correct language codes ({Deliver::Languages})
127
+ # as keys.
128
+ # @raise (AppMetadataParameterError) Is thrown when don't pass a correct hash with correct language codes.
129
+ def update_title(hash)
130
+ update_metadata_key(:title, hash)
131
+ end
132
+
133
+ # Updates the app description which is shown in the AppStore
134
+ # @param (Hash) hash The hash should contain the correct language codes ({Deliver::Languages})
135
+ # as keys.
136
+ # @raise (AppMetadataParameterError) Is thrown when don't pass a correct hash with correct language codes.
137
+ def update_description(hash)
138
+ update_metadata_key(:description, hash)
139
+ end
140
+
141
+ # Updates the app changelog of the latest version
142
+ # @param (Hash) hash The hash should contain the correct language codes ({Deliver::Languages})
143
+ # as keys.
144
+ # @raise (AppMetadataParameterError) Is thrown when don't pass a correct hash with correct language codes.
145
+ def update_changelog(hash)
146
+ update_metadata_key(:version_whats_new, hash)
147
+ end
148
+
149
+ # Updates the Marketing URL
150
+ # @param (Hash) hash The hash should contain the correct language codes ({Deliver::Languages})
151
+ # as keys.
152
+ # @raise (AppMetadataParameterError) Is thrown when don't pass a correct hash with correct language codes.
153
+ def update_marketing_url(hash)
154
+ update_metadata_key(:software_url, hash)
155
+ end
156
+
157
+ # Updates the Support URL
158
+ # @param (Hash) hash The hash should contain the correct language codes ({Deliver::Languages})
159
+ # as keys.
160
+ # @raise (AppMetadataParameterError) Is thrown when don't pass a correct hash with correct language codes.
161
+ def update_support_url(hash)
162
+ update_metadata_key(:support_url, hash)
163
+ end
164
+
165
+ # Updates the Privacy URL
166
+ # @param (Hash) hash The hash should contain the correct language codes ({Deliver::Languages})
167
+ # as keys.
168
+ # @raise (AppMetadataParameterError) Is thrown when don't pass a correct hash with correct language codes.
169
+ def update_privacy_url(hash)
170
+ update_metadata_key(:privacy_url, hash)
171
+ end
172
+
173
+ # Updates the app keywords
174
+ # @param (Hash) hash The hash should contain the correct language codes ({Deliver::Languages})
175
+ # as keys. The value should be an array of keywords (each keyword is a string)
176
+ # @raise (AppMetadataParameterError) Is thrown when don't pass a correct hash with correct language codes.
177
+ def update_keywords(hash)
178
+ update_localized_value('keywords', hash) do |field, keywords, language|
179
+ raise AppMetadataParameterError.new("Parameter needs to be a hash (each language) with an array of keywords in it (given: #{hash})") unless keywords.kind_of?Array
180
+
181
+ if keywords.sort != information[language][:keywords][:value].sort
182
+ field.children.remove # remove old keywords
183
+
184
+ node_set = Nokogiri::XML::NodeSet.new(@data)
185
+ keywords.each do |word|
186
+ keyword = Nokogiri::XML::Node.new('keyword', @data)
187
+ keyword.content = word
188
+ node_set << keyword
189
+ end
190
+
191
+ field.children = node_set
192
+
193
+ information[language][:keywords] = { value: keywords, modified: true }
194
+ end
195
+ end
196
+ end
197
+
198
+ #####################################################
199
+ # @!group Screenshot related
200
+ #####################################################
201
+
202
+ # Removes all currently enabled screenshots for the given language.
203
+ # @param (String) language The language, which has to be in this list: {Deliver::Languages}.
204
+ def clear_all_screenshots(language)
205
+ raise AppMetadataParameterError.new(INVALID_LANGUAGE_ERROR) unless Languages::ALL_LANGUAGES.include?language
206
+
207
+ update_localized_value('software_screenshots', {language => {}}) do |field, useless, language|
208
+ field.children.remove # remove all the screenshots
209
+ end
210
+ information[language][:screenshots] = []
211
+ true
212
+ end
213
+
214
+ # Appends another screenshot to the already existing ones
215
+ # @param (String) language The language, which has to be in this list: {Deliver::Languages}.
216
+ # @param (Deliver::AppScreenshot) app_screenshot The screenshot you want to add to the app metadata.
217
+ # @raise (AppMetadataParameterError) When there are already 5 screenshots (MAXIMUM_NUMBER_OF_SCREENSHOTS).
218
+
219
+ def add_screenshot(language, app_screenshot)
220
+ raise AppMetadataParameterError.new(INVALID_LANGUAGE_ERROR) unless Languages::ALL_LANGUAGES.include?language
221
+
222
+ create_locale_if_not_exists(language)
223
+
224
+ # Fetch the 'software_screenshots' node (array) for the specific locale
225
+ locales = self.fetch_value("//x:locale[@name='#{language}']")
226
+
227
+ screenshots = self.fetch_value("//x:locale[@name='#{language}']/x:software_screenshots").first
228
+
229
+ if not screenshots or screenshots.children.count == 0
230
+ screenshots.remove if screenshots
231
+
232
+ # First screenshot ever
233
+ screenshots = Nokogiri::XML::Node.new('software_screenshots', @data)
234
+ locales.first << screenshots
235
+
236
+ node_set = Nokogiri::XML::NodeSet.new(@data)
237
+ node_set << app_screenshot.create_xml_node(@data, 1)
238
+ screenshots.children = node_set
239
+ else
240
+ # There is already at least one screenshot
241
+ next_index = 1
242
+ screenshots.children.each do |screen|
243
+ if screen['display_target'] == app_screenshot.screen_size
244
+ next_index += 1
245
+ end
246
+ end
247
+
248
+ if next_index > MAXIMUM_NUMBER_OF_SCREENSHOTS
249
+ raise AppMetadataParameterError.new("Only #{MAXIMUM_NUMBER_OF_SCREENSHOTS} screenshots are allowed per language per device type (#{app_screenshot.screen_size})")
250
+ end
251
+
252
+ # Ready for storing the screenshot into the metadata.xml now
253
+ screenshots.children.after(app_screenshot.create_xml_node(@data, next_index))
254
+ end
255
+
256
+ information[language][:screenshots] << app_screenshot
257
+
258
+ app_screenshot.store_file_inside_package(@package_path)
259
+ end
260
+
261
+ # This method will clear all screenshots and set the new ones you pass
262
+ # @param new_screenshots
263
+ # +code+
264
+ # {
265
+ # 'de-DE' => [
266
+ # AppScreenshot.new('path/screenshot1.png', Deliver::ScreenSize::IOS_35),
267
+ # AppScreenshot.new('path/screenshot2.png', Deliver::ScreenSize::IOS_40),
268
+ # AppScreenshot.new('path/screenshot3.png', Deliver::ScreenSize::IOS_IPAD)
269
+ # ]
270
+ # }
271
+ # This method uses {#clear_all_screenshots} and {#add_screenshot} under the hood.
272
+ # @return [bool] true if everything was successful
273
+ # @raise [AppMetadataParameterError] error is raised when parameters are invalid
274
+ def set_all_screenshots(new_screenshots)
275
+ error_text = "Please pass a hash, containing an array of AppScreenshot objects"
276
+ raise AppMetadataParameterError.new(error_text) unless new_screenshots.kind_of?Hash
277
+
278
+ new_screenshots.each do |key, value|
279
+ if key.kind_of?String and value.kind_of?Array and value.count > 0 and value.first.kind_of?AppScreenshot
280
+
281
+ self.clear_all_screenshots(key)
282
+
283
+ value.each do |screen|
284
+ add_screenshot(key, screen)
285
+ end
286
+ else
287
+ raise AppMetadataParameterError.new(error_text)
288
+ end
289
+ end
290
+ true
291
+ end
292
+
293
+ # Automatically add all screenshots contained in the given directory to the app.
294
+ #
295
+ # This method will automatically detect which device type each screenshot is.
296
+ #
297
+ # This will also clear all existing screenshots before setting the new ones.
298
+ # @param (Hash) hash A hash containing a different path for each locale ({Deliver::Languages::ALL_LANGUAGES})
299
+ def set_screenshots_for_each_language(hash)
300
+ raise AppMetadataParameterError.new("Parameter needs to be an hash, containg strings with the new description") unless hash.kind_of?Hash
301
+
302
+ hash.each do |language, current_path|
303
+ resulting_path = "#{current_path}/*.png"
304
+
305
+ raise AppMetadataParameterError.new(INVALID_LANGUAGE_ERROR) unless Languages::ALL_LANGUAGES.include?language
306
+
307
+ if Dir[resulting_path].count == 0
308
+ Helper.log.error("No screenshots found at the given path '#{resulting_path}'")
309
+ else
310
+ self.clear_all_screenshots(language)
311
+
312
+ Dir[resulting_path].sort.each do |path|
313
+ add_screenshot(language, Deliver::AppScreenshot.new(path))
314
+ end
315
+ end
316
+ end
317
+
318
+ true
319
+ end
320
+
321
+ # This method will run through all the available locales, check if there is
322
+ # a folder for this language (e.g. 'en-US') and use all screenshots in there
323
+ # @param (String) path A path to the folder, which contains a folder for each locale
324
+ def set_all_screenshots_from_path(path)
325
+ raise AppMetadataParameterError.new("Parameter needs to be a path (string)") unless path.kind_of?String
326
+
327
+ found = false
328
+ Deliver::Languages::ALL_LANGUAGES.each do |language|
329
+ full_path = path + "/#{language}"
330
+ if File.directory?(full_path)
331
+ found = true
332
+ set_screenshots_for_each_language({
333
+ language => full_path
334
+ })
335
+ end
336
+ end
337
+ return found
338
+ end
339
+
340
+
341
+ #####################################################
342
+ # @!group Manually fetching elements from the metadata.xml
343
+ #####################################################
344
+
345
+ # Directly fetch XML nodes from the metadata.xml.
346
+ # @example Fetch all keywords
347
+ # fetch_value("//x:keyword")
348
+ # @example Fetch a specific locale
349
+ # fetch_value("//x:locale[@name='de-DE']")
350
+ # @example Fetch the node that contains all screenshots for a specific language
351
+ # fetch_value("//x:locale[@name='de-DE']/x:software_screenshots")
352
+ # @return the requests XML nodes or node set
353
+ def fetch_value(xpath)
354
+ @data.xpath(xpath, "x" => ITUNES_NAMESPACE)
355
+ end
356
+
357
+
358
+ #####################################################
359
+ # @!group Uploading the updated metadata
360
+ #####################################################
361
+
362
+ # Actually uploads the updated metadata to Apple.
363
+ # This method might take a while.
364
+ # @raise (TransporterTransferError) When something goes wrong when uploading
365
+ # the metadata/app
366
+ def upload!
367
+ unless Helper.is_test?
368
+ # First: Write the current XML state to disk
369
+ File.write("#{@package_path}/#{METADATA_FILE_NAME}", @data.to_xml)
370
+ end
371
+
372
+ transporter.upload(@app, @metadata_dir)
373
+ end
374
+
375
+ private
376
+ def update_metadata_key(key, hash)
377
+ update_localized_value(key, hash) do |field, new_val, language|
378
+ raise AppMetadataParameterError.new("Parameter needs to be an hash, containg strings.") unless new_val.kind_of?String
379
+ if field.content != new_val
380
+ field.content = new_val
381
+ information[language][key] = { value: new_val, modified: true }
382
+ end
383
+ end
384
+ end
385
+
386
+ # @return (Deliver::ItunesTransporter) The iTunesTranspoter which is
387
+ # used to upload/download the app metadata.
388
+ def transporter
389
+ @transporter ||= ItunesTransporter.new
390
+ end
391
+
392
+ def update_localized_value(xpath_name, new_value)
393
+ raise AppMetadataParameterError.new("Please pass a hash of languages to this method") unless new_value.kind_of?Hash
394
+ raise AppMetadataParameterError.new("Please pass a block, which updates the resulting node") unless block_given?
395
+
396
+ xpath_name = xpath_name.to_s
397
+
398
+ # Run through all the locales given by the 'user'
399
+ new_value.each do |language, value|
400
+ create_locale_if_not_exists(language)
401
+
402
+ locale = fetch_value("//x:locale[@name='#{language}']").first
403
+
404
+ raise AppMetadataParameterError.new("#{INVALID_LANGUAGE_ERROR} (#{language})") unless Languages::ALL_LANGUAGES.include?language
405
+
406
+
407
+ field = locale.search(xpath_name).first
408
+
409
+ if not field
410
+ # This entry does not exist yet, so we have to create it
411
+ field = Nokogiri::XML::Node.new(xpath_name, @data)
412
+ locale << field
413
+ end
414
+
415
+ yield(field, value, language)
416
+ Helper.log.info "Updated #{xpath_name} for locale #{language}"
417
+ end
418
+ end
419
+
420
+ def create_locale_if_not_exists(locale)
421
+ add_new_locale(locale) unless information[locale]
422
+ end
423
+
424
+ # Parses the metadata using nokogiri
425
+ def parse_package(path)
426
+ unless path.include?".itmsp"
427
+ path += "/#{@app.apple_id}.itmsp/"
428
+ end
429
+ @package_path = path
430
+
431
+ @data ||= Nokogiri::XML(File.read("#{path}/#{METADATA_FILE_NAME}"))
432
+ verify_package
433
+ clean_package
434
+ fill_in_data
435
+ end
436
+
437
+ # Checks if there is a non live version available
438
+ # (a new version, or a new app)
439
+ def verify_package
440
+ versions = fetch_value("//x:version")
441
+
442
+ raise AppMetadataError.new("metadata_token is missing. This package seems to be broken") if fetch_value("//x:metadata_token").count != 1
443
+ end
444
+
445
+ # Cleans up the package of stuff we do not want to modify/upload
446
+ def clean_package
447
+
448
+ # Remove the live version (if it exists)
449
+ versions = fetch_value("//x:version")
450
+ while versions.count > 1
451
+ versions.last.remove
452
+ versions = fetch_value("//x:version")
453
+ end
454
+ Helper.log.info "Modifying version '#{versions.first['string']}' of app #{@app.app_identifier}"
455
+
456
+ # Remove all GameCenter related code
457
+ fetch_value("//x:game_center").remove
458
+
459
+ fetch_value("//x:software_screenshots").remove
460
+ end
461
+
462
+ # This will fill in all information we got (from the downloaded metadata.xml file) into self.information
463
+ def fill_in_data
464
+ locales = fetch_value("//x:locale")
465
+ locales.each do |locale|
466
+ language = locale['name']
467
+ information[language] ||= {}
468
+
469
+ all_keys = [:title, :description, :version_whats_new, :software_url, :support_url, :privacy_url]
470
+
471
+ all_keys.each do |key|
472
+ information[language][key] = {
473
+ value: (locale.search(key.to_s).first.content rescue ''),
474
+ modified: false
475
+ }
476
+ end
477
+
478
+ information[language][:keywords] = { value: [], modified: false}
479
+ locale.search('keyword').each do |current|
480
+ information[language][:keywords][:value] << current.content
481
+ end
482
+
483
+ information[language][:screenshots] = []
484
+ end
485
+ end
486
+ end
487
+ end