fastlane-plugin-google_data_safety 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,585 @@
1
+ require 'fastlane/action'
2
+ require 'googleauth'
3
+ require_relative '../helper/prompt_create_data_safety_csv_helper'
4
+ require 'net/http'
5
+ require 'json'
6
+ require 'csv'
7
+
8
+ module Fastlane
9
+ module Actions
10
+ class PromptCreateDataSafetyCsvAction < Action
11
+ FORMULA_CHARS = %w[= + - @].freeze
12
+
13
+ # Run the prompt_create_data_safety_csv action.
14
+ #
15
+ # @param param [hash] Fastlane parameters
16
+ def self.run(params)
17
+ if !UI.interactive?
18
+ UI.error("Prompts questions can only display in interactive.")
19
+ end
20
+
21
+ csv_file_path = params[:csv_file]
22
+ csv_file_path = File.expand_path(csv_file_path)
23
+
24
+ hash = {}
25
+
26
+ self.headerDisplay(csv_file: csv_file_path)
27
+ self.startingQuestion(hash: hash)
28
+
29
+ template_data = Helper::PromptCreateDataSafetyHelper.template_array()
30
+
31
+ safety_data = template_data.each_with_index do | row |
32
+ hash_key = "#{row[0]},#{row[1]}"
33
+ if hash.key?(hash_key)
34
+ row[2] = hash[hash_key]
35
+ end
36
+ end
37
+
38
+ self.saveCsvFile(data: safety_data, csv_file_path: csv_file_path)
39
+ end
40
+
41
+ # Array of valid answer strings for true.
42
+ def self.valid_true_answers = ["y", "yes", "true", "t"]
43
+ # Array of valid answer strings for false.
44
+ def self.valid_false_answers = ["n", "no", "false", "f"]
45
+ # Array of valid answer strings for ninety days selection (delete account data).
46
+ def self.valid_90_day_answers = ["90", "ninety"]
47
+
48
+ # Displays terminal header of Fastlane action and about the upcoming prompts.
49
+ #
50
+ # @param csv_file [String] The CSV file location that display to user where the file will be saved in
51
+ def self.headerDisplay(csv_file: nil)
52
+ UI.important("")
53
+ UI.important(" ____ _ ____ _ ____ __ _ ")
54
+ UI.important(" / ___| ___ ___ __ _| | ___ | _ \\ __ _| |_ __ _ / ___| __ _ / _| ___| |_ _ _ ")
55
+ UI.important(" | | _ / _ \\ / _ \\ / _` | |/ _ \\ | | | |/ _` | __/ _` | \\___ \\ / _` | |_ / _ | __| | | |")
56
+ UI.important(" | |_| | (_) | (_) | (_| | | __/ | |_| | (_| | || (_| | ___) | (_| | _| __| |_| |_| |")
57
+ UI.important(" \\____|\\___/ \\___/ \\__, |_|\\___| |____/ \\__,_|\\__\\__,_| |____/ \\__,_|_| \\___|\\__|\\__, |")
58
+ UI.important(" |___/ |___/ ")
59
+ UI.important("")
60
+ UI.important("By: Owen Bean")
61
+ UI.important("Report problems at https://github.com/owenbean400/fastlane-plugin-google_data_safety/issues")
62
+ UI.important("")
63
+ UI.important("Answer the following prompts below to generate CSV file to upload at #{csv_file}")
64
+ UI.important("")
65
+ end
66
+
67
+ # Prompt questions for Google Data Safety sheet.
68
+ #
69
+ # @param hash [hash] The hash to save data of answers to questions
70
+ def self.startingQuestion(hash: nil)
71
+ isDataInputAppInfo = self.promptForAnswerBoolean(
72
+ question: "Does your app collect or share any of the required user data types?",
73
+ hash: hash,
74
+ question_id: "PSL_DATA_COLLECTION_COLLECTS_PERSONAL_DATA",
75
+ response_id: "",
76
+ is_false_blank: false)
77
+
78
+ if isDataInputAppInfo
79
+
80
+ self.promptForAnswerBoolean(
81
+ question: "Is all of the user data collected by your app encrypted in transit?",
82
+ hash: hash,
83
+ question_id: "PSL_DATA_COLLECTION_ENCRYPTED_IN_TRANSIT",
84
+ response_id: "",
85
+ is_false_blank: false)
86
+
87
+ isDataInputAccountCreate = self.promptForAnswerBoolean(
88
+ question: "Does the app allow for account creation from within the app?",
89
+ hash: nil,
90
+ question_id: nil,
91
+ response_id: nil,
92
+ is_false_blank: false)
93
+
94
+ if isDataInputAccountCreate
95
+ self.promptForAnswerBoolean(
96
+ question: "Does the app allow for account creation from username and password?",
97
+ hash: hash,
98
+ question_id: "PSL_SUPPORTED_ACCOUNT_CREATION_METHODS",
99
+ response_id: "PSL_ACM_USER_ID_PASSWORD",
100
+ is_false_blank: true)
101
+
102
+ self.promptForAnswerBoolean(
103
+ question: "Does the app allow for account creation from username and other authentication?",
104
+ hash: hash,
105
+ question_id: "PSL_SUPPORTED_ACCOUNT_CREATION_METHODS",
106
+ response_id: "PSL_ACM_USER_ID_OTHER_AUTH",
107
+ is_false_blank: true)
108
+
109
+ self.promptForAnswerBoolean(
110
+ question: "Does the app allow for account creation from username, password, and other authentication?",
111
+ hash: hash,
112
+ question_id: "PSL_SUPPORTED_ACCOUNT_CREATION_METHODS",
113
+ response_id: "PSL_ACM_USER_ID_PASSWORD_OTHER_AUTH",
114
+ is_false_blank: true)
115
+
116
+ self.promptForAnswerBoolean(
117
+ question: "Does the app allow for account creation from OAuth?",
118
+ hash: hash,
119
+ question_id: "PSL_SUPPORTED_ACCOUNT_CREATION_METHODS",
120
+ response_id: "PSL_ACM_OAUTH",
121
+ is_false_blank: true)
122
+
123
+ isDataInputCreateOther = self.promptForAnswerBoolean(
124
+ question: "Does the app allow for other account creation method?",
125
+ hash: hash,
126
+ question_id: "PSL_SUPPORTED_ACCOUNT_CREATION_METHODS",
127
+ response_id: "PSL_ACM_OTHER",
128
+ is_false_blank: true)
129
+
130
+ if isDataInputCreateOther
131
+ self.promptForAnswer(
132
+ question: "Describe the method of account creation that your app supports",
133
+ hash: hash,
134
+ question_id: "PSL_ACM_SPECIFY",
135
+ response_id: "")
136
+ end
137
+
138
+ self.promptForUrl(
139
+ question: "Add a valid URL for account deletion",
140
+ hash: hash,
141
+ question_id: "PSL_ACCOUNT_DELETION_URL",
142
+ response_id: "")
143
+ else
144
+ isDataInputLoginOutside = self.promptForAnswerBoolean(
145
+ question: "Can users login to your app with accounts created outside of the app?",
146
+ hash: hash,
147
+ question_id: "PSL_HAS_OUTSIDE_APP_ACCOUNTS",
148
+ response_id: "",
149
+ is_false_blank: true)
150
+
151
+ if isDataInputLoginOutside
152
+ self.promptForAnswerBoolean(
153
+ question: "Can users create account outside of app by app identification (e.g. SIM binding, service subscription)?",
154
+ hash: hash,
155
+ question_id: "PSL_OUTSIDE_APP_ACCOUNT_TYPES",
156
+ response_id: "PSL_LOGIN_WITH_OUTSIDE_APP_ID",
157
+ is_false_blank: true)
158
+
159
+ self.promptForAnswerBoolean(
160
+ question: "Can users create account outside of app by employment or enterprise account?",
161
+ hash: hash,
162
+ question_id: "PSL_OUTSIDE_APP_ACCOUNT_TYPES",
163
+ response_id: "PSL_LOGIN_THROUGH_EMPLOYMENT_OR_ENTERPRISE_ACCOUNT",
164
+ is_false_blank: true)
165
+
166
+ isDataInputLoginOutsideOther = self.promptForAnswerBoolean(
167
+ question: "Is there other way users can create account outside of app?",
168
+ hash: hash,
169
+ question_id: "PSL_OUTSIDE_APP_ACCOUNT_TYPES",
170
+ response_id: "PSL_OUTSIDE_APP_ACCOUNT_TYPE_OTHER",
171
+ is_false_blank: true)
172
+
173
+ if isDataInputLoginOutsideOther
174
+ self.promptForAnswer(
175
+ question: "Describe how these accounts are created",
176
+ hash: hash,
177
+ question_id: "PSL_OUTSIDE_APP_ACCOUNT_TYPE_SPECIFY",
178
+ response_id: "")
179
+ end
180
+ end
181
+ end
182
+
183
+ dataInputDeleteData = UI.input("Do you provide a way for users to request that their data is deleted? Type 90 for deletion after 90 days. Y/N/90")
184
+
185
+ while !self.valid_true_answers.include?(dataInputDeleteData.downcase) &&
186
+ !self.valid_false_answers.include?(dataInputDeleteData.downcase) &&
187
+ !self.valid_90_day_answers.include?(dataInputDeleteData.downcase)
188
+ UI.important("Please provide an answer of either yes, no, or 90")
189
+ dataInputDeleteData = UI.input("Do you provide a way for users to request that their data is deleted? Type 90 for deletion after 90 days. Y/N/90")
190
+ end
191
+
192
+ if self.valid_true_answers.include?(dataInputDeleteData.downcase)
193
+ self.addToHash(hash: hash, question_id: "PSL_SUPPORT_DATA_DELETION_BY_USER", response_id: "DATA_DELETION_YES", value: "true")
194
+
195
+ self.promptForUrl(
196
+ question: "What URL link can users request to delete data?",
197
+ hash: hash,
198
+ question_id: "PSL_DATA_DELETION_URL",
199
+ response_id: "")
200
+ elsif self.valid_90_day_answers.include?(dataInputDeleteData.downcase)
201
+ self.addToHash(hash: hash, question_id: "PSL_SUPPORT_DATA_DELETION_BY_USER", response_id: "DATA_DELETION_NO_AUTO_DELETED", value: "true")
202
+ else
203
+ self.addToHash(hash: hash, question_id: "PSL_SUPPORT_DATA_DELETION_BY_USER", response_id: "DATA_DELETION_NO", value: "true")
204
+ end
205
+
206
+ self.questionDataInfo(hash: hash)
207
+ end
208
+ end
209
+
210
+ # Prompt question data type group question for Google Data Safety sheet.
211
+ #
212
+ # @param hash [hash] The hash to save data of answers to questions
213
+ def self.questionDataInfo(hash: nil)
214
+ UI.important("*** Below questions will be asked about location data ***")
215
+ self.questionDataType(title: "approximate location", code: "PSL_APPROX_LOCATION", group_code: "PSL_DATA_TYPES_LOCATION", hash: hash)
216
+ self.questionDataType(title: "precise location", code: "PSL_PRECISE_LOCATION", group_code: "PSL_DATA_TYPES_LOCATION", hash: hash)
217
+
218
+ UI.important("*** Below questions will be asked about personal information data ***")
219
+ self.questionDataType(title: "name", code: "PSL_NAME", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
220
+ self.questionDataType(title: "email", code: "PSL_EMAIL", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
221
+ self.questionDataType(title: "user ID", code: "PSL_USER_ACCOUNT", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
222
+ self.questionDataType(title: "address", code: "PSL_ADDRESS", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
223
+ self.questionDataType(title: "phone", code: "PSL_PHONE", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
224
+ self.questionDataType(title: "race and ethnicity", code: "PSL_RACE_ETHNICITY", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
225
+ self.questionDataType(title: "political or religious beliefs", code: "PSL_POLITICAL_RELIGIOUS", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
226
+ self.questionDataType(title: "sexual orientation", code: "PSL_SEXUAL_ORIENTATION_GENDER_IDENTITY", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
227
+ self.questionDataType(title: "other personal information", code: "PSL_OTHER_PERSONAL", group_code: "PSL_DATA_TYPES_PERSONAL", hash: hash)
228
+
229
+ UI.important("*** Below questions will be asked about financial data ***")
230
+ self.questionDataType(title: "user payment information", code: "PSL_CREDIT_DEBIT_BANK_ACCOUNT_NUMBER", group_code: "PSL_DATA_TYPES_FINANCIAL", hash: hash)
231
+ self.questionDataType(title: "purchase history", code: "PSL_PURCHASE_HISTORY", group_code: "PSL_DATA_TYPES_FINANCIAL", hash: hash)
232
+ self.questionDataType(title: "credit score", code: "PSL_CREDIT_SCORE", group_code: "PSL_DATA_TYPES_FINANCIAL", hash: hash)
233
+ self.questionDataType(title: "other financial info", code: "PSL_OTHER", group_code: "PSL_DATA_TYPES_FINANCIAL", hash: hash)
234
+
235
+ UI.important("\n*** Below questions will be asked about audio and music data ***")
236
+ self.questionDataType(title: "health info", code: "PSL_HEALTH", group_code: "PSL_DATA_TYPES_HEALTH_AND_FITNESS", hash: hash)
237
+ self.questionDataType(title: "fitness info", code: "PSL_FITNESS", group_code: "PSL_DATA_TYPES_HEALTH_AND_FITNESS", hash: hash)
238
+
239
+ UI.important("*** Below questions will be asked about email and text data ***")
240
+ self.questionDataType(title: "email", code: "PSL_EMAILS", group_code: "PSL_DATA_TYPES_EMAIL_AND_TEXT", hash: hash)
241
+ self.questionDataType(title: "SMS or MMS", code: "PSL_SMS_CALL_LOG", group_code: "PSL_DATA_TYPES_EMAIL_AND_TEXT", hash: hash)
242
+ self.questionDataType(title: "Other in-app messages", code: "PSL_OTHER_MESSAGES", group_code: "PSL_DATA_TYPES_EMAIL_AND_TEXT", hash: hash)
243
+
244
+ UI.important("*** Below questions will be asked about photo and video data ***")
245
+ self.questionDataType(title: "photo", code: "PSL_PHOTOS", group_code: "PSL_DATA_TYPES_PHOTOS_AND_VIDEOS", hash: hash)
246
+ self.questionDataType(title: "videos", code: "PSL_VIDEOS", group_code: "PSL_DATA_TYPES_PHOTOS_AND_VIDEOS", hash: hash)
247
+
248
+ UI.important("*** Below questions will be asked about audio and music data ***")
249
+ self.questionDataType(title: "voice or sound recordings", code: "PSL_AUDIO", group_code: "PSL_DATA_TYPES_AUDIO", hash: hash)
250
+ self.questionDataType(title: "music files", code: "PSL_MUSIC", group_code: "PSL_DATA_TYPES_AUDIO", hash: hash)
251
+ self.questionDataType(title: "Other audio files", code: "PSL_OTHER_AUDIO", group_code: "PSL_DATA_TYPES_AUDIO", hash: hash)
252
+
253
+ UI.important("*** Below questions will be asked about files and documents data ***")
254
+ self.questionDataType(title: "files and docs", code: "PSL_FILES_AND_DOCS", group_code: "PSL_DATA_TYPES_FILES_AND_DOCS", hash: hash)
255
+
256
+ UI.important("*** Below questions will be asked about calendar data ***")
257
+ self.questionDataType(title: "calendar events", code: "PSL_CALENDAR", group_code: "PSL_DATA_TYPES_CALENDAR", hash: hash)
258
+
259
+ UI.important("*** Below questions will be asked about contact data ***")
260
+ self.questionDataType(title: "contacts", code: "PSL_CONTACTS", group_code: "PSL_DATA_TYPES_CONTACTS", hash: hash)
261
+
262
+ UI.important("*** Below questions will be asked about app activity data ***")
263
+ self.questionDataType(title: "app interactions", code: "PSL_USER_INTERACTION", group_code: "PSL_DATA_TYPES_APP_ACTIVITY", hash: hash)
264
+ self.questionDataType(title: "in-app search history", code: "PSL_IN_APP_SEARCH_HISTORY", group_code: "PSL_DATA_TYPES_APP_ACTIVITY", hash: hash)
265
+ self.questionDataType(title: "installed apps", code: "PSL_APPS_ON_DEVICE", group_code: "PSL_DATA_TYPES_APP_ACTIVITY", hash: hash)
266
+ self.questionDataType(title: "other user-generated content", code: "PSL_USER_GENERATED_CONTENT", group_code: "PSL_DATA_TYPES_APP_ACTIVITY", hash: hash)
267
+ self.questionDataType(title: "other actions", code: "PSL_OTHER_APP_ACTIVITY", group_code: "PSL_DATA_TYPES_APP_ACTIVITY", hash: hash)
268
+
269
+ UI.important("*** Below questions will be asked about location data ***")
270
+ self.questionDataType(title: "web browser history", code: "PSL_WEB_BROWSING_HISTORY", group_code: "PSL_DATA_TYPES_SEARCH_AND_BROWSING", hash: hash)
271
+
272
+ UI.important("*** Below questions will be asked about app performance data ***")
273
+ self.questionDataType(title: "crash log", code: "PSL_CRASH_LOGS", group_code: "PSL_DATA_TYPES_APP_PERFORMANCE", hash: hash)
274
+ self.questionDataType(title: "performance diagnostics", code: "PSL_PERFORMANCE_DIAGNOSTICS", group_code: "PSL_DATA_TYPES_APP_PERFORMANCE", hash: hash)
275
+ self.questionDataType(title: "other app performance data", code: "PSL_OTHER_PERFORMANCE", group_code: "PSL_DATA_TYPES_APP_PERFORMANCE", hash: hash)
276
+
277
+ UI.important("*** Below questions will be asked about identifier data ***")
278
+ self.questionDataType(title: "device or other IDs", code: "PSL_DEVICE_ID", group_code: "PSL_DATA_TYPES_IDENTIFIERS", hash: hash)
279
+ end
280
+
281
+ # Prompt question of a specific data type about data sharing and data collection for Google Data Safety sheet.
282
+ #
283
+ # @param title [String] The data type title
284
+ # @param code [String] Google Data Safety code name for CSV file format
285
+ # @param group_code [String] Google Data Safety group code name for CSV file format
286
+ # @param hash [hash] The hash to save data of answers to questions
287
+ def self.questionDataType(title: "", code: "", group_code: "", hash: nil)
288
+ isDataInputCollectOrShare = self.promptForAnswerBoolean(
289
+ question: "Is #{title} data being collected or shared?",
290
+ hash: hash,
291
+ question_id: group_code,
292
+ response_id: code,
293
+ is_false_blank: true)
294
+
295
+ if isDataInputCollectOrShare
296
+ isDataInputShared = self.promptForAnswerBoolean(
297
+ question: "Is #{title} data being shared?",
298
+ hash: hash,
299
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:PSL_DATA_USAGE_COLLECTION_AND_SHARING",
300
+ response_id: "PSL_DATA_USAGE_ONLY_SHARED",
301
+ is_false_blank: true)
302
+
303
+ if isDataInputShared
304
+ self.promptForAnswerBoolean(
305
+ question: "Is #{title} shared for app functionality?",
306
+ hash: hash,
307
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
308
+ response_id: "PSL_APP_FUNCTIONALITY",
309
+ is_false_blank: true)
310
+
311
+ self.promptForAnswerBoolean(
312
+ question: "Is #{title} shared for analytics?",
313
+ hash: hash,
314
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
315
+ response_id: "PSL_ANALYTICS",
316
+ is_false_blank: true)
317
+
318
+ self.promptForAnswerBoolean(
319
+ question: "Is #{title} shared for developer communications?",
320
+ hash: hash,
321
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
322
+ response_id: "PSL_DEVELOPER_COMMUNICATIONS",
323
+ is_false_blank: true)
324
+
325
+ self.promptForAnswerBoolean(
326
+ question: "Is #{title} shared for advertising or marketing?",
327
+ hash: hash,
328
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
329
+ response_id: "PSL_ADVERTISING",
330
+ is_false_blank: true)
331
+
332
+ self.promptForAnswerBoolean(
333
+ question: "Is #{title} shared for fraud prevention, security, or compliance?",
334
+ hash: hash,
335
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
336
+ response_id: "PSL_FRAUD_PREVENTION_SECURITY",
337
+ is_false_blank: true)
338
+
339
+ self.promptForAnswerBoolean(
340
+ question: "Is #{title} shared for personalizing the app?",
341
+ hash: hash,
342
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
343
+ response_id: "PSL_PERSONALIZATION",
344
+ is_false_blank: true)
345
+
346
+ self.promptForAnswerBoolean(
347
+ question: "Is #{title} shared to manage account?",
348
+ hash: hash,
349
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_SHARING_PURPOSE",
350
+ response_id: "PSL_ACCOUNT_MANAGEMENT",
351
+ is_false_blank: true)
352
+ end
353
+
354
+ isDataInputCollected = self.promptForAnswerBoolean(
355
+ question: "Is #{title} data being collected?",
356
+ hash: hash,
357
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:PSL_DATA_USAGE_COLLECTION_AND_SHARING",
358
+ response_id: "PSL_DATA_USAGE_ONLY_COLLECTED",
359
+ is_false_blank: true)
360
+
361
+ if isDataInputCollected
362
+ self.promptForAnswerBoolean(
363
+ question: "Is #{title} data processed ephemerally?",
364
+ hash: hash,
365
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:PSL_DATA_USAGE_EPHEMERAL",
366
+ response_id: "",
367
+ is_false_blank: false)
368
+
369
+ isDataInputRequired = self.promptForAnswerBoolean(
370
+ question: "Is #{title} data required for your app?",
371
+ hash: nil,
372
+ question_id: nil,
373
+ response_id: nil,
374
+ is_false_blank: false)
375
+
376
+ if isDataInputRequired
377
+ self.addToHash(hash: hash, question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_USER_CONTROL", response_id: "PSL_DATA_USAGE_USER_CONTROL_REQUIRED", value: "true")
378
+ else
379
+ self.addToHash(hash: hash, question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_USER_CONTROL", response_id: "PSL_DATA_USAGE_USER_CONTROL_OPTIONAL", value: "true")
380
+ end
381
+
382
+ self.promptForAnswerBoolean(
383
+ question: "Is #{title} collected for app functionality?",
384
+ hash: hash,
385
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
386
+ response_id: "PSL_APP_FUNCTIONALITY",
387
+ is_false_blank: true)
388
+
389
+ self.promptForAnswerBoolean(
390
+ question: "Is #{title} collected for analytics?",
391
+ hash: hash,
392
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
393
+ response_id: "PSL_ANALYTICS",
394
+ is_false_blank: true)
395
+
396
+ self.promptForAnswerBoolean(
397
+ question: "Is #{title} collected for developer communications?",
398
+ hash: hash,
399
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
400
+ response_id: "PSL_DEVELOPER_COMMUNICATIONS",
401
+ is_false_blank: true)
402
+
403
+ self.promptForAnswerBoolean(
404
+ question: "Is #{title} collected for advertising or marketing?",
405
+ hash: hash,
406
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
407
+ response_id: "PSL_ADVERTISING",
408
+ is_false_blank: true)
409
+
410
+ self.promptForAnswerBoolean(
411
+ question: "Is #{title} collected for fraud prevention, security, or compliance?",
412
+ hash: hash,
413
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
414
+ response_id: "PSL_FRAUD_PREVENTION_SECURITY",
415
+ is_false_blank: true)
416
+
417
+ self.promptForAnswerBoolean(
418
+ question: "Is #{title} collected for personalizing the app?",
419
+ hash: hash,
420
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
421
+ response_id: "PSL_PERSONALIZATION",
422
+ is_false_blank: true)
423
+
424
+ self.promptForAnswerBoolean(
425
+ question: "Is #{title} collected to manage account?",
426
+ hash: hash,
427
+ question_id: "PSL_DATA_USAGE_RESPONSES:#{code}:DATA_USAGE_COLLECTION_PURPOSE",
428
+ response_id: "PSL_ACCOUNT_MANAGEMENT",
429
+ is_false_blank: true)
430
+ end
431
+ end
432
+ end
433
+
434
+ # Save CSV file with data sanitation.
435
+ #
436
+ # @param data [hash] Google Data Safety hash of CSV content
437
+ # @param csv_file_path [String] CSV file path to save file at
438
+ def self.saveCsvFile(data: nil, csv_file_path: "")
439
+ FileUtils.mkdir_p(File.dirname(csv_file_path))
440
+
441
+ CSV.open(csv_file_path, "w") do |csv|
442
+ data.each do |row|
443
+ sanitized_row = row.map do |cell|
444
+ if cell.is_a?(String)
445
+ cleaned = cell.gsub(/[\t\r\n]/, '').strip
446
+
447
+ if cleaned.lstrip.start_with?(*FORMULA_CHARS)
448
+ cleaned = "'#{cleaned}"
449
+ end
450
+
451
+ cleaned
452
+ else
453
+ cell
454
+ end
455
+ end
456
+
457
+ csv << sanitized_row
458
+ end
459
+ end
460
+ end
461
+
462
+ # Prompt question to terminal for a yes or no answer. Reprompt user if answer is not a valid yes or no.
463
+ #
464
+ # @param question [String] The question that displays in terminal
465
+ # @param hash [hash] The hash data for Google Data Safety.
466
+ # @param question_id [String] The question id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
467
+ # @param response_id [String] The response id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
468
+ # @param is_false_blank [Boolean] Whether the false answer should be false or blank in CSV file. It is dependent on the question ID and response ID for Google Data Safety sheet format.
469
+ # @return [Boolean] The user's answer to the question
470
+ def self.promptForAnswerBoolean(question: "", hash: nil, question_id: nil, response_id: "", is_false_blank: false)
471
+ answer = UI.input(question + " Y/N")
472
+
473
+ while !self.valid_true_answers.include?(answer.downcase) && !self.valid_false_answers.include?(answer.downcase)
474
+ UI.important("Please provide \"y\", \"yes\", \"true\", or \"t\" for yes; and \"n\", \"no\", \"false\", \"f\" for no")
475
+ answer = UI.input(question + " Y/N")
476
+ end
477
+
478
+ if self.valid_true_answers.include?(answer.downcase)
479
+ self.addToHash(hash: hash, question_id: question_id, response_id: response_id, value: "true")
480
+ return true
481
+ else
482
+ self.addToHash(hash: hash, question_id: question_id, response_id: response_id, value: is_false_blank ? "" : "false")
483
+ return false
484
+ end
485
+ end
486
+
487
+ # Prompt question to terminal for a string answer. Reprompt user if answer is empty string.
488
+ #
489
+ # @param question [String] The question that displays in terminal
490
+ # @param hash [hash] The hash data for Google Data Safety
491
+ # @param question_id [String] The question id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
492
+ # @param response_id [String] The response id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
493
+ # @return [String] The user's answer to question
494
+ def self.promptForAnswer(question: "", hash: nil, question_id: nil, response_id: "")
495
+ answer = UI.input(question)
496
+
497
+ while answer.empty?
498
+ UI.important("Please provide an answer")
499
+ answer = UI.input(question)
500
+ end
501
+
502
+ self.addToHash(hash: hash, question_id: question_id, response_id: response_id, value: answer)
503
+ return answer
504
+ end
505
+
506
+ # Prompt question to terminal for URL answer. Reprompt user if answer is not a valid URL.
507
+ #
508
+ # @param question [String] The question that displays in terminal
509
+ # @param hash [hash] The hash data for Google Data Safety
510
+ # @param question_id [String] The question id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
511
+ # @param response_id [String] The response id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
512
+ # @return [String] The user's answer to question
513
+ def self.promptForUrl(question: "", hash: nil, question_id: nil, response_id: "")
514
+ answer = UI.input(question)
515
+
516
+ while !self.validUrl?(answer)
517
+ UI.important("Please provide a proper URL")
518
+ answer = UI.input(question)
519
+ end
520
+
521
+ self.addToHash(hash: hash, question_id: question_id, response_id: response_id, value: answer)
522
+ return answer
523
+ end
524
+
525
+ # Detect if URL is valid format.
526
+ #
527
+ # @param url [String] The url to validate
528
+ # @return [Boolean] The url is valid
529
+ def self.validUrl?(url)
530
+ uri = URI.parse(url)
531
+ uri.is_a?(URI::HTTP) && !uri.host.nil?
532
+ rescue URI::InvalidURIError
533
+ false
534
+ end
535
+
536
+ # Prompt question and answer to Google Data Safety hash
537
+ #
538
+ # @param hash [hash] The hash data for Google Data Safety
539
+ # @param question_id [String] The question id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
540
+ # @param response_id [String] The response id to add in the hash data. Review CSV file from Google Data Safety sheet for more details.
541
+ # @param value [String] The answer value
542
+ def self.addToHash(hash: nil, question_id: nil, response_id: "", value: nil)
543
+ if !value.nil? && !hash.nil?
544
+ hash["#{question_id},#{response_id}"] = value
545
+ end
546
+ end
547
+
548
+ def self.description
549
+ "Prompt user questions to generate Google data safety sheet"
550
+ end
551
+
552
+ def self.authors
553
+ ["Owen Bean"]
554
+ end
555
+
556
+ def self.return_value
557
+ # If your method provides a return value, you can describe here what it does
558
+ end
559
+
560
+ def self.details
561
+ "Command line question prompts for information about Android app data collection"
562
+ end
563
+
564
+ def self.available_options
565
+ [
566
+ FastlaneCore::ConfigItem.new(
567
+ key: :csv_file,
568
+ env_name: "CSV_FILE",
569
+ description: "File location to save csv file for Google data safety sheet",
570
+ optional: false,
571
+ type: String
572
+ )
573
+ ]
574
+ end
575
+
576
+ def self.is_supported?(platform)
577
+ # Adjust this if your plugin only works for a particular platform (iOS vs. Android, for example)
578
+ # See: https://docs.fastlane.tools/advanced/#control-configuration-by-lane-and-by-platform
579
+ #
580
+ # [:ios, :mac, :android].include?(platform)
581
+ true
582
+ end
583
+ end
584
+ end
585
+ end
@@ -17,7 +17,13 @@ module Fastlane
17
17
 
18
18
  access_token = body["access_token"]
19
19
 
20
- self.send_data_safety_sheet(package_name: params[:package_name], auth_token: access_token, csv_content: csv_content)
20
+ debug = false
21
+
22
+ if !params[:debug].nil?
23
+ debug = params[:debug]
24
+ end
25
+
26
+ self.send_data_safety_sheet(package_name: params[:package_name], auth_token: access_token, csv_content: csv_content, debug: debug)
21
27
 
22
28
  end
23
29
 
@@ -79,7 +85,7 @@ module Fastlane
79
85
  token
80
86
  end
81
87
 
82
- def self.send_data_safety_sheet(package_name: nil, auth_token: nil, csv_content: nil)
88
+ def self.send_data_safety_sheet(package_name: nil, auth_token: nil, csv_content: nil, debug: false)
83
89
  if package_name.nil?
84
90
  if UI.interactive?
85
91
  UI.important("To not be asked package name, you can specify it using 'package_name'.")
@@ -111,6 +117,11 @@ module Fastlane
111
117
  else
112
118
  UI.error("Google Data Safety sheet upload error with API")
113
119
  end
120
+
121
+ if debug
122
+ UI.important("Google Data Safety API response body")
123
+ UI.important(response.body)
124
+ end
114
125
  end
115
126
 
116
127
  end
@@ -169,6 +180,13 @@ module Fastlane
169
180
  optional: true,
170
181
  type: Hash
171
182
  ),
183
+ FastlaneCore::ConfigItem.new(
184
+ key: :debug,
185
+ env_name: "DEBUG",
186
+ description: "Set to debug mode for troubleshooting",
187
+ optional: true,
188
+ type: Boolean
189
+ ),
172
190
  ]
173
191
  end
174
192