Class: Deliver::ItunesConnect

Inherits:
Object
  • Object
show all
Includes:
Capybara::DSL
Defined in:
lib/deliver/itunes_connect.rb

Overview

Everything that can’t be achived using the ItunesTransporter will be scripted using the iTunesConnect frontend.

Every method you call here, might take a time

Defined Under Namespace

Classes: ItunesConnectGeneralError, ItunesConnectLoginError

Constant Summary collapse

ITUNESCONNECT_URL =
"https://itunesconnect.apple.com/"
APP_DETAILS_URL =
"https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/ra/ng/app/[[app_id]]"
BUTTON_STRING_NEW_VERSION =
"New Version"
BUTTON_STRING_SUBMIT_FOR_REVIEW =
"Submit for Review"
BUTTON_ADD_NEW_BUILD =
'Click + to add a build before you submit your app.'
WAITING_FOR_REVIEW =
"Waiting For Review"
PROCESSING_TEXT =
"Processing"

Constructive/Destructive Methods collapse

Instance Method Summary collapse

Constructor Details

#initializeItunesConnect

Returns a new instance of ItunesConnect.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/deliver/itunes_connect.rb', line 36

def initialize
  super

  DependencyChecker.check_dependencies
  
  Capybara.run_server = false
  Capybara.default_driver = :poltergeist
  Capybara.javascript_driver = :poltergeist
  Capybara.current_driver = :poltergeist
  Capybara.app_host = ITUNESCONNECT_URL

  # Since Apple has some SSL errors, we have to configure the client properly:
  # https://github.com/ariya/phantomjs/issues/11239
  Capybara.register_driver :poltergeist do |a|
    conf = ['--debug=no', '--ignore-ssl-errors=yes', '--ssl-protocol=TLSv1']
    Capybara::Poltergeist::Driver.new(a, {
      phantomjs_options: conf,
      phantomjs_logger: File.open("/tmp/poltergeist_log.txt", "a"),
      js_errors: false
    })
  end

  page.driver.headers = { "Accept-Language" => "en" }

  self.
end

Instance Method Details

#create_new_version!(app, version_number) ⇒ Object

This method creates a new version of your app using the iTunesConnect frontend. This will happen directly after calling this method. the new version that should be created

Parameters:

  • app (Deliver::App)

    the app you want to modify

  • version_number (String)

    the version number as string for



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/deliver/itunes_connect.rb', line 211

def create_new_version!(app, version_number)
  begin
    current_version = get_live_version(app)

    verify_app(app)
    open_app_page(app)

    if page.has_content?BUTTON_STRING_NEW_VERSION

      if current_version == version_number
        # This means, this version is already live on the App Store
        raise "Version #{version_number} is already created, submitted and released on iTC. Please verify you're using a new version number."
      end

      click_on BUTTON_STRING_NEW_VERSION

      Helper.log.info "Creating a new version (#{version_number})"
      
      all(".fullWidth.nobottom.ng-isolate-scope.ng-pristine").last.set(version_number.to_s)
      click_on "Create"

      while not page.has_content?"Prepare for Submission"
        sleep 1
        Helper.log.debug("Waiting for 'Prepare for Submission'")
      end
    else
      Helper.log.warn "Can not create version #{version_number} on iTunesConnect. Maybe it was already created."
      Helper.log.info "Check out '#{current_url}' what's the latest version."

      begin
        created_version = first(".status.waiting").text.split(" ").first
        if created_version != version_number
          raise "Some other version ('#{created_version}') was created instead of the one you defined ('#{version_number}')"
        end
      rescue Exception => ex
        # Can not fetch the version number of the new version (this happens, when it's e.g. 'Developer Rejected')
        unless page.has_content?version_number
          raise "Some other version was created instead of the one you defined ('#{version_number}')."
        end
      end
    end

    true
  rescue Exception => ex
    error_occured(ex)
  end
end

#get_app_status(app) ⇒ Object

This method will fetch the current status (App::AppStatus) of your app and return it. This method uses a headless browser under the hood, so it might take some time until you get the result

Parameters:

  • app (Deliver::App)

    the app you want this information from

Raises:



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/deliver/itunes_connect.rb', line 150

def get_app_status(app)
  begin
    verify_app(app)

    open_app_page(app)

    if page.has_content?WAITING_FOR_REVIEW
      # That's either Upload Received or Waiting for Review
      if page.has_content?"To submit a new build, you must remove this version from review"
        return App::AppStatus::WAITING_FOR_REVIEW
      else
        return App::AppStatus::UPLOAD_RECEIVED
      end
    elsif page.has_content?BUTTON_STRING_NEW_VERSION
      return App::AppStatus::READY_FOR_SALE
    elsif page.has_content?BUTTON_STRING_SUBMIT_FOR_REVIEW
      return App::AppStatus::PREPARE_FOR_SUBMISSION
    else
      raise "App status not yet implemented"
    end
  rescue Exception => ex
    error_occured(ex)
  end
