module BranchIOCLI::Helper::IOSHelper
Constants
- APPLINKS
- ASSOCIATED_DOMAINS
- CODE_SIGN_ENTITLEMENTS
- DEVELOPMENT_TEAM
- PRODUCT_BUNDLE_IDENTIFIER
- RELEASE_CONFIGURATION
Public Instance Methods
add_branch_universal_link_domains_to_info_plist(domains)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 78 def add_branch_universal_link_domains_to_info_plist(domains) # Add all supplied domains unless all are app.link domains. return if domains.all? { |d| d =~ /app\.link$/ } config.xcodeproj.build_configurations.each do |c| update_info_plist_setting c.name do |info_plist| info_plist["branch_universal_link_domains"] = domains end end end
add_custom_build_setting()
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 37 def add_custom_build_setting return unless config.setting config.target.build_configurations.each do |c| key = uses_test_key?(c) ? config.keys[:test] : config.keys[:live] # Reuse the same key if both not present key ||= uses_test_key?(c) ? config.keys[:live] : config.keys[:test] c.build_settings[config.setting] = key end end
add_keys_to_info_plist(keys)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 48 def add_keys_to_info_plist(keys) if has_multiple_info_plists? config.xcodeproj.build_configurations.each do |c| update_info_plist_setting c.name do |info_plist| if keys.count > 1 && !config.setting # Use test key in debug configs and live key in release configs info_plist["branch_key"] = c.debug? ? keys[:test] : keys[:live] elsif config.setting info_plist["branch_key"] = "$(#{config.setting})" else info_plist["branch_key"] = keys[:live] ? keys[:live] : keys[:test] end end end else update_info_plist_setting RELEASE_CONFIGURATION do |info_plist| # add/overwrite Branch key(s) if keys.count > 1 && !config.setting info_plist["branch_key"] = keys elsif config.setting info_plist["branch_key"] = "$(#{config.setting})" elsif keys[:live] info_plist["branch_key"] = keys[:live] else info_plist["branch_key"] = keys[:test] end end end end
add_universal_links_to_project(domains, remove_existing, configuration = RELEASE_CONFIGURATION)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 142 def add_universal_links_to_project(domains, remove_existing, configuration = RELEASE_CONFIGURATION) project = config.xcodeproj target = config.target relative_entitlements_path = target.expanded_build_setting CODE_SIGN_ENTITLEMENTS, configuration project_parent = File.dirname project.path if relative_entitlements_path.nil? relative_entitlements_path = File.join target.name, "#{target.name}.entitlements" entitlements_path = File.expand_path relative_entitlements_path, project_parent # Add CODE_SIGN_ENTITLEMENTS setting to each configuration target.build_configuration_list.set_setting CODE_SIGN_ENTITLEMENTS, relative_entitlements_path # Add the file to the project project.new_file relative_entitlements_path entitlements = {} current_domains = [] add_change project.path new_path = entitlements_path else entitlements_path = File.expand_path relative_entitlements_path, project_parent # Raises entitlements = File.open(entitlements_path) { |f| Plist.parse_xml f } raise "Failed to parse entitlements file #{entitlements_path}" if entitlements.nil? if remove_existing current_domains = [] else current_domains = entitlements[ASSOCIATED_DOMAINS] end end current_domains += domains.map { |d| "#{APPLINKS}:#{d}" } all_domains = current_domains.uniq entitlements[ASSOCIATED_DOMAINS] = all_domains Plist::Emit.save_plist entitlements, entitlements_path add_change entitlements_path new_path end
app_ids_from_aasa_file(domain)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 239 def app_ids_from_aasa_file(domain) data = contents_of_aasa_file domain # errors reported in the method above return nil if data.nil? # raises file = JSON.parse data applinks = file[APPLINKS] @errors << "[#{domain}] No #{APPLINKS} found in AASA file" and return if applinks.nil? details = applinks["details"] @errors << "[#{domain}] No details found for #{APPLINKS} in AASA file" and return if details.nil? identifiers = details.map { |d| d["appID"] }.uniq @errors << "[#{domain}] No appID found in AASA file" and return if identifiers.count <= 0 identifiers rescue JSON::ParserError => e @errors << "[#{domain}] Failed to parse AASA file: #{e.message}" nil end
branch_apps_from_project(configurations)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 498 def branch_apps_from_project(configurations) branch_keys_from_project(configurations).map { |key| BranchApp[key] } end
branch_keys_from_project(configurations)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 478 def branch_keys_from_project(configurations) configurations.map do |c| path = info_plist_path(c) info_plist = info_plist(path).symbolize_keys branch_key = info_plist[:branch_key] if branch_key.blank? say "branch_key not found in Info.plist. ❌" return [] end if branch_key.kind_of?(Hash) keys = branch_key.values else keys = [branch_key] end keys.map { |key| config.target.expand_build_settings key, c } end.compact.flatten.uniq end
config()
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 22 def config Configuration::Configuration.current end
contents_of_aasa_file(domain)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 265 def contents_of_aasa_file(domain) @aasa_files ||= {} return @aasa_files[domain] if @aasa_files[domain] uris = [ URI("https://#{domain}/.well-known/apple-app-site-association"), URI("https://#{domain}/apple-app-site-association") # URI("http://#{domain}/.well-known/apple-app-site-association"), # URI("http://#{domain}/apple-app-site-association") ] data = nil uris.each do |uri| break unless data.nil? Net::HTTP.start uri.host, uri.port, use_ssl: uri.scheme == "https" do |http| request = Net::HTTP::Get.new uri spinner = TTY::Spinner.new "[:spinner] GET #{uri}.", format: :flip spinner.auto_spin response = http.request request # Better to use Net::HTTPRedirection and Net::HTTPSuccess here, but # having difficulty with the unit tests. if (300..399).cover?(response.code.to_i) spinner.error "#{response.code} #{response.message}" say "#{uri} cannot result in a redirect. Ignoring." next elsif response.code.to_i != 200 # Try the next URI. spinner.error "#{response.code} #{response.message}" say "Could not retrieve #{uri}. Ignoring." next end spinner.success "#{response.code} #{response.message}" content_type = response["Content-type"] @errors << "[#{domain}] AASA Response does not contain a Content-type header" and next if content_type.nil? case content_type when %r{application/pkcs7-mime} # Verify/decrypt PKCS7 (non-Branch domains) cert_store = OpenSSL::X509::Store.new signature = OpenSSL::PKCS7.new response.body # raises signature.verify nil, cert_store, nil, OpenSSL::PKCS7::NOVERIFY data = signature.data else @errors << "[#{domain}] Unsigned AASA files must be served via HTTPS" and next if uri.scheme == "http" data = response.body end end end @errors << "[#{domain}] Failed to retrieve AASA file" and return nil if data.nil? @aasa_files[domain] = data data rescue IOError, SocketError => e @errors << "[#{domain}] Socket error: #{e.message}" nil rescue OpenSSL::PKCS7::PKCS7Error => e @errors << "[#{domain}] Failed to verify signed AASA file: #{e.message}" nil end
domains_from_project(configuration = RELEASE_CONFIGURATION)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 414 def domains_from_project(configuration = RELEASE_CONFIGURATION) project = config.xcodeproj target = config.target relative_entitlements_path = target.expanded_build_setting CODE_SIGN_ENTITLEMENTS, configuration return [] if relative_entitlements_path.nil? project_parent = File.dirname project.path entitlements_path = File.expand_path relative_entitlements_path, project_parent # Raises entitlements = File.open(entitlements_path) { |f| Plist.parse_xml f } raise "Failed to parse entitlements file #{entitlements_path}" if entitlements.nil? associated_domains = entitlements[ASSOCIATED_DOMAINS] return [] if associated_domains.nil? associated_domains.select { |d| d =~ /^applinks:/ }.map { |d| d.sub(/^applinks:/, "") } end
ensure_uri_scheme_in_info_plist()
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 89 def ensure_uri_scheme_in_info_plist uri_scheme = config.uri_scheme # No URI scheme specified. Do nothing. return if uri_scheme.nil? config.xcodeproj.build_configurations.each do |c| update_info_plist_setting c.name do |info_plist| url_types = info_plist["CFBundleURLTypes"] || [] uri_schemes = url_types.inject([]) { |schemes, t| schemes + t["CFBundleURLSchemes"] } # Already present. Don't mess with the identifier. next if uri_schemes.include? uri_scheme # Not found. Add. Don't worry about the CFBundleURLName (reverse-DNS identifier) # TODO: Should we prompt here to add or let them change the Dashboard? If there's already # a URI scheme in the app, seems likely they'd want to use it. They may have just made # a typo at the CLI or in the Dashboard. url_types << { "CFBundleURLSchemes" => [uri_scheme] } info_plist["CFBundleURLTypes"] = url_types end end end
has_multiple_info_plists?()
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 26 def has_multiple_info_plists? config.xcodeproj.build_configurations.inject([]) do |files, c| files + [config.target.expanded_build_setting("INFOPLIST_FILE", c.name)] end.uniq.count > 1 end
info_plist(path)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 126 def info_plist(path) # try to open and parse the Info.plist (raises) info_plist = File.open(path) { |f| Plist.parse_xml f } raise "Failed to parse #{path}" if info_plist.nil? info_plist end
info_plist_path(configuration)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 115 def info_plist_path(configuration) # find the Info.plist paths for this configuration info_plist_path = config.target.expanded_build_setting "INFOPLIST_FILE", configuration raise "Info.plist not found for configuration #{configuration}" if info_plist_path.nil? project_parent = File.dirname config.xcodeproj_path File.expand_path info_plist_path, project_parent end
project_valid?(configuration)
click to toggle source
Validates Branch-related settings in a project (keys, domains, URI schemes)
# File lib/branch_io_cli/helper/ios_helper.rb, line 435 def project_valid?(configuration) @errors = [] info_plist_path = info_plist_path(configuration) info_plist = info_plist(info_plist_path).symbolize_keys branch_key = info_plist[:branch_key] if branch_key.blank? say "branch_key not found in Info.plist. ❌" return false end if branch_key.kind_of?(Hash) branch_keys = branch_key.map { |k, v| v } else branch_keys = [branch_key] end branch_keys = branch_keys.map { |key| config.target.expand_build_settings key, configuration } # Retrieve app data from Branch API for all keys in the Info.plist apps = branch_keys.map { |k| BranchApp[k] }.compact.uniq invalid_keys = apps.reject(&:valid?).map(&:key) valid = invalid_keys.empty? say "Invalid Branch key(s) in Info.plist for #{configuration} configuration: #{invalid_keys}. ❌" unless valid # Get domains and URI schemes loaded from API domains_from_api = domains apps # Make sure all domains and URI schemes are present in the project. domains = domains_from_project(configuration) missing_domains = domains_from_api - domains unless missing_domains.empty? valid = false missing_domains.each do |domain| say "[#{domain}] Domain from Dashboard missing from #{configuration} configuration. ❌" end end valid end
report_app_id_mismatch(domain, app_id, identifiers)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 351 def report_app_id_mismatch(domain, app_id, identifiers) error_string = "[#{domain}] appID mismatch. Project #{reportable_app_id app_id}\n" if identifiers.count <= 20 error_string << " Apps from AASA:\n" identifiers.each do |identifier| reportable = reportable_app_id identifier error_string << " #{reportable}\n" end else error_string << " Please check your settings in the Branch Dashboard (https://dashboard.branch.io)" end @errors << error_string end
reportable_app_id(identifier)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 193 def reportable_app_id(identifier) team, bundle = team_and_bundle_from_app_id identifier "Signing team: #{team.inspect}, Bundle identifier: #{bundle.inspect}" end
reset_aasa_cache()
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 261 def reset_aasa_cache @aasa_files = {} end
target_from_project(project, target_name)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 399 def target_from_project(project, target_name) if target_name target = project.targets.find { |t| t.name == target_name } raise "Target #{target} not found" if target.nil? elsif config.respond_to?(:scheme) && project.targets.map(&:name).include?(config.scheme) # Return a target with the same name as the scheme, if there is one. target = project.targets.find { |t| t.name == config.scheme } else # find the first application target target = project.targets.find { |t| t.name == File.basename(project.path, '.xcodeproj') } || project.targets.select { |t| !t.extension_target_type? && !t.test_target_type? }.first end target end
team_and_bundle_from_app_id(identifier)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 188 def team_and_bundle_from_app_id(identifier) matches = /^(.*?)\.(.*)$/.match identifier matches[1, 2] end
update_info_plist_setting(configuration = RELEASE_CONFIGURATION) { |info_plist| ... }
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 133 def update_info_plist_setting(configuration = RELEASE_CONFIGURATION, &b) info_plist_path = info_plist_path(configuration) info_plist = info_plist(info_plist_path) yield info_plist Plist::Emit.save_plist info_plist, info_plist_path add_change info_plist_path end
update_team_and_bundle_ids(team, bundle)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 386 def update_team_and_bundle_ids(team, bundle) target = config.target target.build_configuration_list.set_setting PRODUCT_BUNDLE_IDENTIFIER, bundle target.build_configuration_list.set_setting DEVELOPMENT_TEAM, team # also update the team in the first test target target = project.targets.find(&:test_target_type?) return if target.nil? target.build_configuration_list.set_setting DEVELOPMENT_TEAM, team end
update_team_and_bundle_ids_from_aasa_file(domain)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 198 def update_team_and_bundle_ids_from_aasa_file(domain) # raises identifiers = app_ids_from_aasa_file domain raise "Multiple appIDs found in AASA file" if identifiers.count > 1 identifier = identifiers[0] team, bundle = team_and_bundle_from_app_id identifier update_team_and_bundle_ids team, bundle add_change config.xcodeproj_path end
uri_schemes_from_project(configurations)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 502 def uri_schemes_from_project(configurations) schemes = configurations.map do |c| path = info_plist_path(c) info_plist = info_plist(path) url_types = info_plist["CFBundleURLTypes"] || [] url_types.map { |t| t["CFBundleURLSchemes"] } end schemes.compact.flatten.uniq end
uses_test_key?(build_configuration)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 32 def uses_test_key?(build_configuration) return build_configuration.debug? unless config.setting && config.test_configurations config.test_configurations.include? build_configuration.name end
validate_project_domains(expected, configuration = RELEASE_CONFIGURATION)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 366 def validate_project_domains(expected, configuration = RELEASE_CONFIGURATION) @errors = [] project_domains = domains_from_project configuration valid = expected.count == project_domains.count if valid sorted = expected.sort project_domains.sort.each_with_index do |domain, index| valid = false and break unless sorted[index] == domain end end unless valid @errors << "Project domains do not match :domains parameter" @errors << "Project domains: #{project_domains}" @errors << ":domains parameter: #{expected}" end valid end
validate_team_and_bundle_ids(domain, configuration)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 332 def validate_team_and_bundle_ids(domain, configuration) target = config.target product_bundle_identifier = target.expanded_build_setting PRODUCT_BUNDLE_IDENTIFIER, configuration development_team = target.expanded_build_setting DEVELOPMENT_TEAM, configuration identifiers = app_ids_from_aasa_file domain return false if identifiers.nil? app_id = "#{development_team}.#{product_bundle_identifier}" match_found = identifiers.include? app_id unless match_found report_app_id_mismatch domain, app_id, identifiers end match_found end
validate_team_and_bundle_ids_from_aasa_files(domains = [], remove_existing = false, configuration = RELEASE_CONFIGURATION)
click to toggle source
# File lib/branch_io_cli/helper/ios_helper.rb, line 210 def validate_team_and_bundle_ids_from_aasa_files(domains = [], remove_existing = false, configuration = RELEASE_CONFIGURATION) @errors = [] valid = true # Include any domains already in the project. # Raises. Returns a non-nil array of strings. if remove_existing # Don't validate domains to be removed (#16) all_domains = domains else all_domains = (domains + domains_from_project(configuration)).uniq end if all_domains.empty? # Cannot get here from SetupBranchAction, since the domains passed in will never be empty. # If called from ValidateUniversalLinksAction, this is a failure, possibly caused by # failure to add applinks:. @errors << "No Universal Link domains in project. Be sure each Universal Link domain is prefixed with applinks:." return false end all_domains.each do |domain| domain_valid = validate_team_and_bundle_ids domain, configuration valid &&= domain_valid say "Valid Universal Link configuration for #{domain} ✅" if domain_valid end valid end