branch_io_cli 0.12.10 → 0.13.0.pre.1

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.
@@ -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