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 +4 -4
- data/README.md +18 -0
- data/bin/spaceship +2 -2
- data/lib/spaceship/client.rb +101 -0
- data/lib/spaceship/du/du_client.rb +1 -1
- data/lib/spaceship/du/upload_file.rb +4 -1
- data/lib/spaceship/du/utilities.rb +6 -1
- data/lib/spaceship/portal/portal_client.rb +11 -23
- data/lib/spaceship/tunes/app_version.rb +13 -1
- data/lib/spaceship/tunes/app_version_generated_promocodes.rb +35 -0
- data/lib/spaceship/tunes/app_version_promocodes.rb +34 -0
- data/lib/spaceship/tunes/application.rb +17 -0
- data/lib/spaceship/tunes/build.rb +4 -21
- data/lib/spaceship/tunes/recovery_device.rb +69 -0
- data/lib/spaceship/tunes/tunes.rb +4 -0
- data/lib/spaceship/tunes/tunes_client.rb +39 -50
- data/lib/spaceship/two_step_client.rb +155 -0
- data/lib/spaceship/version.rb +1 -1
- metadata +49 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c8b9baae7a579a3b8791d93715b93f4ab301bdbb
|
4
|
+
data.tar.gz: 846ed6e24a001c17d75a5fb7ec6c551a697927ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/spaceship/client.rb
CHANGED
@@ -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
|
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
|
-
|
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 =
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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/
|
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'
|
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
|
data/lib/spaceship/version.rb
CHANGED
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.
|
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-
|
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.
|
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
|