b0VIM 8.23b joshholtzJoshs-MacBook-Air.local~joshholtz/Developer/fastlane/fastlane/spaceship/lib/spaceship/tunes/tunes_client.rbutf-8 3210#"! UtpJL^a]Rfn t _H N c X<o_F eOkYhxM)-Qad J^;Z2 { z _ ^ K ? >   F { S +  h ^ ] =   g f P  B|43qpV `VU~V:Fq first_team = teams.first["contentProvider"] puts("Alternatively you can pass the team name or team ID using the `FASTLANE_ITC_TEAM_ID` or `FASTLANE_ITC_TEAM_NAME` environment variable") puts("Note: to automatically choose the team, provide either the App Store Connect Team ID, or the Team Name in your fastlane/Appfile:") if ENV["FASTLANE_HIDE_TEAM_INFORMATION"].to_s.length == 0 puts("Multiple #{'App Store Connect teams'.yellow} found, please enter the number of the team you want to use: ") loop do # user didn't specify a team... #thisiswhywecanthavenicethings end return self.team_id self.team_id = t_id # actually set the team id here puts("Looking for App Store Connect Team with ID #{t_id}") if Spaceship::Globals.verbose? if t_id.length > 0 t_id = teams.first['contentProvider']['contentProviderId'].to_s if teams.count == 1 end p t_id = t['providerId'].to_s if t['name'].casecmp(t_name).zero? #t_id = t['contentProvider']['conten t_id = t['providerId'].to_s if t['name'].casecmp(t_name).zero? teams.each do |t| puts("Looking for App Store Connect Team with name #{t_name}") if Spaceship::Globals.verbose? if t_name.length > 0 && t_id.length.zero? # we prefer IDs over names, they are unique t_name = (team_name || ENV['FASTLANE_ITC_TEAM_NAME'] || '').strip t_id = (team_id || ENV['FASTLANE_ITC_TEAM_ID'] || '').strip def select_team(team_id: nil, team_name: nil) # @param team_name (String) (optional): The name of an App Store Connect team # @param team_id (String) (optional): The ID of an App Store Connect team # # called on CI systems # Shows a team selection for the user in the terminal. This should not be end "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/" def self.hostname ##################################################### # @!group Init and Login ##################################################### end end r r = [r[1], r[0]] if is_portrait r = resolutions[device] } 'ipadPro129' => [2732, 2048] 'ipadPro11' => [2388, 1668], 'ipadPro' => [2732, 2048], 'ipad105' => [2224, 1668], 'ipad' => [1024, 768], 'iphone65' => [2688, 1242], 'iphone58' => [2436, 1125], 'iphone6Plus' => [2208, 1242], 'iphone6' => [1334, 750], 'iphone4' => [1136, 640], resolutions = { def video_preview_resolution_for(device, is_portrait) # trailer preview screenshots are required to have a specific size class << self end @additional_headers = { 'x-csrf-itc': 'itc' } # Used by most WebObjects requests starting in July 2021 @du_client = DUClient.new super def initialize attr_reader :du_client ITunesConnectPotentialServerError = Tunes::PotentialServerError ITunesConnectTemporaryError = Tunes::TemporaryError ITunesConnectError = Tunes::Error # Legacy support class TunesClient < Spaceship::Client # rubocop:disable Metrics/ClassLengthmodule Spaceshiprequire_relative 'territory'require_relative 'pricing_tier'require_relative 'iap_subscription_pricing_tier'require_relative 'errors'require_relative 'availability'require_relative 'app_version_ref'require_relative 'app_version_common'require_relative '../du/upload_file'require_relative '../du/du_client'require_relative '../client'require "securerandom"ad (pbVLDC! aYS,('end # rubocop:enable Metrics/ClassLength end end handle_itc_response(data) data = parse_response(r, 'data') end req.headers['Content-Type'] = 'application/json' req.body = data.to_json req.url(url) r = request(:post) do |req| } ] } } value: testing testing: { }, value: tester.last_name lastName: { },adi vu54tXNM >  first_team = teams.first["contentProvider"] puts("Alternatively you can first_team = teams.first["contentProvider"] puts("Alternatively you can pass the team name or team ID using the `FASTLANE_ITC_TEAM_ID` or `FASTLANE_ITC_TEAM_NAME` environment variable") puts("Note: to automatically choose the team, provide either the App Store Connect Team ID, or the Team Name in your fastlane/Appfile:") if ENV["FASTLANE_HIDE_TEAM_INFORMATION"].to_s.length == 0 puts("Multiple #{'App Store Connect teams'.yellow} found, please enter the number of the team you want to use: ") loop do # user didn't specify a team... #thisiswhywecanthavenicethings end return self.team_id self.team_id = t_id # actually set the team id here puts("Looking for App Store Connect Team with ID #{t_id}") if Spaceship::Globals.verbose? if t_id.length > 0 t_id = teams.first['providerId'].to_s if teams.count == 1 end puts("Could not find team with name '#{t_name}', trying to fallback to default team") if t_id.length.zero? endad&^~k/w . Y M L  U 7 + !   n f e , 2 vSIH/zc[Z(qY21X*)^TSxY)( errors += handle_response_hash.call(value, current_language) hash.each do |key, value| current_language ||= hash["language"] if hash.kind_of?(Hash) errors = [] handle_response_hash = lambda do |hash, current_language = nil| # what language the error was caused in # We pass on the `current_language` so that the error message tells the user end logger.debug("Request was successful") if errors_in_data.count == 0 && errors_in_version_info.count == 0 # If we have any errors or "info" we need to treat them as warnings or errors errors_in_version_info = fetch_errors_in_data(data_section: data, sub_section_name: "versionInfo", keys: error_and_info_keys_to_check) errors_in_data = fetch_errors_in_data(data_section: data, keys: error_and_info_keys_to_check) error_and_info_keys_to_check = error_keys + info_keys info_keys = ["sectionInfoKeys", "sectionWarningKeys"] error_keys = ["sectionErrorKeys", "validationErrors", "serviceErrors"] data = raw['data'] || raw # sometimes it's with data, sometimes it isn't return unless raw.kind_of?(Hash) return unless raw def handle_itc_response(raw, flaky_api_call: false) # Patience is a virtue. # If the response is coming from a flaky api, set flaky_api_call to true so we retry a little. # rubocop:disable Metrics/PerceivedComplexity end return error_map end error_map[key] = errors if errors.count > 0 errors = sub_section.fetch(key, []) keys.each do |key| error_map = {} end return {} unless sub_section end sub_section = data_section else sub_section = data_section[sub_section_name] if data_section && sub_section_name def fetch_errors_in_data(data_section: nil, sub_section_name: nil, keys: nil) # Returns a mapping of keys to data array if we find anything, otherwise, empty map # where we should check # along with the name of the sub_section of your original data # This method allows you to pass in a set of keys to check for # Sometimes we get errors or info nested in our data end return result store_cookie result = send_shared_login_request(user, password) clear_user_cached_data def send_login_request(user, password) end end end return self.team_id self.team_id = team_to_use['contentProvider']['contentProviderId'].to_s # actually set the team id here if team_to_use team_to_use = teams[selected] if selected >= 0 selected = ($stdin.gets || '').strip.to_i - 1 end raise "Multiple App Store Connect Teams found; unable to choose, terminal not interactive!" puts("Please check that you set FASTLANE_ITC_TEAM_ID or FASTLANE_ITC_TEAM_NAME to the right value.") puts("Multiple teams found on App Store Connect, Your Terminal is running in non-interactive mode! Cannot continue from here.") unless Spaceship::Client::UserInterface.interactive? end puts("#{i + 1}) \"#{team['contentProvider']['name']}\" (#{team['contentProvider']['contentProviderId']})") teams.each_with_index do |team, i| # We're not using highline here, as spaceship doesn't have a dependency to fastlane_core or highline end puts("") puts(" itc_team_name \"#{first_team['name']}\"") puts("") puts("or") puts("") puts(" itc_team_id \"#{first_team['contentProviderId']}\"") puts("")ad<]MbC  n m % X W  M  K  8 h p!v]SRnSt?fDS # This can't be longer than 255 characters. # @param name (String): The name of your app as it will appear on the App Store. # Creates a new application on App Store Connect end handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.body = data.to_json req.url("ra/apps/#{app_id}/details") r = request(:post) do |req| def update_app_details!(app_id, data) end parse_response(r, 'data') r = request(:get, "ra/appbundles/metadetail/#{app_id}") def bundle_details(app_id) end parse_response(r, 'data') r = request(:get, "ra/apps/#{app_id}/details") def app_details(app_id) end parse_response(r, 'data')['summaries'] r = request(:get, 'ra/apps/manageyourapps/summary/v2') def applications ##################################################### # @!group Applications ##################################################### # rubocop:enable Metrics/PerceivedComplexity end return data end puts(info_value) info_in_version_info.each do |info_key, info_value| end puts(info_value) info_in_data.each do |info_key, info_value| info_in_version_info = fetch_errors_in_data(data_section: data, sub_section_name: "versionInfo", keys: info_keys) info_in_data = fetch_errors_in_data(data_section: data, keys: info_keys) # Search at data level, as well as "versionInfo" level for info and warnings end end raise ITunesConnectError.new, errors.join(' ') else raise ITunesConnectPotentialServerError.new, errors.join(' ') elsif flaky_api_call raise_insufficient_permission_error! elsif errors.count == 1 && errors.first.include?("Forbidden") raise ITunesConnectTemporaryError.new, errors.first elsif errors.count == 1 && errors.first.include?("try again later") # This is a special error which we really don't care about if errors.count == 1 && errors.first == "You haven't made any changes." # Sample `error` content: [["Forbidden"]] if errors.count > 0 # they are separated by `.` by default errors << different_error if different_error different_error = raw.fetch('messages', {}).fetch('error', nil) # e.g. {"warn"=>nil, "error"=>["operation_failed"], "info"=>nil} # Sometimes there is a different kind of error in the JSON response errors = errors.flat_map { |value| value } errors += errors_in_version_info.values if errors_in_version_info.values errors += errors_in_data.values if errors_in_data.values errors_in_version_info = fetch_errors_in_data(data_section: data, sub_section_name: "versionInfo", keys: error_keys) errors_in_data = fetch_errors_in_data(data_section: data, keys: error_keys) # Search at data level, as well as "versionInfo" level for errors errors = handle_response_hash.call(data) end return errors end # else: We don't care about simple values end errors += handle_response_hash.call(value) hash.each do |value| elsif hash.kind_of?(Array) end end current_language ? "[#{current_language}]: #{current_error_message}" : current_error_message errors += value.collect do |current_error_message| # Prepend the error with the language so it's easier to understand for the user next unless key == 'errorKeys' && value.kind_of?(Array) && value.count > 0adRGv ; : R + *  x < I   f(zyQ/ _WV|UI7sZ0y- version: version_number, versionId: version_id, id: thread_id, threads: [{ appNotes: { req.body = { req.url("ra/apps/#{app_id}/platforms/#{platform}/resolutionCenter") r = request(:post) do |req| def post_resolution_center(app_id, platform, thread_id, version_id, version_number, from, message_body) end parse_response(r, 'data') r = request(:get, "ra/apps/#{app_id}/platforms/#{platform}/resolutionCenter?v=latest") def get_resolution_center(app_id, platform) end return data['bundleIds'].keys data = parse_response(r, 'data') r = request(:get, "ra/apps/create/v2/?platformString=#{platform}") platform ||= "ios" def get_available_bundle_ids(platform: nil) end handle_itc_response(data) data = parse_response(r, 'data') end req.headers['Content-Type'] = 'application/json' }.to_json } value: version_number.to_s version: { req.body = { req.url("ra/apps/#{app_id}/platforms/#{platform}/versions/create/") r = request(:post) do |req| def create_version!(app_id, version_number, platform = 'ios') end handle_itc_response(data) data = parse_response(r, 'data') end req.headers['Content-Type'] = 'application/json' req.body = data.to_json req.url('ra/apps/create/v2') r = request(:post) do |req| # Now send back the modified hash end data['iTunesConnectUsers']['grantedUsers'] = data['iTunesConnectUsers']['availableUsers'].select { |user| itunes_connect_users.include?(user['username']) } data['iTunesConnectUsers']['grantedAllUsers'] = false unless itunes_connect_users.nil? data['enabledPlatformsForCreation'] = { value: platforms || [platform] } data['initialPlatform'] = platform data['enabledPlatformsForCreation'] = { value: [platform] } data['companyName'] = { value: company_name } if company_name data['bundleIdSuffix'] = { value: bundle_id_suffix } data['vendorId'] = { value: sku } data['primaryLocaleCode'] = { value: primary_language.to_itc_locale } data['primaryLanguage'] = { value: primary_language } data['bundleId'] = { value: bundle_id } data['name'] = { value: name } # some values are nil, that's why there is a hash # Now fill in the values we have data = parse_response(r, 'data') r = request(:get, "ra/apps/create/v2/?platformString=#{platform}") platform ||= "ios" primary_language ||= "English" # First, we need to fetch the data from Apple, which we then modify with the user's values puts("The `version` parameter is deprecated. Use `Spaceship::Tunes::Application.ensure_version!` method instead") if version def create_application!(name: nil, primary_language: nil, version: nil, sku: nil, bundle_id: nil, bundle_id_suffix: nil, company_name: nil, platform: nil, platforms: nil, itunes_connect_users: nil) # can't be changed after you submit your first build. # @param bundle_id (String): The bundle ID must match the one you used in Xcode. It # @param sku (String): A unique ID for your app that is not visible on the App Store. # (String): The version number is shown on the App Store and should match the one you used in Xcode. # @param version *DEPRECATED: Use `Spaceship::Tunes::Application.ensure_version!` method instead* # App Store territory, the information from your primary language will be used instead. # @param primary_language (String): If localized app information isn't available in anadfrWF7+= f % $ p K 4 3 c ;  > = = <  wv.o54W:~}4(ZY? req.url("ra/apps/#{app_id}/platforms/ios/versions/#{version_id}") r = request(:post) do |req| with_tunes_retry do raise "version_id is required" unless version_id.to_i > 0 raise "app_id is required" unless app_id def update_app_version!(app_id, version_id, data) end parse_response(r, 'data') r = request(:get, "ra/apps/#{app_id}/platforms/#{version_platform}/versions/#{version_id}") raise "version_id is required" unless version_id raise "version_platform is required" unless version_platform raise "app_id is required" unless app_id def app_version_data(app_id, version_platform: nil, version_id: nil) end app_version_data(app_id, version_platform: version_platform, version_id: version_id) version_platform = platform['platformString'] return nil unless version_id version_id = Spaceship::Tunes::AppVersionCommon.find_version_id(platform, is_live) return nil unless platform platform = Spaceship::Tunes::AppVersionCommon.find_platform(platforms, search_platform: platform) platforms = parse_response(r, 'data')['platforms'] r = request(:get, "ra/apps/#{app_id}/overview") # First we need to fetch the IDs for the edit / live version raise "app_id is required" unless app_id def app_version(app_id, is_live, platform: nil) ##################################################### # @!group AppVersions ##################################################### end all_reviews end end break else index += per_page if all_reviews.count < parse_response(r, 'data')['reviewCount'] end break all_reviews = all_reviews.select { |review| Time.at(review['value']['lastModified'] / 1000) > upto_date } if upto_date && last_review_date < upto_date last_review_date = Time.at(all_reviews[-1]['value']['lastModified'] / 1000) break if all_reviews.count == 0 # The following lines throw errors when there are no reviews so exit out of the loop before them if the app has no reviews all_reviews.concat(parse_response(r, 'data')['reviews']) r = request(:get, rating_url) rating_url << "&versionId=#{version_id}" unless version_id.empty? rating_url << "&storefront=#{storefront}" unless storefront.empty? rating_url << "&index=#{index}" rating_url << "sort=REVIEW_SORT_ORDER_MOST_RECENT" rating_url = "ra/apps/#{app_id}/platforms/#{platform}/reviews?" loop do upto_date = Time.parse(upto_date) unless upto_date.nil? all_reviews = [] per_page = 100 # apple default index = 0 def get_reviews(app_id, platform, storefront, version_id, upto_date = nil) end parse_response(r, 'data') r = request(:get, rating_url, params) params['version_id'] = version_id unless version_id.empty? params['storefront'] = storefront unless storefront.empty? params = {} rating_url = "ra/apps/#{app_id}/platforms/#{platform}/reviews/summary" # if storefront or version_id is empty api fails def get_ratings(app_id, platform, version_id = '', storefront = '') end handle_itc_response(data) data = parse_response(r, 'data') end req.headers['Content-Type'] = 'application/json' }.to_json } }] }] tokens: [] body: message_body, date: DateTime.now.strftime('%Q'), from: from, messages: [{ad t\RJI]UT5 t l N '  Y *   X W + * j 7 )     {B8 lk?>~K=1'& * ~E;%qU7/.   r = request(:post) do |req| } startTime: start_time measures: measures, group: group_for_view_by(view_by, measures), frequency: frequency, endTime: end_time, dimensionFilters: [], adamId: app_ids, data = { def time_series_analytics(app_ids, measures, start_time, end_time, frequency, view_by) ##################################################### # @!group AppAnalytics ##################################################### end handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.body = data.to_json req.url("ra/users/itc/#{member.user_id}/roles") r = request(:post) do |req| # send the changes back to Apple end data["user"]["userSoftwares"] = { value: { grantAllSoftware: false, grantedSoftwareAdamIds: apps } } else data["user"]["userSoftwares"] = { value: { grantAllSoftware: true, grantedSoftwareAdamIds: [] } } if apps.length == 0 end end end data["user"]["roles"] << template_role if template_role["value"]["name"] == role data["roles"].each do |template_role| # find role from template roles.each do |role| data["user"]["roles"] = [] roles << "admin" if roles.length == 0 data = parse_response(r, 'data') r = request(:get, "ra/users/itc/#{member.user_id}/roles") def update_member_roles!(member, roles: [], apps: []) end handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.body = data.to_json req.url("ra/users/itc/create") r = request(:post) do |req| # send the changes back to Apple end data["user"]["userSoftwares"] = { value: { grantAllSoftware: false, grantedSoftwareAdamIds: apps } } else data["user"]["userSoftwares"] = { value: { grantAllSoftware: true, grantedSoftwareAdamIds: [] } } if apps.length == 0 end end end data["user"]["roles"] << template_role if template_role["value"]["name"] == role data["roles"].each do |template_role| # find role from template roles.each do |role| data["user"]["roles"] = [] roles << "admin" if roles.length == 0 data["user"]["emailAddress"] = { value: email_address } data["user"]["lastName"] = { value: lastname } data["user"]["firstName"] = { value: firstname } data = parse_response(r, 'data') r = request(:get, "ra/users/itc/create") def create_member!(firstname: nil, lastname: nil, email_address: nil, roles: [], apps: []) end end req.headers['Content-Type'] = 'application/json' req.body = payload.to_json req.url("ra/users/itc/delete") request(:post) do |req| } email: email dsId: user_id, payload << { payload = [] def delete_member!(user_id, email) end request(:post, "ra/users/itc/#{email}/resendInvitation") def reinvite_member(email) end parse_response(r, 'data')["users"] r = request(:get, "ra/users/itc") def members ##################################################### # @!group Members ##################################################### end end handle_itc_response(r.body, flaky_api_call: true) end req.headers['Content-Type'] = 'application/json' req.body = data.to_jsonad_P ON y + x v  | { = w U   rDUG;/%$B`P6m>[MA5+*   r = request(:get, "ra/apps/#{app_id}/pricing/intervals") def price_tier(app_id) end intervals_array end end } } "grandfathered" => grandfathered "country" => language_code, "priceTierEndDate" => value["priceTierEndDate"], "priceTierEffectiveDate" => value["priceTierEffectiveDate"], "tierStem" => value["tierStem"], "value" => { { end { "value" => "FUTURE_NONE" } else existing_interval[:grandfathered].clone if existing_interval grandfathered = end pricing_intervals.find { |interval| interval[:country] == language_code } if pricing_intervals existing_interval = intervals_array = pricing_calculator.map do |language_code, value| pricing_calculator = iap_subscription_pricing_target(app_id: app_id, purchase_id: purchase_id, currency: subscription_price_target[:currency], tier: subscription_price_target[:tier]) if subscription_price_target end end } } "grandfathered" => interval[:grandfathered] "country" => interval[:country] || "WW", "priceTierEndDate" => interval[:end_date], "priceTierEffectiveDate" => interval[:begin_date], "tierStem" => interval[:tier], "value" => { { intervals_array = pricing_intervals.map do |interval| if pricing_intervals intervals_array = [] def transform_to_raw_pricing_intervals(app_id = nil, purchase_id = nil, pricing_intervals = 5, subscription_price_target = nil) end handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.body = data.to_json req.url("ra/apps/#{app_id}/pricing/intervals") r = request(:post) do |req| # send the changes back to Apple end end c c.delete('region') # we don't care about le region data["countries"] = supported_countries.collect do |c| if first_price # first price, need to set all countries data["theWorld"] = true data["countriesChanged"] = first_price data["pricingIntervalsFieldTO"]["value"].first["priceTierEndDate"] = nil data["pricingIntervalsFieldTO"]["value"].first["priceTierEffectiveDate"] = effective_date effective_date = (first_price ? nil : Time.now.to_i * 1000) data["pricingIntervalsFieldTO"]["value"].first["tierStem"] = price_tier.to_s data["pricingIntervalsFieldTO"]["value"] << {} if data["pricingIntervalsFieldTO"]["value"].count == 0 data["pricingIntervalsFieldTO"]["value"] ||= [] first_price = (data["pricingIntervalsFieldTO"]["value"] || []).count == 0 # first price data.delete('preOrder') # values that can cause a failure (invalid dates) so we are removing it # preOrder isn't needed for for the request and has some data = parse_response(r, 'data') r = request(:get, "ra/apps/#{app_id}/pricing/intervals") def update_price_tier!(app_id, price_tier) ##################################################### # @!group Pricing ##################################################### end data = parse_response(r) end req.headers['X-Requested-By'] = 'appstoreconnect.apple.com' req.headers['Content-Type'] = 'application/json' req.body = data.to_json req.url("https://appstoreconnect.apple.com/analytics/api/v1/data/time-series")ad"vN|pf^]'!6- r O , p Q  { z @ % / r ? NGa f{Y"vu data = parse_response(r, 'data') handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.body = data.to_json req.url("ra/apps/#{app_id}/pricing/intervals") r = request(:post) do |req| # send the changes back to Apple data["b2bOrganizations"] = availability.b2b_app_enabled ? availability.b2b_organizations.map { |org| { "value" => { "type" => org.type, "depCustomerId" => org.dep_customer_id, "organizationId" => org.dep_organization_id, "name" => org.name } } } : [] data["b2bUsers"] = availability.b2b_app_enabled ? availability.b2b_users.map { |user| { "value" => { "add" => user.add, "delete" => user.delete, "dsUsername" => user.ds_username } } } : [] data["preOrder"]["appAvailableDate"] = { "value" => app_available_date, "isEditable" => true, "isRequired" => true, "errorKeys" => nil } data["preOrder"]["clearedForPreOrder"] = { "value" => cleared_for_preorder, "isEditable" => true, "isRequired" => true, "errorKeys" => nil } data["educationalDiscount"] = availability.educational_discount data["b2bAppEnabled"] = availability.b2b_app_enabled app_available_date = cleared_for_preorder ? availability.app_available_date : nil cleared_for_preorder = availability.cleared_for_preorder # API will error out if cleared_for_preorder is false and app_available_date has a date # This is need for apps that have never set either of these before # Sets app_available_date to nil if cleared_for_preorder if false data["preOrder"] ||= {} # InitializespreOrder (if needed) data["theWorld"] = availability.include_future_territories.nil? ? true : availability.include_future_territories data["countries"] = availability.territories.map { |territory| { 'code' => territory.code } } data["countriesChanged"] = true data = parse_response(r, 'data') r = request(:get, "ra/apps/#{app_id}/pricing/intervals") def update_availability!(app_id, availability) # @return [Spaceship::Tunes::Availability] the new Availability # # @param availability (Availability): The availability update # @param app_id (String): The id of your app # @note Although this information is publicly available, the current spaceship implementation requires you to have a logged in client to access it # # Updates the availability ##################################################### # @!group Availability ##################################################### end end data.map { |tier| Spaceship::Tunes::PricingTier.factory(tier) } data = parse_response(r, 'data')['pricingTiers'] r = request(:get, "ra/apps/#{app_id}/iaps/pricing/matrix") @pricing_tiers ||= begin def pricing_tiers(app_id) # ... # }, { # ... # }, { # "fWholesalePrice": "$0.00" # "fRetailPrice": "$0.00", # "retailPrice": 0.0, # "wholesalePrice": 0.0, # "currencyCode": "USD", # "currencySymbol": "$", # "countryCode": "US", # "country": "United States", # "pricingInfo": [{ # "tierName": "Free", # "tierStem": "0", # [{ # @return [Array] the PricingTier objects (Spaceship::Tunes::PricingTier) # # @note Although this information is publicly available, the current spaceship implementation requires you to have a logged in client to access it # # Returns an array of all available pricing tiers end end nil rescue data["pricingIntervalsFieldTO"]["value"].first["tierStem"] begin data = parse_response(r, 'data')adchA4. [ S R , # h H @ ? #  u ; O  s k j O  ~E  f+' CiH9 ~O  "assetToken" => data["token"], "value" => { { data = du_client.upload_purchase_review_screenshot(app_id, upload_image, content_provider_id, sso_token_for_image) def upload_purchase_review_screenshot(app_id, upload_image) # @return [JSON] the screenshot data, ready to be added to an In-App-Purchase # @param upload_image (UploadFile): The icon to upload # @param app_id (AppId): The id of the app # Uploads an In-App-Purchase Review screenshot end } "isActive" => false "showByDefault" => true, ], } "status" => "proposed" }, "errorKeys" => nil "isREquired" => false, "isEditable" => true, }, "checksum" => data["md5"] "width" => data["width"], "height" => data["height"], "originalFileName" => upload_image.file_name, "assetToken" => data["token"], "value" => { "image" => { "id" => nil, { "images" => [ { data = du_client.upload_purchase_merch_screenshot(app_id, upload_image, content_provider_id, sso_token_for_image) def upload_purchase_merch_screenshot(app_id, upload_image) # @return [JSON] the image data, ready to be added to an In-App-Purchase # @param upload_image (UploadFile): The icon to upload # Uploads an In-App-Purchase Promotional image end du_client.upload_watch_icon(app_version, upload_image, content_provider_id, sso_token_for_image) raise "upload_image is required" unless upload_image raise "app_version is required" unless app_version def upload_watch_icon(app_version, upload_image) # @return [JSON] the response # @param upload_image (UploadFile): The icon to upload # @param app_version (AppVersion): The version of your app # Uploads a watch icon end du_client.upload_large_icon(app_version, upload_image, content_provider_id, sso_token_for_image) raise "upload_image is required" unless upload_image raise "app_version is required" unless app_version def upload_large_icon(app_version, upload_image) # @return [JSON] the response # @param upload_image (UploadFile): The icon to upload # @param app_version (AppVersion): The version of your app # Uploads a large icon ##################################################### # @!group App Icons ##################################################### end parse_response(r, 'data')['detailLocales'] r = request(:get, "ra/ref") def available_languages end parse_response(r, 'data') r = request(:get, "ra/apps/pricing/supportedCountries") def supported_countries # ... # }, { # "region": "Europe" # "name": "Albania", # "code": "AL", # [{ # An array of supported countries end data.map { |country| Spaceship::Tunes::Territory.factory(country) } data = supported_countries def supported_territories # @return [Array] the Territory objects (Spaceship::Tunes::Territory) # # @note Although this information is publicly available, the current spaceship implementation requires you to have a logged in client to access it # # Returns an array of all supported territories end Spaceship::Tunes::Availability.factory(data) data = parse_response(r, 'data') r = request(:get, "ra/apps/#{app_id}/pricing/intervals") def availability(app_id) end Spaceship::Tunes::Availability.factory(data)ad b:I  s ) X  ` 0  T % $ A  zA@r2dcClogf, ^#~} raise "app_version is required" unless app_version def upload_app_review_attachment(app_version, upload_attachment_file) # @return [JSON] the response # @param upload_attachment_file (file): File to upload # @param app_version (AppVersion): The version of your app(must be edit version) # Uploads a attachment file ##################################################### # @!review attachment file ##################################################### end du_client.upload_trailer_preview(app_version, upload_trailer_preview, content_provider_id, sso_token_for_image, device) raise "device is required" unless device raise "upload_trailer_preview is required" unless upload_trailer_preview raise "app_version is required" unless app_version def upload_trailer_preview(app_version, upload_trailer_preview, device) # @return [JSON] the response # @param device (string): The target device # @param upload_trailer_preview (UploadFile): The trailer preview to upload # @param app_version (AppVersion): The version of your app # Uploads the trailer preview end du_client.upload_trailer(app_version, upload_trailer, content_provider_id, sso_token_for_video) raise "upload_trailer is required" unless upload_trailer raise "app_version is required" unless app_version def upload_trailer(app_version, upload_trailer) # @return [JSON] the response # @param upload_trailer (UploadFile): The trailer to upload # @param app_version (AppVersion): The version of your app # Uploads the transit app file end du_client.upload_geojson(app_version, upload_file, content_provider_id, sso_token_for_image) raise "upload_file is required" unless upload_file raise "app_version is required" unless app_version def upload_geojson(app_version, upload_file) # @return [JSON] the response # @param upload_file (UploadFile): The image to upload # @param app_version (AppVersion): The version of your app # Uploads the transit app file end du_client.upload_messages_screenshot(app_version, upload_image, content_provider_id, sso_token_for_image, device) raise "device is required" unless device raise "upload_image is required" unless upload_image raise "app_version is required" unless app_version def upload_messages_screenshot(app_version, upload_image, device) # @return [JSON] the response # @param device (string): The target device # @param upload_image (UploadFile): The image to upload # @param app_version (AppVersion): The version of your app # Uploads an iMessage screenshot end du_client.upload_screenshot(app_version, upload_image, content_provid puts "🔥🔥 #{content_provider_id}" raise "device is required" unless device raise "upload_image is required" unless upload_image raise "app_version is required" unless app_version def upload_screenshot(app_version, upload_image, device, is_messages) # @return [JSON] the response # @param is_messages (Bool): True if the screenshot is for iMessage # @param device (string): The target device # @param upload_image (UploadFile): The image to upload # @param app_version (AppVersion): The version of your app # Uploads a screenshot end } } "checksum" => data["md5"] "width" => data["width"], "height" => data["height"], "size" => data["length"], "originalFileName" => upload_image.file_name, "type" => du_client.get_picture_type(upload_image), "sortOrder" => 0,ad_ji|k c ) ( } u t :  C  y R  e 6  t H@o;:ukjH@?J,P(g? end handle_itc_response(r.body) r = request(:get, "ra/apps/#{app_id}/trains/#{train}/buildHistory?platform=#{platform}") platform = 'ios' if platform.nil? def all_builds_for_train(app_id: nil, train: nil, platform: 'ios') end handle_itc_response(r.body) r = request(:get, "ra/apps/#{app_id}/buildHistory?platform=#{platform}") platform = 'ios' if platform.nil? def all_build_trains(app_id: nil, platform: 'ios') # All build trains, even if there is no TestFlight end handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.body = {}.to_json req.url("ra/apps/#{app_id}/platforms/#{platform}/trains/#{train}/builds/#{build_number}/reject") r = request(:post) do |req| def remove_testflight_build_from_review!(app_id: nil, train: nil, build_number: nil, platform: 'ios') end handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.body = data.to_json req.url("ra/apps/#{app_id}/testingTypes/#{testing_type}/trains/") r = request(:post) do |req| data.delete("dailySubmissionCountByPlatform") # The request fails if this key is present in the data raise "app_id is required" unless app_id def update_build_trains!(app_id, testing_type, data) # rubocop:enable Metrics/BlockNesting end raise Spaceship::Client::UnexpectedResponse, "Temporary App Store Connect error: #{ex}" end end retry sleep(3) unless Object.const_defined?("SpecHelper") logger.warn("Received temporary server error from App Store Connect. Retrying the request...") if tries > 0 tries -= 1 if retry_error_messages.any? { |message| ex.to_s.include?(message) } ].freeze "Service Unavailable" "Internal Server Error", "ITC.response.error.OPERATION_FAILED", retry_error_messages = [ # https://github.com/fastlane/fastlane/issues/6419 # we need to catch those errors and retry # Build trains fail randomly very often rescue Spaceship::Client::UnexpectedResponse => ex return parse_response(r, 'data') r = request(:get, url) url += "&platform=#{platform}" unless platform.nil? url = "ra/apps/#{app_id}/trains/?testingType=#{testing_type}" raise "app_id is required" unless app_id def build_trains(app_id, testing_type, tries = 5, platform: nil) # @param (testing_type) internal or external # rubocop:disable Metrics/BlockNesting ##################################################### # @!group Build Trains ##################################################### end parse_response(r, 'data')['builds'] r = request(:get, "ra/apps/#{app_id}/versions/#{version_id}/candidateBuilds") def candidate_builds(app_id, version_id) ##################################################### # @!group CandiateBuilds ##################################################### end Spaceship::Tunes::AppVersionRef.factory(data) data = parse_response(r, 'data') r = request(:get, '/WebObjects/iTunesConnect.woa/ra/apps/version/ref') def ref_data # @return [AppVersionRef] the response # Fetches the App Version Reference information from ITC end du_client.upload_app_review_attachment(app_version, upload_attachment_file, content_provider_id, sso_token_for_image) raise "upload_attachment_file is required" unless upload_attachment_file raise "app_version must be live version" if app_version.is_live?ad?sF4  wvH x a # G q 0  Z t6x<wv5k,h(l87bsr current['privacyPolicyUrl']['value'] = privacy_policy_url if privacy_policy_url current['marketingUrl']['value'] = marketing_url if marketing_url current['feedbackEmail']['value'] = feedback_email if feedback_email current['description']['value'] = description if description current['whatsNew']['value'] = changelog if changelog build_info['details'].each do |current| # First the localized values: # Now fill in the values provided by the user build_info = get_build_info_for_review(app_id: app_id, train: train, build_number: build_number, platform: platform) third_party: false) proprietary: false, is_exempt: false, encryption_updated: false, encryption: false, review_notes: nil, review_password: nil, review_user_name: nil, privacy_policy_url: nil, # Optional Metadata: significant_change: false, phone_number: nil, review_email: nil, last_name: nil, first_name: nil, marketing_url: nil, feedback_email: nil, description: nil, changelog: nil, # Required Metadata: def submit_testflight_build_for_review!(app_id: nil, train: nil, build_number: nil, platform: 'ios', # rubocop:disable Metrics/ParameterLists end handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.body = build_info.to_json req.url(url) r = request(:post) do |req| # same URL, but a POST request # Now send everything back to iTC build_info['reviewAccountRequired']['value'] = (review_user_name.to_s + review_password.to_s).length > 0 review_password = build_info['reviewPassword']['value'] review_user_name = build_info['reviewUserName']['value'] end current["feedbackEmail"]["value"] = feedback_email if feedback_email current["description"]["value"] = description if description current["whatsNew"]["value"] = whats_new if whats_new build_info["details"].each do |current| build_info = get_build_info_for_review(app_id: app_id, train: train, build_number: build_number, platform: platform) url = "ra/apps/#{app_id}/platforms/#{platform}/trains/#{train}/builds/#{build_number}/testInformation" platform: 'ios') feedback_email: nil, description: nil, whats_new: nil, # optional: build_number: nil, train: nil, def update_build_information!(app_id: nil, end handle_itc_response(r.body) r = request(:get, "ra/apps/#{app_id}/platforms/#{platform || 'ios'}/trains/#{train}/builds/#{build_number}/details") def build_details(app_id: nil, train: nil, build_number: nil, platform: nil)ad e{zdC|oL" y l <   I H ! ( L B    m l ; q8.- L*~}[Z^0 n\Q\F   raise "app_id is required" unless app_id def release!(app_id, version) ##################################################### # @!group release ##################################################### end parse_response(r, 'data') end raise "Something went wrong when submitting the app for review. Make sure to pass valid options to submit your app for review" else # success elsif r.body.fetch('messages').fetch('info').last == "Successful POST" raise "Something wrong with your Export Compliance: #{export_error_keys}" elsif export_error_keys.any? raise "Something wrong with your Ad ID information: #{ad_id_error_keys}." if ad_id_error_keys.any? export_error_keys = r.body.fetch('data').fetch('exportCompliance').fetch('sectionErrorKeys') ad_id_error_keys = r.body.fetch('data').fetch('adIdInfo').fetch('sectionErrorKeys') # keys in returned adIdInfo / exportCompliance and prints them out. # was failed because of Ad ID Info / Export Compliance. This checks for any section error # App Store Connect still returns a success status code even the submission handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.body = data.to_json req.url("ra/apps/#{app_id}/versions/#{version}/submit/complete") r = request(:post) do |req| # ra/apps/1039164429/version/submit/complete raise "app_id is required" unless app_id def send_app_submission(app_id, version, data) end parse_response(r, 'data') handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.url("ra/apps/#{app_id}/versions/#{version}/submit/summary") r = request(:get) do |req| raise "version is required" unless version raise "app_id is required" unless app_id def prepare_app_submissions(app_id, version) ##################################################### # @!group Submit for Review ##################################################### end r.body['data'] handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.url(url) r = request(:get) do |req| url = "ra/apps/#{app_id}/platforms/#{platform}/trains/#{train}/builds/#{build_number}/testInformation" def get_build_info_for_review(app_id: nil, train: nil, build_number: nil, platform: 'ios') # rubocop:enable Metrics/ParameterLists end handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.body = review_info.to_json req.url("ra/apps/#{app_id}/platforms/#{platform}/trains/#{train}/builds/#{build_number}/review/submit") r = request(:post) do |req| # same URL, but a POST request } } } "value" => third_party "containsThirdPartyCryptography" => { }, "value" => proprietary "containsProprietaryCryptography" => { }, "value" => is_exempt "isExempt" => { }, "value" => encryption_updated "encryptionUpdated" => { }, "value" => encryption "usesEncryption" => { "exportComplianceTO" => { "buildTestInformationTO" => build_info, }, "value" => significant_change "significantChange" => { review_info = { end current['pageLanguageValue'] = current['language'] # There is no valid reason why we need this, only iTC being iTCadPkd+ |Y l  q i h .  S 7 / .   ^ + k *   U80/g_^)a6 -#q0n0[O+! end end handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.body = data.to_json req.url("ra/apps/#{app_id}/iaps/#{purchase_id}") r = request(:put) do |req| with_tunes_retry do def update_iap!(app_id: nil, purchase_id: nil, data: nil) # updates an In-App-Purchases end end handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.body = data.to_json req.url("ra/apps/#{app_id}/iaps/family/#{family_id}/") r = request(:put) do |req| with_tunes_retry do def update_iap_family!(app_id: nil, family_id: nil, data: nil) # updates an In-App-Purchases-Family end end data.map { |tier| Spaceship::Tunes::IAPSubscriptionPricingTier.factory(tier) } data = parse_response(r, "data")["pricingTiers"] r = request(:get, "ra/apps/#{app_id}/iaps/pricing/matrix/recurring") @subscription_pricing_tiers ||= begin def subscription_pricing_tiers(app_id) # @return ([Spaceship::Tunes::IAPSubscriptionPricingTier]) An array of pricing tiers # @param app_id (String) The Apple ID of any app # # note: the matrix is the same for any app_id # Loads the full In-App-Purchases-Pricing-Matrix end parse_response(r, 'data') r = request(:get, "ra/apps/#{app_id}/iaps/family/#{family_id}") def load_iap_family(app_id: nil, family_id: nil) # Loads the full In-App-Purchases-Family end handle_itc_response(r) r = request(:post, "ra/apps/#{app_id}/iaps/#{purchase_id}/submission") def submit_iap!(app_id: nil, purchase_id: nil) # Submit the In-App-Purchase for review end parse_response(r, 'data') r = request(:get, "ra/apps/#{app_id}/iaps/#{purchase_id}") def load_iap(app_id: nil, purchase_id: nil) # Loads the full In-App-Purchases end handle_itc_response(r) r = request(:delete, "ra/apps/#{app_id}/iaps/#{purchase_id}") def delete_iap!(app_id: nil, purchase_id: nil) # Deletes a In-App-Purchases end return r.body["data"] r = request(:get, "ra/apps/#{app_id}/iaps/families") def iap_families(app_id: nil) # Returns list of all available Families end return r.body["data"] r = request(:get, "ra/apps/#{app_id}/iaps") def iaps(app_id: nil) # Returns list of all available In-App-Purchases ##################################################### # @!group in-app-purchases ##################################################### end parse_response(r, 'data') handle_itc_response(r.body) end req.body = app_id.to_s req.headers['Content-Type'] = 'application/json' req.url("ra/apps/#{app_id}/versions/#{version}/phasedRelease/state/COMPLETE") r = request(:post) do |req| raise "version is required" unless version raise "app_id is required" unless app_id def release_to_all_users!(app_id, version) ##################################################### # @!group release to all users ##################################################### end parse_response(r, 'data') handle_itc_response(r.body) end req.body = app_id.to_s req.headers['Content-Type'] = 'application/json' req.url("ra/apps/#{app_id}/versions/#{version}/releaseToStore") r = request(:post) do |req| raise "version is required" unless versionadAYeIWK' i a ` M  ^ >  N X<r= ;S+r2 Q"QP6 upload_file = UploadFile.from_path(merch_screenshot) # Upload App Store Promotional image (Optional) if merch_screenshot data['versions'][0]["reviewNotes"] = { value: review_notes } data["versions"][0]["details"]["value"] = versions_array end } } localeCode: k.to_s name: { value: v[:name] }, description: { value: v[:description] }, value: { versions_array << { versions.each do |k, v| versions_array = [] end end } } priceTierEffectiveDate: interval[:begin_date] priceTierEndDate: interval[:end_date], tierStem: interval[:tier].to_s, country: interval[:country] || "WW", value: { data['pricingIntervals'] << { pricing_intervals.each do |interval| data['pricingIntervals'] = [] if pricing_intervals # pricing tier data['freeTrialDurationType'] = { value: subscription_free_trial } if subscription_free_trial data['pricingDurationType'] = { value: subscription_duration } if subscription_duration data['clearedForSale'] = { value: cleared_for_sale } data['referenceName'] = { value: reference_name } data['productId'] = { value: product_id } data['familyId'] = family_id.to_s if family_id # some values are nil, that's why there is a hash # Now fill in the values we have data = parse_response(r, 'data') r = request(:get, "ra/apps/#{app_id}/iaps/#{type}/template") type ||= "consumable" # Load IAP Template based on Type def create_iap!(app_id: nil, type: nil, versions: nil, reference_name: nil, product_id: nil, cleared_for_sale: true, merch_screenshot: nil, review_notes: nil, review_screenshot: nil, pricing_intervals: nil, family_id: nil, subscription_duration: nil, subscription_free_trial: nil) # Creates an In-App-Purchases end parse_response(r, 'data') r = request(:get, "ra/apps/#{app_id}/iaps/#{purchase_id}/pricing/equalize/#{currency}/#{tier}") def iap_subscription_pricing_target(app_id: nil, purchase_id: nil, currency: nil, tier: nil) # returns pricing goal array end handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.body = data.to_json req.url("ra/apps/#{app_id}/iaps/family/") r = request(:post) do |req| data["details"]["value"] = versions data['name'] = { value: name } data['activeAddOns'][0]['referenceName'] = { value: reference_name } data['activeAddOns'][0]['productId'] = { value: product_id } data = parse_response(r, 'data') r = request(:get, "ra/apps/#{app_id}/iaps/family/template") def create_iap_family(app_id: nil, name: nil, product_id: nil, reference_name: nil, versions: []) end parse_response(r, 'data') r = request(:get, "ra/apps/#{app_id}/iaps/#{purchase_id}/pricing") def load_recurring_iap_pricing(app_id: nil, purchase_id: nil) end end handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.body = pricing_data.to_json pricing_data["subscriptions"] = pricing_intervals req.url("ra/apps/#{app_id}/iaps/#{purchase_id}/pricing/subscriptions") pricing_data = {} r = request(:post) do |req| with_tunes_retry do def update_recurring_iap_pricing!(app_id: nil, purchase_id: nil, pricing_intervals: nil)adhyx]@oed< k c b 3  P $ \ >  | t s s G  d@~l3)~`XW!w\NB.G[x> r = request(:get, "ra/apps/#{app_id}/promocodes/versions") def app_promocodes(app_id: nil) ##################################################### # @!group Promo codes ##################################################### end parse_response(r, 'data') r = request(:get, "ra/apps/#{app_id}/versions/#{version_id}/stateHistory?platform=#{platform}") def version_states_history(app_id, platform, version_id) end parse_response(r, 'data')['versions'] r = request(:get, "ra/apps/#{app_id}/stateHistory?platform=#{platform}") def versions_history(app_id, platform) ##################################################### # @!group State History ##################################################### end true end req.headers['Content-Type'] = 'application/json' end.to_json } } value: email emailAddress: { { req.body = emails.map do |email| req.url(url) request(:post) do |req| url = tester_class.url[:delete] def delete_sandbox_testers!(tester_class, emails) end response_object['user'] raise ITunesConnectError, errors.join(' ') unless errors.empty? errors = response_object['sectionErrorKeys'] response_object = parse_response(r, 'data') end req.headers['Content-Type'] = 'application/json' }.to_json } sandboxAccount: nil secretAnswer: { value: SecureRandom.hex }, secretQuestion: { value: SecureRandom.hex }, birthMonth: { value: 1 }, birthDay: { value: 1 }, storeFront: { value: country }, lastName: { value: last_name }, firstName: { value: first_name }, confirmPassword: { value: password }, password: { value: password }, emailAddress: { value: email }, user: { req.body = { req.url(url) r = request(:post) do |req| url = tester_class.url[:create] def create_sandbox_tester!(tester_class: nil, email: nil, password: nil, first_name: nil, last_name: nil, country: nil) end parse_response(r, 'data') r = request(:get, url) url = tester_class.url[:index] def sandbox_testers(tester_class) ##################################################### # @!group Sandbox Testers ##################################################### end data['sharedSecret'] data = parse_response(r, 'data') r = request(:post, "ra/apps/#{app_id}/iaps/appSharedSecret") def generate_shared_secret(app_id: nil) # Generates app-specific shared secret key end data['sharedSecret'] data = parse_response(r, 'data') r = request(:get, "ra/apps/#{app_id}/iaps/appSharedSecret") def get_shared_secret(app_id: nil) # Retrieves app-specific shared secret key end handle_itc_response(r.body) end req.headers['Content-Type'] = 'application/json' req.body = data.to_json req.url("ra/apps/#{app_id}/iaps") r = request(:post) do |req| # Now send back the modified hash end data["versions"][0]["reviewScreenshot"] = screenshot_data screenshot_data = upload_purchase_review_screenshot(app_id, upload_file) upload_file = UploadFile.from_path(review_screenshot) # Upload Screenshot: if review_screenshot end data["versions"][0]["merch"] = merch_data merch_data = upload_purchase_merch_screenshot(app_id, upload_file)ad%MueD$r9/ n f e +  Z Y 7 r R J I = < y Y R D :  trdZKCB#|`&%uml-"}_A.$w[9* value: tester.first_name firstName: { }, value: tester.email emailAddress: { { users: [ data = { url = tester.class.url(app_id)[:update_by_app] def update_tester_from_app!(tester, app_id, testing) end end } limit: 3 rank: "DESCENDING", dimension: view_by, metric: measures.first, return { else return nil if view_by.nil? || measures.nil? def group_for_view_by(view_by, measures) # Using rank=DESCENDING and limit=3 as this is what the App Store Connect analytics dashboard uses. # generates group hash used in the analytics time_series API. end @sso_token_for_video ||= ref_data.sso_token_for_video def sso_token_for_video # the ssoTokenForVideo found in the AppVersionRef instance end @sso_token_for_image ||= ref_data.sso_token_for_image def sso_token_for_image # the ssoTo pp user_details_data require 'pp pp user_detai return @content_provider_id if @content_provider_id def content_provider_id # the contentProviderId found in the user details data end @sso_token_for_video = nil @sso_token_for_image = nil @content_provider_id = nil def clear_user_cached_data end raise ex end retry sleep(seconds_to_sleep) unless Object.const_defined?("SpecHelper") logger.warn(msg) puts(msg) msg = "Potential server error received: '#{ex.message}'. Retrying after 10 seconds (remaining: #{potential_server_error_tries})..." unless (potential_server_error_tries -= 1).zero? seconds_to_sleep = 10 rescue Spaceship::TunesClient::ITunesConnectPotentialServerError => ex raise ex # re-raise the exception end retry sleep(seconds_to_sleep) unless Object.const_defined?("SpecHelper") logger.warn(msg) puts(msg) msg = "App Store Connect temporary error received: '#{ex.message}'. Retrying after #{seconds_to_sleep} seconds (remaining: #{tries})..." unless (tries -= 1).zero? seconds_to_sleep = 60 rescue Spaceship::TunesClient::ITunesConnectTemporaryError => ex return yield def with_tunes_retry(tries = 5, potential_server_error_tries = 3, &_block) private end parse_response(r, 'data') handle_itc_response(r.body) end req.body = app_id.to_s req.headers['Content-Type'] = 'application/json' req.url("ra/apps/#{app_id}/versions/#{version}/reject") r = request(:post) do |req| raise "version is required" unless version raise "app_id is required" unless app_id def reject!(app_id, version) ##################################################### # @!group reject ##################################################### end parse_response(r, 'data')['requests'] r = request(:get, "ra/apps/#{app_id}/promocodes/history") def app_promocodes_history(app_id: nil) end parse_response(r, 'data') end req.headers['Content-Type'] = 'application/json' req.body = data.to_json req.url(url) r = request(:post) do |req| url = "ra/apps/#{app_id}/promocodes/versions" }] versionId: version_id agreedToContract: true, numberOfCodes: quantity, data = [{ def generate_app_version_promocodes!(app_id: nil, version_id: nil, quantity: nil) end parse_response(r, 'data')['versions']ad<|{VC  '   _ . X P O , W  x p o M  l ihN.|2 raise "app_version is required" unless app_version def upload_app_review_attachment(app_version, upload_attachment_file) # @return [JSON] the response # @param upload_attachment_file (file): File to upload # @param app_version (AppVersion): The version of your app(must be edit version) # Uploads a attachment file ##################################################### # @!review attachment file ##################################################### end du_client.upload_trailer_preview(app_version, upload_trailer_preview, content_provider_id, sso_token_for_image, device) raise "device is required" unless device raise "upload_trailer_preview is required" unless upload_trailer_preview raise "app_version is required" unless app_version def upload_trailer_preview(app_version, upload_trailer_preview, device) # @return [JSON] the response # @param device (string): The target device # @param upload_trailer_preview (UploadFile): The trailer preview to upload # @param app_version (AppVersion): The version of your app # Uploads the trailer preview end du_client.upload_trailer(app_version, upload_trailer, content_provider_id, sso_token_for_video) raise "upload_trailer is required" unless upload_trailer raise "app_version is required" unless app_version def upload_trailer(app_version, upload_trailer) # @return [JSON] the response # @param upload_trailer (UploadFile): The trailer to upload # @param app_version (AppVersion): The version of your app # Uploads the transit app file end du_client.upload_geojson(app_version, upload_file, content_provider_id, sso_token_for_image) raise "upload_file is required" unless upload_file raise "app_version is required" unless app_version def upload_geojson(app_version, upload_file) # @return [JSON] the response # @param upload_file (UploadFile): The image to upload # @param app_version (AppVersion): The version of your app # Uploads the transit app file end du_client.upload_messages_screenshot(app_version, upload_image, content_provider_id, sso_token_for_image, device) raise "device is required" unless device raise "upload_image is required" unless upload_image raise "app_version is required" unless app_version def upload_messages_screenshot(app_version, upload_image, device) # @return [JSON] the response # @param device (string): The target device # @param upload_image (UploadFile): The image to upload # @param app_version (AppVersion): The version of your app # Uploads an iMessage screenshot end du_client.upload_screenshot(app_version, upload_image, content_provider_id, sso_token_for_image, device, is_messages)ad| < )g_^c' t G  v l d c * | c < value: tester.first_name firstName: { }, value: tester.email emailAddress: { { users: [ data = { url = tester.class.url(app_id)[:update_by_app] def update_tester_from_app!(tester, app_id, testing) end end } limit: 3 rank: "DESCENDING", dimension: view_by, metric: measures.first, return { else return nil if view_by.nil? || measures.nil? def group_for_view_by(view_by, measures) # Using rank=DESCENDING and limit=3 as this is what the App Store Connect analytics dashboard uses. # generates group hash used in the analytics time_series API. end @sso_token_for_video ||= ref_data.sso_token_for_video def sso_token_for_video # the ssoTokenForVideo found in the AppVersionRef instance end @sso_token_for_image ||= ref_data.sso_token_for_image def sso_token_for_image # the ssoTokenForImage found in the AppVersionRef instance end return @content_provider_id @content_provider_id ||= provider.to_s if provider provider = user_details_data["provider"]["providerId"]