end

#get_live_version(app) ⇒ Object

This method will fetch the version number of the currently live version of your app and return it. This method uses a headless browser under the hood, so it might take some time until you get the result

Parameters:

  • app (Deliver::App)

    the app you want this information from

Raises:



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/deliver/itunes_connect.rb', line 182

def get_live_version(app)
  begin
    verify_app(app)

    open_app_page(app)

    begin
      return first(".status.ready").text.split(" ").first
    rescue
      Helper.log.debug "Could not fetch version number of the live version for app #{app}."
      return nil
    end
  rescue Exception => ex
    error_occured(ex)
  end
end

#login(user = nil, password = nil) ⇒ bool

Loggs in a user with the given login data on the iTC Frontend. You don’t need to pass a username and password. It will Automatically be fetched using the PasswordManager. This method will also automatically be called when triggering other actions like #open_app_page

Parameters:

  • user (String) (defaults to: nil)

    (optional) The username/email address

  • password (String) (defaults to: nil)

    (optional) The password

Returns:

  • (bool)

    true if everything worked fine

Raises:



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/deliver/itunes_connect.rb', line 74

def (user = nil, password = nil)
  begin
    Helper.log.info "Logging into iTunesConnect"

    user ||= PasswordManager.shared_manager.username
    password ||= PasswordManager.shared_manager.password

    result = visit ITUNESCONNECT_URL
    raise "Could not open iTunesConnect" unless result['status'] == 'success'

    (wait_for_elements('#accountpassword') rescue nil) # when the user is already logged in, this will raise an exception

    if page.has_content?"My Apps"
      # Already logged in
      return true
    end

    fill_in "accountname", with: user
    fill_in "accountpassword", with: password

    begin
      (wait_for_elements(".enabled").first.click rescue nil) # Login Button
      wait_for_elements('.homepageWrapper.ng-scope')
      
      if page.has_content?"My Apps"
        # Everything looks good
      else
        raise ItunesConnectLoginError.new("Looks like your login data was correct, but you do not have access to the apps.")
      end
    rescue Exception => ex
      Helper.log.debug(ex)
      raise ItunesConnectLoginError.new("Error logging in user #{user} with the given password. Make sure you entered them correctly.")
    end

    Helper.log.info "Successfully logged into iTunesConnect"

    true
  rescue Exception => ex
    error_occured(ex)
  end
end

#open_app_page(app) ⇒ bool

Opens the app details page of the given app.

Parameters:

Returns:

  • (bool)

    true if everything worked fine

Raises:



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/deliver/itunes_connect.rb', line 122

def open_app_page(app)
  begin
    verify_app(app)

    Helper.log.info "Opening detail page for app #{app}"

    visit APP_DETAILS_URL.gsub("[[app_id]]", app.apple_id.to_s)

    wait_for_elements('.page-subnav')
    sleep 3

    if current_url.include?"wa/defaultError" # app could not be found
      raise "Could not open app details for app '#{app}'. Make sure you're using the correct Apple ID and the correct Apple developer account (#{PasswordManager.shared_manager.username}).".red
    end

    true
  rescue Exception => ex
    error_occured(ex)
  end
end

#put_build_into_beta_testing!(app, version_number) ⇒ Object

This will put the latest uploaded build as a new beta build



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/deliver/itunes_connect.rb', line 285

def put_build_into_beta_testing!(app, version_number)
  begin
    verify_app(app)
    open_app_page(app)

    Helper.log.info("Choosing the latest build on iTunesConnect for beta distribution")

    click_on "Prerelease"

    wait_for_preprocessing

    if all(".switcher.ng-binding").count == 0
      raise "Could not find beta build on '#{current_url}'. Make sure it is available there"
    end

    if first(".switcher.ng-binding")['class'].include?"checked"
      Helper.log.warn("Beta Build seems to be already active. Take a look at '#{current_url}'")
      return true
    end

    first(".switcher.ng-binding").click
    if page.has_content?"Are you sure you want to start testing"
      click_on "Start"
    end


    return true
  rescue Exception => ex
    error_occured(ex)
  end
end

#put_build_into_production!(app, version_number) ⇒ Object

This will choose the latest uploaded build on iTunesConnect as the production one After this method, you still have to call submit_for_review to actually submit the whole update

Parameters:

  • app (Deliver::App)

    the app you want to choose the build for

  • version_number (String)

    the version number as string for



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/deliver/itunes_connect.rb', line 322

