branch_io_cli 0.12.10 → 0.13.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -79,11 +79,14 @@ module BranchIOCLI
79
79
  attr_reader :workspace_path
80
80
  attr_reader :pod_repo_update
81
81
  attr_reader :sdk
82
+ attr_reader :keys
83
+ attr_reader :apps
82
84
 
83
85
  def initialize(options)
84
86
  @options = options
85
87
  @pod_repo_update = options.pod_repo_update if self.class.available_options.map(&:name).include?(:pod_repo_update)
86
88
  @sdk = "iphonesimulator" # to load Xcode build settings for commands without a --sdk option
89
+ @confirm = options.confirm
87
90
 
88
91
  Configuration.current = self
89
92
 
@@ -269,6 +272,54 @@ EOF
269
272
  end
270
273
  end
271
274
 
275
+ def validate_keys(optional: false)
276
+ @keys = {}
277
+ @apps = {}
278
+
279
+ # 1. Check the options passed in. If nothing (nil) passed, continue.
280
+ validate_key options.live_key, :live, accept_nil: true
281
+ validate_key options.test_key, :test, accept_nil: true
282
+
283
+ # 2. Did we find a valid key above?
284
+ while !optional && @keys.empty?
285
+ # 3. If not, prompt.
286
+ say "A live key, a test key or both is required."
287
+ validate_key nil, :live
288
+ validate_key nil, :test
289
+ end
290
+
291
+ # 4. We have at least one valid key now, unless optional is truthy.
292
+ end
293
+
294
+ def key_valid?(key, type)
295
+ return false if key.nil?
296
+ return true if key.empty?
297
+ unless key =~ /^key_#{type}_.+/
298
+ say "#{key.inspect} is not a valid #{type} Branch key. It must begin with key_#{type}_."
299
+ return false
300
+ end
301
+
302
+ # For now: When using --no-validate with the setup command, don't call the Branch API.
303
+ return true unless respond_to?(:validate) && validate
304
+
305
+ begin
306
+ # Retrieve info from the API
307
+ app = BranchApp[key]
308
+ @apps[key] = app
309
+ true
310
+ rescue StandardError => e
311
+ say "Error fetching app for key #{key} from Branch API: #{e.message}"
312
+ false
313
+ end
314
+ end
315
+
316
+ def validate_key(key, type, options = {})
317
+ return if options[:accept_nil] && key.nil?
318
+ key = ask "Please enter your #{type} Branch key or use --#{type}-key [enter for none]: " until key_valid? key, type
319
+ @keys[type] = key unless key.empty?
320
+ instance_variable_set "@#{type}_key", key
321
+ end
322
+
272
323
  def pod_install_required?
273
324
  # If this is set, its existence has been verified.
274
325
  return false unless podfile_path
@@ -344,6 +395,10 @@ EOF
344
395
  path && path.sub(/\.h$/, '.m')
345
396
  end
346
397
 
398
+ def ios_urischemes_from_api(apps = @apps)
399
+ Set.new apps.map(&:ios_uri_scheme).compact
400
+ end
401
+
347
402
  # TODO: How many of these can vary by configuration?
348
403
 
349
404
  def modules_enabled?
@@ -1,6 +1,17 @@
1
1
  module BranchIOCLI
2
2
  module Configuration
3
3
  class Option
4
+ def self.global_options
5
+ [
6
+ new(
7
+ name: :confirm,
8
+ description: "Enable or disable many prompts",
9
+ default_value: true,
10
+ skip_confirmation: true
11
+ )
12
+ ]
13
+ end
14
+
4
15
  attr_accessor :name
5
16
  attr_accessor :env_name
6
17
  attr_accessor :type
@@ -12,11 +12,6 @@ module BranchIOCLI
12
12
 
13
13
  attr_reader :report_path
14
14
 
15
- def initialize(options)
16
- @confirm = options.confirm
17
- super
18
- end
19
-
20
15
  def validate_options
21
16
  @clean = options.clean
22
17
  @header_only = options.header_only
@@ -67,6 +62,7 @@ EOF
67
62
  <%= color('Xcode project:', BOLD) %> #{xcodeproj_path || '(none)'}
