lib/spaceship/tunes/build.rb



module Spaceship
  module Tunes
    # Represents a build which is inside the build train
    class Build < TunesBase
      #####################################################
      # @!group General metadata
      #####################################################

      # @return (String) The App identifier of this app, provided by iTunes Connect
      # @example
      #   "1013943394"
      attr_accessor :apple_id

      # @return (Spaceship::Tunes::BuildTrain) A reference to the build train this build is contained in
      attr_accessor :build_train

      # @return (Integer) The ID generated by iTunes Connect
      attr_accessor :id

      # @return (Boolean)
      attr_accessor :valid

      # @return (String) The build version (not the version number), but also is named `build number`
      attr_accessor :build_version

      # @return (String) The version number (e.g. 1.3)
      attr_accessor :train_version

      # @return (Boolean) Is this build currently processing?
      attr_accessor :processing

      # @return (Integer) The number of ticks since 1970 (e.g. 1413966436000)
      attr_accessor :upload_date

      # @return (String) URL to the app icon of this build (150x150px)
      attr_accessor :icon_url

      # @return (String) The name of the app this build is for
      attr_accessor :app_name

      # @return (String) The platform of this build (e.g. 'ios')
      attr_accessor :platform

      # @return (Integer) When is this build going to be invalid
      attr_accessor :internal_expiry_date

      # @return (Integer): When is the external build going to expire?
      attr_accessor :external_expiry_date

      # @return (Bool) Is external beta testing enabled for this train? Only one train can have enabled testing.
      attr_reader :external_testing_enabled

      # @return (Bool) Is internal beta testing enabled for this train? Only one train can have enabled testing.
      attr_reader :internal_testing_enabled

      # @return (String) The status of internal testflight testing for this build. One of active, submitForReview, approvedInactive, waiting
      attr_reader :external_testing_status

      # @return (Bool) Does this build support WatchKit?
      attr_accessor :watch_kit_enabled

      # @return (Bool):
      attr_accessor :ready_to_install

      #####################################################
      # @!group Analytics
      #####################################################

      # @return (Integer) Number of installs of this build
      attr_accessor :install_count

      # @return (Integer) Number of installs for this build that come from internal users
      attr_accessor :internal_install_count

      # @return (Integer) Number of installs for this build that come from external users
      attr_accessor :external_install_count

      # @return (Integer) Might be nil. The number of sessions for this build
      attr_accessor :session_count

      # @return (Integer) Might be nil. The number of crashes of this build
      attr_accessor :crash_count

      attr_mapping(
        'uploadDate' => :upload_date,
        'iconUrl' => :icon_url,
        'buildVersion' => :build_version,
        'trainVersion' => :train_version,
        'appName' => :app_name,
        'platform' => :platform,
        'id' => :id,
        'valid' => :valid,
        'processing' => :processing,

        'installCount' => :install_count,
        'internalInstallCount' => :internal_install_count,
        'externalInstallCount' => :external_install_count,
        'sessionCount' => :session_count,
        'crashCount' => :crash_count,
        'internalExpiry' => :internal_expiry_date,
        'externalExpiry' => :external_expiry_date,
        'watchKitEnabled' => :watch_kit_enabled,
        'readyToInstall' => :ready_to_install,
        'internalTesting.value' => :internal_testing_enabled,
        'externalTesting.value' => :external_testing_enabled,
        'buildTestInformationTO.externalStatus' => :external_testing_status
      )

      class << self
        # Create a new object based on a hash.
        # This is used to create a new object based on the server response.
        def factory(attrs)
          self.new(attrs)
        end
      end

      def setup
        super

        self.external_expiry_date ||= 0
        self.internal_expiry_date ||= 0
      end

      def details
        response = client.build_details(app_id: self.apple_id,
                                         train: self.train_version,
                                  build_number: self.build_version)
        response['apple_id'] = self.apple_id
        BuildDetails.factory(response)
      end

      def apple_id
        return @apple_id if @apple_id
        return self.build_train.application.apple_id
      end

      def update_build_information!(whats_new: nil,
                                    description: nil,
                                    feedback_email: nil)
        parameters = {
          app_id: self.apple_id,
          train: self.train_version,
          build_number: self.build_version,
          platform: self.platform
        }.merge({
          whats_new: whats_new,
          description: description,
          feedback_email: feedback_email
        })
        client.update_build_information!(parameters)
      end

      # This will submit this build for TestFlight beta review
      # @param metadata [Hash] A hash containing the following information (keys must be symbols):
      #  {
      #    # Required Metadata:
      #     changelog: "Changelog",
      #     description: "Your Description",
      #     feedback_email: "Email Address for Feedback",
      #     marketing_url: "https://marketing.com",
      #     first_name: "Felix",
      #     last_name: "Krause",
      #     review_email: "Contact email address for Apple",
      #     phone_number: "0128383383",
      #
      #   # Optional Metadata:
      #     privacy_policy_url: nil,
      #     review_notes: nil,
      #     review_user_name: nil,
      #     review_password: nil,
      #     encryption: false
      #  }
      def submit_for_beta_review!(metadata)
        parameters = {
          app_id: self.apple_id,
          train: self.train_version,
          build_number: self.build_version,
          platform: self.platform,

          # Required Metadata:
          changelog: "No changelog provided",
          description: "No app description provided",
          feedback_email: "contact@company.com",
          marketing_url: "http://marketing.com",
          first_name: "Felix",
          last_name: "Krause",
          review_email: "contact@company.com",
          phone_number: "0123456789",
          significant_change: false,

          # Optional Metadata:
          privacy_policy_url: nil,
          review_user_name: nil,
          review_password: nil,
          encryption: false
        }.merge(metadata)

        client.submit_testflight_build_for_review!(parameters)

        # Last, enable beta testing for this train (per iTC requirement). This will fail until the app has been approved for beta testing
        self.build_train.update_testing_status!(true, 'external', self)

        return parameters
      end

      # @return [String] A nicely formatted string about the state of this build
      # @examples:
      #   External, Internal, Inactive, Expired
      def testing_status
        testing ||= "External" if self.external_testing_enabled
        testing ||= "Internal" if self.internal_testing_enabled

        if Time.at(self.internal_expiry_date / 1000) > Time.now
          testing ||= "Inactive"
        else
          testing = "Expired"
        end

        return testing
      end

      # This will cancel the review process for this TestFlight build
      def cancel_beta_review!
        client.remove_testflight_build_from_review!(app_id: self.apple_id,
                                                     train: self.train_version,
                                              build_number: self.build_version,
                                                  platform: self.platform)
      end
    end
  end