def put_build_into_production!(app, version_number)
  begin
    verify_app(app)
    open_app_page(app)

    Helper.log.info("Choosing the latest build on iTunesConnect for release")

    click_on "Prerelease"

    wait_for_preprocessing

    ################# Apple is finished processing the ipa file #################

    Helper.log.info("Apple finally finished processing the ipa file")
    open_app_page(app)

    begin
      first('a', :text => BUTTON_ADD_NEW_BUILD).click
      wait_for_elements(".buildModalList")
      sleep 1
    rescue
      if page.has_content?"Upload Date"
        # That's fine, the ipa was already selected
        return true
      else
        raise "Could not find Build Button. It looks like the ipa file was not properly uploaded."
      end
    end

    if page.all('td', :text => version_number).count > 1
      Helper.log.fatal "There were multiple submitted builds found. Don't know which one to choose. Just choosing the top one for now"
    end

    result = page.first('td', :text => version_number).first(:xpath,"./..").first(:css, ".small").click
    click_on "Done" # Save the modal dialog
    click_on "Save" # on the top right to save everything else

    error = page.has_content?BUTTON_ADD_NEW_BUILD
    raise "Could not put build itself onto production. Try opening '#{current_url}'" if error

    return true
  rescue Exception => ex
    error_occured(ex)
  end
end

#submit_for_review!(app, perms = nil) ⇒ Object

Submits the update itself to Apple, this includes the app metadata and the ipa file This can easily cause exceptions, which will be shown on iTC.

Parameters:

  • app (Deliver::App)

    the app you want to submit

  • perms (Hash) (defaults to: nil)

    information about content rights, …



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
# File 'lib/deliver/itunes_connect.rb', line 372

def submit_for_review!(app, perms = nil)
  begin
    verify_app(app)
    open_app_page(app)

    Helper.log.info("Submitting app for Review")

    if not page.has_content?BUTTON_STRING_SUBMIT_FOR_REVIEW
      if page.has_content?WAITING_FOR_REVIEW
        Helper.log.info("App is already Waiting For Review")
        return true
      else
        raise "Couldn't find button with name '#{BUTTON_STRING_SUBMIT_FOR_REVIEW}'"
      end
    end

    click_on BUTTON_STRING_SUBMIT_FOR_REVIEW
    sleep 4

    errors = (all(".pagemessage.error") || []).count > 0
    raise "Some error occured when submitting the app for review: '#{current_url}'" if errors

    wait_for_elements(".savingWrapper.ng-scope.ng-pristine")
    wait_for_elements(".radiostyle")
    sleep 3
    
    if page.has_content?"Content Rights"
      # Looks good.. just a few more steps

      perms ||= {
        export_compliance: {
          encryption_updated: false,
          cryptography_enabled: false,
          is_exempt: false
        },
        third_party_content: {
          contains_third_party_content: false,
          has_rights: false
        },
        advertising_identifier: false
      }

      basic = "//*[@itc-radio='submitForReviewAnswers"

      #####################
      # Export Compliance #
      #####################
      if page.has_content?"Export"
        if not perms[:export_compliance][:encryption_updated] and perms[:export_compliance][:cryptography_enabled]
          raise "encryption_updated must be enabled if cryptography_enabled is enabled!"
        end

        begin
          first(:xpath, "#{basic}.exportCompliance.encryptionUpdated.value' and @radio-value='#{perms[:export_compliance][:encryption_updated]}']//input").trigger('click')
          first(:xpath, "#{basic}.exportCompliance.usesEncryption.value' and @radio-value='#{perms[:export_compliance][:cryptography_enabled]}']//input").trigger('click')
          first(:xpath, "#{basic}.exportCompliance.isExempt.value' and @radio-value='#{perms[:export_compliance][:is_exempt]}']//input").trigger('click')
        rescue
        end
      end

      ##################
      # Content Rights #
      ##################
      if page.has_content?"Content Rights"
        if not perms[:third_party_content][:contains_third_party_content] and perms[:third_party_content][:has_rights]
          raise "contains_third_party_content must be enabled if has_rights is enabled".red
        end

        begin
          first(:xpath, "#{basic}.contentRights.containsThirdPartyContent.value' and @radio-value='#{perms[:third_party_content][:contains_third_party_content]}']//input").trigger('click')
          first(:xpath, "#{basic}.contentRights.hasRights.value' and @radio-value='#{perms[:third_party_content][:has_rights]}']//input").trigger('click')
        rescue
        end
      end

      ##########################
      # Advertising Identifier #
      ##########################
      if page.has_content?"Advertising Identifier"
        first(:xpath, "#{basic}.adIdInfo.usesIdfa.value' and @radio-value='#{perms[:advertising_identifier]}']//input").trigger('click') rescue nil

        if perms[:advertising_identifier]
          raise "Sorry, the advertising_identifier menu is not yet supported. Open '#{current_url}' in your browser and manally submit the app".red
        end
      end
      

      Helper.log.info("Filled out the export compliance and other information on iTC".green)

      click_on "Submit"
      sleep 5

      if page.has_content?WAITING_FOR_REVIEW
        # Everything worked :)
        Helper.log.info("Successfully submitted App for Review".green)
        return true
      else
        raise "So close, it looks like there went something wrong with the actual deployment. Checkout '#{current_url}'".red
      end
    else
      raise "Something is missing here.".red
    end
    return false
  rescue Exception => ex
    error_occured(ex)
  end
end