class Fastlane::Actions::UploadAppPrivacyDetailsToAppStoreAction

Constants

DEFAULT_FILE_NAME
DEFAULT_PATH

Public Class Methods

ask_interactive_questions_for_json(show_intro = true) click to toggle source
# File fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb, line 58
def self.ask_interactive_questions_for_json(show_intro = true)
  if show_intro
    UI.important("You did not provide a JSON file for updating the app data usages")
    UI.important("fastlane will now run you through interactive question to generate the JSON file")
    UI.important("")
    UI.important("This JSON file can be saved in source control and used in this action with the :json_file option")

    unless UI.confirm("Ready to start?")
      UI.user_error!("Cancelled")
    end
  end

  # Fetch categories and purposes used for generating interactive questions
  categories = Spaceship::ConnectAPI::AppDataUsageCategory.all(includes: "grouping")
  purposes = Spaceship::ConnectAPI::AppDataUsagePurpose.all

  json = []

  unless UI.confirm("Are you collecting data?")
    json << {
      "data_protections" => [Spaceship::ConnectAPI::AppDataUsageDataProtection::ID::DATA_NOT_COLLECTED]
    }

    return json
  end

  categories.each do |category|
    # Ask if using category
    next unless UI.confirm("Collect data for #{category.id}?")

    purpose_names = purposes.map(&:id).join(', ')
    UI.message("How will this data be used? You'll be offered with #{purpose_names}")

    # Ask purposes
    selected_purposes = []
    loop do
      purposes.each do |purpose|
        selected_purposes << purpose if UI.confirm("Used for #{purpose.id}?")
      end

      break unless selected_purposes.empty?
      break unless UI.confirm("No purposes selected. Do you want to try again?")
    end

    # Skip asking protections if purposes were skipped
    next if selected_purposes.empty?

    # Ask protections
    is_linked_to_user = UI.confirm("Is #{category.id} linked to the user?")
    is_used_for_tracking = UI.confirm("Is #{category.id} used for tracking purposes?")

    # Map answers to values for API requests
    protection_id = is_linked_to_user ? Spaceship::ConnectAPI::AppDataUsageDataProtection::ID::DATA_LINKED_TO_YOU : Spaceship::ConnectAPI::AppDataUsageDataProtection::ID::DATA_NOT_LINKED_TO_YOU
    tracking_id = is_used_for_tracking ? Spaceship::ConnectAPI::AppDataUsageDataProtection::ID::DATA_USED_TO_TRACK_YOU : nil

    json << {
      "category" => category.id,
      "purposes" => selected_purposes.map(&:id).sort.uniq,
      "data_protections" => [
        protection_id, tracking_id
      ].compact.sort.uniq
    }
  end

  json.sort_by! { |c| c["category"] }

  # Recursively call this method if no categories were selected for data collection
  if json.empty?
    UI.error("No categories were selected for data collection.")
    json = ask_interactive_questions_for_json(false)
  end

  return json
end
author() click to toggle source
# File fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb, line 257
def self.author
  "joshdholtz"