68
63
  <%= color('Scheme:', BOLD) %> #{scheme || '(none)'}
69
64
  <%= color('Target:', BOLD) %> #{target || '(none)'}
65
+ <%= color('Target type:', BOLD) %> #{target.product_type}
70
66
  <%= color('Configuration:', BOLD) %> #{configuration || '(none)'}
71
67
  <%= color('SDK:', BOLD) %> #{sdk}
72
68
  <%= color('Podfile:', BOLD) %> #{relative_path(podfile_path) || '(none)'}
@@ -77,14 +77,8 @@ module BranchIOCLI
77
77
  example: "./report.txt",
78
78
  type: String,
79
79
  env_name: "BRANCH_REPORT_PATH"
80
- ),
81
- Option.new(
82
- name: :confirm,
83
- description: "Confirm before running certain commands",
84
- default_value: true,
85
- skip_confirmation: true
86
80
  )
87
- ]
81
+ ] + Option.global_options
88
82
  end
89
83
  end
90
84
  end
@@ -27,11 +27,9 @@ module BranchIOCLI
27
27
  "Skip adding the framework to the project." => :skip
28
28
  }
29
29
 
30
- attr_reader :keys
31
30
  attr_reader :all_domains
32
31
 
33
32
  def initialize(options)
34
- @confirm = options.confirm
35
33
  super
36
34
  # Configuration has been validated and logged to the screen.
37
35
  confirm_with_user if options.confirm
@@ -52,7 +50,7 @@ module BranchIOCLI
52
50
 
53
51
  validate_xcodeproj_path
54
52
  validate_target
55
- validate_keys_from_setup_options options
53
+ validate_keys
56
54
  validate_all_domains options, !target.extension_target_type?
57
55
  validate_uri_scheme options
58
56
  validate_setting options
@@ -77,6 +75,7 @@ module BranchIOCLI
77
75
  message = <<-EOF
78
76
  <%= color('Xcode project:', BOLD) %> #{xcodeproj_path}
79
77
  <%= color('Target:', BOLD) %> #{target.name}
78
+ <%= color('Target type:', BOLD) %> #{target.product_type}
80
79
  <%= color('Live key:', BOLD) %> #{keys[:live] || '(none)'}
81
80
  <%= color('Test key:', BOLD) %> #{keys[:test] || '(none)'}
82
81
  <%= color('Domains:', BOLD) %> #{all_domains}
@@ -118,39 +117,6 @@ module BranchIOCLI
118
117
  say message
119
118
  end
120
119
 
121
- def validate_keys_from_setup_options(options)
122
- @keys = {}
123
-
124
- # 1. Check the options passed in. If nothing (nil) passed, continue.
125
- validate_key options.live_key, :live, accept_nil: true
126
- validate_key options.test_key, :test, accept_nil: true
127
-
128
- # 2. Did we find a valid key above?
129
- while @keys.empty?
130
- # 3. If not, prompt.
131
- say "A live key, a test key or both is required."
132
- validate_key nil, :live
133
- validate_key nil, :test
134
- end
135
-
136
- # 4. We have at least one valid key now.
137
- end
138
-
139
- def key_valid?(key, type)
140
- return false if key.nil?
141
- key.empty? || key =~ /^key_#{type}_/
142
- end
143
-
144
- def validate_key(key, type, options = {})
145
- return if options[:accept_nil] && key.nil?
146
- until key_valid? key, type
147
- say "#{key.inspect} is not a valid #{type} Branch key. It must begin with key_#{type}_." if key
148
- key = ask "Please enter your #{type} Branch key or use --#{type}-key [enter for none]: "
149
- end
150
- @keys[type] = key unless key.empty?
151
- instance_variable_set "@#{type}_key", key
152
- end
153
-
154
120
  def validate_all_domains(options, required = true)
155
121
  app_link_roots = app_link_roots_from_domains options.domains
156
122
 
@@ -174,6 +140,10 @@ module BranchIOCLI
174
140
  end
175
141
  end
176
142
 
143
+ def domains_from_api
144
+ helper.domains @apps
145
+ end
146
+
177
147
  def validate_uri_scheme(options)
178
148
  # No validation at the moment. Just strips off any trailing ://
