spaceship 0.24.1 → 0.25.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 50850ceb32b1a3f3d079b3b9ea2fdbd51beadded
4
- data.tar.gz: 4d629b20fd53a3e8738c50ec14c58058b62ac944
3
+ metadata.gz: c8b9baae7a579a3b8791d93715b93f4ab301bdbb
4
+ data.tar.gz: 846ed6e24a001c17d75a5fb7ec6c551a697927ee
5
5
  SHA512:
6
- metadata.gz: 720445803f69421d4a57a4da5fc8fd45656e5bc78328df7fb0fe32ae5a651ff4415d6d0592e7cfa3af30734b29d2b8f23a7cd88be86d904128da6b6cfc8e7571
7
- data.tar.gz: 0a0ab3274518cd139b1b7a9d675d91f6d9ef8e9d5d1175a9b70e03b0f968d0bc1454d78b36a1b244f486a7391822502db5a72551a5ef9611fac93f4216aa7b14
6
+ metadata.gz: b56a873a114281b421cc7032582e203097e50e41406699510a4593c941db4cc8e42c77942a402e6acff9a67ab9cea08094ad565931543123989d56d5fa699e44
7
+ data.tar.gz: db26d7aa79f1ea99432bc45d2c142eddc7a86bc43404fe858a068a5f7296b1094333ac058710b3806e5e29978375732cfb412b10f8507c3f13ec37be42390ddd
data/README.md CHANGED
@@ -112,6 +112,24 @@ This requires you to install `pry` using `sudo gem install pry`. `pry` is not in
112
112
 
113
113
  ##### Open [iTunesConnect.md](docs/iTunesConnect.md) for code samples
114
114
 
115
+ ## 2 Step Verification
116
+
117
+ When your Apple account has 2 step verification enabled, you'll automatically be asked to verify your identity using your phone. The resulting session will be stored in `~/.spaceship/[email]/cookie`. The session should be valid for about one month, however there is no way to test this without actually waiting for over a month.
118
+
119
+ Since your CI system probably doesn't allow you to input values (like the verification code), you can use `spaceauth`:
120
+
121
+ ```sh
122
+ spaceauth -u apple@krausefx.com
123
+ ```
124
+
125
+ This will authenticate you and provide a string that can be transfered to your CI system:
126
+
127
+ ```
128
+ export FASTLANE_SESSION='---\n- !ruby/object:HTTP::Cookie\n name: DES5c148586dfd451e55afbaaa5f62418f91\n value: HSARMTKNSRVTWFla1+yO4gVPowH17VaaaxPFnUdMUegQZxqy1Ie1c2v6bM1vSOzIbuOmrl/FNenlScsd/NbF7/Lw4cpnL15jsyg0TOJwP32tC/NguPiyOaaaU+jrj4tf4uKdIywVaaaFSRVT\n domain: idmsa.apple.com\n for_domain: true\n path: "/"\n secure: true\n httponly: true\n expires: 2016-04-27 23:55:56.000000000 Z\n max_age: \n created_at: 2016-03-28 16:55:57.032086000 -07:00\n accessed_at: 2016-03-28 19:11:17.828141000 -07:00\n'
129
+ ```
130
+
131
+ Copy everything from `---\n` to your CI server and provide it as environment variable named `FASTLANE_SESSION`.
132
+
115
133
  ### Spaceship in use
116
134
 