end

# Example Response
# {"sectionErrorKeys"=>[],
#  "sectionInfoKeys"=>[],
#  "sectionWarningKeys"=>[],
#  "buildVersion"=>"1",
#  "trainVersion"=>"1.0",
#  "uploadDate"=>1441975590000,
#  "iconUrl"=>
#   "https://is2-ssl.mzstatic.com/image/thumb/Newsstand3/v4/a9/f9/8b/a9f98b23-592d-af2e-6e10-a04873bed5df/Icon-76@2x.png.png/150x150bb-80.png",
#  "iconAssetToken"=>
#   "Newsstand3/v4/a9/f9/8b/a9f98b23-592d-af2e-6e10-a04873bed5df/Icon-76@2x.png.png",
#  "appName"=>"Updated by fastlane",
#  "platform"=>"ios",
#  "betaEntitled"=>true,
#  "exceededFileSizeLimit"=>false,
#  "wentLiveWithVersion"=>false,
#  "processing"=>false,
#  "id"=>5298023,
#  "valid"=>true,
#  "missingExportCompliance"=>false,
#  "waitingForExportComplianceApproval"=>false,
#  "addedInternalUsersCount"=>0,
#  "addedExternalUsersCount"=>0,
#  "invitedExternalUsersCount"=>0,
#  "invitedInternalUsersCount"=>0,
#  "acceptedInternalUsersCount"=>1,
#  "acceptedExternalUsersCount"=>0,
#  "installCount"=>0,
#  "internalInstallCount"=>0,
#  "externalInstallCount"=>0,
#  "sessionCount"=>0,
#  "internalSessionCount"=>0,
#  "externalSessionCount"=>0,
#  "crashCount"=>0,
#  "internalCrashCount"=>0,
#  "externalCrashCount"=>0,
#  "promotedVersion"=>nil,
#  "internalState"=>"inactive",
#  "betaState"=>"submitForReview",
#  "internalExpiry"=>1444567590000,
#  "externalExpiry"=>0,
#  "watchKitEnabled"=>false,
#  "readyToInstall"=>true,
#  "sdkBuildWhitelisted"=>true,
#  "internalTesting"=>{"value"=>false, "isEditable"=>true, "isRequired"=>false, "errorKeys"=>nil},
#  "externalTesting"=>{"value"=>false, "isEditable"=>true, "isRequired"=>false, "errorKeys"=>nil}