179
149
  uri_scheme = options.uri_scheme
@@ -139,14 +139,8 @@ module BranchIOCLI
139
139
  example: "message",
140
140
  argument_optional: true,
141
141
  label: "Commit message"
142
- ),
143
- Option.new(
144
- name: :confirm,
145
- description: "Confirm configuration before proceeding",
146
- default_value: true,
147
- skip_confirmation: true
148
142
  )
149
- ]
143
+ ] + Option.global_options
150
144
  end
151
145
  end
152
146
  end
@@ -19,6 +19,7 @@ module BranchIOCLI
19
19
  def validate_options
20
20
  validate_xcodeproj_path
21
21
  validate_target
22
+ validate_keys optional: true
22
23
  end
23
24
 
24
25
  def log
@@ -26,6 +27,9 @@ module BranchIOCLI
26
27
  say <<EOF
27
28
  <%= color('Xcode project:', BOLD) %> #{xcodeproj_path}
28
29
  <%= color('Target:', BOLD) %> #{target.name}
30
+ <%= color('Target type:', BOLD) %> #{target.product_type}
31
+ <%= color('Live key:', BOLD) %> #{keys[:live] || '(none)'}
32
+ <%= color('Test key:', BOLD) %> #{keys[:test] || '(none)'}
29
33
  <%= color('Domains:', BOLD) %> #{domains || '(none)'}
30
34
  <%= color('Configurations:', BOLD) %> #{(configurations || xcodeproj.build_configurations.map(&:name)).join(',')}
31
35
  EOF
@@ -4,9 +4,23 @@ module BranchIOCLI
4
4
  class << self
5
5
  def available_options
6
6
  [
7
+ Option.new(
8
+ name: :live_key,
9
+ description: "Branch live key expected in project",
10
+ example: "key_live_xxxx",
11
+ type: String,
12
+ aliases: "-L"
13
+ ),
14
+ Option.new(
15
+ name: :test_key,
16
+ description: "Branch test key expected in project",
17
+ example: "key_test_yyyy",
18
+ type: String,
19
+ aliases: "-T"
20
+ ),
7
21
  Option.new(
8
22
  name: :domains,
9
- description: "Comma-separated list of domains to validate (Branch domains or non-Branch domains)",
23
+ description: "Comma-separated list of domains expected to be configured in the project (Branch domains or non-Branch domains)",
10
24
  type: Array,
11
25
  example: "example.com,www.example.com",
12
26
  aliases: "-D",
@@ -14,7 +28,7 @@ module BranchIOCLI
14
28
  ),
15
29
  Option.new(
16
30
  name: :xcodeproj,
17
- description: "Path to an Xcode project to update",
31
+ description: "Path to an Xcode project to validate",
18
32
  type: String,
19
33
  example: "MyProject.xcodeproj"
20
34
  ),
@@ -29,8 +43,13 @@ module BranchIOCLI
29
43
  description: "Comma-separated list of configurations to validate (default: all)",
30
44
  type: Array,
31
45
  example: "Debug,Release"
46
+ ),
47
+ Option.new(
48
+ name: :universal_links_only,
49
+ description: "Validate only the Universal Link configuration",
50
+ default_value: false
32
51
  )
33
- ]
52
+ ] + Option.global_options
34
53
  end
35
54
  end
36
55
  end
@@ -73,7 +73,7 @@ module Xcodeproj
73
73
  end
74
74
 
75
75
  # TODO: What is the correct resolution order here? Which overrides which in
76
- # Xcode? Or does it matter here?
76
+ # Xcode?
77
77
  if setting_value.nil? && defined?(BranchIOCLI::Configuration::XcodeSettings)
78
78
  setting_value = BranchIOCLI::Configuration::XcodeSettings[configuration][setting_name]
79
79
  end
@@ -92,6 +92,8 @@ module Xcodeproj
92
92
  # @param configuration [String] Name of any valid configuration for this target
93
93
  # @return [String] A copy of the original string with all embedded build settings expanded
94
94
  def expand_build_settings(string, configuration)
95
+ return nil if string.nil?
96
+
95
97
  search_position = 0
96
98
  string = string.clone
97
99
 