end
available_options() click to toggle source
# File fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb, line 188
def self.available_options
  user = CredentialsManager::AppfileConfig.try_fetch_value(:itunes_connect_id)
  user ||= CredentialsManager::AppfileConfig.try_fetch_value(:apple_id)

  [
    FastlaneCore::ConfigItem.new(key: :username,
                                 env_name: "FASTLANE_USER",
                                 description: "Your Apple ID Username for App Store Connect",
                                 default_value: user,
                                 default_value_dynamic: true),
    FastlaneCore::ConfigItem.new(key: :app_identifier,
                                 env_name: "UPLOAD_APP_PRIVACY_DETAILS_TO_APP_STORE_APP_IDENTIFIER",
                                 description: "The bundle identifier of your app",
                                 code_gen_sensitive: true,
                                 default_value: CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier),
                                 default_value_dynamic: true),
    FastlaneCore::ConfigItem.new(key: :team_id,
                                 env_name: "FASTLANE_ITC_TEAM_ID",
                                 description: "The ID of your App Store Connect team if you're in multiple teams",
                                 optional: true,
                                 skip_type_validation: true, # as we also allow integers, which we convert to strings anyway
                                 code_gen_sensitive: true,
                                 default_value: CredentialsManager::AppfileConfig.try_fetch_value(:itc_team_id),
                                 default_value_dynamic: true),
    FastlaneCore::ConfigItem.new(key: :team_name,
                                 env_name: "FASTLANE_ITC_TEAM_NAME",
                                 description: "The name of your App Store Connect team if you're in multiple teams",
                                 optional: true,
                                 code_gen_sensitive: true,
                                 default_value: CredentialsManager::AppfileConfig.try_fetch_value(:itc_team_name),
                                 default_value_dynamic: true),

    # JSON paths
    FastlaneCore::ConfigItem.new(key: :json_path,
                                 env_name: "UPLOAD_APP_PRIVACY_DETAILS_TO_APP_STORE_JSON_PATH",
                                 description: "Path to the app usage data JSON",
                                 optional: true,
                                 verify_block: proc do |value|
                                   UI.user_error!("Could not find JSON file at path '#{File.expand_path(value)}'") unless File.exist?(value)
                                   UI.user_error!("'#{value}' doesn't seem to be a JSON file") unless FastlaneCore::Helper.json_file?(File.expand_path(value))
                                 end),
    FastlaneCore::ConfigItem.new(key: :output_json_path,
                                 env_name: "UPLOAD_APP_PRIVACY_DETAILS_TO_APP_STORE_OUTPUT_JSON_PATH",
                                 description: "Path to the app usage data JSON file generated by interactive questions",
                                 conflicting_options: [:skip_json_file_saving],
                                 default_value: File.join(DEFAULT_PATH, DEFAULT_FILE_NAME)),

    # Skipping options
    FastlaneCore::ConfigItem.new(key: :skip_json_file_saving,
                                 env_name: "UPLOAD_APP_PRIVACY_DETAILS_TO_APP_STORE_OUTPUT_SKIP_JSON_FILE_SAVING",
                                 description: "Whether to skip the saving of the JSON file",
                                 conflicting_options: [:skip_output_json_path],
                                 type: Boolean,
                                 default_value: false),
    FastlaneCore::ConfigItem.new(key: :skip_upload,
                                 env_name: "UPLOAD_APP_PRIVACY_DETAILS_TO_APP_STORE_OUTPUT_SKIP_UPLOAD",
                                 description: "Whether to skip the upload and only create the JSON file with interactive questions",
                                 conflicting_options: [:skip_publish],
                                 type: Boolean,
                                 default_value: false),
    FastlaneCore::ConfigItem.new(key: :skip_publish,
                                 env_name: "UPLOAD_APP_PRIVACY_DETAILS_TO_APP_STORE_OUTPUT_SKIP_PUBLISH",
                                 description: "Whether to skip the publishing",
                                 conflicting_options: [:skip_upload],
                                 type: Boolean,
                                 default_value: false)
  ]
end
category() click to toggle source
# File fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb, line 285
def self.category
  :production
end
description() click to toggle source
# File fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb, line 184
def self.description
  "Upload App Privacy Details for an app in App Store Connect"
end
details() click to toggle source
# File fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb, line 265
def self.details
  "Upload App Privacy Details for an app in App Store Connect. For more detail information, view https://docs.fastlane.tools/uploading-app-privacy-details"
end
example_code() click to toggle source
# File fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb, line 269
def self.example_code
  [
    'upload_app_privacy_details_to_app_store(
      username: "your@email.com",
      team_name: "Your Team",
      app_identifier: "com.your.bundle"
    )',
    'upload_app_privacy_details_to_app_store(
      username: "your@email.com",
      team_name: "Your Team",
      app_identifier: "com.your.bundle",
      json_path: "fastlane/app_data_usages.json"
    )'
  ]
