require_relative '../test_flight/group' require_relative '../test_flight/build' require_relative 'app_analytics' require_relative 'app_details' require_relative 'app_ratings' require_relative 'app_submission' require_relative 'app_version' require_relative 'app_version_generated_promocodes' require_relative 'app_version_history' require_relative 'build_train' require_relative 'iap' require_relative 'tunes_base' require_relative 'version_set' module Spaceship module Tunes class Application < TunesBase # @return (String) The App identifier of this app, provided by App Store Connect # @example # "1013943394" attr_accessor :apple_id # @return (String) The name you provided for this app (in the default language) # @example # "Spaceship App" attr_accessor :name # @return (String) The SKU (Stock keeping unit) you provided for this app for internal tracking # @example # "1435592086" attr_accessor :vendor_id # @return (String) The bundle_id (app identifier) of your app # @example # "com.krausefx.app" attr_accessor :bundle_id # @return (String) Last modified attr_accessor :last_modified # @return (Integer) The number of issues provided by App Store Connect attr_accessor :issues_count # @return (String) The URL to a low resolution app icon of this app (340x340px). Might be nil # @example # "https://is1-ssl.mzstatic.com/image/thumb/Purple7/v4/cd/a3/e2/cda3e2ac-4034-c6af-ee0c-3e4d9a0bafaa/pr_source.png/340x340bb-80.png" # @example # nil attr_accessor :app_icon_preview_url # @return (Array) An array of all versions sets attr_accessor :version_sets attr_mapping( 'adamId' => :apple_id, 'name' => :name, 'vendorId' => :vendor_id, 'bundleId' => :bundle_id, 'lastModifiedDate' => :last_modified, 'issuesCount' => :issues_count, 'iconUrl' => :app_icon_preview_url ) class << self # @return (Array) Returns all apps available for this account def all client.applications.map { |application| self.factory(application) } end # @return (Spaceship::Tunes::Application) Returns the application matching the parameter # as either the App ID or the bundle identifier def find(identifier, mac: false) all.find do |app| ((app.apple_id && app.apple_id.casecmp(identifier.to_s) == 0) || (app.bundle_id && app.bundle_id.casecmp(identifier.to_s) == 0)) && app.version_sets.any? { |v| (mac ? ["osx"] : ["ios", "xros", "appletvos"]).include?(v.platform) } end end # Creates a new application on App Store Connect # @param name (String): The name of your app as it will appear on the App Store. # This can't be longer than 255 characters. # @param primary_language (String): If localized app information isn't available in an # App Store territory, the information from your primary language will be used instead. # @param version *DEPRECATED: Use `ensure_version!` method instead* # (String): The version number is shown on the App Store and should match the one you used in Xcode. # @param sku (String): A unique ID for your app that is not visible on the App Store. # @param bundle_id (String): The bundle ID must match the one you used in Xcode. It # can't be changed after you submit your first build. # @param company_name (String): The company name or developer name to display on the App Store for your apps. # It cannot be changed after you create your first app. # @param platform (String): Platform one of (ios,osx) # should it be an ios or an osx app def create!(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) puts("The `version` parameter is deprecated. Use `ensure_version!` method instead") if version client.create_application!(name: name, primary_language: primary_language, sku: sku, bundle_id: bundle_id, bundle_id_suffix: bundle_id_suffix, company_name: company_name, platform: platform, platforms: platforms, itunes_connect_users: itunes_connect_users) end def available_bundle_ids(platform: nil) client.get_available_bundle_ids(platform: platform) end end ##################################################### # @!group Getting information ##################################################### def version_set_for_platform(platform) version_sets.each do |version_set| return version_set if version_set.platform == platform end nil end # @return (Spaceship::Tunes::AppVersion) Receive the version that is currently live on the # App Store. You can't modify all values there, so be careful. def live_version(platform: nil) Spaceship::Tunes::AppVersion.find(self, self.apple_id, true, platform: platform) end # @return (Spaceship::Tunes::AppVersion) Receive the version that can fully be edited def edit_version(platform: nil) Spaceship::Tunes::AppVersion.find(self, self.apple_id, false, platform: platform) end # @return (Spaceship::Tunes::AppVersion) This will return the `edit_version` if available # and fallback to the `live_version`. Use this to just access the latest data def latest_version(platform: nil) edit_version(platform: platform) || live_version(platform: platform) end # @return (String) An URL to this specific resource. You can enter this URL into your browser def url "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/ng/app/#{self.apple_id}" end def analytics if self.live_version.nil? raise 'Analytics are only available for live apps.' end attrs = {} attrs[:apple_id] = self.apple_id Spaceship::Tunes::AppAnalytics.factory(attrs) end # @return (Hash) Contains the reason for rejection. # if everything is alright, the result will be # `{"sectionErrorKeys"=>[], "sectionInfoKeys"=>[], "sectionWarningKeys"=>[], "replyConstraints"=>{"minLength"=>1, "maxLength"=>4000}, "appNotes"=>{"threads"=>[]}, "betaNotes"=>{"threads"=>[]}, "appMessages"=>{"threads"=>[]}}` def resolution_center client.get_resolution_center(apple_id, platform) end def reply_resolution_center(app_id, platform, thread_id, version_id, version_number, from, message_body) client.post_resolution_center(app_id, platform, thread_id, version_id, version_number, from, message_body) end def ratings(version_id: '', storefront: '') attrs = client.get_ratings(apple_id, platform, version_id, storefront) attrs[:application] = self Tunes::AppRatings.new(attrs) end def platforms platforms = [] version_sets.each do |version_set| platforms << version_set.platform end platforms end def type if self.version_sets.nil? raise 'The application has no version sets and Spaceship does not know what to do here.' end if self.version_sets.length == 1 version_sets[0].platform end platform = Spaceship::Tunes::AppVersionCommon.find_platform(raw_data['versionSets']) platform['type'] end # kept for backward compatibility # tries to guess the platform of the currently submitted apps # note that as ITC now supports multiple app types, this might break # if your app supports more than one def platform if self.version_sets.nil? raise 'The application has no version sets and Spaceship does not know what to do here.' end if self.version_sets.length == 1 version_sets[0].platform elsif self.platforms == %w(ios appletvos) 'ios' end Spaceship::Tunes::AppVersionCommon.find_platform(raw_data['versionSets'])['platformString'] end def details attrs = client.app_details(apple_id) attrs[:application] = self Tunes::AppDetails.factory(attrs) end def versions_history ensure_not_a_bundle versions = client.versions_history(apple_id, platform) versions.map do |attrs| attrs[:application] = self Tunes::AppVersionHistory.factory(attrs) end end ##################################################### # @!group Modifying ##################################################### # Create a new version of your app # Since we have stored the outdated raw_data, we need to refresh this object # otherwise `edit_version` will return nil def create_version!(version_number, platform: nil) if edit_version(platform: platform) raise "Cannot create a new version for this app as there already is an `edit_version` available" end client.create_version!(apple_id, version_number, platform.nil? ? 'ios' : platform) # Future: implemented -reload method end # Will make sure the current edit_version matches the given version number # This will either create a new version or change the version number # from an existing version # @return (Bool) Was something changed? def ensure_version!(version_number, platform: nil) if (e = edit_version(platform: platform)) if e.version.to_s != version_number.to_s # Update an existing version e.version = version_number e.save! return true end return false else create_version!(version_number, platform: platform) return true end end def reject_version_if_possible! can_reject = edit_version.can_reject_version if can_reject client.reject!(apple_id, edit_version.version_id) end return can_reject end # set the price tier. This method doesn't require `save` to be called def update_price_tier!(price_tier) client.update_price_tier!(self.apple_id, price_tier) end # The current price tier def price_tier client.price_tier(self.apple_id) end # set the availability. This method doesn't require `save` to be called def update_availability!(availability) client.update_availability!(self.apple_id, availability) end # The current availability. def availability client.availability(self.apple_id) end ##################################################### # @!group in_app_purchases ##################################################### # Get base In-App-Purchases object def in_app_purchases attrs = {} attrs[:application] = self Tunes::IAP.factory(attrs) end ##################################################### # @!group Builds ##################################################### # TestFlight: A reference to all the build trains # @return [Hash] a hash, the version number and platform being the key def build_trains(platform: nil) TestFlight::BuildTrains.all(app_id: self.apple_id, platform: platform || self.platform) end # The numbers of all build trains that were uploaded # @return [Array] An array of train version numbers def all_build_train_numbers(platform: nil) return self.build_trains(platform: platform || self.platform).versions end # Receive the build details for a specific build # useful if the app is not listed in the TestFlight build list # which might happen if you don't use TestFlight # This is used to receive dSYM files from Apple def all_builds_for_train(train: nil, platform: nil) return TestFlight::Build.builds_for_train(app_id: self.apple_id, platform: platform || self.platform, train_version: train) end # @return [Array] This will return an array of *all* processing builds # this include pre-processing or standard processing def all_processing_builds(platform: nil) return TestFlight::Build.all_processing_builds(app_id: self.apple_id, platform: platform || self.platform) end def tunes_all_build_trains(app_id: nil, platform: nil) resp = client.all_build_trains(app_id: apple_id, platform: platform) trains = resp["trains"] or [] trains.map do |attrs| attrs['application'] = self Tunes::BuildTrain.factory(attrs) end end def tunes_all_builds_for_train(train: nil, platform: nil) resp = client.all_builds_for_train(app_id: apple_id, train: train, platform: platform) items = resp["items"] or [] items.map do |attrs| attrs['apple_id'] = apple_id Tunes::Build.factory(attrs) end end def tunes_build_details(train: nil, build_number: nil, platform: nil) resp = client.build_details(app_id: apple_id, train: train, build_number: build_number, platform: platform) resp['apple_id'] = apple_id Tunes::BuildDetails.factory(resp) end # Get all builds that are already processed for all build trains # You can either use the return value (array) or pass a block def builds(platform: nil) all = TestFlight::Build.all(app_id: self.apple_id, platform: platform || self.platform) return all unless block_given? all.each { |build| yield(build) } end ##################################################### # @!group Submit for Review ##################################################### def create_submission(platform: nil) version = self.latest_version(platform: platform) if version.nil? raise "Could not find a valid version to submit for review" end Spaceship::Tunes::AppSubmission.create(self, version, platform: platform) end # Cancels all ongoing TestFlight beta submission for this application def cancel_all_testflight_submissions! self.builds do |build| begin build.cancel_beta_review! rescue # We really don't care about any errors here end end true end ##################################################### # @!group release ##################################################### def release! version = self.edit_version if version.nil? raise "Could not find a valid version to release" end version.release! end ##################################################### # @!group release to all users ##################################################### def release_to_all_users! version = self.live_version if version.nil? raise "Could not find a valid version to release" end version.release_to_all_users! end ##################################################### # @!group General ##################################################### def setup super @version_sets = (self.raw_data['versionSets'] || []).map do |attrs| attrs[:application] = self Tunes::VersionSet.factory(attrs) end end ##################################################### # @!group Testers ##################################################### def default_external_group TestFlight::Group.default_external_group(app_id: self.apple_id) end ##################################################### # @!group Promo codes ##################################################### def promocodes data = client.app_promocodes(app_id: self.apple_id) data.map do |attrs| Tunes::AppVersionPromocodes.factory(attrs) end end def promocodes_history data = client.app_promocodes_history(app_id: self.apple_id) data.map do |attrs| Tunes::AppVersionGeneratedPromocodes.factory(attrs) end end # private to module def ensure_not_a_bundle # we only support applications raise "We do not support BUNDLE types right now" if self.type == 'BUNDLE' end end end end