spaceship 0.24.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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