@@ -118,13 +120,13 @@ module Xcodeproj
118
120
  # Everything else becomes a hyphen, including underscores.
119
121
  expanded_macro.gsub!(/[^A-Za-z0-9-]/, '-') if modifier == "rfc1034identifier"
120
122
 
121
- string.gsub!(/\$\(#{original_macro}\)|\$\{#{original_macro}\}/, expanded_macro)
123
+ string.gsub!(/\$\(#{original_macro}\)|\$\{#{original_macro}\}|^#{original_macro}/, expanded_macro)
122
124
  search_position += expanded_macro.length
123
125
  end
124
126
 
125
127
  # HACK: When matching against an xcconfig, as here, sometimes the macro is just returned
126
- # without delimiters as the entire string or as a path component, e.g. TARGET_NAME or
127
- # PROJECT_DIR/PROJECT_NAME/BridgingHeader.h.
128
+ # without delimiters, e.g. TARGET_NAME or PROJECT_DIR/PROJECT_NAME/BridgingHeader.h. We allow
129
+ # these two patterns for now.
128
130
  string = string.split("/").map do |component|
129
131
  next component unless component =~ /^[A-Z0-9_]+$/
130
132
  expanded_build_setting(component, configuration) || component
@@ -1,3 +1,4 @@
1
+ require "active_support/core_ext/hash"
1
2
  require "branch_io_cli/helper/android_helper"
2
3
  require "branch_io_cli/helper/ios_helper"
3
4
  require "net/http"
@@ -67,6 +68,13 @@ module BranchIOCLI
67
68
  end
68
69
  end
69
70
  end
71
+
72
+ def domains(apps)
73
+ apps.inject Set.new do |result, k, v|
74
+ next result unless v
75
+ result + v.domains
76
+ end
77
+ end
70
78
  end
71
79
  end
72
80
  end
@@ -1,3 +1,4 @@
1
+ require "active_support/core_ext/object"
1
2
  require "json"
2
3
  require "openssl"
3
4
  require "plist"
@@ -110,7 +111,7 @@ module BranchIOCLI
110
111
  end
111
112
  end
112
113
 
113
- def update_info_plist_setting(configuration = RELEASE_CONFIGURATION, &b)
114
+ def info_plist_path(configuration)
114
115
  # find the Info.plist paths for this configuration
115
116
  info_plist_path = config.target.expanded_build_setting "INFOPLIST_FILE", configuration
116
117
 
@@ -118,12 +119,19 @@ module BranchIOCLI
118
119
 
119
120
  project_parent = File.dirname config.xcodeproj_path
120
121
 
121
- info_plist_path = File.expand_path info_plist_path, project_parent
122
+ File.expand_path info_plist_path, project_parent
123
+ end
122
124
 
125
+ def info_plist(path)
123
126
  # try to open and parse the Info.plist (raises)
124
- info_plist = File.open(info_plist_path) { |f| Plist.parse_xml f }
125
- raise "Failed to parse #{info_plist_path}" if info_plist.nil?
127
+ info_plist = File.open(path) { |f| Plist.parse_xml f }
128
+ raise "Failed to parse #{path}" if info_plist.nil?
129
+ info_plist
130
+ end
126
131
 
132
+ def update_info_plist_setting(configuration = RELEASE_CONFIGURATION, &b)
133
+ info_plist_path = info_plist_path(configuration)
134
+ info_plist = info_plist(info_plist_path)
127
135
  yield info_plist
128
136
 
129
137
  Plist::Emit.save_plist info_plist, info_plist_path
@@ -249,7 +257,14 @@ module BranchIOCLI
249
257
  nil
250
258
  end
251
259
 
260
+ def reset_aasa_cache
261
+ @aasa_files = {}
262
+ end
263
+
252
264
  def contents_of_aasa_file(domain)
265
+ @aasa_files ||= {}
266
+ return @aasa_files[domain] if @aasa_files[domain]
267
+
253
268
  uris = [
254
269
  URI("https://#{domain}/.well-known/apple-app-site-association"),
255
270
  URI("https://#{domain}/apple-app-site-association")
@@ -289,7 +304,7 @@ module BranchIOCLI
289
304
  signature.verify nil, cert_store, nil, OpenSSL::PKCS7::NOVERIFY
290
305
  data = signature.data
291
306
  else
292
- @error << "[#{domain}] Unsigned AASA files must be served via HTTPS" and next if uri.scheme == "http"
307
+ @errors << "[#{domain}] Unsigned AASA files must be served via HTTPS" and next if uri.scheme == "http"
293
308
  data = response.body
294
309
  end
295
310
 
@@ -299,6 +314,7 @@ module BranchIOCLI
299
314
 
300
315
  @errors << "[#{domain}] Failed to retrieve AASA file" and return nil if data.nil?
301
316
 
317
+ @aasa_files[domain] = data
302
318
  data
303
319
  rescue IOError, SocketError => e
304
320
  @errors << "[#{domain}] Socket error: #{e.message}"
@@ -409,6 +425,92 @@ module BranchIOCLI
409
425
 
410
426
  associated_domains.select { |d| d =~ /^applinks:/ }.map { |d| d.sub(/^applinks:/, "") }
411
427
  end
428
+
429
+ # Validates Branch-related settings in a project (keys, domains, URI schemes)
430
+ def project_valid?(configuration)
431
+ @errors = []
432
+
433
+ info_plist_path = info_plist_path(configuration)
434
+ info_plist = info_plist(info_plist_path).symbolize_keys
435
+ branch_key = info_plist[:branch_key]
436
+
437
+ if branch_key.blank?
438
+ say "branch_key not found in Info.plist. ❌"
439
+ return false
440
+ end
441
+
442
+ if branch_key.kind_of?(Hash)
443
+ branch_keys = branch_key.map { |k, v| v }
444
+ else
445
+ branch_keys = [branch_key]
446
+ end
447
+
448
+ branch_keys = branch_keys.map { |key| config.target.expand_build_settings key, configuration }
449
+
450
+ valid = true
451
+
452
+ # Retrieve app data from Branch API for all keys in the Info.plist
453
+ apps = branch_keys.map do |key|
454
+ begin
455
+ BranchApp[key]
456
+ rescue StandardError => e
457
+ # Failed to retrieve a key in the Info.plist from the API.
458
+ say "[#{key}] #{e.message} ❌"
459
+ valid = false
460
+ nil
461
+ end
462
+ end.compact.uniq
463
+
464
+ # Get domains and URI schemes loaded from API
465
+ domains_from_api = domains apps
466
+
467
+ # Make sure all domains and URI schemes are present in the project.
468
+ domains = domains_from_project(configuration)
469
+ missing_domains = domains_from_api - domains
470
+ unless missing_domains.empty?
471
+ valid = false
472
+ missing_domains.each do |domain|
473
+ say "[#{domain}] Domain from Dashboard missing from #{configuration} configuration. ❌"
474
+ end
475
+ end
476
+
477
+ valid
478
+ end
479
+
480
+ def branch_keys_from_project(configurations)
481
+ configurations.map do |c|
482
+ path = info_plist_path(c)
483
+ info_plist = info_plist(path).symbolize_keys
484
+ branch_key = info_plist[:branch_key]
485
+ if branch_key.blank?
486
+ say "branch_key not found in Info.plist. ❌"
487
+ return []
488
+ end
489
+
490
+ if branch_key.kind_of?(Hash)
491
+ keys = branch_key.values
492
+ else
493
+ keys = [branch_key]
494
+ end
495
+
496
+ keys.map { |key| config.target.expand_build_settings key, c }
497
+ end.compact.flatten.uniq
498
+ end
499
+
500
+ def branch_apps_from_project(configurations)
501
+ branch_keys_from_project(configurations).map { |key| BranchApp[key] }
502
+ end
503
+
504
+ def uri_schemes_from_project(configurations)
505
+ schemes = configurations.map do |c|
506
+ path = info_plist_path(c)
507
+ info_plist = info_plist(path)
508
+ url_types = info_plist["CFBundleURLTypes"] || []
509
+ url_types.map { |t| t["CFBundleURLSchemes"] }
510
+ end
511
+
512
+ schemes.compact.flatten.uniq
513
+ end
412
514
  end
413
515
  end
414
516
  end