spaceship 0.0.15 → 0.1.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 +4 -4
- data/lib/assets/languageMapping.json +224 -0
- data/lib/spaceship.rb +20 -63
- data/lib/spaceship/base.rb +71 -14
- data/lib/spaceship/client.rb +9 -274
- data/lib/spaceship/launcher.rb +1 -1
- data/lib/spaceship/portal/app.rb +125 -0
- data/lib/spaceship/portal/certificate.rb +273 -0
- data/lib/spaceship/portal/device.rb +102 -0
- data/lib/spaceship/portal/portal.rb +6 -0
- data/lib/spaceship/portal/portal_base.rb +13 -0
- data/lib/spaceship/portal/portal_client.rb +289 -0
- data/lib/spaceship/portal/provisioning_profile.rb +369 -0
- data/lib/spaceship/portal/spaceship.rb +94 -0
- data/lib/spaceship/{ui → portal/ui}/select_team.rb +0 -0
- data/lib/spaceship/tunes/app_screenshot.rb +28 -0
- data/lib/spaceship/tunes/app_status.rb +63 -0
- data/lib/spaceship/tunes/app_submission.rb +149 -0
- data/lib/spaceship/tunes/app_version.rb +337 -0
- data/lib/spaceship/tunes/application.rb +253 -0
- data/lib/spaceship/tunes/build.rb +128 -0
- data/lib/spaceship/tunes/build_train.rb +79 -0
- data/lib/spaceship/tunes/language_converter.rb +44 -0
- data/lib/spaceship/tunes/language_item.rb +54 -0
- data/lib/spaceship/tunes/processing_build.rb +30 -0
- data/lib/spaceship/tunes/spaceship.rb +26 -0
- data/lib/spaceship/tunes/tester.rb +177 -0
- data/lib/spaceship/tunes/tunes.rb +12 -0
- data/lib/spaceship/tunes/tunes_base.rb +15 -0
- data/lib/spaceship/tunes/tunes_client.rb +360 -0
- data/lib/spaceship/version.rb +1 -1
- metadata +27 -7
- data/lib/spaceship/app.rb +0 -125
- data/lib/spaceship/certificate.rb +0 -271
- data/lib/spaceship/device.rb +0 -100
- data/lib/spaceship/provisioning_profile.rb +0 -367
@@ -0,0 +1,273 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module Spaceship
|
4
|
+
module Portal
|
5
|
+
# Represents a certificate from the Apple Developer Portal.
|
6
|
+
#
|
7
|
+
# This can either be a code signing identity or a push profile
|
8
|
+
class Certificate < PortalBase
|
9
|
+
# @return (String) The ID given from the developer portal. You'll probably not need it.
|
10
|
+
# @example
|
11
|
+
# "P577TH3PAA"
|
12
|
+
attr_accessor :id
|
13
|
+
|
14
|
+
# @return (String) The name of the certificate
|
15
|
+
# @example Company
|
16
|
+
# "SunApps GmbH"
|
17
|
+
# @example Push Profile
|
18
|
+
# "com.krausefx.app"
|
19
|
+
attr_accessor :name
|
20
|
+
|
21
|
+
# @return (String) Status of the certificate
|
22
|
+
# @example
|
23
|
+
# "Issued"
|
24
|
+
attr_accessor :status
|
25
|
+
|
26
|
+
# @return (Date) The date and time when the certificate was created
|
27
|
+
# @example
|
28
|
+
# 2015-04-01 21:24:00 UTC
|
29
|
+
attr_accessor :created
|
30
|
+
|
31
|
+
# @return (Date) The date and time when the certificate will expire
|
32
|
+
# @example
|
33
|
+
# 2016-04-01 21:24:00 UTC
|
34
|
+
attr_accessor :expires
|
35
|
+
|
36
|
+
# @return (String) The owner type that defines if it's a push profile
|
37
|
+
# or a code signing identity
|
38
|
+
#
|
39
|
+
# @example Code Signing Identity
|
40
|
+
# "team"
|
41
|
+
# @example Push Certificate
|
42
|
+
# "bundle"
|
43
|
+
attr_accessor :owner_type
|
44
|
+
|
45
|
+
# @return (String) The name of the owner
|
46
|
+
#
|
47
|
+
# @example Code Signing Identity (usually the company name)
|
48
|
+
# "SunApps Gmbh"
|
49
|
+
# @example Push Certificate (the name of your App ID)
|
50
|
+
# "Awesome App"
|
51
|
+
attr_accessor :owner_name
|
52
|
+
|
53
|
+
# @return (String) The ID of the owner, that can be used to
|
54
|
+
# fetch more information
|
55
|
+
# @example
|
56
|
+
# "75B83SPLAA"
|
57
|
+
attr_accessor :owner_id
|
58
|
+
|
59
|
+
# Indicates the type of this certificate
|
60
|
+
# which is automatically used to determine the class of
|
61
|
+
# the certificate. Available values listed in CERTIFICATE_TYPE_IDS
|
62
|
+
# @return (String) The type of the certificate
|
63
|
+
# @example Production Certificate
|
64
|
+
# "R58UK2EWSO"
|
65
|
+
# @example Development Certificate
|
66
|
+
# "5QPB9NHCEI"
|
67
|
+
attr_accessor :type_display_id
|
68
|
+
|
69
|
+
attr_mapping({
|
70
|
+
'certificateId' => :id,
|
71
|
+
'name' => :name,
|
72
|
+
'statusString' => :status,
|
73
|
+
'dateCreated' => :created,
|
74
|
+
'expirationDate' => :expires,
|
75
|
+
'ownerType' => :owner_type,
|
76
|
+
'ownerName' => :owner_name,
|
77
|
+
'ownerId' => :owner_id,
|
78
|
+
'certificateTypeDisplayId' => :type_display_id
|
79
|
+
})
|
80
|
+
|
81
|
+
#####################################################
|
82
|
+
# Certs are not associated with apps
|
83
|
+
#####################################################
|
84
|
+
|
85
|
+
# A development code signing certificate used for development environment
|
86
|
+
class Development < Certificate; end
|
87
|
+
|
88
|
+
# A production code signing certificate used for distribution environment
|
89
|
+
class Production < Certificate; end
|
90
|
+
|
91
|
+
# An In House code signing certificate used for enterprise distributions
|
92
|
+
class InHouse < Certificate; end
|
93
|
+
|
94
|
+
#####################################################
|
95
|
+
# Certs that are specific for one app
|
96
|
+
#####################################################
|
97
|
+
|
98
|
+
# Abstract class for push certificates. Check out the subclasses
|
99
|
+
# DevelopmentPush, ProductionPush, WebsitePush and VoipPush
|
100
|
+
class PushCertificate < Certificate; end
|
101
|
+
|
102
|
+
# A push notification certificate for development environment
|
103
|
+
class DevelopmentPush < PushCertificate; end
|
104
|
+
|
105
|
+
# A push notification certificate for production environment
|
106
|
+
class ProductionPush < PushCertificate; end
|
107
|
+
|
108
|
+
# A push notification certificate for websites
|
109
|
+
class WebsitePush < PushCertificate; end
|
110
|
+
|
111
|
+
# A push notification certificate for the VOIP environment
|
112
|
+
class VoipPush < PushCertificate; end
|
113
|
+
|
114
|
+
# Passbook certificate
|
115
|
+
class Passbook < Certificate; end
|
116
|
+
|
117
|
+
# ApplePay certificate
|
118
|
+
class ApplePay < Certificate; end
|
119
|
+
|
120
|
+
CERTIFICATE_TYPE_IDS = {
|
121
|
+
"5QPB9NHCEI" => Development,
|
122
|
+
"R58UK2EWSO" => Production,
|
123
|
+
"9RQEK7MSXA" => InHouse,
|
124
|
+
"LA30L5BJEU" => Certificate,
|
125
|
+
"BKLRAVXMGM" => DevelopmentPush,
|
126
|
+
"3BQKVH9I2X" => ProductionPush,
|
127
|
+
"Y3B2F3TYSI" => Passbook,
|
128
|
+
"3T2ZP62QW8" => WebsitePush,
|
129
|
+
"E5D663CMZW" => WebsitePush,
|
130
|
+
"4APLUP237T" => ApplePay
|
131
|
+
}
|
132
|
+
|
133
|
+
#class methods
|
134
|
+
class << self
|
135
|
+
# Create a new code signing request that can be used to
|
136
|
+
# generate a new certificate
|
137
|
+
# @example
|
138
|
+
# Create a new certificate signing request
|
139
|
+
# csr, pkey = Spaceship.certificate.create_certificate_signing_request
|
140
|
+
#
|
141
|
+
# # Use the signing request to create a new distribution certificate
|
142
|
+
# Spaceship.certificate.production.create!(csr: csr)
|
143
|
+
def create_certificate_signing_request
|
144
|
+
key = OpenSSL::PKey::RSA.new 2048
|
145
|
+
csr = OpenSSL::X509::Request.new
|
146
|
+
csr.version = 0
|
147
|
+
csr.subject = OpenSSL::X509::Name.new([
|
148
|
+
['CN', 'PEM', OpenSSL::ASN1::UTF8STRING]
|
149
|
+
])
|
150
|
+
csr.public_key = key.public_key
|
151
|
+
csr.sign(key, OpenSSL::Digest::SHA1.new)
|
152
|
+
return [csr, key]
|
153
|
+
end
|
154
|
+
|
155
|
+
# Create a new object based on a hash.
|
156
|
+
# This is used to create a new object based on the server response.
|
157
|
+
def factory(attrs)
|
158
|
+
# Example:
|
159
|
+
# => {"name"=>"iOS Distribution: SunApps GmbH",
|
160
|
+
# "certificateId"=>"XC5PH8DAAA",
|
161
|
+
# "serialNumber"=>"797E732CCE8B7AAA",
|
162
|
+
# "status"=>"Issued",
|
163
|
+
# "statusCode"=>0,
|
164
|
+
# "expirationDate"=>#<DateTime: 2015-11-25T22:45:50+00:00 ((2457352j,81950s,0n),+0s,2299161j)>,
|
165
|
+
# "certificatePlatform"=>"ios",
|
166
|
+
# "certificateType"=>
|
167
|
+
# {"certificateTypeDisplayId"=>"R58UK2EAAA",
|
168
|
+
# "name"=>"iOS Distribution",
|
169
|
+
# "platform"=>"ios",
|
170
|
+
# "permissionType"=>"distribution",
|
171
|
+
# "distributionType"=>"store",
|
172
|
+
# "distributionMethod"=>"app",
|
173
|
+
# "ownerType"=>"team",
|
174
|
+
# "daysOverlap"=>364,
|
175
|
+
# "maxActive"=>2}}
|
176
|
+
|
177
|
+
if attrs['certificateType']
|
178
|
+
# On some accounts this is nested, so we need to flatten it
|
179
|
+
attrs.merge!(attrs['certificateType'])
|
180
|
+
attrs.delete('certificateType')
|
181
|
+
end
|
182
|
+
|
183
|
+
# Parse the dates
|
184
|
+
attrs['expirationDate'] = (Time.parse(attrs['expirationDate']) rescue attrs['expirationDate'])
|
185
|
+
attrs['dateCreated'] = (Time.parse(attrs['dateCreated']) rescue attrs['dateCreated'])
|
186
|
+
|
187
|
+
# Here we go
|
188
|
+
klass = CERTIFICATE_TYPE_IDS[attrs['certificateTypeDisplayId']]
|
189
|
+
klass ||= Certificate
|
190
|
+
klass.client = @client
|
191
|
+
klass.new(attrs)
|
192
|
+
end
|
193
|
+
|
194
|
+
# @return (Array) Returns all certificates of this account.
|
195
|
+
# If this is called from a subclass of Certificate, this will
|
196
|
+
# only include certificates matching the current type.
|
197
|
+
def all
|
198
|
+
if (self == Certificate) # are we the base-class?
|
199
|
+
types = CERTIFICATE_TYPE_IDS.keys
|
200
|
+
else
|
201
|
+
types = [CERTIFICATE_TYPE_IDS.key(self)]
|
202
|
+
end
|
203
|
+
|
204
|
+
client.certificates(types).map do |cert|
|
205
|
+
factory(cert)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# @return (Certificate) Find a certificate based on the ID of the certificate.
|
210
|
+
def find(certificate_id)
|
211
|
+
all.find do |c|
|
212
|
+
c.id == certificate_id
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Generate a new certificate based on a code certificate signing request
|
217
|
+
# @param csr (OpenSSL::X509::Request) (required): The certificate signing request to use. Get one using
|
218
|
+
# `create_certificate_signing_request`
|
219
|
+
# @param bundle_id (String) (optional): The app identifier this certificate is for.
|
220
|
+
# This value is only needed if you create a push profile. For normal code signing
|
221
|
+
# certificates, you must only pass a certificate signing request.
|
222
|
+
# @example
|
223
|
+
# # Create a new certificate signing request
|
224
|
+
# csr, pkey = Spaceship::Certificate.create_certificate_signing_request
|
225
|
+
#
|
226
|
+
# # Use the signing request to create a new distribution certificate
|
227
|
+
# Spaceship::Certificate::Production.create!(csr: csr)
|
228
|
+
# @return (Device): The newly created device
|
229
|
+
def create!(csr: nil, bundle_id: nil)
|
230
|
+
type = CERTIFICATE_TYPE_IDS.key(self)
|
231
|
+
|
232
|
+
# look up the app_id by the bundle_id
|
233
|
+
if bundle_id
|
234
|
+
app = Spaceship::App.find(bundle_id)
|
235
|
+
raise "Could not find app with bundle id '#{bundle_id}'" unless app
|
236
|
+
app_id = app.app_id
|
237
|
+
end
|
238
|
+
|
239
|
+
# ensure csr is a OpenSSL::X509::Request
|
240
|
+
csr = OpenSSL::X509::Request.new(csr) if csr.is_a?(String)
|
241
|
+
|
242
|
+
# if this succeeds, we need to save the .cer and the private key in keychain access or wherever they go in linux
|
243
|
+
response = client.create_certificate!(type, csr.to_pem, app_id)
|
244
|
+
# munge the response to make it work for the factory
|
245
|
+
response['certificateTypeDisplayId'] = response['certificateType']['certificateTypeDisplayId']
|
246
|
+
self.new(response)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# instance methods
|
251
|
+
|
252
|
+
# @return (String) Download the raw data of the certificate without parsing
|
253
|
+
def download_raw
|
254
|
+
client.download_certificate(id, type_display_id)
|
255
|
+
end
|
256
|
+
|
257
|
+
# @return (OpenSSL::X509::Certificate) Downloads and parses the certificate
|
258
|
+
def download
|
259
|
+
OpenSSL::X509::Certificate.new(download_raw)
|
260
|
+
end
|
261
|
+
|
262
|
+
# Revoke the certificate. You shouldn't use this method probably.
|
263
|
+
def revoke!
|
264
|
+
client.revoke_certificate!(id, type_display_id)
|
265
|
+
end
|
266
|
+
|
267
|
+
# @return (Bool): Is this certificate a push profile for apps?
|
268
|
+
def is_push?
|
269
|
+
self.kind_of?PushCertificate
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Spaceship
|
2
|
+
module Portal
|
3
|
+
# Represents a device from the Apple Developer Portal
|
4
|
+
class Device < PortalBase
|
5
|
+
# @return (String) The ID given from the developer portal. You'll probably not need it.
|
6
|
+
# @example
|
7
|
+
# "XJXGVS46MW"
|
8
|
+
attr_accessor :id
|
9
|
+
|
10
|
+
# @return (String) The name of the device
|
11
|
+
# @example
|
12
|
+
# "Felix Krause's iPhone 6"
|
13
|
+
attr_accessor :name
|
14
|
+
|
15
|
+
# @return (String) The UDID of the device
|
16
|
+
# @example
|
17
|
+
# "4c24a7ee5caaa4847f49aaab2d87483053f53b65"
|
18
|
+
attr_accessor :udid
|
19
|
+
|
20
|
+
# @return (String) The platform of the device. This is probably always "ios"
|
21
|
+
# @example
|
22
|
+
# "ios"
|
23
|
+
attr_accessor :platform
|
24
|
+
|
25
|
+
# @return (String) Status of the device. Probably always "c"
|
26
|
+
# @example
|
27
|
+
# "c"
|
28
|
+
attr_accessor :status
|
29
|
+
|
30
|
+
attr_mapping({
|
31
|
+
'deviceId' => :id,
|
32
|
+
'name' => :name,
|
33
|
+
'deviceNumber' => :udid,
|
34
|
+
'devicePlatform' => :platform,
|
35
|
+
'status' => :status
|
36
|
+
})
|
37
|
+
|
38
|
+
class << self
|
39
|
+
# Create a new object based on a hash.
|
40
|
+
# This is used to create a new object based on the server response.
|
41
|
+
def factory(attrs)
|
42
|
+
self.new(attrs)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return (Array) Returns all devices registered for this account
|
46
|
+
def all
|
47
|
+
client.devices.map { |device| self.factory(device) }
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return (Device) Find a device based on the ID of the device. *Attention*:
|
51
|
+
# This is *not* the UDID. nil if no device was found.
|
52
|
+
def find(device_id)
|
53
|
+
all.find do |device|
|
54
|
+
device.id == device_id
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return (Device) Find a device based on the UDID of the device. nil if no device was found.
|
59
|
+
def find_by_udid(device_udid)
|
60
|
+
all.find do |device|
|
61
|
+
device.udid == device_udid
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return (Device) Find a device based on its name. nil if no device was found.
|
66
|
+
def find_by_name(device_name)
|
67
|
+
all.find do |device|
|
68
|
+
device.name == device_name
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Register a new device to this account
|
73
|
+
# @param name (String) (required): The name of the new device
|
74
|
+
# @param udid (String) (required): The UDID of the new device
|
75
|
+
# @example
|
76
|
+
# Spaceship.device.create!(name: "Felix Krause's iPhone 6", udid: "4c24a7ee5caaa4847f49aaab2d87483053f53b65")
|
77
|
+
# @return (Device): The newly created device
|
78
|
+
def create!(name: nil, udid: nil)
|
79
|
+
# Check whether the user has passed in a UDID and a name
|
80
|
+
unless (udid and name)
|
81
|
+
raise "You cannot create a device without a device_id (UDID) and name"
|
82
|
+
end
|
83
|
+
|
84
|
+
# Find the device by UDID, raise an exception if it already exists
|
85
|
+
if self.find_by_udid(udid)
|
86
|
+
raise "The device UDID '#{udid}' already exists on this team."
|
87
|
+
end
|
88
|
+
|
89
|
+
# Find the device by name, raise an exception if it already exists
|
90
|
+
if self.find_by_name(name)
|
91
|
+
raise "The device name '#{name}' already exists on this team, use different one."
|
92
|
+
end
|
93
|
+
|
94
|
+
device = client.create_device!(name, udid)
|
95
|
+
|
96
|
+
# Update self with the new device
|
97
|
+
self.new(device)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,289 @@
|
|
1
|
+
module Spaceship
|
2
|
+
class PortalClient < Spaceship::Client
|
3
|
+
|
4
|
+
#####################################################
|
5
|
+
# @!group Init and Login
|
6
|
+
#####################################################
|
7
|
+
|
8
|
+
def self.hostname
|
9
|
+
"https://developer.apple.com/services-account/#{PROTOCOL_VERSION}/"
|
10
|
+
end
|
11
|
+
|
12
|
+
# Fetches the latest API Key from the Apple Dev Portal
|
13
|
+
def api_key
|
14
|
+
cache_path = "/tmp/spaceship_api_key.txt"
|
15
|
+
begin
|
16
|
+
cached = File.read(cache_path)
|
17
|
+
rescue Errno::ENOENT
|
18
|
+
end
|
19
|
+
return cached if cached
|
20
|
+
|
21
|
+
landing_url = "https://developer.apple.com/membercenter/index.action"
|
22
|
+
logger.info("GET: " + landing_url)
|
23
|
+
headers = @client.get(landing_url).headers
|
24
|
+
results = headers['location'].match(/.*appIdKey=(\h+)/)
|
25
|
+
if results.length > 1
|
26
|
+
api_key = results[1]
|
27
|
+
File.write(cache_path, api_key)
|
28
|
+
return api_key
|
29
|
+
else
|
30
|
+
raise "Could not find latest API Key from the Dev Portal"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
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['Set-Cookie'] =~ /myacinfo=(\w+);/
|
42
|
+
@cookie = "myacinfo=#{$1};"
|
43
|
+
return @client
|
44
|
+
else
|
45
|
+
# User Credentials are wrong
|
46
|
+
raise InvalidUserCredentialsError.new(response)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return (Array) A list of all available teams
|
51
|
+
def teams
|
52
|
+
r = request(:post, 'account/listTeams.action')
|
53
|
+
parse_response(r, 'teams')
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return (String) The currently selected Team ID
|
57
|
+
def team_id
|
58
|
+
return @current_team_id if @current_team_id
|
59
|
+
|
60
|
+
if teams.count > 1
|
61
|
+
puts "The current user is in #{teams.count} teams. Pass a team ID or call `select_team` to choose a team. Using the first one for now."
|
62
|
+
end
|
63
|
+
@current_team_id ||= teams[0]['teamId']
|
64
|
+
end
|
65
|
+
|
66
|
+
# Shows a team selection for the user in the terminal. This should not be
|
67
|
+
# called on CI systems
|
68
|
+
def select_team
|
69
|
+
@current_team_id = self.UI.select_team
|
70
|
+
end
|
71
|
+
|
72
|
+
# Set a new team ID which will be used from now on
|
73
|
+
def team_id=(team_id)
|
74
|
+
@current_team_id = team_id
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return (Hash) Fetches all information of the currently used team
|
78
|
+
def team_information
|
79
|
+
teams.find do |t|
|
80
|
+
t['teamId'] == team_id
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Is the current session from an Enterprise In House account?
|
85
|
+
def in_house?
|
86
|
+
# TODO: move to portal
|
87
|
+
return @in_house unless @in_house.nil?
|
88
|
+
@in_house = (team_information['type'] == 'In-House')
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
#####################################################
|
93
|
+
# @!group Apps
|
94
|
+
#####################################################
|
95
|
+
|
96
|
+
def apps
|
97
|
+
paging do |page_number|
|
98
|
+
r = request(:post, 'account/ios/identifiers/listAppIds.action', {
|
99
|
+
teamId: team_id,
|
100
|
+
pageNumber: page_number,
|
101
|
+
pageSize: page_size,
|
102
|
+
sort: 'name=asc'
|
103
|
+
})
|
104
|
+
parse_response(r, 'appIds')
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def details_for_app(app)
|
109
|
+
r = request(:post, 'account/ios/identifiers/getAppIdDetail.action', {
|
110
|
+
teamId: team_id,
|
111
|
+
appIdId: app.app_id
|
112
|
+
})
|
113
|
+
parse_response(r, 'appId')
|
114
|
+
end
|
115
|
+
|
116
|
+
def create_app!(type, name, bundle_id)
|
117
|
+
ident_params = case type.to_sym
|
118
|
+
when :explicit
|
119
|
+
{
|
120
|
+
type: 'explicit',
|
121
|
+
explicitIdentifier: bundle_id,
|
122
|
+
appIdentifierString: bundle_id,
|
123
|
+
push: 'on',
|
124
|
+
inAppPurchase: 'on',
|
125
|
+
gameCenter: 'on'
|
126
|
+
}
|
127
|
+
when :wildcard
|
128
|
+
{
|
129
|
+
type: 'wildcard',
|
130
|
+
wildcardIdentifier: bundle_id,
|
131
|
+
appIdentifierString: bundle_id
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
params = {
|
136
|
+
appIdName: name,
|
137
|
+
teamId: team_id
|
138
|
+
}
|
139
|
+
|
140
|
+
params.merge!(ident_params)
|
141
|
+
|
142
|
+
r = request(:post, 'account/ios/identifiers/addAppId.action', params)
|
143
|
+
parse_response(r, 'appId')
|
144
|
+
end
|
145
|
+
|
146
|
+
def delete_app!(app_id)
|
147
|
+
r = request(:post, 'account/ios/identifiers/deleteAppId.action', {
|
148
|
+
teamId: team_id,
|
149
|
+
appIdId: app_id
|
150
|
+
})
|
151
|
+
parse_response(r)
|
152
|
+
end
|
153
|
+
|
154
|
+
#####################################################
|
155
|
+
# @!group Devices
|
156
|
+
#####################################################
|
157
|
+
|
158
|
+
def devices
|
159
|
+
paging do |page_number|
|
160
|
+
r = request(:post, 'account/ios/device/listDevices.action', {
|
161
|
+
teamId: team_id,
|
162
|
+
pageNumber: page_number,
|
163
|
+
pageSize: page_size,
|
164
|
+
sort: 'name=asc'
|
165
|
+
})
|
166
|
+
parse_response(r, 'devices')
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def create_device!(device_name, device_id)
|
171
|
+
r = request(:post) do |r|
|
172
|
+
r.url "https://developerservices2.apple.com/services/#{PROTOCOL_VERSION}/ios/addDevice.action"
|
173
|
+
r.params = {
|
174
|
+
teamId: team_id,
|
175
|
+
deviceNumber: device_id,
|
176
|
+
name: device_name
|
177
|
+
}
|
178
|
+
end
|
179
|
+
|
180
|
+
parse_response(r, 'device')
|
181
|
+
end
|
182
|
+
|
183
|
+
#####################################################
|
184
|
+
# @!group Certificates
|
185
|
+
#####################################################
|
186
|
+
|
187
|
+
def certificates(types)
|
188
|
+
paging do |page_number|
|
189
|
+
r = request(:post, 'account/ios/certificate/listCertRequests.action', {
|
190
|
+
teamId: team_id,
|
191
|
+
types: types.join(','),
|
192
|
+
pageNumber: page_number,
|
193
|
+
pageSize: page_size,
|
194
|
+
sort: 'certRequestStatusCode=asc'
|
195
|
+
})
|
196
|
+
parse_response(r, 'certRequests')
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def create_certificate!(type, csr, app_id = nil)
|
201
|
+
r = request(:post, 'account/ios/certificate/submitCertificateRequest.action', {
|
202
|
+
teamId: team_id,
|
203
|
+
type: type,
|
204
|
+
csrContent: csr,
|
205
|
+
appIdId: app_id #optional
|
206
|
+
})
|
207
|
+
parse_response(r, 'certRequest')
|
208
|
+
end
|
209
|
+
|
210
|
+
def download_certificate(certificate_id, type)
|
211
|
+
{type: type, certificate_id: certificate_id}.each { |k, v| raise "#{k} must not be nil" if v.nil? }
|
212
|
+
|
213
|
+
r = request(:post, 'https://developer.apple.com/account/ios/certificate/certificateContentDownload.action', {
|
214
|
+
teamId: team_id,
|
215
|
+
displayId: certificate_id,
|
216
|
+
type: type
|
217
|
+
})
|
218
|
+
parse_response(r)
|
219
|
+
end
|
220
|
+
|
221
|
+
def revoke_certificate!(certificate_id, type)
|
222
|
+
r = request(:post, 'account/ios/certificate/revokeCertificate.action', {
|
223
|
+
teamId: team_id,
|
224
|
+
certificateId: certificate_id,
|
225
|
+
type: type
|
226
|
+
})
|
227
|
+
parse_response(r, 'certRequests')
|
228
|
+
end
|
229
|
+
|
230
|
+
#####################################################
|
231
|
+
# @!group Provisioning Profiles
|
232
|
+
#####################################################
|
233
|
+
|
234
|
+
def provisioning_profiles
|
235
|
+
r = request(:post) do |r|
|
236
|
+
r.url "https://developerservices2.apple.com/services/#{PROTOCOL_VERSION}/ios/listProvisioningProfiles.action"
|
237
|
+
r.params = {
|
238
|
+
teamId: team_id,
|
239
|
+
includeInactiveProfiles: true,
|
240
|
+
onlyCountLists: true,
|
241
|
+
}
|
242
|
+
end
|
243
|
+
|
244
|
+
parse_response(r, 'provisioningProfiles')
|
245
|
+
end
|
246
|
+
|
247
|
+
def create_provisioning_profile!(name, distribution_method, app_id, certificate_ids, device_ids)
|
248
|
+
r = request(:post, 'account/ios/profile/createProvisioningProfile.action', {
|
249
|
+
teamId: team_id,
|
250
|
+
provisioningProfileName: name,
|
251
|
+
appIdId: app_id,
|
252
|
+
distributionType: distribution_method,
|
253
|
+
certificateIds: certificate_ids,
|
254
|
+
deviceIds: device_ids
|
255
|
+
})
|
256
|
+
parse_response(r, 'provisioningProfile')
|
257
|
+
end
|
258
|
+
|
259
|
+
def download_provisioning_profile(profile_id)
|
260
|
+
r = request(:get, 'https://developer.apple.com/account/ios/profile/profileContentDownload.action', {
|
261
|
+
teamId: team_id,
|
262
|
+
displayId: profile_id
|
263
|
+
})
|
264
|
+
parse_response(r)
|
265
|
+
end
|
266
|
+
|
267
|
+
def delete_provisioning_profile!(profile_id)
|
268
|
+
r = request(:post, 'account/ios/profile/deleteProvisioningProfile.action', {
|
269
|
+
teamId: team_id,
|
270
|
+
provisioningProfileId: profile_id
|
271
|
+
})
|
272
|
+
parse_response(r)
|
273
|
+
end
|
274
|
+
|
275
|
+
def repair_provisioning_profile!(profile_id, name, distribution_method, app_id, certificate_ids, device_ids)
|
276
|
+
r = request(:post, 'account/ios/profile/regenProvisioningProfile.action', {
|
277
|
+
teamId: team_id,
|
278
|
+
provisioningProfileId: profile_id,
|
279
|
+
provisioningProfileName: name,
|
280
|
+
appIdId: app_id,
|
281
|
+
distributionType: distribution_method,
|
282
|
+
certificateIds: certificate_ids.first, # we are most of the times only allowed to pass one
|
283
|
+
deviceIds: device_ids
|
284
|
+
})
|
285
|
+
|
286
|
+
parse_response(r, 'provisioningProfile')
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|