117
135
  Most [fastlane tools](https://fastlane.tools) already use `spaceship`, like `sigh`, `cert`, `produce`, `pilot` and `boarding`.
data/bin/spaceship CHANGED
@@ -42,7 +42,7 @@ begin
42
42
  puts "Successfully logged in to iTunes Connect".green
43
43
  puts ""
44
44
  rescue
45
- puts "Could not login to iTunes Connect..."
45
+ puts "Could not login to iTunes Connect...".red
46
46
  end
47
47
  begin
48
48
  puts "Logging into the Developer Portal (#{username})..."
@@ -50,7 +50,7 @@ begin
50
50
  puts "Successfully logged in to the Developer Portal".green
51
51
  puts ""
52
52
  rescue
53
- puts "Could not login to the Developer Portal..."
53
+ puts "Could not login to the Developer Portal...".red
54
54
  end
55
55
 
56
56
  puts "---------------------------------------".green
@@ -148,6 +148,21 @@ module Spaceship
148
148
  @cookie.map(&:to_s).join(';')
149
149
  end
150
150
 
151
+ def store_cookie(path: nil)
152
+ path ||= persistent_cookie_path
153
+
154
+ # really important to specify the session to true
155
+ # otherwise myacinfo and more won't be stored
156
+ @cookie.save(path, :yaml, session: true)
157
+ return File.read(path)
158
+ end
159
+
160
+ def persistent_cookie_path
161
+ path = File.expand_path(File.join("~", ".spaceship", self.user, "cookie"))
162
+ FileUtils.mkdir_p(File.expand_path("..", path))
163
+ return path
164
+ end
165
+
151
166
  #####################################################
152
167
  # @!group Automatic Paging
153
168
  #####################################################
@@ -219,6 +234,90 @@ module Spaceship
219
234
  end
220
235
  end
221
236
 
237
+ # This method is used for both the Apple Dev Portal and iTunes Connect
238
+ # This will also handle 2 step verification
239
+ def send_shared_login_request(user, password)
240
+ # First we see if we have a stored cookie for 2 step enabled accounts
241
+ # this is needed as it stores the information on if this computer is a
242
+ # trusted one. In general I think spaceship clients should be trusted
243
+ load_session_from_file
244
+ # If this is a CI, the user can pass the session via environment variable
245
+ load_session_from_env
246
+
247
+ data = {
248
+ accountName: user,
249
+ password: password,
250
+ rememberMe: true
251
+ }
252
+
253
+ begin
254
+ # The below workaround is only needed for 2 step verified machines
255
+ # Due to escaping of cookie values we have a little workaround here
256
+ # By default the cookie jar would generate the following header
257
+ # DES5c148...=HSARM.......xaA/O69Ws/CHfQ==SRVT
258
+ # However we need the following
259
+ # DES5c148...="HSARM.......xaA/O69Ws/CHfQ==SRVT"
260
+ # There is no way to get the cookie jar value with " around the value
261
+ # so we manually modify the cookie (only this one) to be properly escaped
262
+ # Afterwards we pass this value manually as a header
263
+ # It's not enough to just modify @cookie, it needs to be done after self.cookie
264
+ # as a string operation
265
+ important_cookie = @cookie.store.entries.find { |a| a.name.include?("DES") }
266
+ if important_cookie
267
+ modified_cookie = self.cookie # returns a string of all cookies
268
+ unescaped_important_cookie = "#{important_cookie.name}=#{important_cookie.value}"
269
+ escaped_important_cookie = "#{important_cookie.name}=\"#{important_cookie.value}\""
270
+ modified_cookie.gsub!(unescaped_important_cookie, escaped_important_cookie)
271
+ end
272
+
273
+ response = request(:post) do |req|
274
+ req.url "https://idmsa.apple.com/appleauth/auth/signin?widgetKey=#{itc_service_key}"
275
+ req.body = data.to_json
276
+ req.headers['Content-Type'] = 'application/json'
277
+ req.headers['X-Requested-With'] = 'XMLHttpRequest'
278
+ req.headers['Accept'] = 'application/json, text/javascript'
279
+ req.headers["Cookie"] = modified_cookie if modified_cookie
280
+ end
281
+ rescue UnauthorizedAccessError
282
+ raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
283
+ end
284
+
285
+ # get woinst, wois, and itctx cookie values
286
+ request(:get, "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/wa/route?noext")
287
+ request(:get, "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa")
288
+
289
+ case response.status
290
+ when 403
291
+ raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
292
+ when 200
293
+ return response
294
+ else
295
+ if response["Location"] == "/auth" # redirect to 2 step auth page
296
+ handle_two_step(response)
297
+ return true
298
+ elsif (response.body || "").include?('invalid="true"')
299
+ # User Credentials are wrong
300
+ raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
301
+ elsif (response['Set-Cookie'] || "").include?("itctx")
302
+ raise "Looks like your Apple ID is not enabled for iTunes Connect, make sure to be able to login online"
303
+ else
304
+ info = [response.body, response['Set-Cookie']]
305
+ raise ITunesConnectError.new, info.join("\n")
306
+ end
307
+ end
308
+ end
309
+
310
+ def itc_service_key
311
+ return @service_key if @service_key
312
+ # We need a service key from a JS file to properly auth
313
+ js = request(:get, "https://itunesconnect.apple.com/itc/static-resources/controllers/login_cntrl.js")
314
+ @service_key ||= js.body.match(/itcServiceKey = '(.*)'/)[1]
315
+ end
316
+
317
+ #####################################################
318
+ # @!group Helpers
319
+ #####################################################
320
+
222
321
  def with_retry(tries = 5, &_block)
223
322
  return yield
224
323
  rescue Faraday::Error::ConnectionFailed, Faraday::Error::TimeoutError, AppleTimeoutError, Errno::EPIPE => ex # New Faraday version: Faraday::TimeoutError => ex
@@ -339,3 +438,5 @@ module Spaceship
339
438
  end
340
439
  end
341
440
  end
441
+
442
+ require 'spaceship/two_step_client'
@@ -34,7 +34,7 @@ module Spaceship
34
34
  upload_file(app_version, upload_file, '/upload/purple-video', content_provider_id, sso_token_for_video)
35
35
  end
36
36
 
37
- def upload_video_preview(app_version, upload_file, content_provider_id, sso_token_for_image)
37
+ def upload_trailer_preview(app_version, upload_file, content_provider_id, sso_token_for_image)
38
38
  upload_file(app_version, upload_file, '/upload/app-screenshot-image', content_provider_id, sso_token_for_image)
39
39
  end
40
40
 
@@ -12,12 +12,15 @@ module Spaceship
12
12
  class << self
13
13
  def from_path(path)
14
14
  raise "Image must exists at path: #{path}" unless File.exist?(path)
15
+
16
+ # md5 from original. keeping track of md5s allows to skip previously uploaded in deliver
17
+ content_md5 = Spaceship::Utilities.md5digest(path)
15
18
  path = remove_alpha_channel(path) if File.extname(path).casecmp('.png').zero?
16
19
 
17
20
  content_type = Utilities.content_type(path)
18
21
  self.new(
19
22
  file_path: path,
20
- file_name: File.basename(path),
23
+ file_name: 'ftl_' + content_md5 + '_' + File.basename(path),
21
24
  file_size: File.size(path),
22
25
  content_type: content_type,
23
26
  bytes: File.read(path)
@@ -70,6 +70,11 @@ module Spaceship
70
70
  [res[1].to_i, res[2].to_i]
71
71
  end
72
72
 
73
- module_function :content_type, :grab_video_preview, :portrait?, :resolution, :video_resolution
73
+ # @return (String) md5 checksum of given file
74
+ def md5digest(file_path)
75
+ Digest::MD5.hexdigest(File.read(file_path))
76
+ end
77
+
78
+ module_function :content_type, :grab_video_preview, :portrait?, :resolution, :video_resolution, :md5digest
74
79
  end
75
80
  end
@@ -32,29 +32,17 @@ module Spaceship
32
32
  end
33
33
 
34
34
  def send_login_request(user, password)
35
- response = request(:post, "https://idmsa.apple.com/IDMSWebAuth/authenticate", {
36
- appleId: user,
37
- accountPassword: password,
38
- appIdKey: api_key
39
- })
40
-
41
- if (response.body || "").include?("Your Apple ID or password was entered incorrectly")
42
- # User Credentials are wrong
43
- raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
44
- elsif (response.body || "").include?("Verify your identity")
45
- raise "spaceship / fastlane doesn't support 2 step enabled accounts yet. Please temporary disable 2 step verification until spaceship was updated."
46
- end
47
-
48
- case response.status
49
- when 302
50
- return response
51
- when 200
52
- raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
53
- else
54
- # Something went wrong. Was it invalid credentials or server issue
55
- info = [response.body, response['Set-Cookie']]
56
- raise UnexpectedResponse.new, info.join("\n")
57
- end
35
+ response = send_shared_login_request(user, password)
36
+ return response if self.cookie.include?("myacinfo")
37
+
38
+ # When the user has 2 step enabled, we might have to call this method again
39
+ # This only occurs when the user doesn't have a team on iTunes Connect
40
+ # For 2 step verification we use the iTunes Connect back-end
41
+ # which is enough to get the DES... cookie, however we don't get a valid
42
+ # myacinfo cookie at that point. That means, after getting the DES... cookie
43
+ # we have to send the login request again. This will then get us a valid myacinfo
44
+ # cookie, additionally to the DES... cookie
45
+ return send_shared_login_request(user, password)
58
46
  end
59
47
 
60
48
  # @return (Array) A list of all available teams
@@ -237,7 +237,7 @@ module Spaceship
237
237
  # })
238
238
  #
239
239
  # Available Values
240
- # https://github.com/KrauseFx/deliver/blob/master/Reference.md
240
+ # https://github.com/fastlane/fastlane/blob/master/deliver/Reference.md
241
241
  def update_rating(hash)
242
242
  raise "Must be a hash" unless hash.kind_of?(Hash)
243
243
 
@@ -457,6 +457,18 @@ module Spaceship
457
457
  client.release!(self.application.apple_id, self.version_id)
458
458
  end
459
459
 
460
+ #####################################################
461
+ # @!group Promo codes
462
+ #####################################################
463
+ def generate_promocodes!(quantity)
464
+ data = client.generate_app_version_promocodes!(
465
+ app_id: self.application.apple_id,
466
+ version_id: self.version_id,
467
+ quantity: quantity
468
+ )
469
+ Tunes::AppVersionGeneratedPromocodes.factory(data)
470
+ end
471
+
460
472
  # These methods takes care of properly parsing values that
461
473
  # are not returned in the right format, e.g. boolean as string
462
474
  def release_on_approval
@@ -0,0 +1,35 @@
1
+ module Spaceship
2
+ module Tunes
3
+ # Represents the information about the generation of promocodes
4
+ class AppVersionGeneratedPromocodes < TunesBase
5
+ # @return
6
+ attr_reader :effective_date
7
+ attr_reader :expiration_date
8
+ attr_reader :username
9
+ # the AppVersionPromocodes this relates to
10
+ attr_reader :version
11
+ # Array of String
12
+ attr_reader :codes
13
+
14
+ attr_mapping({
15
+ 'effectiveDate' => :effective_date,
16
+ 'expirationDate' => :expiration_date,
17
+ 'username' => :username
18
+ })
19
+
20
+ class << self
21
+ # Create a new object based on a hash.
22
+ # This is used to create a new object based on the server response.
23
+ def factory(attrs)
24
+ obj = self.new(attrs)
25
+ return obj
26
+ end
27
+ end
28
+
29
+ def setup
30
+ @version = Tunes::AppVersionPromocodes.factory(raw_data['version'])
31
+ @codes = raw_data['codes']
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,34 @@
1
+ module Spaceship
2
+ module Tunes
3
+ # Represents the information about remaining number of promo codes for an app version
4
+ class AppVersionPromocodes < TunesBase
5
+ # @return
6
+ attr_reader :app_id
7
+ attr_reader :app_name
8
+ attr_reader :version
9
+ attr_reader :platform
10
+ attr_reader :number_of_codes
11
+ attr_reader :maximum_number_of_codes
12
+ attr_reader :contract_file_name
13
+
14
+ attr_mapping({
15
+ 'id' => :app_id,
16
+ 'appName' => :app_name,
17
+ 'version' => :version,
18
+ 'platform' => :platform,
19
+ 'numberOfCodes' => :number_of_codes,
20
+ 'maximumNumberOfCodes' => :maximum_number_of_codes,
21
+ 'contractFileName' => :contract_file_name
22
+ })
23
+
24
+ class << self
25
+ # Create a new object based on a hash.
26
+ # This is used to create a new object based on the server response.
27
+ def factory(attrs)
28
+ obj = self.new(attrs)
29
+ return obj
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -376,6 +376,23 @@ module Spaceship
376
376
  tester.remove_from_app!(self.apple_id)
377
377
  end
378
378
 
379
+ #####################################################
380
+ # @!group Promo codes
381
+ #####################################################
382
+ def promocodes
383
+ data = client.app_promocodes(app_id: self.apple_id)
384
+ data.map do |attrs|
385
+ Tunes::AppVersionPromocodes.factory(attrs)
386
+ end
387
+ end
388
+
389
+ def promocodes_history
390
+ data = client.app_promocodes_history(app_id: self.apple_id)
391
+ data.map do |attrs|
392
+ Tunes::AppVersionGeneratedPromocodes.factory(attrs)
393
+ end
394
+ end
395
+
379
396
  # private to module
380
397
  def ensure_not_a_bundle
381
398
  # we only support applications
@@ -162,6 +162,7 @@ module Spaceship
162
162
  # last_name: "Krause",
163
163
  # review_email: "Contact email address for Apple",
164
164
  # phone_number: "0128383383",
165
+ # review_notes: "Review notes"
165
166
  #
166
167
  # # Optional Metadata:
167
168
  # privacy_policy_url: nil,
@@ -170,36 +171,18 @@ module Spaceship
170
171
  # review_password: nil,
171
172
  # encryption: false
172
173
  # }
174
+ # Note that iTC will pull a lot of this information from previous builds or the app store information,
175
+ # all of the required values must be set either in this hash or automatically for this to work
173
176
  def submit_for_beta_review!(metadata)
174
177
  parameters = {
175
178
  app_id: self.apple_id,
176
179
  train: self.train_version,
177
180
  build_number: self.build_version,
178
- platform: self.platform,
179
-
180
- # Required Metadata:
181
- changelog: "No changelog provided",
182
- description: "No app description provided",
183
- feedback_email: "contact@company.com",
184
- marketing_url: "http://marketing.com",
185
- first_name: "Felix",
186
- last_name: "Krause",
187
- review_email: "contact@company.com",
188
- phone_number: "0123456789",
189
- significant_change: false,
190
-
191
- # Optional Metadata:
192
- privacy_policy_url: nil,
193
- review_user_name: nil,
194
- review_password: nil,
195
- encryption: false
181
+ platform: self.platform
196
182
  }.merge(metadata)
197
183
 
198
184
  client.submit_testflight_build_for_review!(parameters)
199
185
 
200
- # Last, enable beta testing for this train (per iTC requirement). This will fail until the app has been approved for beta testing
201
- self.build_train.update_testing_status!(true, 'external', self)
202
-
203
186
  return parameters
204
187
  end
205
188
 
@@ -0,0 +1,69 @@
1
+ module Spaceship
2
+ module Tunes
3
+ class RecoveryDevice < TunesBase
4
+ # @return (String) ID provided by Apple
5
+ # @example
6
+ # "1801231651"
7
+ attr_accessor :device_id
8
+
9
+ # @return (String) The name of the device
10
+ # @example
11
+ # "Felix Krause's iPhone 6"
12
+ attr_accessor :name
13
+
14
+ # @return (Bool) This device looks suspicious [add emoji here]
15
+ # this will probably always be true, otherwise the device
16
+ # doesn't show up
17
+ # @example
18
+ # true
19
+ attr_accessor :trusted
20
+
21
+ # @return (Bool)
22
+ # @example
23
+ # true
24
+ attr_accessor :status
25
+
26
+ # @return (String) Remote URL to an image representing this device
27
+ # This shows the attention to detail by Apple <3
28
+ # @example
29
+ # "https://appleid.cdn-apple.com/static/deviceImages-5.0/iPhone/iPhone8,1-e4e7e8-dadcdb/online-sourcelist__3x.png"
30
+ # @example
31
+ # "https://appleid.cdn-apple.com/appleauth/static/bin/cb2613252489/images/sms@3x.png"
32
+ attr_accessor :device_image
33
+
34
+ # @return (String)
35
+ # @example
36
+ # "iPad Air"
37
+ # @example
38
+ # nil # e.g. when it's a phone number
39
+ attr_accessor :model_name
40
+
41
+ # @return (String)
42
+ # @example
43
+ # "79"
44
+ attr_accessor :last_two_digits
45
+
46
+ # @return (Number)
47
+ # @example
48
+ # 1446488271926
49
+ attr_accessor :update_date
50
+
51
+ attr_mapping(
52
+ 'id' => :device_id,
53
+ 'name' => :name,
54
+ 'trusted' => :trusted,
55
+ 'status' => :status,
56
+ 'imageLocation3x' => :device_image,
57
+ 'modelName' => :model_name,
58
+ 'lastTwoDigits' => :last_two_digits,
59
+ 'updateDate' => :update_date
60
+ )
61
+
62
+ class << self
63
+ def factory(attrs)
64
+ return self.new(attrs)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -23,6 +23,10 @@ require 'spaceship/tunes/app_trailer'
23
23
  require 'spaceship/tunes/tester'
24
24
  require 'spaceship/tunes/app_details'
25
25
  require 'spaceship/tunes/pricing_tier'
26
+ require 'spaceship/tunes/recovery_device'
27
+
28
+ require 'spaceship/tunes/app_version_promocodes'
29
+ require 'spaceship/tunes/app_version_generated_promocodes'
26
30
 
27
31
  # File Uploads
28
32
  require 'spaceship/du/utilities'
@@ -116,56 +116,9 @@ module Spaceship
116
116
  end
117
117
  end
118
118
 
119
- def service_key
120
- return @service_key if @service_key
121
- # We need a service key from a JS file to properly auth
122
- js = request(:get, "https://itunesconnect.apple.com/itc/static-resources/controllers/login_cntrl.js")
123
- @service_key ||= js.body.match(/itcServiceKey = '(.*)'/)[1]
124
- end
125
-
126
119
  def send_login_request(user, password)
127
120
  clear_user_cached_data
128
-
129
- data = {
130
- accountName: user,
131
- password: password,
132
- rememberMe: true
133
- }
134
-
135
- begin
136
- response = request(:post) do |req|
137
- req.url "https://idmsa.apple.com/appleauth/auth/signin?widgetKey=#{service_key}"
138
- req.body = data.to_json
139
- req.headers['Content-Type'] = 'application/json'
140
- req.headers['X-Requested-With'] = 'XMLHttpRequest'
141
- req.headers['Accept'] = 'application/json, text/javascript'
142
- end
143
- rescue UnauthorizedAccessError
144
- raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
145
- end
146
-
147
- # get woinst, wois, and itctx cookie values
148
- request(:get, "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/wa/route?noext")
149
- request(:get, "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa")
150
-
151
- case response.status
152
- when 403
153
- raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
154
- when 200
155
- return response
156
- else
157
- if response["Location"] == "/auth" # redirect to 2 step auth page
158
- raise "spaceship / fastlane doesn't support 2 step enabled accounts yet. Please temporary disable 2 step verification until spaceship was updated."
159
- elsif (response.body || "").include?('invalid="true"')
160
- # User Credentials are wrong
161
- raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
162
- elsif (response['Set-Cookie'] || "").include?("itctx")
163
- raise "Looks like your Apple ID is not enabled for iTunes Connect, make sure to be able to login online"
164
- else
165
- info = [response.body, response['Set-Cookie']]
166
- raise ITunesConnectError.new, info.join("\n")
167
- end
168
- end
121
+ send_shared_login_request(user, password)
169
122
  end
170
123
 
171
124
  # rubocop:disable Metrics/CyclomaticComplexity
@@ -178,7 +131,8 @@ module Spaceship
178
131
 
179
132
  if data.fetch('sectionErrorKeys', []).count == 0 and
180
133
  data.fetch('sectionInfoKeys', []).count == 0 and
181
- data.fetch('sectionWarningKeys', []).count == 0
134
+ data.fetch('sectionWarningKeys', []).count == 0 and
135
+ data.fetch('validationErrors', []).count == 0
182
136
 
183
137
  logger.debug("Request was successful")
184
138
  end
@@ -203,7 +157,8 @@ module Spaceship
203
157
  end
204
158
 
205
159
  errors = handle_response_hash.call(data)
206
- errors += data.fetch('sectionErrorKeys') if data['sectionErrorKeys']
160
+ errors += data.fetch('sectionErrorKeys', [])
161
+ errors += data.fetch('validationErrors', [])
207
162
 
208
163
  # Sometimes there is a different kind of error in the JSON response
209
164
  # e.g. {"warn"=>nil, "error"=>["operation_failed"], "info"=>nil}
@@ -556,6 +511,9 @@ module Spaceship
556
511
  def update_build_trains!(app_id, testing_type, data)
557
512
  raise "app_id is required" unless app_id
558
513
 
514
+ # The request fails if this key is present in the data
515
+ data.delete("dailySubmissionCountByPlatform")
516
+
559
517
  r = request(:post) do |req|
560
518
  req.url "ra/apps/#{app_id}/testingTypes/#{testing_type}/trains/"
561
519
  req.body = data.to_json
@@ -622,6 +580,7 @@ module Spaceship
622
580
  handle_itc_response(r.body)
623
581
  end
624
582
 
583
+ # rubocop:disable Metrics/ParameterLists
625
584
  def submit_testflight_build_for_review!(app_id: nil, train: nil, build_number: nil, platform: 'ios',
626
585
  # Required Metadata:
627
586
  changelog: nil,
@@ -638,6 +597,7 @@ module Spaceship
638
597
  privacy_policy_url: nil,
639
598
  review_user_name: nil,
640
599
  review_password: nil,
600
+ review_notes: nil,
641
601
  encryption: false)
642
602
 
643
603
  build_info = get_build_info_for_review(app_id: app_id, train: train, build_number: build_number, platform: platform)
@@ -660,6 +620,7 @@ module Spaceship
660
620
  build_info['testInfo']['reviewEmail']['value'] = review_email if review_email
661
621
  build_info['testInfo']['reviewUserName']['value'] = review_user_name if review_user_name
662
622
  build_info['testInfo']['reviewPassword']['value'] = review_password if review_password
623
+ build_info['testInfo']['reviewNotes']['value'] = review_notes if review_notes
663
624
 
664
625
  r = request(:post) do |req| # same URL, but a POST request
665
626
  req.url "ra/apps/#{app_id}/platforms/#{platform}/trains/#{train}/builds/#{build_number}/submit/start"
@@ -677,6 +638,7 @@ module Spaceship
677
638
  encryption_info: encryption_info,
678
639
  encryption: encryption)
679
640
  end
641
+ # rubocop:enable Metrics/ParameterLists
680
642
 
681
643
  def get_build_info_for_review(app_id: nil, train: nil, build_number: nil, platform: 'ios')
682
644
  r = request(:get) do |req|
@@ -864,6 +826,33 @@ module Spaceship
864
826
  parse_response(r, 'data')
865
827
  end
866
828
 
829
+ #####################################################
830
+ # @!group Promo codes
831
+ #####################################################
832
+ def app_promocodes(app_id: nil)
833
+ r = request(:get, "ra/apps/#{app_id}/promocodes/versions")
834
+ parse_response(r, 'data')['versions']
835
+ end
836
+
837
+ def generate_app_version_promocodes!(app_id: nil, version_id: nil, quantity: nil)
838
+ data = {
839
+ numberOfCodes: { value: quantity },
840
+ agreedToContract: { value: true }
841
+ }
842
+ url = "ra/apps/#{app_id}/promocodes/versions/#{version_id}"
843
+ r = request(:post) do |req|
844
+ req.url url
845
+ req.body = data.to_json
846
+ req.headers['Content-Type'] = 'application/json'
847
+ end
848
+ parse_response(r, 'data')
849
+ end
850
+
851
+ def app_promocodes_history(app_id: nil)
852
+ r = request(:get, "ra/apps/#{app_id}/promocodes/history")
853
+ parse_response(r, 'data')['requests']
854
+ end
855
+
867
856
  private
868
857
 
869
858
  def with_tunes_retry(tries = 5, &_block)
@@ -0,0 +1,155 @@
1
+ module Spaceship
2
+ class Client
3
+ def handle_two_step(response)
4
+ @x_apple_web_session_token = response["x-apple-web-session-token"]
5
+ @scnt = response["scnt"]
6
+
7
+ r = request(:get) do |req|
8
+ req.url "https://idmsa.apple.com/appleauth/auth"
9
+ req.headers["scnt"] = @scnt
10
+ req.headers["X-Apple-Web-Session-Token"] = @x_apple_web_session_token
11
+ req.headers["Accept"] = "application/json"
12
+ end
13
+
14
+ if r.body.kind_of?(Hash) && r.body["trustedDevices"].kind_of?(Array)
15
+ if r.body.fetch("securityCode", {})["tooManyCodesLock"].to_s.length > 0
16
+ raise ITunesConnectError.new, "Too many verification codes have been sent. Enter the last code you received, use one of your devices, or try again later."
17
+ end
18
+
19
+ old_client = (begin
20
+ Tunes::RecoveryDevice.client
21
+ rescue
22
+ nil # since client might be nil, which raises an exception
23
+ end)
24
+ Tunes::RecoveryDevice.client = self # temporary set it as it's required by the factory method
25
+ devices = r.body["trustedDevices"].collect do |current|
26
+ Tunes::RecoveryDevice.factory(current)
27
+ end
28
+ Tunes::RecoveryDevice.client = old_client
29
+
30
+ puts "Two Step Verification for account '#{self.user}' is enabled"
31
+ puts "Please select a device to verify your identity"
32
+ available = devices.collect do |c|
33
+ "#{c.name}\t#{c.model_name || 'SMS'}\t(#{c.device_id})"
34
+ end
35
+ result = choose(*available)
36
+ device_id = result.match(/.*\t.*\t\((.*)\)/)[1]
37
+ select_device(r, device_id)
38
+ else
39
+ raise "Invalid 2 step response #{r.body}"
40
+ end
41
+ end
42
+
43
+ # Only needed for 2 step
44
+ def load_session_from_file
45
+ if File.exist?(persistent_cookie_path)
46
+ puts "Loading session from '#{persistent_cookie_path}'" if $verbose
47
+ @cookie.load(persistent_cookie_path)
48
+ return true
49
+ end
50
+ return false
51
+ end
52
+
53
+ def load_session_from_env
54
+ yaml_text = ENV["FASTLANE_SESSION"] || ENV["SPACESHIP_SESSION"]
55
+ return if yaml_text.to_s.length == 0
56
+ puts "Loading session from environment variable" if $verbose
57
+
58
+ file = Tempfile.new('cookie.yml')
59
+ file.write(yaml_text.gsub("\\n", "\n"))
60
+ file.close
61
+
62
+ begin
63
+ @cookie.load(file.path)
64
+ rescue => ex
65
+ puts "Error loading session from environment"
66
+ puts "Make sure to pass the session in a valid format"
67
+ raise ex
68
+ ensure
69
+ file.unlink
70
+ end
71
+ end
72
+
73
+ def select_device(r, device_id)
74
+ # Request Token
75
+ r = request(:put) do |req|
76
+ req.url "https://idmsa.apple.com/appleauth/auth/verify/device/#{device_id}/securitycode"
77
+ req.headers["Accept"] = "application/json"
78
+ req.headers["scnt"] = @scnt
79
+ req.headers["X-Apple-Web-Session-Token"] = @x_apple_web_session_token
80
+ end
81
+
82
+ # we use `Spaceship::TunesClient.new.handle_itc_response`
83
+ # since this might be from the Dev Portal, but for 2 step
84
+ Spaceship::TunesClient.new.handle_itc_response(r.body)
85
+
86
+ puts "Successfully requested notification"
87
+ code = ask("Please enter the 4 digit code: ")
88
+ puts "Requesting session..."
89
+
90
+ # Send token back to server to get a valid session
91
+ r = request(:post) do |req|
92
+ req.url "https://idmsa.apple.com/appleauth/auth/verify/device/#{device_id}/securitycode"
93
+ req.headers["Accept"] = "application/json"
94
+ req.headers["scnt"] = @scnt
95
+ req.headers["X-Apple-Web-Session-Token"] = @x_apple_web_session_token
96
+ req.body = { "code" => code.to_s }.to_json
97
+ req.headers['Content-Type'] = 'application/json'
98
+ end
99
+
100
+ begin
101
+ Spaceship::TunesClient.new.handle_itc_response(r.body) # this will fail if the code is invalid
102
+ rescue => ex
103
+ # If the code was entered wrong
104
+ # {
105
+ # "securityCode": {
106
+ # "code": "1234"
107
+ # },
108
+ # "securityCodeLocked": false,
109
+ # "recoveryKeyLocked": false,
110
+ # "recoveryKeySupported": true,
111
+ # "manageTrustedDevicesLinkName": "appleid.apple.com",
112
+ # "suppressResend": false,
113
+ # "authType": "hsa",
114
+ # "accountLocked": false,
115
+ # "validationErrors": [{
116
+ # "code": "-21669",
117
+ # "title": "Incorrect Verification Code",
118
+ # "message": "Incorrect verification code."
119
+ # }]
120
+ # }
121
+ if ex.to_s.include?("verification code") # to have a nicer output
122
+ puts "Error: Incorrect verification code"
123
+ return select_device(r, device_id)
124
+ end
125
+
126
+ raise ex
127
+ end
128
+
129
+ # If the request was successful, r.body is actually nil
130
+ # The previous request will fail if the user isn't on a team
131
+ # on iTunes Connect, but it still works, so we're good
132
+
133
+ # Tell iTC that we are trustworthy (obviously)
134
+ # This will update our local cookies to something new
135
+ # They probably have a longer time to live than the other poor cookies
136
+ # Changed Keys
137
+ # - myacinfo
138
+ # - DES5c148586dfd451e55afb0175f62418f91
139
+ # We actually only care about the DES value
140
+
141
+ request(:get) do |req|
142
+ req.url "https://idmsa.apple.com/appleauth/auth/2sv/trust"
143
+ req.headers["scnt"] = @scnt
144
+ req.headers["X-Apple-Web-Session-Token"] = @x_apple_web_session_token
145
+ end
146
+ # This request will fail if the user isn't added to a team on iTC
147
+ # However we don't really care, this request will still return the
148
+ # correct DES... cookie
149
+
150
+ self.store_cookie
151
+
152
+ return true
153
+ end
154
+ end
155
+ end
@@ -1,3 +1,3 @@
1
1
  module Spaceship
2
- VERSION = "0.24.1".freeze
2
+ VERSION = "0.25.0".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spaceship
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.24.1
4
+ version: 0.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Felix Krause
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-03-24 00:00:00.000000000 Z
12
+ date: 2016-04-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: credentials_manager
@@ -137,6 +137,20 @@ dependencies:
137
137
  - - ">="
138
138
  - !ruby/object:Gem::Version
139
139
  version: '0'
140
+ - !ruby/object:Gem::Dependency
141
+ name: pry-byebug
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
140
154
  - !ruby/object:Gem::Dependency
141
155
  name: bundler
142
156
  requirement: !ruby/object:Gem::Requirement
@@ -193,6 +207,34 @@ dependencies:
193
207
  - - ">="
194
208
  - !ruby/object:Gem::Version
195
209
  version: '0'
210
+ - !ruby/object:Gem::Dependency
211
+ name: diff_matcher
212
+ requirement: !ruby/object:Gem::Requirement
213
+ requirements:
214
+ - - ">="
215
+ - !ruby/object:Gem::Version
216
+ version: '0'
217
+ type: :development
218
+ prerelease: false
219
+ version_requirements: !ruby/object:Gem::Requirement
220
+ requirements:
221
+ - - ">="
222
+ - !ruby/object:Gem::Version
223
+ version: '0'
224
+ - !ruby/object:Gem::Dependency
225
+ name: multi_json
226
+ requirement: !ruby/object:Gem::Requirement
227
+ requirements:
228
+ - - ">="
229
+ - !ruby/object:Gem::Version
230
+ version: '0'
231
+ type: :development
232
+ prerelease: false
233
+ version_requirements: !ruby/object:Gem::Requirement
234
+ requirements:
235
+ - - ">="
236
+ - !ruby/object:Gem::Version
237
+ version: '0'
196
238
  - !ruby/object:Gem::Dependency
197
239
  name: rspec
198
240
  requirement: !ruby/object:Gem::Requirement
@@ -306,7 +348,9 @@ files:
306
348
  - lib/spaceship/tunes/app_trailer.rb
307
349
  - lib/spaceship/tunes/app_version.rb
308
350
  - lib/spaceship/tunes/app_version_common.rb
351
+ - lib/spaceship/tunes/app_version_generated_promocodes.rb
309
352
  - lib/spaceship/tunes/app_version_history.rb
353
+ - lib/spaceship/tunes/app_version_promocodes.rb
310
354
  - lib/spaceship/tunes/app_version_ref.rb
311
355
  - lib/spaceship/tunes/app_version_states_history.rb
312
356
  - lib/spaceship/tunes/application.rb
@@ -318,6 +362,7 @@ files:
318
362
  - lib/spaceship/tunes/language_item.rb
319
363
  - lib/spaceship/tunes/pricing_tier.rb
320
364
  - lib/spaceship/tunes/processing_build.rb
365
+ - lib/spaceship/tunes/recovery_device.rb
321
366
  - lib/spaceship/tunes/spaceship.rb
322
367
  - lib/spaceship/tunes/tester.rb
323
368
  - lib/spaceship/tunes/transit_app_file.rb
@@ -325,6 +370,7 @@ files:
325
370
  - lib/spaceship/tunes/tunes_base.rb
326
371
  - lib/spaceship/tunes/tunes_client.rb
327
372
  - lib/spaceship/tunes/user_detail.rb
373
+ - lib/spaceship/two_step_client.rb
328
374
  - lib/spaceship/ui.rb
329
375
  - lib/spaceship/version.rb
330
376
  homepage: https://fastlane.tools
@@ -347,7 +393,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
347
393
  version: '0'
348
394
  requirements: []
349
395
  rubyforge_project:
350
- rubygems_version: 2.5.1
396
+ rubygems_version: 2.4.0
351
397
  signing_key:
352
398
  specification_version: 4
353
399
  summary: Because you would rather spend your time building stuff than fighting provisioning