end
is_supported?(platform) click to toggle source
# File fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb, line 261
def self.is_supported?(platform)
  [:ios, :mac, :tvos].include?(platform)
end
load_json_file(params) click to toggle source
# File fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb, line 47
def self.load_json_file(params)
  path = params[:json_path]
  return nil if path.nil?
  return JSON.parse(File.read(path))
end
output_path(params) click to toggle source
# File fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb, line 53
def self.output_path(params)
  path = params[:output_json_path]
  return File.absolute_path(path)
end
run(params) click to toggle source
# File fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb, line 7
def self.run(params)
  require 'spaceship'

  # Prompts select team if multiple teams and none specified
  UI.message("Login to App Store Connect (#{params[:username]})")
  Spaceship::ConnectAPI.login(params[:username], use_portal: false, use_tunes: true, tunes_team_id: params[:team_id], team_name: params[:team_name])
  UI.message("Login successful")

  # Get App
  app = Spaceship::ConnectAPI::App.find(params[:app_identifier])
  unless app
    UI.user_error!("Could not find app with bundle identifier '#{params[:app_identifier]}' on account #{params[:username]}")
  end

  # Attempt to load JSON file
  usages_config = load_json_file(params)

  # Start interactive questions to generate and save JSON file
  unless usages_config
    usages_config = ask_interactive_questions_for_json

    if params[:skip_json_file_saving]
      UI.message("Skipping JSON file saving...")
    else
      json = JSON.pretty_generate(usages_config)
      path = output_path(params)

      UI.message("Writing file to #{path}")
      File.write(path, json)
    end
  end

  # Process JSON file to save app data usages to API
  if params[:skip_upload]
    UI.message("Skipping uploading of data... (so you can verify your JSON file)")
  else
    upload_app_data_usages(params, app, usages_config)
  end
end
upload_app_data_usages(params, app, usages_config) click to toggle source
# File fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb, line 133
def self.upload_app_data_usages(params, app, usages_config)
  UI.message("Preparing to upload App Data Usage")

  # Delete all existing usages for new ones
  all_usages = Spaceship::ConnectAPI::AppDataUsage.all(app_id: app.id, includes: "category,grouping,purpose,dataProtection", limit: 500)
  all_usages.each(&:delete!)

  usages_config.each do |usage_config|
    category = usage_config["category"]
    purposes = usage_config["purposes"] || []
    data_protections = usage_config["data_protections"] || []

    # There will not be any purposes if "not collecting data"
    # However, an AppDataUsage still needs to be created for not collecting data
    # Creating an array with nil so that purposes can be iterated over and
    # that AppDataUsage can be created
    purposes = [nil] if purposes.empty?

    purposes.each do |purpose|
      data_protections.each do |data_protection|
        if data_protection == Spaceship::ConnectAPI::AppDataUsageDataProtection::ID::DATA_NOT_COLLECTED
          UI.message("Setting #{data_protection}")
        else
          UI.message("Setting #{category} and #{purpose} to #{data_protection}")
        end

        Spaceship::ConnectAPI::AppDataUsage.create(
          app_id: app.id,
          app_data_usage_category_id: category,
          app_data_usage_protection_id: data_protection,
          app_data_usage_purpose_id: purpose
        )
      end
    end
  end

  # Publish
  if params[:skip_publish]
    UI.message("Skipping app data usage publishing... (so you can verify on App Store Connect)")
  else
    publish_state = Spaceship::ConnectAPI::AppDataUsagesPublishState.get(app_id: app.id)
    if publish_state.published
      UI.important("App data usage is already published")
    else
      UI.important("App data usage not published! Going to publish...")
      publish_state.publish!
      UI.important("App data usage is now published")
    end
  end
end