Class: Fastlane::Actions::UploadAppPrivacyDetailsToAppStoreAction

Inherits:
Fastlane::Action
  • Object
show all
Defined in:
fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb

Constant Summary collapse

DEFAULT_PATH =
Fastlane::Helper.fastlane_enabled_folder_path
DEFAULT_FILE_NAME =
"app_privacy_details.json"

Constants inherited from Fastlane::Action

Fastlane::Action::AVAILABLE_CATEGORIES, Fastlane::Action::RETURN_TYPES

Class Method Summary collapse

Methods inherited from Fastlane::Action

action_name, authors, deprecated_notes, lane_context, method_missing, other_action, output, return_type, return_value, sample_return_value, shell_out_should_use_bundle_exec?, step_text

Class Method Details

.ask_interactive_questions_for_json(show_intro = true) ⇒ Object



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# 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

.authorObject



258
259
260
# File 'fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb', line 258

def self.author
  "joshdholtz"
end

.available_optionsObject



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# 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,
                                 is_string: false, # 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",
                                 is_string: true,
                                 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

.categoryObject



286
287
288
# File 'fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb', line 286

def self.category
  :production
end

.descriptionObject



184
185
186
# 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

.detailsObject



266
267
268
# File 'fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb', line 266

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_codeObject



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb', line 270

def self.example_code
  [
    'upload_app_privacy_details_to_app_store(
      username: "[email protected]",
      team_name: "Your Team",
      app_identifier: "com.your.bundle"
    )',
    'upload_app_privacy_details_to_app_store(
      username: "[email protected]",
      team_name: "Your Team",
      app_identifier: "com.your.bundle",
      json_path: "fastlane/app_data_usages.json"
    )'
  ]
end

.is_supported?(platform) ⇒ Boolean

Returns:



262
263
264
# File 'fastlane/lib/fastlane/actions/upload_app_privacy_details_to_app_store.rb', line 262

def self.is_supported?(platform)
  [:ios, :mac, :tvos].include?(platform)
end

.load_json_file(params) ⇒ Object



47
48
49
50
51
# 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) ⇒ Object



53
54
55
56
# 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) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 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.(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) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# 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