├── .circleci └── config.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── assets └── branch3.gif ├── bin ├── br └── branch_io ├── branch_io_cli.gemspec ├── examples ├── BranchPluginExample │ ├── .gitignore │ ├── BranchPluginExample.xcodeproj │ │ └── project.pbxproj │ ├── BranchPluginExample.xcworkspace │ │ └── contents.xcworkspacedata │ ├── BranchPluginExample │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── ViewController.swift │ ├── Podfile │ └── Podfile.lock ├── BranchPluginExampleCarthage │ ├── .gitignore │ ├── BranchPluginExampleCarthage.xcodeproj │ │ └── project.pbxproj │ ├── BranchPluginExampleCarthage │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── ViewController.swift │ ├── Cartfile │ └── Cartfile.resolved ├── BranchPluginExampleObjc │ ├── .gitignore │ ├── BranchPluginExampleObjc.xcodeproj │ │ └── project.pbxproj │ ├── BranchPluginExampleObjc.xcworkspace │ │ └── contents.xcworkspacedata │ ├── BranchPluginExampleObjc │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── ViewController.h │ │ ├── ViewController.m │ │ └── main.m │ ├── Podfile │ └── Podfile.lock └── README.md ├── fastlane ├── Fastfile └── Pluginfile ├── lib ├── assets │ ├── artwork │ │ └── branch_ascii_art.txt │ ├── completions │ │ ├── completion.bash │ │ └── completion.zsh │ ├── patches │ │ ├── ContinueUserActivity.m │ │ ├── ContinueUserActivity.swift │ │ ├── ContinueUserActivityNew.m │ │ ├── ContinueUserActivityNew.swift │ │ ├── DidFinishLaunching.m │ │ ├── DidFinishLaunching.swift │ │ ├── MessagesDidBecomeActive.m │ │ ├── MessagesDidBecomeActive.swift │ │ ├── OpenUrl.m │ │ ├── OpenUrl.swift │ │ ├── OpenUrlNew.m │ │ ├── OpenUrlNew.swift │ │ ├── OpenUrlSourceApplication.m │ │ ├── OpenUrlSourceApplication.swift │ │ ├── cartfile.yml │ │ ├── continue_user_activity_new_objc.yml │ │ ├── continue_user_activity_new_swift.yml │ │ ├── continue_user_activity_objc.yml │ │ ├── continue_user_activity_swift.yml │ │ ├── did_finish_launching_new_objc.yml │ │ ├── did_finish_launching_new_swift.yml │ │ ├── did_finish_launching_objc.yml │ │ ├── did_finish_launching_swift.yml │ │ ├── messages_did_become_active_new_objc.yml │ │ ├── messages_did_become_active_new_swift.yml │ │ ├── messages_did_become_active_objc.yml │ │ ├── messages_did_become_active_swift.yml │ │ ├── objc_import.yml │ │ ├── objc_import_at_end.yml │ │ ├── objc_import_include_guard.yml │ │ ├── open_url_new_objc.yml │ │ ├── open_url_new_swift.yml │ │ ├── open_url_objc.yml │ │ ├── open_url_source_application_objc.yml │ │ ├── open_url_source_application_swift.yml │ │ ├── open_url_swift.yml │ │ └── swift_import.yml │ └── templates │ │ ├── command.erb │ │ ├── completion.bash.erb │ │ ├── completion.zsh.erb │ │ ├── env_description.erb │ │ ├── program_description.erb │ │ ├── report_description.erb │ │ ├── setup_description.erb │ │ └── validate_description.erb ├── branch_io_cli.rb └── branch_io_cli │ ├── ascii_art.rb │ ├── branch_app.rb │ ├── cli.rb │ ├── command.rb │ ├── command │ ├── command.rb │ ├── env_command.rb │ ├── report_command.rb │ ├── setup_command.rb │ └── validate_command.rb │ ├── configuration.rb │ ├── configuration │ ├── configuration.rb │ ├── env_configuration.rb │ ├── env_options.rb │ ├── environment.rb │ ├── option.rb │ ├── option_wrapper.rb │ ├── report_configuration.rb │ ├── report_options.rb │ ├── setup_configuration.rb │ ├── setup_options.rb │ ├── validate_configuration.rb │ ├── validate_options.rb │ └── xcode_settings.rb │ ├── core_ext.rb │ ├── core_ext │ ├── io.rb │ ├── regexp.rb │ ├── tty_platform.rb │ └── xcodeproj.rb │ ├── format.rb │ ├── format │ ├── highline_format.rb │ ├── markdown_format.rb │ └── shell_format.rb │ ├── helper.rb │ ├── helper │ ├── android_helper.rb │ ├── branch_helper.rb │ ├── ios_helper.rb │ ├── methods.rb │ ├── patch_helper.rb │ ├── report_helper.rb │ ├── task.rb │ ├── tool_helper.rb │ └── util.rb │ ├── rake_task.rb │ └── version.rb └── spec ├── io_ext_spec.rb ├── ios_helper_spec.rb ├── option_spec.rb ├── option_wrapper_spec.rb ├── spec_helper.rb └── xcodeproj_ext_spec.rb /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Ruby CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-ruby/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/ruby:2.6.3 11 | 12 | working_directory: ~/repo 13 | 14 | steps: 15 | - checkout 16 | 17 | # Download and cache dependencies 18 | - restore_cache: 19 | keys: 20 | - v1-dependencies-{{ checksum "Gemfile" }} 21 | # fallback to using the latest cache if no exact match is found 22 | - v1-dependencies- 23 | 24 | - run: 25 | name: install dependencies 26 | command: | 27 | bundle check || bundle install --jobs=4 --retry=3 --path vendor/bundle 28 | 29 | - save_cache: 30 | paths: 31 | - ./vendor 32 | key: v1-dependencies-{{ checksum "Gemfile" }} 33 | 34 | # run tests! 35 | - run: 36 | name: run tests 37 | command: | 38 | bundle exec rake 39 | 40 | # collect reports 41 | - store_test_results: 42 | path: ~/repo/test-results 43 | - store_artifacts: 44 | path: ~/repo/test-results 45 | destination: test-results 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | Gemfile.lock 3 | 4 | ## Documentation cache and generated files: 5 | /.yardoc/ 6 | /_yardoc/ 7 | /doc/ 8 | /rdoc/ 9 | fastlane/report.xml 10 | fastlane/README.md 11 | 12 | coverage 13 | test-results 14 | build 15 | report.txt 16 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --color 3 | --format d 4 | --format RspecJunitFormatter 5 | --out test-results/rspec/rspec.xml 6 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - "examples/**/*" 4 | - "vendor/**/*" 5 | TargetRubyVersion: 2.0 6 | 7 | Naming/HeredocDelimiterNaming: 8 | Enabled: false 9 | 10 | Style/MultipleComparison: 11 | Enabled: false 12 | 13 | Style/PercentLiteralDelimiters: 14 | Enabled: false 15 | 16 | # kind_of? is a good way to check a type 17 | Style/ClassCheck: 18 | EnforcedStyle: kind_of? 19 | 20 | Style/FrozenStringLiteralComment: 21 | Enabled: false 22 | 23 | # This doesn't work with older versions of Ruby (pre 2.4.0) 24 | Style/SafeNavigation: 25 | Enabled: false 26 | 27 | # This doesn't work with older versions of Ruby (pre 2.4.0) 28 | Performance/RegexpMatch: 29 | Enabled: false 30 | 31 | # This suggests use of `tr` instead of `gsub`. While this might be more performant, 32 | # these methods are not at all interchangable, and behave very differently. This can 33 | # lead to people making the substitution without considering the differences. 34 | Performance/StringReplacement: 35 | Enabled: false 36 | 37 | # .length == 0 is also good, we don't always want .zero? 38 | Style/NumericPredicate: 39 | Enabled: false 40 | 41 | # this would cause errors with long lanes 42 | Metrics/BlockLength: 43 | Enabled: false 44 | 45 | # this is a bit buggy 46 | Metrics/ModuleLength: 47 | Enabled: false 48 | 49 | # certificate_1 is an okay variable name 50 | Naming/VariableNumber: 51 | Enabled: false 52 | 53 | # This is used a lot across the fastlane code base for config files 54 | Style/MethodMissing: 55 | Enabled: false 56 | 57 | # 58 | # File.chmod(0777, f) 59 | # 60 | # is easier to read than 61 | # 62 | # File.chmod(0o777, f) 63 | # 64 | Style/NumericLiteralPrefix: 65 | Enabled: false 66 | 67 | # 68 | # command = (!clean_expired.nil? || !clean_pattern.nil?) ? CLEANUP : LIST 69 | # 70 | # is easier to read than 71 | # 72 | # command = !clean_expired.nil? || !clean_pattern.nil? ? CLEANUP : LIST 73 | # 74 | Style/TernaryParentheses: 75 | Enabled: false 76 | 77 | # sometimes it is useful to have those empty methods 78 | Style/EmptyMethod: 79 | Enabled: false 80 | 81 | # It's better to be more explicit about the type 82 | Style/BracesAroundHashParameters: 83 | Enabled: false 84 | 85 | # specs sometimes have useless assignments, which is fine 86 | Lint/UselessAssignment: 87 | Exclude: 88 | - '**/spec/**/*' 89 | 90 | # We could potentially enable the 2 below: 91 | Layout/IndentHash: 92 | Enabled: false 93 | 94 | Layout/AlignHash: 95 | Enabled: false 96 | 97 | # HoundCI doesn't like this rule 98 | Layout/DotPosition: 99 | Enabled: false 100 | 101 | # We allow !! as it's an easy way to convert ot boolean 102 | Style/DoubleNegation: 103 | Enabled: false 104 | 105 | # Prevent to replace [] into %i 106 | Style/SymbolArray: 107 | Enabled: false 108 | 109 | # We still support Ruby 2.0.0 110 | Layout/IndentHeredoc: 111 | Enabled: false 112 | 113 | # This cop would not work fine with rspec 114 | Style/MixinGrouping: 115 | Exclude: 116 | - '**/spec/**/*' 117 | 118 | # Sometimes we allow a rescue block that doesn't contain code 119 | Lint/HandleExceptions: 120 | Enabled: false 121 | 122 | # Cop supports --auto-correct. 123 | Lint/UnusedBlockArgument: 124 | Enabled: false 125 | 126 | Lint/AmbiguousBlockAssociation: 127 | Enabled: false 128 | 129 | # Needed for $verbose 130 | Style/GlobalVars: 131 | Enabled: false 132 | 133 | # We want to allow class Fastlane::Class 134 | Style/ClassAndModuleChildren: 135 | Enabled: false 136 | 137 | # $? Exit 138 | Style/SpecialGlobalVars: 139 | Enabled: false 140 | 141 | Metrics/AbcSize: 142 | Enabled: false 143 | 144 | Metrics/MethodLength: 145 | Enabled: false 146 | 147 | Metrics/CyclomaticComplexity: 148 | Enabled: false 149 | 150 | # The %w might be confusing for new users 151 | Style/WordArray: 152 | MinSize: 19 153 | 154 | # raise and fail are both okay 155 | Style/SignalException: 156 | Enabled: false 157 | 158 | # Better too much 'return' than one missing 159 | Style/RedundantReturn: 160 | Enabled: false 161 | 162 | # Having if in the same line might not always be good 163 | Style/IfUnlessModifier: 164 | Enabled: false 165 | 166 | # and and or is okay 167 | Style/AndOr: 168 | Enabled: false 169 | 170 | # Configuration parameters: CountComments. 171 | Metrics/ClassLength: 172 | Max: 320 173 | 174 | 175 | # Configuration parameters: AllowURI, URISchemes. 176 | Metrics/LineLength: 177 | Max: 370 178 | 179 | # Configuration parameters: CountKeywordArgs. 180 | Metrics/ParameterLists: 181 | Max: 17 182 | 183 | Metrics/PerceivedComplexity: 184 | Max: 18 185 | 186 | # Sometimes it's easier to read without guards 187 | Style/GuardClause: 188 | Enabled: false 189 | 190 | # We allow both " and ' 191 | Style/StringLiterals: 192 | Enabled: false 193 | 194 | # something = if something_else 195 | # that's confusing 196 | Style/ConditionalAssignment: 197 | Enabled: false 198 | 199 | # Better to have too much self than missing a self 200 | Style/RedundantSelf: 201 | Enabled: false 202 | 203 | # e.g. 204 | # def self.is_supported?(platform) 205 | # we may never use `platform` 206 | Lint/UnusedMethodArgument: 207 | Enabled: false 208 | 209 | # the let(:key) { ... } 210 | Lint/ParenthesesAsGroupedExpression: 211 | Exclude: 212 | - '**/spec/**/*' 213 | 214 | # This would reject is_ in front of methods 215 | # We use `is_supported?` everywhere already 216 | Naming/PredicateName: 217 | Enabled: false 218 | 219 | # We allow the $ 220 | Style/PerlBackrefs: 221 | Enabled: false 222 | 223 | # They have not to be snake_case 224 | Naming/FileName: 225 | Exclude: 226 | - '**/Gemfile' 227 | - '**/Rakefile' 228 | - '**/*.gemspec' 229 | 230 | # We're not there yet 231 | Style/Documentation: 232 | Enabled: false 233 | 234 | # Added after upgrade to 0.38.0 235 | Style/MutableConstant: 236 | Enabled: false 237 | 238 | # length > 0 is good 239 | Style/ZeroLengthPredicate: 240 | Enabled: false 241 | 242 | # Adds complexity 243 | Style/IfInsideElse: 244 | Enabled: false 245 | 246 | # Sometimes we just want to 'collect' 247 | Style/CollectionMethods: 248 | Enabled: false 249 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 6 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-18 Branch Metrics, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | require 'rspec/core/rake_task' 4 | RSpec::Core::RakeTask.new 5 | 6 | require 'rubocop/rake_task' 7 | RuboCop::RakeTask.new(:rubocop) 8 | 9 | require 'branch_io_cli/rake_task' 10 | require 'branch_io_cli/format' 11 | BranchIOCLI::RakeTask.new 12 | 13 | require 'pattern_patch' 14 | 15 | task default: [:spec, :rubocop] 16 | 17 | # 18 | # Example tasks 19 | # 20 | 21 | desc "Run setup, validate, report and report:full in order" 22 | task all: [:setup, :validate, :report, "report:full"] 23 | 24 | IOS_REPO_DIR = File.expand_path "../../ios-branch-deep-linking", __FILE__ 25 | LIVE_KEY = "key_live_fgvRfyHxLBuCjUuJAKEZNdeiAueoTL6R" 26 | TEST_KEY = "key_test_efBNprLtMrryfNERzPVh2gkhxyliNN14" 27 | 28 | def all_projects 29 | projects = Dir[File.expand_path("../examples/*Example*", __FILE__)] 30 | if Dir.exist? IOS_REPO_DIR 31 | projects += Dir[File.expand_path("{Branch-TestBed*,Examples/*}", IOS_REPO_DIR)].reject { |p| p =~ /Xcode-7|README/ } 32 | end 33 | projects 34 | end 35 | 36 | desc "Set up all repo examples" 37 | task :setup do 38 | projects = Dir[File.expand_path("../examples/*Example*", __FILE__)] 39 | Rake::Task["branch:setup"].invoke( 40 | projects, 41 | live_key: LIVE_KEY, 42 | test_key: TEST_KEY, 43 | domains: %w(k272.app.link), 44 | uri_scheme: "branchfastlaneexample", 45 | validate: true, 46 | pod_repo_update: false, 47 | setting: true, 48 | confirm: false, 49 | trace: true 50 | ) 51 | end 52 | 53 | desc "Validate repo examples" 54 | task :validate do 55 | projects = Dir[File.expand_path("../examples/*Example*", __FILE__)] 56 | Rake::Task["branch:validate"].invoke( 57 | projects, 58 | # Expect all projects to have exactly these keys and domains 59 | live_key: LIVE_KEY, 60 | test_key: TEST_KEY, 61 | domains: %w( 62 | k272.app.link 63 | k272-alternate.app.link 64 | k272.test-app.link 65 | k272-alternate.test-app.link 66 | ), 67 | trace: true 68 | ) 69 | end 70 | 71 | desc "Validate iOS repo examples" 72 | task "validate:ios" do 73 | projects = Dir[File.expand_path("../examples/*Example*", __FILE__)] 74 | Rake::Task["branch:validate"].invoke( 75 | all_projects - projects, 76 | trace: true 77 | ) 78 | end 79 | 80 | desc "Report on all examples in repo" 81 | task :report do 82 | Rake::Task["branch:report"].invoke all_projects, header_only: true, trace: true 83 | end 84 | 85 | namespace :report do 86 | desc "Perform a full build of all examples in the repo" 87 | task :full do 88 | Rake::Task["branch:report"].invoke all_projects, pod_repo_update: false, confirm: false, trace: true 89 | end 90 | end 91 | 92 | # 93 | # Repo maintenance 94 | # 95 | 96 | desc "Regenerate reference documentation in the repo" 97 | task "readme" do 98 | include BranchIOCLI::Format::MarkdownFormat 99 | 100 | text = "\\1\n" 101 | text += %i(setup validate report env).inject("") do |t, command| 102 | t + render_command(command) 103 | end 104 | text += "\n\\2" 105 | 106 | PatternPatch::Patch.new( 107 | regexp: /(\ 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/BranchPluginExample/BranchPluginExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/BranchPluginExample/BranchPluginExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/BranchPluginExample/BranchPluginExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // BranchPluginExample 4 | // 5 | // Created by Jimmy Dee on 4/14/17. 6 | // Copyright © 2017 Branch Metrics. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /examples/BranchPluginExample/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | platform :ios, "8.0" 3 | 4 | pod "Cartography" 5 | 6 | target "BranchPluginExample" 7 | -------------------------------------------------------------------------------- /examples/BranchPluginExample/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Cartography (2.0.0) 3 | 4 | DEPENDENCIES: 5 | - Cartography 6 | 7 | SPEC CHECKSUMS: 8 | Cartography: d295eb25ab54bb57eecd8c2f04e9648c850f1281 9 | 10 | PODFILE CHECKSUM: 885d8431fdda8810c5a6d9bd98ff2213680cbc7b 11 | 12 | COCOAPODS: 1.3.1 13 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleCarthage/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Carthage 3 | xcuserdata 4 | project.xcworkspace 5 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleCarthage/BranchPluginExampleCarthage/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // BranchPluginExampleCarthage 4 | // 5 | // Created by Jimmy Dee on 9/2/17. 6 | // Copyright © 2017 Branch Metrics. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | return true 18 | } 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleCarthage/BranchPluginExampleCarthage/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /examples/BranchPluginExampleCarthage/BranchPluginExampleCarthage/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleCarthage/BranchPluginExampleCarthage/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleCarthage/BranchPluginExampleCarthage/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleCarthage/BranchPluginExampleCarthage/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // BranchPluginExampleCarthage 4 | // 5 | // Created by Jimmy Dee on 9/2/17. 6 | // Copyright © 2017 Branch Metrics. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | } 14 | 15 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleCarthage/Cartfile: -------------------------------------------------------------------------------- 1 | git "https://github.com/robb/Cartography" 2 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleCarthage/Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "robb/Cartography" "2.1.0" 2 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleObjc/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Pods 3 | xcuserdata 4 | project.xcworkspace 5 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleObjc/BranchPluginExampleObjc.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleObjc/BranchPluginExampleObjc/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // BranchPluginExampleObjc 4 | // 5 | // Created by Jimmy Dee on 9/2/17. 6 | // Copyright © 2017 Branch Metrics. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | @end 16 | 17 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleObjc/BranchPluginExampleObjc/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // BranchPluginExampleObjc 4 | // 5 | // Created by Jimmy Dee on 9/2/17. 6 | // Copyright © 2017 Branch Metrics. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @implementation AppDelegate 12 | 13 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 14 | return YES; 15 | } 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleObjc/BranchPluginExampleObjc/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /examples/BranchPluginExampleObjc/BranchPluginExampleObjc/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleObjc/BranchPluginExampleObjc/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleObjc/BranchPluginExampleObjc/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleObjc/BranchPluginExampleObjc/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // BranchPluginExampleObjc 4 | // 5 | // Created by Jimmy Dee on 9/2/17. 6 | // Copyright © 2017 Branch Metrics. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | @end 14 | 15 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleObjc/BranchPluginExampleObjc/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // BranchPluginExampleObjc 4 | // 5 | // Created by Jimmy Dee on 9/2/17. 6 | // Copyright © 2017 Branch Metrics. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | 11 | @interface ViewController () 12 | 13 | @end 14 | 15 | @implementation ViewController 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleObjc/BranchPluginExampleObjc/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // BranchPluginExampleObjc 4 | // 5 | // Created by Jimmy Dee on 9/2/17. 6 | // Copyright © 2017 Branch Metrics. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleObjc/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | platform :ios, "8.0" 3 | 4 | pod "AFNetworking" 5 | 6 | target "BranchPluginExampleObjc" 7 | -------------------------------------------------------------------------------- /examples/BranchPluginExampleObjc/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AFNetworking (3.1.0): 3 | - AFNetworking/NSURLSession (= 3.1.0) 4 | - AFNetworking/Reachability (= 3.1.0) 5 | - AFNetworking/Security (= 3.1.0) 6 | - AFNetworking/Serialization (= 3.1.0) 7 | - AFNetworking/UIKit (= 3.1.0) 8 | - AFNetworking/NSURLSession (3.1.0): 9 | - AFNetworking/Reachability 10 | - AFNetworking/Security 11 | - AFNetworking/Serialization 12 | - AFNetworking/Reachability (3.1.0) 13 | - AFNetworking/Security (3.1.0) 14 | - AFNetworking/Serialization (3.1.0) 15 | - AFNetworking/UIKit (3.1.0): 16 | - AFNetworking/NSURLSession 17 | 18 | DEPENDENCIES: 19 | - AFNetworking 20 | 21 | SPEC CHECKSUMS: 22 | AFNetworking: 5e0e199f73d8626b11e79750991f5d173d1f8b67 23 | 24 | PODFILE CHECKSUM: 2f4db3bb6fdde5ed89e00c5a42332e195dc6df7d 25 | 26 | COCOAPODS: 1.3.1 27 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Example projects 2 | 3 | There are several example application projects in this folder. Each is an empty 4 | app that just displays a blank screen. These projects may or may not already 5 | have other dependencies via CocoaPods, Carthage or direct integration. The 6 | CLI may be used to integrate the Branch SDK with each project. These projects 7 | will not pass validation without first being set up. 8 | 9 | ## [BranchPluginExample](./BranchPluginExample) 10 | 11 | This project uses Swift and CocoaPods. 12 | 13 | ## [BranchPluginExampleCarthage](./BranchPluginExampleCarthage) 14 | 15 | This project uses Swift and Carthage. 16 | 17 | ## [BranchPluginExampleObjc](./BranchPluginExampleObjc) 18 | 19 | This project uses Objective-C and CocoaPods. 20 | 21 | --- 22 | 23 | Each project will pass validation if `k272.app.link` is used for the domain. If 24 | you wish to try them with your own Branch parameters, you must first manually 25 | change the bundle identifier and signing team in the project. To test basic 26 | integration without modification (using a dummy key), change to each subdirectory: 27 | 28 | ```bash 29 | branch_io setup -D k272.app.link -L key_live_xxxx 30 | ``` 31 | 32 | Validation will fail before setup and pass afterward. 33 | 34 | ```bash 35 | branch_io validate -D k272.app.link,k272-alternate.app.link 36 | ``` 37 | 38 | To use the command from this repo rather than your PATH, first 'bundle install' 39 | and then: 40 | 41 | ```bash 42 | bundle exec branch_io setup # or validate 43 | ``` 44 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | fastlane_version "2.69.0" 2 | 3 | lane :rubocop_update do 4 | update_rubocop 5 | end 6 | -------------------------------------------------------------------------------- /fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | gem "fastlane-plugin-maintenance" 2 | -------------------------------------------------------------------------------- /lib/assets/artwork/branch_ascii_art.txt: -------------------------------------------------------------------------------- 1 | ____ _ 2 | | _ \ | | 3 | | |_) |_ __ __ _ _ __ ___| |__ 4 | | _ <| '__/ _` | '_ \ / __| '_ \ 5 | | |_) | | | (_| | | | | (__| | | | 6 | |____/|_| \__,_|_| |_|\___|_| |_| 7 | -------------------------------------------------------------------------------- /lib/assets/completions/completion.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file is generated. Run rake readme to regenerate it. 3 | 4 | _branch_io_complete() 5 | { 6 | local cur prev opts global_opts setup_opts validate_opts commands cmd 7 | COMPREPLY=() 8 | cur="${COMP_WORDS[COMP_CWORD]}" 9 | prev="${COMP_WORDS[COMP_CWORD-1]}" 10 | cmd="${COMP_WORDS[1]}" 11 | 12 | commands="env setup report validate" 13 | global_opts="-h --help -t --trace -v --version" 14 | 15 | 16 | env_opts="-c --completion-script -s --shell -V --verbose" 17 | 18 | setup_opts="-L --live-key -T --test-key -D --domains --app-link-subdomain -U --uri-scheme -s --setting --test-configurations --xcodeproj --target --podfile --cartfile --carthage-command --frameworks --no-pod-repo-update --no-validate --force --no-add-sdk --no-patch-source --commit --no-confirm" 19 | 20 | report_opts="--workspace --xcodeproj --scheme --target --configuration --sdk --podfile --cartfile --no-clean -H --header-only --no-pod-repo-update -o --out --no-confirm" 21 | 22 | validate_opts="-L --live-key -T --test-key -D --domains --xcodeproj --target --configurations --universal-links-only --no-confirm" 23 | 24 | 25 | if [[ ${cur} == -* ]] ; then 26 | case "${cmd}" in 27 | report) 28 | opts=$report_opts 29 | ;; 30 | setup) 31 | opts=$setup_opts 32 | ;; 33 | validate) 34 | opts=$validate_opts 35 | ;; 36 | env) 37 | opts=$env_opts 38 | ;; 39 | *) 40 | opts=$global_opts 41 | ;; 42 | esac 43 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 44 | elif [[ ${prev} == branch_io || ${prev} == br ]] ; then 45 | COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) ) 46 | else 47 | COMPREPLY=( $(compgen -o default ${cur}) ) 48 | fi 49 | return 0 50 | } 51 | complete -F _branch_io_complete branch_io 52 | complete -F _branch_io_complete br 53 | -------------------------------------------------------------------------------- /lib/assets/completions/completion.zsh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | # This file is generated. Run rake readme to regenerate it. 3 | 4 | _branch_io_complete() { 5 | local word opts 6 | word="$1" 7 | opts="-h --help -t --trace -v --version" 8 | opts="$opts -L --live-key -T --test-key -D --domains --app-link-subdomain -U --uri-scheme -s --setting --test-configurations --xcodeproj --target --podfile --cartfile --carthage-command --frameworks --no-pod-repo-update --no-validate --force --no-add-sdk --no-patch-source --commit --no-confirm" 9 | 10 | reply=( "${(ps: :)opts}" ) 11 | } 12 | 13 | compctl -K _branch_io_complete branch_io 14 | compctl -K _branch_io_complete br 15 | -------------------------------------------------------------------------------- /lib/assets/patches/ContinueUserActivity.m: -------------------------------------------------------------------------------- 1 | // TODO: Adjust your method as you see fit. 2 | if ([[Branch getInstance] continueUserActivity:userActivity]) { 3 | return YES; 4 | } 5 | -------------------------------------------------------------------------------- /lib/assets/patches/ContinueUserActivity.swift: -------------------------------------------------------------------------------- 1 | // TODO: Adjust your method as you see fit. 2 | if Branch.getInstance().continue(userActivity) { 3 | return true 4 | } 5 | -------------------------------------------------------------------------------- /lib/assets/patches/ContinueUserActivityNew.m: -------------------------------------------------------------------------------- 1 | 2 | 3 | - (BOOL)application:(UIApplication *)app continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray * _Nullable))restorationHandler 4 | { 5 | return [[Branch getInstance] continueUserActivity:userActivity]; 6 | } 7 | -------------------------------------------------------------------------------- /lib/assets/patches/ContinueUserActivityNew.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { 4 | return Branch.getInstance().continue(userActivity) 5 | } 6 | -------------------------------------------------------------------------------- /lib/assets/patches/DidFinishLaunching.m: -------------------------------------------------------------------------------- 1 | <% if is_new_method %> 2 | 3 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 4 | <% end %> 5 | <% if use_conditional_test_key? %> 6 | #ifdef DEBUG 7 | [Branch setUseTestBranchKey:YES]; 8 | #endif // DEBUG 9 | <% end %> 10 | [[Branch getInstance] initSessionWithLaunchOptions:launchOptions 11 | andRegisterDeepLinkHandlerUsingBranchUniversalObject:^(BranchUniversalObject *universalObject, BranchLinkProperties *linkProperties, NSError *error){ 12 | // TODO: Route Branch links 13 | }]; 14 | <% if is_new_method %> 15 | return YES; 16 | } 17 | <% end %> 18 | -------------------------------------------------------------------------------- /lib/assets/patches/DidFinishLaunching.swift: -------------------------------------------------------------------------------- 1 | <% if is_new_method %> 2 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 3 | <% end %> 4 | <% if use_conditional_test_key? %> 5 | #if DEBUG 6 | Branch.setUseTestBranchKey(true) 7 | #endif 8 | <% end %> 9 | Branch.getInstance().initSession(launchOptions: launchOptions) { 10 | universalObject, linkProperties, error in 11 | 12 | // TODO: Route Branch links 13 | } 14 | <% if is_new_method %> 15 | return true 16 | } 17 | <% end %> 18 | -------------------------------------------------------------------------------- /lib/assets/patches/MessagesDidBecomeActive.m: -------------------------------------------------------------------------------- 1 | <% if is_new_method %> 2 | 3 | -(void)didBecomeActiveWithConversation:(MSConversation *)conversation { 4 | <% end %> 5 | <% if use_conditional_test_key? %> 6 | #ifdef DEBUG 7 | [Branch setUseTestBranchKey:YES]; 8 | #endif // DEBUG 9 | <% end %> 10 | [[Branch getInstance] initSessionWithLaunchOptions:@{} 11 | andRegisterDeepLinkHandlerUsingBranchUniversalObject:^(BranchUniversalObject *universalObject, BranchLinkProperties *linkProperties, NSError *error){ 12 | // TODO: Route Branch links 13 | }]; 14 | <% if is_new_method %> 15 | } 16 | <% end %> 17 | -------------------------------------------------------------------------------- /lib/assets/patches/MessagesDidBecomeActive.swift: -------------------------------------------------------------------------------- 1 | <% if is_new_method %> 2 | override func didBecomeActive(with conversation: MSConversation) { 3 | <% end %> 4 | <% if use_conditional_test_key? %> 5 | #if DEBUG 6 | Branch.setUseTestBranchKey(true) 7 | #endif 8 | <% end %> 9 | Branch.getInstance().initSession(launchOptions: [:]) { 10 | universalObject, linkProperties, error in 11 | 12 | // TODO: Route Branch links 13 | } 14 | <% if is_new_method %> 15 | } 16 | <% end %> 17 | -------------------------------------------------------------------------------- /lib/assets/patches/OpenUrl.m: -------------------------------------------------------------------------------- 1 | // TODO: Adjust your method as you see fit. 2 | if ([[Branch getInstance] application:app openURL:url options:options]) { 3 | return YES; 4 | } 5 | -------------------------------------------------------------------------------- /lib/assets/patches/OpenUrl.swift: -------------------------------------------------------------------------------- 1 | // TODO: Adjust your method as you see fit. 2 | if Branch.getInstance().application(app, open: url, options: options) { 3 | return true 4 | } 5 | -------------------------------------------------------------------------------- /lib/assets/patches/OpenUrlNew.m: -------------------------------------------------------------------------------- 1 | 2 | 3 | - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options 4 | { 5 | return [[Branch getInstance] application:app openURL:url options:options]; 6 | } 7 | -------------------------------------------------------------------------------- /lib/assets/patches/OpenUrlNew.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { 4 | return Branch.getInstance().application(app, open: url, options: options) 5 | } 6 | -------------------------------------------------------------------------------- /lib/assets/patches/OpenUrlSourceApplication.m: -------------------------------------------------------------------------------- 1 | // TODO: Adjust your method as you see fit. 2 | if ([[Branch getInstance] application:application openURL:url sourceApplication:sourceApplication annotation:annotation]) { 3 | return YES; 4 | } 5 | -------------------------------------------------------------------------------- /lib/assets/patches/OpenUrlSourceApplication.swift: -------------------------------------------------------------------------------- 1 | // TODO: Adjust your method as you see fit. 2 | if Branch.getInstance().application(application, open: url, sourceApplication: sourceApplication, annotation: annotation) { 3 | return true 4 | } 5 | -------------------------------------------------------------------------------- /lib/assets/patches/cartfile.yml: -------------------------------------------------------------------------------- 1 | regexp: '\z' 2 | text: "github \"BranchMetrics/ios-branch-deep-linking\"\n" 3 | mode: append 4 | -------------------------------------------------------------------------------- /lib/assets/patches/continue_user_activity_new_objc.yml: -------------------------------------------------------------------------------- 1 | regexp: '/\n\s*@end[^@]*\Z/m' 2 | mode: prepend 3 | text_file: ContinueUserActivityNew.m 4 | -------------------------------------------------------------------------------- /lib/assets/patches/continue_user_activity_new_swift.yml: -------------------------------------------------------------------------------- 1 | regexp: '/\n\s*\}[^{}]*\Z/m' 2 | mode: prepend 3 | text_file: ContinueUserActivityNew.swift 4 | -------------------------------------------------------------------------------- /lib/assets/patches/continue_user_activity_objc.yml: -------------------------------------------------------------------------------- 1 | regexp: '/application:.*continueUserActivity:.*restorationHandler:.*?\{.*?\n/m' 2 | mode: append 3 | text_file: ContinueUserActivity.m 4 | -------------------------------------------------------------------------------- /lib/assets/patches/continue_user_activity_swift.yml: -------------------------------------------------------------------------------- 1 | regexp: '/application:.*continue userActivity:.*restorationHandler:.*?\{.*?\n/m' 2 | mode: append 3 | text_file: ContinueUserActivity.swift 4 | -------------------------------------------------------------------------------- /lib/assets/patches/did_finish_launching_new_objc.yml: -------------------------------------------------------------------------------- 1 | regexp: '/^@implementation.*?\n/m' 2 | mode: append 3 | text_file: DidFinishLaunching.m 4 | -------------------------------------------------------------------------------- /lib/assets/patches/did_finish_launching_new_swift.yml: -------------------------------------------------------------------------------- 1 | regexp: '/var\s+window\s?:\s?UIWindow\?.*?\n/m' 2 | mode: append 3 | text_file: DidFinishLaunching.swift 4 | -------------------------------------------------------------------------------- /lib/assets/patches/did_finish_launching_objc.yml: -------------------------------------------------------------------------------- 1 | regexp: '/didFinishLaunchingWithOptions.*?\{[^\n]*\n/m' 2 | mode: append 3 | text_file: DidFinishLaunching.m 4 | -------------------------------------------------------------------------------- /lib/assets/patches/did_finish_launching_swift.yml: -------------------------------------------------------------------------------- 1 | regexp: '/didFinishLaunchingWithOptions.*?\{[^\n]*\n/m' 2 | mode: append 3 | text_file: DidFinishLaunching.swift 4 | -------------------------------------------------------------------------------- /lib/assets/patches/messages_did_become_active_new_objc.yml: -------------------------------------------------------------------------------- 1 | regexp: '/^@implementation.*?\n/m' 2 | mode: append 3 | text_file: MessagesDidBecomeActive.m 4 | -------------------------------------------------------------------------------- /lib/assets/patches/messages_did_become_active_new_swift.yml: -------------------------------------------------------------------------------- 1 | regexp: '/class.*:\s+MSMessagesAppViewController\s*{\n/m' 2 | mode: append 3 | text_file: MessagesDidBecomeActive.swift 4 | -------------------------------------------------------------------------------- /lib/assets/patches/messages_did_become_active_objc.yml: -------------------------------------------------------------------------------- 1 | regexp: '/didBecomeActiveWithConversation.*?\{[^\n]*\n/m' 2 | mode: append 3 | text_file: MessagesDidBecomeActive.m 4 | -------------------------------------------------------------------------------- /lib/assets/patches/messages_did_become_active_swift.yml: -------------------------------------------------------------------------------- 1 | regexp: '/didBecomeActive\(with.*?\{[^\n]*\n/m' 2 | mode: append 3 | text_file: MessagesDidBecomeActive.swift 4 | -------------------------------------------------------------------------------- /lib/assets/patches/objc_import.yml: -------------------------------------------------------------------------------- 1 | regexp: '^\s+@import|^\s+#import.*$' 2 | text: "\n#import " 3 | mode: prepend 4 | -------------------------------------------------------------------------------- /lib/assets/patches/objc_import_at_end.yml: -------------------------------------------------------------------------------- 1 | regexp: '\z' 2 | mode: append 3 | text: "\n#import \n" 4 | -------------------------------------------------------------------------------- /lib/assets/patches/objc_import_include_guard.yml: -------------------------------------------------------------------------------- 1 | regexp: '/\n\s*#ifndef\s+(\w+).*\n\s*#define\s+\1.*?\n/m' 2 | mode: append 3 | text: "\n#import \n" 4 | -------------------------------------------------------------------------------- /lib/assets/patches/open_url_new_objc.yml: -------------------------------------------------------------------------------- 1 | regexp: '/\n\s*@end[^@]*\Z/m' 2 | mode: prepend 3 | text_file: OpenUrlNew.m 4 | -------------------------------------------------------------------------------- /lib/assets/patches/open_url_new_swift.yml: -------------------------------------------------------------------------------- 1 | regexp: '/\n\s*\}[^{}]*\Z/m' 2 | mode: prepend 3 | text_file: OpenUrlNew.swift 4 | -------------------------------------------------------------------------------- /lib/assets/patches/open_url_objc.yml: -------------------------------------------------------------------------------- 1 | regexp: '/application:.*openURL:.*options:.*?\{.*?\n/m' 2 | mode: append 3 | text_file: OpenUrl.m 4 | -------------------------------------------------------------------------------- /lib/assets/patches/open_url_source_application_objc.yml: -------------------------------------------------------------------------------- 1 | regexp: '/application:.*openURL:.*sourceApplication:.*?\{.*?\n/m' 2 | mode: append 3 | text_file: OpenUrlSourceApplication.m 4 | -------------------------------------------------------------------------------- /lib/assets/patches/open_url_source_application_swift.yml: -------------------------------------------------------------------------------- 1 | regexp: '/application.*open\s+url.*sourceApplication:.*?\{.*?\n/m' 2 | mode: append 3 | text_file: OpenUrlSourceApplication.swift 4 | -------------------------------------------------------------------------------- /lib/assets/patches/open_url_swift.yml: -------------------------------------------------------------------------------- 1 | regexp: '/application.*open\s+url.*options:.*?\{.*?\n/m' 2 | mode: append 3 | text_file: OpenUrl.swift 4 | -------------------------------------------------------------------------------- /lib/assets/patches/swift_import.yml: -------------------------------------------------------------------------------- 1 | regexp: '^\s*import .*$' 2 | text: "\nimport Branch" 3 | mode: prepend 4 | -------------------------------------------------------------------------------- /lib/assets/templates/command.erb: -------------------------------------------------------------------------------- 1 | <%= header "#{@command.command_name.to_s.capitalize} command", 3 %> 2 | 3 | ```bash 4 | branch_io <%= @command.command_name %> [OPTIONS] 5 | br <%= @command.command_name %> [OPTIONS] 6 | ``` 7 | 8 | <%= render "#{@command.command_name}_description" %> 9 | 10 | <%= header "Options", 4 %> 11 | 12 | |Option|Description|Env. var.| 13 | |------|-----------|---------| 14 | |-h, --help|Prints a list of commands or help for each command|| 15 | |-v, --version|Prints the current version of the CLI|| 16 | |-t, --trace|Prints a stack trace when exceptions are raised|| 17 | <%= table_options %> 18 | 19 | <% if @command.respond_to?(:examples) && !@command.examples.blank? %> 20 | <%= header "Examples", 4 %> 21 | <% @command.examples.each_key do |text| %> 22 | <% example = @command.examples[text] %> 23 | <%= header text, 5 %> 24 | 25 | ```bash 26 | <%= example %> 27 | ``` 28 | <% end %> 29 | <% end %> 30 | 31 | <% if @command.respond_to?(:return_value) && !@command.return_value.nil? %> 32 | <% end %> 33 | -------------------------------------------------------------------------------- /lib/assets/templates/completion.bash.erb: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file is generated. Run rake readme to regenerate it. 3 | 4 | _branch_io_complete() 5 | { 6 | local cur prev opts global_opts setup_opts validate_opts commands cmd 7 | COMPREPLY=() 8 | cur="${COMP_WORDS[COMP_CWORD]}" 9 | prev="${COMP_WORDS[COMP_CWORD-1]}" 10 | cmd="${COMP_WORDS[1]}" 11 | 12 | commands="<%= all_commands.join(' ') %>" 13 | global_opts="-h --help -t --trace -v --version" 14 | 15 | <% all_commands.each do |command| %> 16 | <%= %(#{command}_opts="#{options_for_command command}") %> 17 | <% end %> 18 | 19 | if [[ ${cur} == -* ]] ; then 20 | case "${cmd}" in 21 | report) 22 | opts=$report_opts 23 | ;; 24 | setup) 25 | opts=$setup_opts 26 | ;; 27 | validate) 28 | opts=$validate_opts 29 | ;; 30 | env) 31 | opts=$env_opts 32 | ;; 33 | *) 34 | opts=$global_opts 35 | ;; 36 | esac 37 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 38 | elif [[ ${prev} == branch_io || ${prev} == br ]] ; then 39 | COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) ) 40 | else 41 | COMPREPLY=( $(compgen -o default ${cur}) ) 42 | fi 43 | return 0 44 | } 45 | complete -F _branch_io_complete branch_io 46 | complete -F _branch_io_complete br 47 | -------------------------------------------------------------------------------- /lib/assets/templates/completion.zsh.erb: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | # This file is generated. Run rake readme to regenerate it. 3 | 4 | _branch_io_complete() { 5 | local word opts 6 | word="$1" 7 | opts="-h --help -t --trace -v --version" 8 | <%= %(opts="$opts #{options_for_command 'setup'}") %> 9 | 10 | reply=( "${(ps: :)opts}" ) 11 | } 12 | 13 | compctl -K _branch_io_complete branch_io 14 | compctl -K _branch_io_complete br 15 | -------------------------------------------------------------------------------- /lib/assets/templates/env_description.erb: -------------------------------------------------------------------------------- 1 | Output information about CLI environment. 2 | -------------------------------------------------------------------------------- /lib/assets/templates/program_description.erb: -------------------------------------------------------------------------------- 1 | Command-line tool to integrate the Branch SDK into mobile app projects (currently 2 | iOS only) and validate Universal Link domains 3 | -------------------------------------------------------------------------------- /lib/assets/templates/report_description.erb: -------------------------------------------------------------------------------- 1 | This command optionally cleans and then builds a workspace or project, generating a verbose 2 | report with additional diagnostic information suitable for opening a support ticket. 3 | 4 | Use the <%= option :header_only %> option to output only a brief diagnostic report without 5 | building. 6 | -------------------------------------------------------------------------------- /lib/assets/templates/setup_description.erb: -------------------------------------------------------------------------------- 1 | Integrates the Branch SDK into a native app project. This currently supports iOS only. 2 | It will infer the project location if there is exactly one .xcodeproj anywhere under 3 | the current directory, excluding any in a Pods or Carthage folder. Otherwise, specify 4 | the project location using the <%= option :xcodeproj %> option, or the CLI will prompt you for the 5 | location. 6 | 7 | If a Podfile or Cartfile is detected, the Branch SDK will be added to the relevant 8 | configuration file and the dependencies updated to include the Branch framework. 9 | This behavior may be suppressed using <%= option :no_add_sdk %>. If no Podfile or Cartfile 10 | is found, and Branch.framework is not already among the project's dependencies, 11 | you will be prompted for a number of choices, including setting up CocoaPods or 12 | Carthage for the project or directly installing the Branch.framework. 13 | 14 | By default, all supplied Universal Link domains are validated. If validation passes, 15 | the setup continues. If validation fails, no further action is taken. Suppress 16 | validation using <%= option :no_validate %> or force changes when validation fails using 17 | <%= option :force %>. 18 | 19 | By default, this command will look for the first app target in the project. Test 20 | targets are not supported. To set up an extension target, supply the <%= option :target %> option. 21 | 22 | All relevant target settings are modified. The Branch keys are added to the Info.plist, 23 | along with the <%= highlight 'branch_universal_link_domains' %> key for custom domains (when <%= option :domains %> 24 | is used). For app targets, all domains are added to the project's Associated Domains 25 | entitlement. An entitlements file is also added for app targets if none is found. 26 | Optionally, if <%= option :frameworks %> is specified, this command can add a list of system 27 | frameworks to the target's dependencies (e.g., AdSupport, CoreSpotlight, SafariServices). 28 | 29 | A language-specific patch is applied to the AppDelegate (Swift or Objective-C). 30 | This can be suppressed using <%= option :no_patch_source %>. 31 | 32 | <%= header 'Prerequisites', 4 %> 33 | 34 | Before using this command, make sure to set up your app in the Branch Dashboard 35 | (https://dashboard.branch.io). See https://docs.branch.io/pages/dashboard/integrate/ 36 | for details. To use the <%= highlight 'setup' %> command, you need: 37 | 38 | - Branch key(s), either live, test or both 39 | - Domain name(s) used for Branch links 40 | - Location of your Xcode project (may be inferred in simple projects) 41 | 42 | If using the <%= option :commit %> option, <%= highlight 'git' %> is required. If not using <%= option :no_add_sdk %>, 43 | the <%= highlight 'pod' %> or <%= highlight 'carthage' %> command may be required. If not found, the CLI will 44 | offer to install and set up these command-line tools for you. Alternately, you can arrange 45 | that the relevant commands are available in your <%= highlight 'PATH' %>. 46 | 47 | All parameters are optional. A live key or test key, or both is required, as well 48 | as at least one domain. Specify <%= option :live_key %>, <%= option :test_key %> or both and <%= option :app_link_subdomain %>, 49 | <%= option :domains %> or both. If these are not specified, this command will prompt you 50 | for this information. 51 | 52 | See https://github.com/BranchMetrics/branch_io_cli#setup-command for more information. 53 | -------------------------------------------------------------------------------- /lib/assets/templates/validate_description.erb: -------------------------------------------------------------------------------- 1 | This command validates all Branch-related settings for a target in an Xcode project, 2 | including validation of the apple-app-site-association file from each domain. 3 | Multiple targets may be validated by running the command multiple times using 4 | the <%= option :target %> option. Test targets are not supported. 5 | 6 | For each Branch key present in the Info.plist, it retrieves the settings from Branch's 7 | system. If the information cannot be retrieved (or if the `branch_key` is not present), 8 | an error is recorded. If the <%= option :live_key %> or <%= option :test_key %> option is present, 9 | the set of all keys used by the target must exactly match the options. 10 | 11 | All domains and URI schemes configured for the target must include all domains 12 | and URI schemes configured for all keys used by the target. Other domains or 13 | URI schemes may also be present in the project. 14 | 15 | This command validates all Universal Link domains configured in an application target 16 | without making any modification. It validates both Branch and non-Branch domains. Unlike 17 | web-based Universal Link validators, this command operates directly on the project. It 18 | finds the bundle and signing team identifiers in the project as well as the app's 19 | Associated Domains. It requests the apple-app-site-association file for each domain 20 | and validates the file against the project's settings. 21 | 22 | By default, all build configurations in the project are validated. To validate a different list 23 | of configurations, including a single configuration, specify the <%= option :configurations %> option. 24 | 25 | If <%= option :domains %> is specified, the list of Universal Link domains in the Associated 26 | Domains entitlement must exactly match this list, without regard to order, for all 27 | configurations under validation. If no <%= option :domains %> are provided, validation passes 28 | if at least one Universal Link domain is configured for each configuration and passes 29 | validation, and no Universal Link domain is present in any configuration that does not 30 | pass validation. 31 | 32 | All parameters are optional. 33 | 34 | See https://github.com/BranchMetrics/branch_io_cli#validate-command for more information. 35 | -------------------------------------------------------------------------------- /lib/branch_io_cli.rb: -------------------------------------------------------------------------------- 1 | require_relative "branch_io_cli/ascii_art" 2 | require_relative "branch_io_cli/branch_app" 3 | require_relative "branch_io_cli/cli" 4 | require_relative "branch_io_cli/command" 5 | require_relative "branch_io_cli/configuration" 6 | require_relative "branch_io_cli/core_ext" 7 | require_relative "branch_io_cli/format" 8 | require_relative "branch_io_cli/helper" 9 | require_relative "branch_io_cli/version" 10 | -------------------------------------------------------------------------------- /lib/branch_io_cli/ascii_art.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | # Courtesy of artii https://github.com/miketierney/artii 3 | # artii Branch 4 | ASCII_ART = File.read(File.expand_path(File.join("..", "..", "assets", "artwork", "branch_ascii_art.txt"), __FILE__)) 5 | end 6 | -------------------------------------------------------------------------------- /lib/branch_io_cli/branch_app.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/hash" 2 | require "active_support/json" 3 | require_relative "helper" 4 | require "tty/spinner" 5 | 6 | module BranchIOCLI 7 | class BranchApp 8 | class << self 9 | def [](key) 10 | fetch key 11 | end 12 | 13 | def fetch(key, cache: true) 14 | @apps ||= {} 15 | @apps[key] = new(key) unless cache && @apps[key] 16 | @apps[key] 17 | end 18 | end 19 | 20 | API_ENDPOINT = "https://api.branch.io/v1/app-link-settings/" 21 | 22 | attr_reader :key 23 | attr_reader :alternate_short_url_domain 24 | attr_reader :android_package_name 25 | attr_reader :android_uri_scheme 26 | attr_reader :default_short_url_domain 27 | attr_reader :ios_bundle_id 28 | attr_reader :ios_team_id 29 | attr_reader :ios_uri_scheme 30 | attr_reader :short_url_domain 31 | 32 | def initialize(key) 33 | @key = key 34 | 35 | spinner = TTY::Spinner.new "[:spinner] Fetching configuration from Branch Dashboard for #{key}.", format: :flip 36 | spinner.auto_spin 37 | 38 | begin 39 | @hash = JSON.parse(Helper::BranchHelper.fetch("#{API_ENDPOINT}#{key}", spin: false)).symbolize_keys.merge key: key 40 | spinner.success 41 | @valid = true 42 | rescue StandardError => e 43 | spinner.error 44 | say e.message 45 | @valid = false 46 | return 47 | end 48 | 49 | @alternate_short_url_domain = @hash[:alternate_short_url_domain] 50 | @android_package_name = @hash[:android_package_name] 51 | @android_uri_scheme = @hash[:android_uri_scheme] 52 | @default_short_url_domain = @hash[:default_short_url_domain] 53 | @ios_bundle_id = @hash[:ios_bundle_id] 54 | @ios_team_id = @hash[:ios_team_id] 55 | @ios_uri_scheme = @hash[:ios_uri_scheme] 56 | @short_url_domain = @hash[:short_url_domain] 57 | end 58 | 59 | def valid? 60 | @valid 61 | end 62 | 63 | def domains 64 | [alternate_short_url_domain, default_short_url_domain, short_url_domain].compact.uniq 65 | end 66 | 67 | def to_hash 68 | @hash 69 | end 70 | 71 | def to_s 72 | # Changes 73 | # {:key1=>"value1", :key2=>"value2"} 74 | # to 75 | # key1="value1" key2="value2" 76 | @hash.to_s.sub(/^\{\:/, '').sub(/\}$/, '').gsub(/, \:/, ' ').gsub(/\=\>/, '=') 77 | end 78 | 79 | def inspect 80 | "#" 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/branch_io_cli/cli.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "commander" 3 | require_relative "format" 4 | 5 | module BranchIOCLI 6 | class CLI 7 | include Commander::Methods 8 | include Format::HighlineFormat 9 | 10 | def run 11 | program :name, "Branch.io command-line interface" 12 | program :version, VERSION 13 | program :description, render(:program_description) 14 | 15 | # Automatically detect all commands from branch_io_cli/command. 16 | all_commands = Dir[File.expand_path(File.join("..", "command", "*_command.rb"), __FILE__)].map do |path| 17 | File.basename(path, ".rb").sub(/_command$/, "") 18 | end 19 | 20 | all_commands.each do |command_name| 21 | configuration_class = configuration_class command_name 22 | command_class = command_class command_name 23 | next unless configuration_class && command_class 24 | 25 | command command_name do |c| 26 | c.syntax = "branch_io #{c.name} [OPTIONS]\n br #{c.name} [OPTIONS]" 27 | c.summary = configuration_class.summary if configuration_class.respond_to?(:summary) 28 | 29 | begin 30 | c.description = render "#{c.name}_description" 31 | rescue Errno::ENOENT 32 | end 33 | 34 | add_options_for_command c 35 | 36 | if configuration_class.respond_to?(:examples) && configuration_class.examples 37 | configuration_class.examples.each_key do |text| 38 | example = configuration_class.examples[text] 39 | c.example text, example 40 | end 41 | end 42 | 43 | c.action do |args, options| 44 | options.default configuration_class.defaults 45 | return_value = command_class.new(options).run! 46 | exit(return_value.respond_to?(:to_i) ? return_value.to_i : 0) 47 | end 48 | end 49 | end 50 | 51 | run! 52 | end 53 | 54 | def configuration_class(name) 55 | class_for_command name, :configuration 56 | end 57 | 58 | def command_class(name) 59 | class_for_command name, :command 60 | end 61 | 62 | def class_for_command(name, type) 63 | type_name = type.to_s.capitalize 64 | type_module = BranchIOCLI.const_get(type_name) 65 | candidate = type_module.const_get("#{name.to_s.capitalize}#{type_name}") 66 | return nil unless candidate 67 | 68 | base = type_module.const_get(type_name) 69 | return nil unless candidate.superclass == base 70 | candidate 71 | end 72 | 73 | def add_options_for_command(c) 74 | configuration_class = configuration_class(c.name) 75 | return unless configuration_class.respond_to?(:available_options) 76 | 77 | available_options = configuration_class.available_options 78 | available_options.each do |option| 79 | args = option.aliases 80 | declaration = "--" 81 | declaration += "[no-]" if option.negatable 82 | declaration += option.name.to_s.gsub(/_/, '-') 83 | if option.example 84 | declaration += " " 85 | declaration += "[" if option.argument_optional 86 | declaration += option.example 87 | declaration += "]" if option.argument_optional 88 | end 89 | args << declaration 90 | args << option.type if option.type 91 | 92 | if option.type.nil? 93 | default_value = option.default_value ? "yes" : "no" 94 | else 95 | default_value = option.default_value 96 | end 97 | 98 | default_string = default_value ? " (default: #{default_value})" : nil 99 | args << "#{option.description}#{default_string}" 100 | 101 | c.option(*args) 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/branch_io_cli/command.rb: -------------------------------------------------------------------------------- 1 | require_relative "command/command" 2 | require_relative "command/env_command" 3 | require_relative "command/report_command" 4 | require_relative "command/setup_command" 5 | require_relative "command/validate_command" 6 | -------------------------------------------------------------------------------- /lib/branch_io_cli/command/command.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Command 3 | class Command 4 | class << self 5 | def command_name 6 | matches = /BranchIOCLI::Command::(\w+)Command/.match name 7 | matches[1].downcase 8 | end 9 | 10 | def configuration_class 11 | root = command_name.capitalize 12 | 13 | BranchIOCLI::Configuration.const_get("#{root}Configuration") 14 | end 15 | 16 | def available_options 17 | configuration_class.available_options 18 | end 19 | 20 | def examples 21 | configuration_class.examples if configuration_class.respond_to?(:examples) 22 | end 23 | 24 | def return_value 25 | configuration_class.return_value if configuration_class.respond_to?(:return_value) 26 | end 27 | end 28 | 29 | attr_reader :options # command-specific options from CLI 30 | attr_reader :config # command-specific configuration object 31 | 32 | def initialize(options) 33 | @options = options 34 | @config = self.class.configuration_class.new options 35 | end 36 | 37 | def run! 38 | # implemented by subclasses 39 | end 40 | 41 | def helper 42 | Helper::BranchHelper 43 | end 44 | 45 | def patch_helper 46 | Helper::PatchHelper 47 | end 48 | 49 | def tool_helper 50 | Helper::ToolHelper 51 | end 52 | 53 | def env 54 | Configuration::Environment 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/branch_io_cli/command/env_command.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Command 3 | class EnvCommand < Command 4 | def run! 5 | if config.show_all? 6 | say "\n" unless config.quiet 7 | say "<%= color('CLI version:', BOLD) %> #{VERSION}" 8 | say env.ruby_header(include_load_path: true) 9 | else 10 | script_path = env.completion_script 11 | if script_path.nil? 12 | say "Completion script not available for #{env.shell}" 13 | return 1 14 | end 15 | puts script_path 16 | end 17 | 18 | 0 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/branch_io_cli/command/report_command.rb: -------------------------------------------------------------------------------- 1 | require "shellwords" 2 | require "tty/spinner" 3 | 4 | module BranchIOCLI 5 | module Command 6 | class ReportCommand < Command 7 | def run! 8 | say "\n" 9 | 10 | task = Helper::Task.new 11 | task.begin "Loading settings from Xcode." 12 | 13 | # In case running in a non-CLI context (e.g., Rake or Fastlane) be sure 14 | # to reset Xcode settings each time, since project, target and 15 | # configurations will change. 16 | Configuration::XcodeSettings.reset 17 | if Configuration::XcodeSettings.all_valid? 18 | task.success "Done." 19 | else 20 | task.error "Failed." 21 | say "Failed to load settings from Xcode. Some information may be missing.\n" 22 | end 23 | 24 | if config.header_only 25 | say "\n" 26 | say env.ruby_header 27 | say report_helper.report_header 28 | return 0 29 | end 30 | 31 | if config.report_path == "stdout" 32 | write_report STDOUT 33 | else 34 | File.open(config.report_path, "w") { |f| write_report f } 35 | say "Report generated in #{config.report_path}" 36 | end 37 | 38 | 0 39 | end 40 | 41 | def write_report(report) 42 | report.write "Branch.io Xcode build report v #{VERSION} #{Time.now}\n\n" 43 | report.write "#{config.report_configuration}\n" 44 | 45 | if report == STDOUT 46 | say env.ruby_header 47 | else 48 | report.write env.ruby_header(terminal: false, include_load_path: true) 49 | end 50 | 51 | report.write "#{report_helper.report_header}\n" 52 | 53 | tool_helper.pod_install_if_required report 54 | tool_helper.carthage_bootstrap_if_required report 55 | 56 | # run xcodebuild -list 57 | report.sh(*report_helper.base_xcodebuild_cmd, "-list", obfuscate: true) 58 | 59 | # If using a workspace, -list all the projects as well 60 | if config.workspace_path 61 | config.workspace.file_references.map(&:path).each do |project_path| 62 | path = File.join File.dirname(config.workspace_path), project_path 63 | report.sh "xcodebuild", "-list", "-project", path, obfuscate: true 64 | end 65 | end 66 | 67 | # xcodebuild -showBuildSettings 68 | config.configurations.each do |configuration| 69 | Configuration::XcodeSettings[configuration].log_xcodebuild_showbuildsettings report 70 | end 71 | 72 | base_cmd = report_helper.base_xcodebuild_cmd 73 | # Add more options for the rest of the commands 74 | base_cmd += [ 75 | "-scheme", 76 | config.scheme, 77 | "-configuration", 78 | config.configuration || config.configurations_from_scheme.first, 79 | "-sdk", 80 | config.sdk 81 | ] 82 | 83 | if config.clean 84 | task = Helper::Task.new use_spinner: report != STDOUT 85 | task.begin "Cleaning." 86 | if report.sh(*base_cmd, "clean", obfuscate: true).success? 87 | task.success "Done." 88 | else 89 | task.error "Clean failed." 90 | end 91 | end 92 | 93 | task = Helper::Task.new use_spinner: report != STDOUT 94 | task.begin "Building." 95 | if report.sh(*base_cmd, "-verbose", obfuscate: true).success? 96 | task.success "Done." 97 | else 98 | task.error "Failed." 99 | end 100 | end 101 | 102 | def report_helper 103 | Helper::ReportHelper 104 | end 105 | 106 | def tool_helper 107 | Helper::ToolHelper 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/branch_io_cli/command/setup_command.rb: -------------------------------------------------------------------------------- 1 | require_relative "../helper" 2 | 3 | module BranchIOCLI 4 | module Command 5 | class SetupCommand < Command 6 | include Helper::Methods 7 | 8 | def initialize(options) 9 | super 10 | @keys = config.keys 11 | @domains = config.all_domains 12 | end 13 | 14 | def run! 15 | # Make sure the user stashes or commits before continuing. 16 | return 1 unless check_repo_status 17 | 18 | # Validate Universal Link configuration in an application target. 19 | if config.validate && config.target.symbol_type == :application 20 | valid = validate_universal_links 21 | return 1 unless valid || config.force 22 | end 23 | 24 | return false unless tool_helper.pod_install_if_required 25 | 26 | # Set up Universal Links and Branch key(s) 27 | update_project_settings 28 | 29 | # Add SDK via CocoaPods, Carthage or direct download (no-op if disabled). 30 | add_sdk 31 | 32 | # Patch source code if so instructed. 33 | patch_helper.patch_source config.xcodeproj if config.patch_source 34 | 35 | # Commit changes if so instructed. 36 | commit_changes if config.commit 37 | 38 | say "\nDone ✅" 39 | 40 | # Return success. 41 | 0 42 | end 43 | 44 | def validate_universal_links 45 | say "Validating new Universal Link configuration before making any changes." 46 | valid = true 47 | config.xcodeproj.build_configurations.each do |c| 48 | message = "Validating #{c.name} configuration" 49 | say "\n<%= color('#{message}', [BOLD, CYAN]) %>\n\n" 50 | 51 | configuration_valid = helper.validate_team_and_bundle_ids_from_aasa_files @domains, false, c.name 52 | 53 | if configuration_valid 54 | say "Universal Link configuration passed validation for #{c.name} configuration. ✅\n\n" 55 | else 56 | say "Universal Link configuration failed validation for #{c.name} configuration.\n\n" 57 | helper.errors.each { |error| say " #{error}" } 58 | end 59 | 60 | valid &&= configuration_valid 61 | end 62 | valid 63 | end 64 | 65 | def add_sdk 66 | say "\nMaking sure Branch dependency is available.\n\n" 67 | case config.sdk_integration_mode 68 | when :cocoapods 69 | if File.exist? config.podfile_path 70 | tool_helper.update_podfile config 71 | else 72 | tool_helper.add_cocoapods config 73 | end 74 | when :carthage 75 | if File.exist? config.cartfile_path 76 | tool_helper.update_cartfile config, config.xcodeproj 77 | else 78 | tool_helper.add_carthage config 79 | end 80 | when :direct 81 | tool_helper.add_direct config 82 | end 83 | end 84 | 85 | def update_project_settings 86 | say "Updating project settings.\n\n" 87 | helper.add_custom_build_setting if config.setting 88 | helper.add_keys_to_info_plist @keys 89 | config.target.add_system_frameworks config.frameworks unless config.frameworks.blank? 90 | 91 | return unless config.target.symbol_type == :application 92 | 93 | helper.add_branch_universal_link_domains_to_info_plist @domains 94 | helper.ensure_uri_scheme_in_info_plist 95 | config.xcodeproj.build_configurations.each do |c| 96 | new_path = helper.add_universal_links_to_project @domains, false, c.name 97 | sh "git", "add", new_path if config.commit && new_path 98 | end 99 | ensure 100 | config.xcodeproj.save 101 | end 102 | 103 | def commit_changes 104 | changes = helper.changes.to_a.map { |c| Pathname.new(File.expand_path(c)).relative_path_from(Pathname.pwd).to_s } 105 | 106 | commit_message = config.commit if config.commit.kind_of?(String) 107 | commit_message ||= "[branch_io_cli] Branch SDK integration #{config.relative_path(config.xcodeproj_path)} (#{config.target.name})" 108 | 109 | sh "git", "commit", "-qm", commit_message, *changes 110 | end 111 | 112 | def check_repo_status 113 | # If the git command is not installed, there's not much we can do. 114 | # Don't want to use verify_git here, which will insist on installing 115 | # the command. The logic of that method could change. 116 | return true if `which git`.empty? || !config.confirm 117 | 118 | unless Dir.exist? ".git" 119 | `git rev-parse --git-dir > /dev/null 2>&1` 120 | # Not a git repo 121 | return true unless $?.success? 122 | end 123 | 124 | `git diff-index --quiet HEAD --` 125 | return true if $?.success? 126 | 127 | # Show the user 128 | sh "git status" 129 | 130 | choice = choose do |menu| 131 | menu.header = "There are uncommitted changes in this repo. It's best to stash or commit them before continuing." 132 | menu.readline = true 133 | menu.choice "Stash" 134 | menu.choice "Commit (You will be prompted for a commit message.)" 135 | menu.choice "Quit" 136 | menu.choice "Ignore and continue" 137 | menu.prompt = "Please enter one of the options above: " 138 | end 139 | 140 | case choice 141 | when /^Stash/ 142 | sh %w(git stash -q) 143 | when /^Commit/ 144 | message = ask "Please enter a commit message: " 145 | sh "git", "commit", "-aqm", message 146 | when /^Quit/ 147 | say "Please stash or commit your changes before continuing." 148 | return false 149 | end 150 | 151 | true 152 | end 153 | end 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /lib/branch_io_cli/command/validate_command.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Command 3 | class ValidateCommand < Command 4 | def run! 5 | say "\n" 6 | 7 | configurations = config.configurations || config.xcodeproj.build_configurations.map(&:name) 8 | 9 | return false unless tool_helper.pod_install_if_required 10 | 11 | valid = project_matches_keys?(configurations) 12 | schemes_valid = uri_schemes_valid?(configurations) 13 | valid &&= schemes_valid 14 | 15 | configurations.each do |configuration| 16 | message = "Validating #{configuration} configuration" 17 | say "\n<%= color('#{message}', [BOLD, CYAN]) %>\n\n" 18 | 19 | config_valid = true 20 | 21 | unless config.domains.blank? 22 | domains_valid = helper.validate_project_domains(config.domains, configuration) 23 | 24 | if domains_valid 25 | say "Project domains match domains parameter. ✅" 26 | else 27 | say "Project domains do not match specified domains. ❌" 28 | helper.errors.each { |error| say " #{error}" } 29 | end 30 | 31 | config_valid &&= domains_valid 32 | end 33 | 34 | if config.target.symbol_type == :application 35 | entitlements_valid = helper.validate_team_and_bundle_ids_from_aasa_files [], false, configuration 36 | unless entitlements_valid 37 | say "Universal Link configuration failed validation for #{configuration} configuration. ❌" 38 | helper.errors.each { |error| say " #{error}" } 39 | end 40 | 41 | config_valid &&= entitlements_valid 42 | 43 | say "Universal Link configuration passed validation for #{configuration} configuration. ✅" if config_valid 44 | end 45 | 46 | unless config.universal_links_only 47 | branch_config_valid = helper.project_valid? configuration 48 | unless branch_config_valid 49 | say "Branch configuration failed validation for #{configuration} configuration. ❌" 50 | helper.errors.each { |error| say " #{error}" } 51 | end 52 | 53 | config_valid &&= branch_config_valid 54 | 55 | say "Branch configuration passed validation for #{configuration} configuration. ✅" if config_valid 56 | end 57 | 58 | valid &&= config_valid 59 | end 60 | 61 | unless valid 62 | say "\nValidation failed. See errors above marked with ❌." 63 | say "Please verify your app configuration at https://dashboard.branch.io." 64 | say "If your Dashboard configuration is correct, br setup will fix most errors." 65 | end 66 | 67 | valid ? 0 : 1 68 | end 69 | 70 | def project_matches_keys?(configurations) 71 | expected_keys = [config.live_key, config.test_key].compact 72 | return true if expected_keys.empty? 73 | 74 | # Validate the keys in the project against those passed in by the user. 75 | branch_keys = helper.branch_keys_from_project(configurations).sort 76 | 77 | keys_valid = expected_keys == branch_keys 78 | 79 | say "\n" 80 | if keys_valid 81 | say "Branch keys from project match provided keys. ✅" 82 | else 83 | say "Branch keys from project do not match provided keys. ❌" 84 | say " Expected: #{expected_keys.inspect}" 85 | say " Actual: #{branch_keys.inspect}" 86 | end 87 | 88 | keys_valid 89 | end 90 | 91 | def uri_schemes_valid?(configurations) 92 | uri_schemes = helper.branch_apps_from_project(configurations).map(&:ios_uri_scheme).compact.uniq 93 | expected = uri_schemes.map { |s| BranchIOCLI::Configuration::Configuration.uri_scheme_without_suffix(s) }.sort 94 | return true if expected.empty? 95 | 96 | actual = helper.uri_schemes_from_project(configurations).sort 97 | valid = (expected - actual).empty? 98 | if valid 99 | say "URI schemes from project match schemes from Dashboard. ✅" 100 | else 101 | say "URI schemes from project do not match schemes from Dashboard. ❌" 102 | say " Expected: #{expected.inspect}" 103 | say " Actual: #{actual.inspect}" 104 | end 105 | 106 | valid 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/branch_io_cli/configuration.rb: -------------------------------------------------------------------------------- 1 | require_relative "configuration/configuration" 2 | require_relative "configuration/env_configuration" 3 | require_relative "configuration/env_options" 4 | require_relative "configuration/environment" 5 | require_relative "configuration/option" 6 | require_relative "configuration/option_wrapper" 7 | require_relative "configuration/report_configuration" 8 | require_relative "configuration/report_options" 9 | require_relative "configuration/setup_configuration" 10 | require_relative "configuration/setup_options" 11 | require_relative "configuration/validate_configuration" 12 | require_relative "configuration/validate_options" 13 | require_relative "configuration/xcode_settings" 14 | -------------------------------------------------------------------------------- /lib/branch_io_cli/configuration/env_configuration.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Configuration 3 | class EnvConfiguration < Configuration 4 | class << self 5 | def summary 6 | "Output information about CLI environment." 7 | end 8 | 9 | def examples 10 | { 11 | "Show CLI environment" => "br env", 12 | "Get completion script for zsh" => "br env -cs zsh" 13 | } 14 | end 15 | end 16 | 17 | def initialize(options) 18 | @quiet = !options.verbose 19 | @ruby_version = options.ruby_version 20 | @rubygems_version = options.rubygems_version 21 | @lib_path = options.lib_path 22 | @assets_path = options.assets_path 23 | @completion_script = options.completion_script 24 | @shell = options.shell 25 | super 26 | end 27 | 28 | def log 29 | super 30 | return if quiet 31 | 32 | say < #{completion_script} 34 | <%= color('Shell for completion script:', BOLD) %> #{shell} 35 | EOF 36 | end 37 | 38 | def show_all? 39 | !show_completion_script? 40 | end 41 | 42 | def show_completion_script? 43 | completion_script 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/branch_io_cli/configuration/env_options.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Configuration 3 | class EnvOptions 4 | def self.available_options 5 | [ 6 | Option.new( 7 | name: :completion_script, 8 | description: "Get the path to the completion script for this shell", 9 | default_value: false, 10 | aliases: "-c" 11 | ), 12 | Option.new( 13 | name: :shell, 14 | env_name: "SHELL", 15 | description: "Specify shell for completion script", 16 | type: String, 17 | example: "zsh", 18 | aliases: "-s" 19 | ), 20 | Option.new( 21 | name: :verbose, 22 | description: "Generate verbose output", 23 | default_value: false, 24 | aliases: "-V" 25 | ) 26 | ] 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/branch_io_cli/configuration/environment.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/string" 2 | require "rbconfig" 3 | require_relative "../core_ext/tty_platform" 4 | 5 | module BranchIOCLI 6 | module Configuration 7 | class Environment 8 | PLATFORM = TTY::Platform.new 9 | 10 | class << self 11 | def config 12 | Configuration.current 13 | end 14 | 15 | def os_version 16 | PLATFORM.version 17 | end 18 | 19 | def os_name 20 | PLATFORM.os.to_s.capitalize 21 | end 22 | 23 | def os_arch 24 | PLATFORM.architecture 25 | end 26 | 27 | def operating_system 28 | if PLATFORM.br_high_sierra? 29 | os = "macOS High Sierra" 30 | elsif PLATFORM.br_sierra? 31 | os = "macOS Sierra" 32 | else 33 | os = os_name if os_name 34 | os += " #{os_version}" if os_version 35 | end 36 | 37 | os += " (#{os_arch})" 38 | 39 | os 40 | end 41 | 42 | def ruby_path 43 | File.join(RbConfig::CONFIG["bindir"], 44 | RbConfig::CONFIG["RUBY_INSTALL_NAME"] + 45 | RbConfig::CONFIG["EXEEXT"]) 46 | end 47 | 48 | def from_homebrew? 49 | ENV["BRANCH_IO_CLI_INSTALLED_FROM_HOMEBREW"] == "true" 50 | end 51 | 52 | def lib_path 53 | File.expand_path File.join("..", "..", ".."), __FILE__ 54 | end 55 | 56 | def assets_path 57 | File.join lib_path, "assets" 58 | end 59 | 60 | # Returns the last path component. Uses the SHELL env. var. unless overriden 61 | # at the command line (br env -cs zsh). 62 | def shell 63 | return ENV["SHELL"].split("/").last unless config.class.available_options.map(&:name).include?(:shell) 64 | config.shell.split("/").last 65 | end 66 | 67 | def completion_script 68 | path = File.join assets_path, "completions", "completion.#{shell}" 69 | path if File.readable?(path) 70 | end 71 | 72 | def ruby_header(terminal: true, include_load_path: false) 73 | header = header_item("Operating system", operating_system, terminal: terminal) 74 | header += header_item("Ruby version", RUBY_VERSION, terminal: terminal) 75 | header += header_item("Ruby path", display_path(ruby_path), terminal: terminal) 76 | header += header_item("RubyGems version", Gem::VERSION, terminal: terminal) 77 | header += header_item("Bundler", defined?(Bundler) ? Bundler::VERSION : "no", terminal: terminal) 78 | header += header_item("Installed from Homebrew", from_homebrew? ? "yes" : "no", terminal: terminal) 79 | header += header_item("GEM_HOME", obfuscate_user(Gem.dir), terminal: terminal) 80 | header += header_item("Lib path", display_path(lib_path), terminal: terminal) 81 | header += header_item("LOAD_PATH", $LOAD_PATH.map { |p| display_path(p) }, terminal: terminal) if include_load_path 82 | header += header_item("Shell", ENV["SHELL"], terminal: terminal) 83 | header += "\n" 84 | header 85 | end 86 | 87 | def header_item(label, value, terminal: true) 88 | if terminal 89 | "<%= color('#{label}:', BOLD) %> #{value}\n" 90 | else 91 | "#{label}: #{value}\n" 92 | end 93 | end 94 | 95 | def obfuscate_user(path) 96 | return nil if path.nil? 97 | path.gsub(ENV['HOME'], '~').gsub(ENV['USER'], '$USER') 98 | end 99 | 100 | def display_path(path) 101 | return nil if path.nil? 102 | path = path.gsub(Gem.dir, '$GEM_HOME') 103 | path = obfuscate_user(path) 104 | path 105 | end 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/branch_io_cli/configuration/option.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Configuration 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 | 15 | attr_accessor :name 16 | attr_accessor :env_name 17 | attr_accessor :type 18 | attr_accessor :description 19 | attr_accessor :default_value 20 | attr_accessor :example 21 | attr_accessor :argument_optional 22 | attr_accessor :aliases 23 | attr_accessor :negatable 24 | attr_accessor :confirm_symbol 25 | attr_accessor :valid_values_proc 26 | attr_accessor :validate_proc 27 | attr_accessor :convert_proc 28 | attr_accessor :label 29 | attr_accessor :skip_confirmation 30 | 31 | def initialize(options) 32 | @name = options[:name] 33 | @env_name = options[:env_name] 34 | @type = options[:type] 35 | @description = options[:description] 36 | @default_value = options[:default_value] 37 | @example = options[:example] 38 | @argument_optional = options[:argument_optional] || false 39 | @aliases = options[:aliases] || [] 40 | @aliases = [@aliases] unless @aliases.kind_of?(Array) 41 | @negatable = options[:negatable] 42 | @negatable = options[:type].nil? if options[:negatable].nil? 43 | @confirm_symbol = options[:confirm_symbol] || @name 44 | @valid_values_proc = options[:valid_values_proc] 45 | @validate_proc = options[:validate_proc] 46 | @convert_proc = options[:convert_proc] 47 | @label = options[:label] || @name.to_s.capitalize.gsub(/_/, ' ') 48 | @skip_confirmation = options[:skip_confirmation] 49 | 50 | raise ArgumentError, "Use :validate_proc or :valid_values_proc, but not both." if @valid_values_proc && @validate_proc 51 | 52 | @env_name = "BRANCH_#{@name.to_s.upcase}" if @env_name.nil? 53 | @argument_optional ||= @negatable 54 | end 55 | 56 | def valid_values 57 | return valid_values_proc.call if valid_values_proc && valid_values_proc.kind_of?(Proc) 58 | end 59 | 60 | def ui_type 61 | if type.nil? 62 | "Boolean" 63 | elsif type == Array 64 | "Comma-separated list" 65 | else 66 | type.to_s 67 | end 68 | end 69 | 70 | def env_value 71 | convert(ENV[env_name]) if env_name 72 | end 73 | 74 | def convert(value) 75 | return convert_proc.call(value) if convert_proc 76 | 77 | if type == Array 78 | value = value.split(",") if value.kind_of?(String) 79 | elsif type == String && value.kind_of?(String) 80 | value = value.strip 81 | value = nil if value.empty? 82 | elsif type.nil? 83 | value = true if value.kind_of?(String) && value =~ /^[ty]/i 84 | value = false if value.kind_of?(String) && value =~ /^[fn]/i 85 | end 86 | 87 | value 88 | end 89 | 90 | def display_value(value) 91 | if type.nil? 92 | value ? "yes" : "no" 93 | elsif value.nil? 94 | "(none)" 95 | else 96 | value.to_s 97 | end 98 | end 99 | 100 | def valid?(value) 101 | return validate_proc.call(value) if validate_proc 102 | 103 | return true if value.nil? 104 | 105 | if valid_values && type != Array 106 | valid_values.include? value 107 | elsif valid_values 108 | value.all? { |v| valid_values.include?(v) } 109 | elsif type 110 | value.kind_of? type 111 | else 112 | value == true || value == false 113 | end 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/branch_io_cli/configuration/option_wrapper.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Configuration 3 | # Proxy class for use with Command.new. 4 | class OptionWrapper 5 | attr_reader :hash 6 | attr_reader :options 7 | attr_reader :add_defaults 8 | 9 | def initialize(hash, options, add_defaults = true) 10 | hash ||= {} 11 | 12 | @hash = hash 13 | @options = options 14 | @add_defaults = add_defaults 15 | 16 | build_option_hash 17 | end 18 | 19 | def method_missing(method_sym, *arguments, &block) 20 | option = @option_hash[method_sym] 21 | return super unless option 22 | 23 | value = hash[method_sym] 24 | return value unless add_defaults && value.nil? 25 | 26 | default_value = option.env_value 27 | default_value = option.default_value if default_value.nil? 28 | default_value 29 | end 30 | 31 | def build_option_hash 32 | @option_hash = options.inject({}) { |hash, o| hash.merge o.name => o } 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/branch_io_cli/configuration/report_options.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Configuration 3 | class ReportOptions 4 | class << self 5 | def available_options 6 | [ 7 | Option.new( 8 | name: :workspace, 9 | description: "Path to an Xcode workspace", 10 | type: String, 11 | example: "MyProject.xcworkspace" 12 | ), 13 | Option.new( 14 | name: :xcodeproj, 15 | description: "Path to an Xcode project", 16 | type: String, 17 | example: "MyProject.xcodeproj" 18 | ), 19 | Option.new( 20 | name: :scheme, 21 | description: "A scheme from the project or workspace to build", 22 | type: String, 23 | example: "MyProjectScheme" 24 | ), 25 | Option.new( 26 | name: :target, 27 | description: "A target to build", 28 | type: String, 29 | example: "MyProjectTarget" 30 | ), 31 | Option.new( 32 | name: :configuration, 33 | description: "The build configuration to use (default: Scheme-dependent)", 34 | type: String, 35 | example: "Debug/Release/CustomConfigName" 36 | ), 37 | Option.new( 38 | name: :sdk, 39 | description: "Passed as -sdk to xcodebuild", 40 | type: String, 41 | example: "iphoneos", 42 | default_value: "iphonesimulator" 43 | ), 44 | Option.new( 45 | name: :podfile, 46 | description: "Path to the Podfile for the project", 47 | type: String, 48 | example: "/path/to/Podfile" 49 | ), 50 | Option.new( 51 | name: :cartfile, 52 | description: "Path to the Cartfile for the project", 53 | type: String, 54 | example: "/path/to/Cartfile" 55 | ), 56 | Option.new( 57 | name: :clean, 58 | description: "Clean before attempting to build", 59 | default_value: true 60 | ), 61 | Option.new( 62 | name: :header_only, 63 | description: "Write a report header to standard output and exit", 64 | default_value: false, 65 | aliases: "-H" 66 | ), 67 | Option.new( 68 | name: :pod_repo_update, 69 | description: "Update the local podspec repo before installing", 70 | default_value: true 71 | ), 72 | Option.new( 73 | name: :out, 74 | description: "Report output path", 75 | default_value: "./report.txt", 76 | aliases: "-o", 77 | example: "./report.txt", 78 | type: String, 79 | env_name: "BRANCH_REPORT_PATH" 80 | ) 81 | ] + Option.global_options 82 | end 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/branch_io_cli/configuration/setup_configuration.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Configuration 3 | class SetupConfiguration < Configuration 4 | class << self 5 | def summary 6 | "Integrates the Branch SDK into a native app project" 7 | end 8 | 9 | def examples 10 | { 11 | "Test without validation (can use dummy keys and domains)" => "br setup -L key_live_xxxx -D myapp.app.link --no-validate", 12 | "Use both live and test keys" => "br setup -L key_live_xxxx -T key_test_yyyy -D myapp.app.link", 13 | "Use custom or non-Branch domains" => "br setup -D myapp.app.link,example.com,www.example.com", 14 | "Avoid pod repo update" => "br setup --no-pod-repo-update", 15 | "Install using carthage bootstrap" => "br setup --carthage-command \"bootstrap --no-use-binaries\"" 16 | } 17 | end 18 | end 19 | 20 | APP_LINK_REGEXP = /\.app\.link$|\.test-app\.link$/ 21 | SDK_OPTIONS = 22 | { 23 | "Specify the location of a Podfile or Cartfile" => :specify, 24 | "Set this project up to use CocoaPods and add the Branch SDK." => :cocoapods, 25 | "Set this project up to use Carthage and add the Branch SDK." => :carthage, 26 | "Add Branch.framework directly to the project's dependencies." => :direct, 27 | "Skip adding the framework to the project." => :skip 28 | } 29 | 30 | attr_reader :all_domains 31 | 32 | def initialize(options) 33 | super 34 | # Configuration has been validated and logged to the screen. 35 | confirm_with_user if options.confirm 36 | end 37 | 38 | def validate_options 39 | @validate = options.validate 40 | @patch_source = options.patch_source 41 | @add_sdk = options.add_sdk 42 | @force = options.force 43 | @commit = options.commit 44 | 45 | say "--force is ignored when --no-validate is used." if !options.validate && options.force 46 | if options.cartfile && options.podfile 47 | say "--cartfile and --podfile are mutually exclusive. Please specify the file to patch." 48 | exit 1 49 | end 50 | 51 | validate_xcodeproj_path 52 | validate_target 53 | validate_keys 54 | validate_all_domains options, !target.extension_target_type? 55 | validate_uri_scheme options 56 | validate_setting options 57 | validate_test_configurations options 58 | 59 | # If neither --podfile nor --cartfile is present, arbitrarily look for a Podfile 60 | # first. 61 | 62 | # If --cartfile is present, don't look for a Podfile. Just validate that 63 | # Cartfile. 64 | validate_buildfile_path options.podfile, "Podfile" if options.cartfile.nil? && options.add_sdk 65 | 66 | # If --podfile is present or a Podfile was found, don't look for a Cartfile. 67 | validate_buildfile_path options.cartfile, "Cartfile" if sdk_integration_mode.nil? && options.add_sdk 68 | @carthage_command = options.carthage_command if sdk_integration_mode == :carthage 69 | 70 | validate_sdk_addition options 71 | end 72 | 73 | def log 74 | super 75 | message = <<-EOF 76 | <%= color('Xcode project:', BOLD) %> #{env.display_path(xcodeproj_path)} 77 | <%= color('Target:', BOLD) %> #{target.name} 78 | <%= color('Target type:', BOLD) %> #{target.product_type} 79 | <%= color('Live key:', BOLD) %> #{keys[:live] || '(none)'} 80 | <%= color('Test key:', BOLD) %> #{keys[:test] || '(none)'} 81 | <%= color('Domains:', BOLD) %> #{all_domains} 82 | <%= color('URI scheme:', BOLD) %> #{uri_scheme || '(none)'} 83 | EOF 84 | 85 | if setting 86 | message += <<-EOF 87 | <%= color('Branch key setting:', BOLD) %> #{setting} 88 | EOF 89 | if test_configurations 90 | message += <<-EOF 91 | <%= color('Test configurations:', BOLD) %> #{test_configurations} 92 | EOF 93 | end 94 | end 95 | 96 | message += <<-EOF 97 | <%= color('Podfile:', BOLD) %> #{relative_path(podfile_path) || '(none)'} 98 | <%= color('Cartfile:', BOLD) %> #{relative_path(cartfile_path) || '(none)'} 99 | <%= color('Carthage command:', BOLD) %> #{carthage_command || '(none)'} 100 | <%= color('Pod repo update:', BOLD) %> #{pod_repo_update.inspect} 101 | <%= color('Validate:', BOLD) %> #{validate.inspect} 102 | <%= color('Force:', BOLD) %> #{force.inspect} 103 | <%= color('Add SDK:', BOLD) %> #{add_sdk.inspect} 104 | <%= color('Patch source:', BOLD) %> #{patch_source.inspect} 105 | <%= color('Commit:', BOLD) %> #{commit.inspect} 106 | <%= color('SDK integration mode:', BOLD) %> #{sdk_integration_mode || '(none)'} 107 | EOF 108 | 109 | if swift_version 110 | message += <<-EOF 111 | <%= color('Swift version:', BOLD) %> #{swift_version} 112 | EOF 113 | end 114 | 115 | message += "\n" 116 | 117 | say message 118 | end 119 | 120 | def validate_all_domains(options, required = true) 121 | app_link_roots = app_link_roots_from_domains options.domains 122 | 123 | unless options.app_link_subdomain.nil? || app_link_roots.include?(options.app_link_subdomain) 124 | app_link_roots << options.app_link_subdomain 125 | end 126 | 127 | # app_link_roots now contains options.app_link_subdomain, if supplied, and the roots of any 128 | # .app.link or .test-app.link domains provided via options.domains. 129 | 130 | app_link_subdomains = app_link_subdomains_from_roots app_link_roots 131 | 132 | custom_domains = custom_domains_from_domains options.domains 133 | 134 | @all_domains = (app_link_subdomains + custom_domains).uniq 135 | 136 | while required && @all_domains.empty? 137 | domains = ask "Please enter domains as a comma-separated list: ", ->(str) { str.split "," } 138 | 139 | @all_domains = all_domains_from_domains domains 140 | end 141 | end 142 | 143 | def domains_from_api 144 | helper.domains @apps 145 | end 146 | 147 | def validate_uri_scheme(options) 148 | # No validation at the moment. Just strips off any trailing :// 149 | uri_scheme = options.uri_scheme 150 | 151 | # --no-uri-scheme/uri_scheme: false 152 | if options.uri_scheme == false 153 | @uri_scheme = nil 154 | return 155 | end 156 | 157 | if confirm 158 | uri_scheme ||= ask "Please enter any URI scheme you entered in the Branch Dashboard [enter for none]: " 159 | end 160 | 161 | @uri_scheme = self.class.uri_scheme_without_suffix uri_scheme 162 | end 163 | 164 | def app_link_roots_from_domains(domains) 165 | return [] if domains.nil? 166 | 167 | domains.select { |d| d =~ APP_LINK_REGEXP } 168 | .map { |d| d.sub(APP_LINK_REGEXP, '').sub(/-alternate$/, '') } 169 | .uniq 170 | end 171 | 172 | def custom_domains_from_domains(domains) 173 | return [] if domains.nil? 174 | domains.reject { |d| d =~ APP_LINK_REGEXP }.uniq 175 | end 176 | 177 | def app_link_subdomains(root) 178 | app_link_subdomain = root 179 | return [] if app_link_subdomain.nil? 180 | 181 | live_key = keys[:live] 182 | test_key = keys[:test] 183 | 184 | domains = [] 185 | unless live_key.nil? 186 | domains += [ 187 | "#{app_link_subdomain}.app.link", 188 | "#{app_link_subdomain}-alternate.app.link" 189 | ] 190 | end 191 | unless test_key.nil? 192 | domains += [ 193 | "#{app_link_subdomain}.test-app.link", 194 | "#{app_link_subdomain}-alternate.test-app.link" 195 | ] 196 | end 197 | domains 198 | end 199 | 200 | def app_link_subdomains_from_roots(roots) 201 | roots.inject([]) { |domains, root| domains + app_link_subdomains(root) } 202 | end 203 | 204 | def all_domains_from_domains(domains) 205 | app_link_roots = app_link_roots_from_domains domains 206 | app_link_subdomains = app_link_subdomains_from_roots app_link_roots 207 | custom_domains = custom_domains_from_domains domains 208 | custom_domains + app_link_subdomains 209 | end 210 | 211 | def prompt_for_podfile_or_cartfile 212 | loop do 213 | path = ask("Please enter the location of your Podfile or Cartfile: ").trim 214 | case path 215 | when %r{/?Podfile$} 216 | return if validate_buildfile_at_path path, "Podfile" 217 | when %r{/?Cartfile$} 218 | return if validate_buildfile_at_path path, "Cartfile" 219 | else 220 | say "Path must end in Podfile or Cartfile." 221 | end 222 | end 223 | end 224 | 225 | def validate_sdk_addition(options) 226 | return if !options.add_sdk || sdk_integration_mode 227 | 228 | # If no CocoaPods or Carthage, check to see if the framework is linked. 229 | return if target.frameworks_build_phase.files.map(&:file_ref).map(&:path).any? { |p| p =~ /Branch.framework$/ } 230 | 231 | # --podfile, --cartfile not specified. No Podfile found. No Cartfile found. No Branch.framework in project. 232 | # Prompt the user: 233 | selected = choose do |menu| 234 | menu.header = "No Podfile or Cartfile specified or found. Here are your options" 235 | menu.readline = true 236 | 237 | SDK_OPTIONS.each_key { |k| menu.choice k } 238 | 239 | menu.prompt = "What would you like to do?" 240 | end 241 | 242 | @sdk_integration_mode = SDK_OPTIONS[selected] 243 | 244 | case sdk_integration_mode 245 | when :specify 246 | prompt_for_podfile_or_cartfile 247 | when :cocoapods 248 | @podfile_path = File.expand_path "../Podfile", xcodeproj_path 249 | when :carthage 250 | @cartfile_path = File.expand_path "../Cartfile", xcodeproj_path 251 | @carthage_command = options.carthage_command 252 | end 253 | end 254 | 255 | def validate_setting(options) 256 | setting = options.setting 257 | return if setting.nil? 258 | 259 | @setting = "BRANCH_KEY" and return if setting == true 260 | 261 | loop do 262 | if setting =~ /^[A-Z0-9_]+$/ 263 | @setting = setting 264 | return 265 | end 266 | setting = ask "Invalid build setting. Please enter an all-caps identifier (may include digits and underscores): " 267 | end 268 | end 269 | 270 | def validate_test_configurations(options) 271 | return if options.test_configurations.nil? 272 | unless options.setting 273 | say "--test-configurations ignored without --setting" 274 | return 275 | end 276 | 277 | all_configurations = target.build_configurations.map(&:name) 278 | test_configs = options.test_configurations == false ? [] : options.test_configurations 279 | loop do 280 | invalid_configurations = test_configs.reject { |c| all_configurations.include? c } 281 | @test_configurations = test_configs and return if invalid_configurations.empty? 282 | 283 | say "The following test configurations are invalid: #{invalid_configurations}." 284 | say "Available configurations: #{all_configurations}" 285 | test_configs = ask "Please enter a comma-separated list of configurations to use the Branch test key: ", Array 286 | end 287 | end 288 | end 289 | # rubocop: enable Metrics/ClassLength 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /lib/branch_io_cli/configuration/setup_options.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Configuration 3 | class SetupOptions 4 | class << self 5 | def available_options 6 | [ 7 | Option.new( 8 | name: :live_key, 9 | description: "Branch live key", 10 | example: "key_live_xxxx", 11 | type: String, 12 | aliases: "-L" 13 | ), 14 | Option.new( 15 | name: :test_key, 16 | description: "Branch test key", 17 | example: "key_test_yyyy", 18 | type: String, 19 | aliases: "-T" 20 | ), 21 | Option.new( 22 | name: :domains, 23 | description: "Comma-separated list of custom domain(s) or non-Branch domain(s)", 24 | example: "example.com,www.example.com", 25 | type: Array, 26 | aliases: "-D", 27 | confirm_symbol: :all_domains 28 | ), 29 | Option.new( 30 | name: :app_link_subdomain, 31 | description: "Branch app.link subdomain, e.g. myapp for myapp.app.link", 32 | example: "myapp", 33 | type: String, 34 | label: "app.link subdomain", 35 | skip_confirmation: true 36 | ), 37 | Option.new( 38 | name: :uri_scheme, 39 | description: "Custom URI scheme used in the Branch Dashboard for this app", 40 | example: "myurischeme[://]", 41 | type: String, 42 | aliases: "-U", 43 | label: "URI scheme", 44 | negatable: true, 45 | convert_proc: ->(value) { Configuration.uri_scheme_without_suffix(value) } 46 | ), 47 | Option.new( 48 | name: :setting, 49 | description: "Use a custom build setting for the Branch key (default: Use Info.plist)", 50 | example: "BRANCH_KEY_SETTING", 51 | type: String, 52 | argument_optional: true, 53 | aliases: "-s", 54 | label: "User-defined setting for Branch key" 55 | ), 56 | Option.new( 57 | name: :test_configurations, 58 | description: "List of configurations that use the test key with a user-defined setting (default: Debug configurations)", 59 | example: "config1,config2", 60 | type: Array, 61 | negatable: true, 62 | valid_values_proc: -> { Configuration.current && Configuration.current.xcodeproj.build_configurations.map(&:name) } 63 | ), 64 | Option.new( 65 | name: :xcodeproj, 66 | description: "Path to an Xcode project to update", 67 | example: "MyProject.xcodeproj", 68 | type: String, 69 | confirm_symbol: :xcodeproj_path, 70 | validate_proc: ->(path) { Configuration.open_xcodeproj path } 71 | ), 72 | Option.new( 73 | name: :target, 74 | description: "Name of a target to modify in the Xcode project", 75 | example: "MyAppTarget", 76 | type: String, 77 | confirm_symbol: :target_name, 78 | valid_values_proc: -> { Configuration.current && Configuration.current.xcodeproj.targets.map(&:name) } 79 | ), 80 | Option.new( 81 | name: :podfile, 82 | description: "Path to the Podfile for the project", 83 | example: "/path/to/Podfile", 84 | type: String, 85 | confirm_symbol: :podfile_path, 86 | validate_proc: ->(path) { Configuration.open_podfile path } 87 | ), 88 | Option.new( 89 | name: :cartfile, 90 | description: "Path to the Cartfile for the project", 91 | example: "/path/to/Cartfile", 92 | type: String, 93 | confirm_symbol: :cartfile_path, 94 | validate_proc: ->(path) { !path.nil? && File.exist?(path.to_s) }, 95 | convert_proc: ->(path) { Configuration.absolute_path(path.to_s) unless path.nil? } 96 | ), 97 | Option.new( 98 | name: :carthage_command, 99 | description: "Command to run when installing from Carthage", 100 | example: "bootstrap --no-use-binaries", 101 | type: String, 102 | default_value: "update --platform ios" 103 | ), 104 | Option.new( 105 | name: :frameworks, 106 | description: "Comma-separated list of system frameworks to add to the project", 107 | example: "AdSupport,CoreSpotlight,SafariServices", 108 | type: Array 109 | ), 110 | Option.new( 111 | name: :pod_repo_update, 112 | description: "Update the local podspec repo before installing", 113 | default_value: true 114 | ), 115 | Option.new( 116 | name: :validate, 117 | description: "Validate Universal Link configuration", 118 | default_value: true 119 | ), 120 | Option.new( 121 | name: :force, 122 | description: "Update project even if Universal Link validation fails", 123 | default_value: false 124 | ), 125 | Option.new( 126 | name: :add_sdk, 127 | description: "Add the Branch framework to the project", 128 | default_value: true 129 | ), 130 | Option.new( 131 | name: :patch_source, 132 | description: "Add Branch SDK calls to the AppDelegate", 133 | default_value: true 134 | ), 135 | Option.new( 136 | name: :commit, 137 | description: "Commit the results to Git if non-blank", 138 | type: String, 139 | example: "message", 140 | argument_optional: true, 141 | label: "Commit message" 142 | ) 143 | ] + Option.global_options 144 | end 145 | end 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/branch_io_cli/configuration/validate_configuration.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Configuration 3 | class ValidateConfiguration < Configuration 4 | class << self 5 | def summary 6 | "Validates all Universal Link domains configured in a project" 7 | end 8 | 9 | def return_value 10 | "If validation passes, this command returns 0. If validation fails, it returns 1." 11 | end 12 | 13 | def examples 14 | { 15 | "Ensure project has at least one correctly configured Branch key and domain" => "br validate", 16 | "Ensure project is correctly configured for certain Branch keys" => "br validate -L key_live_xxxx -T key_test_yyyy", 17 | "Ensure project is correctly configured to use specific domains" => "br validate -D myapp.app.link,myapp-alternate.app.link", 18 | "Validate only Universal Link configuration" => "br validate --universal-links-only" 19 | } 20 | end 21 | end 22 | 23 | def initialize(options) 24 | super 25 | @domains = options.domains 26 | end 27 | 28 | def validate_options 29 | validate_xcodeproj_path 30 | validate_target 31 | validate_keys optional: true 32 | end 33 | 34 | def log 35 | super 36 | say < #{env.display_path(xcodeproj_path)} 38 | <%= color('Target:', BOLD) %> #{target.name} 39 | <%= color('Target type:', BOLD) %> #{target.product_type} 40 | <%= color('Live key:', BOLD) %> #{keys[:live] || '(none)'} 41 | <%= color('Test key:', BOLD) %> #{keys[:test] || '(none)'} 42 | <%= color('Domains:', BOLD) %> #{domains || '(none)'} 43 | <%= color('Configurations:', BOLD) %> #{(configurations || xcodeproj.build_configurations.map(&:name)).join(',')} 44 | EOF 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/branch_io_cli/configuration/validate_options.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Configuration 3 | class ValidateOptions 4 | class << self 5 | def available_options 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 | ), 21 | Option.new( 22 | name: :domains, 23 | description: "Comma-separated list of domains expected to be configured in the project (Branch domains or non-Branch domains)", 24 | type: Array, 25 | example: "example.com,www.example.com", 26 | aliases: "-D", 27 | default_value: [] 28 | ), 29 | Option.new( 30 | name: :xcodeproj, 31 | description: "Path to an Xcode project to validate", 32 | type: String, 33 | example: "MyProject.xcodeproj" 34 | ), 35 | Option.new( 36 | name: :target, 37 | description: "Name of a target to validate in the Xcode project", 38 | type: String, 39 | example: "MyAppTarget" 40 | ), 41 | Option.new( 42 | name: :configurations, 43 | description: "Comma-separated list of configurations to validate (default: all)", 44 | type: Array, 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 51 | ) 52 | ] + Option.global_options 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/branch_io_cli/configuration/xcode_settings.rb: -------------------------------------------------------------------------------- 1 | require "open3" 2 | require "shellwords" 3 | require_relative "environment" 4 | 5 | module BranchIOCLI 6 | module Configuration 7 | class XcodeSettings 8 | class << self 9 | def all_valid? 10 | Configuration.current.configurations.map { |c| settings(c) }.all?(&:valid?) 11 | end 12 | 13 | def [](configuration) 14 | settings configuration 15 | end 16 | 17 | def settings(configuration = Configuration.current.configurations.first) 18 | return @settings[configuration] if @settings && @settings[configuration] 19 | @settings ||= {} 20 | 21 | @settings[configuration] = self.new configuration 22 | end 23 | 24 | def reset 25 | @settings = {} 26 | end 27 | end 28 | 29 | attr_reader :configuration 30 | 31 | def initialize(configuration) 32 | @configuration = configuration 33 | 34 | load_settings_from_xcode 35 | end 36 | 37 | def valid? 38 | @xcodebuild_showbuildsettings_status.success? 39 | end 40 | 41 | def config 42 | Configuration.current 43 | end 44 | 45 | def env 46 | Environment 47 | end 48 | 49 | def [](key) 50 | @xcode_settings[key] 51 | end 52 | 53 | def xcodebuild_cmd 54 | [ 55 | "xcodebuild", 56 | "-showBuildSettings", 57 | "-project", 58 | config.xcodeproj_path, 59 | "-target", 60 | config.target.name, 61 | "-sdk", 62 | config.sdk, 63 | "-configuration", 64 | configuration 65 | ].shelljoin 66 | end 67 | 68 | def load_settings_from_xcode 69 | @xcodebuild_showbuildsettings_output = "" 70 | @xcode_settings = {} 71 | Open3.popen2e(xcodebuild_cmd) do |stdin, output, thread| 72 | while (line = output.gets) 73 | @xcodebuild_showbuildsettings_output += env.obfuscate_user(line) 74 | line.strip! 75 | next unless (matches = /^(.+)\s+=\s+(.+)$/.match line) 76 | @xcode_settings[matches[1]] = matches[2] 77 | end 78 | @xcodebuild_showbuildsettings_status = thread.value 79 | end 80 | end 81 | 82 | def log_xcodebuild_showbuildsettings(report = STDOUT) 83 | if report == STDOUT 84 | say "<%= color('$ #{xcodebuild_cmd}', [MAGENTA, BOLD]) %>\n\n" 85 | else 86 | report.write "$ #{env.obfuscate_user(xcodebuild_cmd)}\n\n" 87 | end 88 | 89 | report.write @xcodebuild_showbuildsettings_output 90 | if valid? 91 | report.write "Success.\n\n" 92 | else 93 | report.write "#{@xcodebuild_showbuildsettings_status}.\n\n" 94 | end 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/branch_io_cli/core_ext.rb: -------------------------------------------------------------------------------- 1 | require_relative "core_ext/io.rb" 2 | require_relative "core_ext/regexp.rb" 3 | require_relative "core_ext/tty_platform.rb" 4 | require_relative "core_ext/xcodeproj.rb" 5 | -------------------------------------------------------------------------------- /lib/branch_io_cli/core_ext/io.rb: -------------------------------------------------------------------------------- 1 | require "open3" 2 | require "shellwords" 3 | require_relative "../configuration/environment" 4 | 5 | class IO 6 | # Report the command. Execute the command, capture stdout 7 | # and stderr and report line by line. Report the exit 8 | # status at the end in case of error. Returns a Process::Status 9 | # object. 10 | # 11 | # @param command a shell command to execute and report 12 | def sh(*args) 13 | write "$ #{IO.command_from_args(*args)}\n\n" 14 | 15 | obfuscate = args.last.delete(:obfuscate) if args.last.kind_of?(Hash) 16 | 17 | Open3.popen2e(*args) do |stdin, output, thread| 18 | # output is stdout and stderr merged 19 | output.each do |line| 20 | if obfuscate 21 | puts BranchIOCLI::Configuration::Environment.obfuscate_user(line) 22 | else 23 | puts line 24 | end 25 | end 26 | 27 | status = thread.value 28 | if status == 0 29 | write "Success.\n\n" 30 | else 31 | write "#{status}\n\n" 32 | end 33 | return status 34 | end 35 | end 36 | end 37 | 38 | # Report the command. Execute the command. Stdout and stderr are 39 | # not redirected. Report the exit status at the end if nonzero. 40 | # Returns a Process::Status object. 41 | # 42 | # @param command a shell command to execute and report 43 | def STDOUT.sh(*args) 44 | args.last.delete(:obfuscate) if args.last.kind_of?(Hash) 45 | 46 | # TODO: Improve this implementation? 47 | say "<%= color(%q{$ #{IO.command_from_args(*args)}}, [MAGENTA, BOLD]) %>\n\n" 48 | # May also write to stderr 49 | # Cannot obfuscate here. 50 | system(*args) 51 | 52 | status = $? 53 | if status == 0 54 | write "Success.\n\n" 55 | else 56 | write "#{status}\n\n" 57 | end 58 | status 59 | end 60 | 61 | def IO.command_from_args(*args) 62 | raise ArgumentError, "sh requires at least one argument" unless args.count > 0 63 | 64 | # Ignore any trailing options in the output 65 | if args.last.kind_of?(Hash) 66 | options = args.pop 67 | obfuscate = options[:obfuscate] 68 | end 69 | 70 | command = "" 71 | 72 | # Optional initial environment Hash 73 | if args.first.kind_of?(Hash) 74 | command = args.shift.map { |k, v| "#{k}=#{v.shellescape}" }.join(" ") + " " 75 | end 76 | 77 | # Support [ "/usr/local/bin/foo", "foo" ], "-x", ... 78 | if args.first.kind_of?(Array) 79 | command += args.shift.first.shellescape + " " + args.shelljoin 80 | command.chomp! " " 81 | elsif args.count == 1 && args.first.kind_of?(String) 82 | command += args.first 83 | else 84 | command += args.shelljoin 85 | end 86 | 87 | if obfuscate 88 | BranchIOCLI::Configuration::Environment.obfuscate_user(command) 89 | else 90 | command 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/branch_io_cli/core_ext/regexp.rb: -------------------------------------------------------------------------------- 1 | class Regexp 2 | def match_file(file) 3 | case file 4 | when File 5 | contents = file.read 6 | when String 7 | contents = File.read file 8 | else 9 | raise ArgumentError, "Invalid argument type: #{file.class.name}" 10 | end 11 | 12 | match contents 13 | end 14 | 15 | def match_file?(file) 16 | !match_file(file).nil? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/branch_io_cli/core_ext/tty_platform.rb: -------------------------------------------------------------------------------- 1 | require "tty/platform" 2 | 3 | module TTY 4 | class Platform 5 | def br_sierra? 6 | mac? && version.to_s == "16" 7 | end 8 | 9 | def br_high_sierra? 10 | mac? && version.to_s == "17" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/branch_io_cli/core_ext/xcodeproj.rb: -------------------------------------------------------------------------------- 1 | require "xcodeproj" 2 | 3 | module Xcodeproj 4 | class Project 5 | # Local override to allow for user schemes. 6 | # 7 | # Get list of shared and user schemes in project 8 | # 9 | # @param [String] path 10 | # project path 11 | # 12 | # @return [Array] 13 | # 14 | def self.schemes(project_path) 15 | base_dirs = [File.join(project_path, 'xcshareddata', 'xcschemes'), 16 | File.join(project_path, 'xcuserdata', "#{ENV['USER']}.xcuserdatad", 'xcschemes')] 17 | 18 | # Take any .xcscheme file from base_dirs 19 | schemes = base_dirs.inject([]) { |memo, dir| memo + Dir[File.join dir, '*.xcscheme'] } 20 | .map { |f| File.basename(f, '.xcscheme') } 21 | 22 | # Include any scheme defined in the xcschememanagement.plist, if it exists. 23 | base_dirs.map { |d| File.join d, 'xcschememanagement.plist' } 24 | .select { |f| File.exist? f }.each do |plist_path| 25 | plist = File.open(plist_path) { |f| ::Plist.parse_xml f } 26 | scheme_user_state = plist["SchemeUserState"] 27 | schemes += scheme_user_state.keys.map { |k| File.basename k, '.xcscheme' } 28 | end 29 | 30 | schemes.uniq! 31 | if schemes.empty? && File.exist?(project_path) 32 | # Open the project, get all targets. Add one scheme per target. 33 | project = self.open project_path 34 | schemes += project.targets.reject(&:test_target_type?).map(&:name) 35 | elsif schemes.empty? 36 | schemes << File.basename(project_path, '.xcodeproj') 37 | end 38 | schemes 39 | end 40 | 41 | module Object 42 | class PBXNativeTarget 43 | # List of build settings with values not present in the configuration. 44 | # 45 | # @return [Hash] A hash of fixed build settings 46 | def fixed_build_settings 47 | { 48 | "SRCROOT" => ".", 49 | "TARGET_NAME" => name 50 | } 51 | end 52 | 53 | # Layer on top of #resolved_build_setting to recursively expand all 54 | # build settings as they would be resolved in Xcode. Calls 55 | # #expand_build_settings on the value returned by 56 | # #resolved_build_setting, with the exception of anything defined 57 | # in #fixed_build_settings. Those settings are returned directly. 58 | # 59 | # @param setting_name [String] Name of any valid build setting for this target 60 | # @param configuration [String] Name of any valid configuration for this target 61 | # @return [String, nil] The build setting value with all embedded settings expanded or nil if not found 62 | def expanded_build_setting(setting_name, configuration) 63 | fixed_setting = fixed_build_settings[setting_name] 64 | return fixed_setting.clone if fixed_setting 65 | 66 | # second arg true means if there is an xcconfig, also consult that 67 | begin 68 | setting_value = resolved_build_setting(setting_name, true)[configuration] 69 | rescue Errno::ENOENT 70 | # If not found, look up without it. Unresolved settings will be passed 71 | # unmodified, e.g. $(UNRESOLVED_SETTING_NAME). 72 | setting_value = resolved_build_setting(setting_name, false)[configuration] 73 | end 74 | 75 | # TODO: What is the correct resolution order here? Which overrides which in 76 | # Xcode? Or does it matter here? 77 | if setting_value.nil? && defined?(BranchIOCLI::Configuration::XcodeSettings) 78 | setting_value = BranchIOCLI::Configuration::XcodeSettings[configuration][setting_name] 79 | end 80 | 81 | return nil if setting_value.nil? 82 | 83 | expand_build_settings setting_value, configuration 84 | end 85 | 86 | # Recursively resolves build settings in any string for the given 87 | # configuration. This includes xcconfig expansion and handling for the 88 | # :rfc1034identifier. Unresolved settings are passed unchanged, e.g. 89 | # $(UNRESOLVED_SETTING_NAME). 90 | # 91 | # @param string [String] Any string that may include build settings to be resolved 92 | # @param configuration [String] Name of any valid configuration for this target 93 | # @return [String] A copy of the original string with all embedded build settings expanded 94 | def expand_build_settings(string, configuration) 95 | return nil if string.nil? 96 | 97 | search_position = 0 98 | string = string.clone 99 | 100 | while (matches = /\$\(([^(){}]*)\)|\$\{([^(){}]*)\}/.match(string, search_position)) 101 | original_macro = matches[1] || matches[2] 102 | delimiter_length = 3 # $() or ${} 103 | delimiter_offset = 2 # $( or ${ 104 | search_position = string.index(original_macro) - delimiter_offset 105 | 106 | if (m = /^(.+):(.+)$/.match original_macro) 107 | macro_name = m[1] 108 | modifier = m[2] 109 | else 110 | macro_name = original_macro 111 | end 112 | 113 | expanded_macro = expanded_build_setting macro_name, configuration 114 | 115 | search_position += original_macro.length + delimiter_length and next if expanded_macro.nil? 116 | 117 | # From the Apple dev portal when creating a new app ID: 118 | # You cannot use special characters such as @, &, *, ', " 119 | # From trial and error with Xcode, it appears that only letters, digits and hyphens are allowed. 120 | # Everything else becomes a hyphen, including underscores. 121 | expanded_macro.gsub!(/[^A-Za-z0-9-]/, '-') if modifier == "rfc1034identifier" 122 | 123 | string.gsub!(/\$\(#{original_macro}\)|\$\{#{original_macro}\}/, expanded_macro) 124 | search_position += expanded_macro.length 125 | end 126 | 127 | # HACK: When matching against an xcconfig, as here, sometimes the macro is just returned 128 | # without delimiters as the entire string or as a path component, e.g. TARGET_NAME or 129 | # PROJECT_DIR/PROJECT_NAME/BridgingHeader.h. 130 | string = string.split("/").map do |component| 131 | next component unless component =~ /^[A-Z0-9_]+$/ 132 | expanded_build_setting(component, configuration) || component 133 | end.join("/") 134 | 135 | string 136 | end 137 | end 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /lib/branch_io_cli/format.rb: -------------------------------------------------------------------------------- 1 | require_relative "format/highline_format" 2 | require_relative "format/markdown_format" 3 | require_relative "format/shell_format" 4 | 5 | module BranchIOCLI 6 | module Format 7 | def render(template) 8 | path = File.expand_path(File.join("..", "..", "assets", "templates", "#{template}.erb"), __FILE__) 9 | ERB.new(File.read(path)).result binding 10 | end 11 | 12 | def option(opt) 13 | highlight "--#{opt.to_s.gsub(/_/, '-')}" 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/branch_io_cli/format/highline_format.rb: -------------------------------------------------------------------------------- 1 | require "erb" 2 | 3 | module BranchIOCLI 4 | module Format 5 | module HighlineFormat 6 | include Format 7 | 8 | def header(text, level = 1) 9 | highlight text 10 | end 11 | 12 | def highlight(text) 13 | "<%= color('#{text}', BOLD) %>" 14 | end 15 | 16 | def italics(text) 17 | highlight text 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/branch_io_cli/format/markdown_format.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Format 3 | module MarkdownFormat 4 | include Format 5 | 6 | def header(text, level = 1) 7 | "#" * level + " #{text}" 8 | end 9 | 10 | def highlight(text) 11 | "`#{text}`" 12 | end 13 | 14 | def italics(text) 15 | "_#{text}_" 16 | end 17 | 18 | def table_options 19 | @command.available_options.map { |o| table_option o }.join("\n") 20 | end 21 | 22 | def table_option(option) 23 | text = "|#{option.aliases.join(', ')}" 24 | text += ", " unless option.aliases.blank? 25 | 26 | text += "--" 27 | text += "[no-]" if option.negatable 28 | text += option.name.to_s.gsub(/_/, '-') 29 | 30 | if option.example 31 | text += " " 32 | text += "[" if option.argument_optional 33 | text += option.example 34 | text += "]" if option.argument_optional 35 | end 36 | 37 | text += "|#{option.description}" 38 | 39 | if option.type.nil? 40 | default_value = option.default_value ? "yes" : "no" 41 | else 42 | default_value = option.default_value 43 | end 44 | 45 | if default_value 46 | text += " (default: #{default_value})" 47 | end 48 | 49 | text += "|" 50 | text += option.env_name if option.env_name 51 | 52 | text += "|" 53 | text 54 | end 55 | 56 | def render_command(name) 57 | @command = BranchIOCLI::Command.const_get("#{name.to_s.capitalize}Command") 58 | render :command 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/branch_io_cli/format/shell_format.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Format 3 | module ShellFormat 4 | include Format 5 | 6 | def option(opt) 7 | o = @configuration.available_options.find { |o1| o1.name == opt.to_sym } 8 | 9 | cli_opt = opt.to_s.gsub(/_/, '-') 10 | 11 | all_opts = o.aliases || [] 12 | 13 | if o.nil? || o.default_value.nil? || o.default_value != true 14 | all_opts << "--#{cli_opt}" 15 | else 16 | all_opts << "--no-#{cli_opt}" 17 | end 18 | 19 | all_opts.join(" ") 20 | end 21 | 22 | def all_commands 23 | Dir[File.expand_path(File.join("..", "..", "command", "**_command.rb"), __FILE__)].map { |p| p.sub(%r{^.*/([a-z0-9_]+)_command.rb$}, '\1') } 24 | end 25 | 26 | def options_for_command(command) 27 | @configuration = BranchIOCLI::Configuration.const_get("#{command.capitalize}Configuration") 28 | @configuration.available_options.map { |o| option(o.name) }.join(" ") 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/branch_io_cli/helper.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper/branch_helper" 2 | require_relative "helper/methods" 3 | require_relative "helper/patch_helper" 4 | require_relative "helper/report_helper" 5 | require_relative "helper/task" 6 | require_relative "helper/tool_helper" 7 | require_relative "helper/util" 8 | -------------------------------------------------------------------------------- /lib/branch_io_cli/helper/android_helper.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Helper 3 | module AndroidHelper 4 | def add_keys_to_android_manifest(manifest, keys) 5 | add_metadata_to_manifest manifest, "io.branch.sdk.BranchKey", keys[:live] unless keys[:live].nil? 6 | add_metadata_to_manifest manifest, "io.branch.sdk.BranchKey.test", keys[:test] unless keys[:test].nil? 7 | end 8 | 9 | # TODO: Work on all XML/AndroidManifest formatting 10 | 11 | def add_metadata_to_manifest(manifest, key, value) 12 | element = manifest.elements["//manifest/application/meta-data[@android:name=\"#{key}\"]"] 13 | if element.nil? 14 | application = manifest.elements["//manifest/application"] 15 | application.add_element "meta-data", "android:name" => key, "android:value" => value 16 | else 17 | element.attributes["android:value"] = value 18 | end 19 | end 20 | 21 | def add_intent_filters_to_android_manifest(manifest, domains, uri_scheme, activity_name, remove_existing) 22 | if activity_name 23 | activity = manifest.elements["//manifest/application/activity[@android:name=\"#{activity_name}\""] 24 | else 25 | activity = find_activity manifest 26 | end 27 | 28 | raise "Failed to find an Activity in the Android manifest" if activity.nil? 29 | 30 | if remove_existing 31 | remove_existing_domains(activity) 32 | end 33 | 34 | add_intent_filter_to_activity activity, domains, uri_scheme 35 | end 36 | 37 | def find_activity(manifest) 38 | # try to infer the right activity 39 | # look for the first singleTask 40 | single_task_activity = manifest.elements["//manifest/application/activity[@android:launchMode=\"singleTask\"]"] 41 | return single_task_activity if single_task_activity 42 | 43 | # no singleTask activities. Take the first Activity 44 | # TODO: Add singleTask? 45 | manifest.elements["//manifest/application/activity"] 46 | end 47 | 48 | def add_intent_filter_to_activity(activity, domains, uri_scheme) 49 | # Add a single intent-filter with autoVerify and a data element for each domain and the optional uri_scheme 50 | intent_filter = REXML::Element.new "intent-filter" 51 | intent_filter.attributes["android:autoVerify"] = true 52 | intent_filter.add_element "action", "android:name" => "android.intent.action.VIEW" 53 | intent_filter.add_element "category", "android:name" => "android.intent.category.DEFAULT" 54 | intent_filter.add_element "category", "android:name" => "android.intent.category.BROWSABLE" 55 | intent_filter.elements << uri_scheme_data_element(uri_scheme) unless uri_scheme.nil? 56 | app_link_data_elements(domains).each { |e| intent_filter.elements << e } 57 | 58 | activity.add_element intent_filter 59 | end 60 | 61 | def remove_existing_domains(activity) 62 | # Find all intent-filters that include a data element with android:scheme 63 | # TODO: Can this be done with a single css/at_css call? 64 | activity.elements.each("//manifest//intent-filter") do |filter| 65 | filter.remove if filter.elements["data[@android:scheme]"] 66 | end 67 | end 68 | 69 | def app_link_data_elements(domains) 70 | domains.map do |domain| 71 | element = REXML::Element.new "data" 72 | element.attributes["android:scheme"] = "https" 73 | element.attributes["android:host"] = domain 74 | element 75 | end 76 | end 77 | 78 | def uri_scheme_data_element(uri_scheme) 79 | element = REXML::Element.new "data" 80 | element.attributes["android:scheme"] = uri_scheme 81 | element.attributes["android:host"] = "open" 82 | element 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/branch_io_cli/helper/branch_helper.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/hash" 2 | require_relative "android_helper" 3 | require_relative "ios_helper" 4 | require "net/http" 5 | require "pastel" 6 | require "set" 7 | require "tty/progressbar" 8 | require "tty/spinner" 9 | 10 | module BranchIOCLI 11 | module Helper 12 | class BranchHelper 13 | class << self 14 | attr_accessor :changes # An array of file paths (Strings) that were modified 15 | attr_accessor :errors # An array of error messages (Strings) from validation 16 | 17 | include AndroidHelper 18 | include IOSHelper 19 | 20 | def add_change(change) 21 | @changes ||= Set.new 22 | @changes << change.to_s 23 | end 24 | 25 | def fetch(url, spin: true) 26 | if spin 27 | @spinner = TTY::Spinner.new "[:spinner] GET #{url}.", format: :flip 28 | @spinner.auto_spin 29 | end 30 | 31 | response = Net::HTTP.get_response URI(url) 32 | 33 | case response 34 | when Net::HTTPSuccess 35 | @spinner.success "#{response.code} #{response.message}" if @spinner 36 | @spinner = nil 37 | response.body 38 | when Net::HTTPRedirection 39 | fetch response['location'], spin: false 40 | else 41 | @spinner.error "#{response.code} #{response.message}" if @spinner 42 | @spinner = nil 43 | raise "Error fetching #{url}: #{response.code} #{response.message}" 44 | end 45 | end 46 | 47 | def download(url, dest, size: nil) 48 | uri = URI(url) 49 | 50 | Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http| 51 | request = Net::HTTP::Get.new uri 52 | 53 | http.request request do |response| 54 | case response 55 | when Net::HTTPSuccess 56 | bytes_downloaded = 0 57 | if size 58 | pastel = Pastel.new 59 | green = pastel.on_green " " 60 | yellow = pastel.on_yellow " " 61 | progress = TTY::ProgressBar.new "[:bar] :percent (:eta)", total: 50, complete: green, incomplete: yellow 62 | end 63 | 64 | File.open dest, 'w' do |io| 65 | response.read_body do |chunk| 66 | io.write chunk 67 | 68 | # print progress 69 | bytes_downloaded += chunk.length 70 | progress.ratio = bytes_downloaded.to_f / size.to_f if size 71 | end 72 | end 73 | progress.finish if size 74 | when Net::HTTPRedirection 75 | download response['location'], dest, size: size 76 | else 77 | raise "Error downloading #{url}: #{response.code} #{response.message}" 78 | end 79 | end 80 | end 81 | end 82 | 83 | def domains(apps) 84 | apps.inject Set.new do |result, k, v| 85 | next result unless v 86 | result + v.domains 87 | end 88 | end 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/branch_io_cli/helper/methods.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Helper 3 | class CommandError < RuntimeError 4 | attr_reader :status 5 | def initialize(args) 6 | @args = args[0] 7 | @status = args[1] 8 | super message 9 | end 10 | 11 | def message 12 | if @args.count == 1 13 | return @args.first.shelljoin if @args.first.kind_of?(Array) 14 | return @args.first.to_s 15 | else 16 | return @args.shelljoin 17 | end 18 | end 19 | end 20 | 21 | module Methods 22 | # Execute a shell command with reporting. 23 | # The command itself is logged, then output from 24 | # both stdout and stderr, then a success or failure 25 | # message. Raises CommandError on error. No redirection occurs. 26 | # 27 | # @param command [String, Array] A shell command to execute 28 | def sh(*command) 29 | status = STDOUT.sh(*command) 30 | raise CommandError, [command, status] unless status.success? 31 | end 32 | 33 | # Clear the screen and move the cursor to the top using highline 34 | def clear 35 | say "\e[2J\e[H" 36 | end 37 | 38 | # Ask a yes/no question with a default 39 | def confirm(question, default_value) 40 | yn_opts = default_value ? "Y/n" : "y/N" 41 | value = ask "#{question} (#{yn_opts}) ", nil 42 | 43 | # Convert to true/false 44 | dummy_option = Configuration::Option.new({}) 45 | value = dummy_option.convert(value) 46 | 47 | return default_value if value.nil? || value.kind_of?(String) 48 | value 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/branch_io_cli/helper/patch_helper.rb: -------------------------------------------------------------------------------- 1 | require "pattern_patch" 2 | 3 | module BranchIOCLI 4 | module Helper 5 | class PatchHelper 6 | # Adds patch_dir class attr and patch class method 7 | extend PatternPatch::Methods 8 | 9 | # Set the patch_dir for PatternPatch 10 | self.patch_dir = File.expand_path(File.join('..', '..', '..', 'assets', 'patches'), __FILE__) 11 | self.trim_mode = "<>" 12 | 13 | class << self 14 | def config 15 | Configuration::Configuration.current 16 | end 17 | 18 | def helper 19 | BranchHelper 20 | end 21 | 22 | def add_change(change) 23 | helper.add_change change 24 | end 25 | 26 | def use_conditional_test_key? 27 | config.keys.count > 1 && config.setting.nil? && !helper.has_multiple_info_plists? 28 | end 29 | 30 | def swift_file_includes_branch?(path) 31 | # Can't just check for the import here, since there may be a bridging header. 32 | # This may match branch.initSession (if the Branch instance is stored) or 33 | # Branch.getInstance().initSession, etc. 34 | /branch.*initsession|^\s*import\s+branch/i.match_file? path 35 | end 36 | 37 | def patch_bridging_header 38 | unless config.bridging_header_path 39 | say "Modules not available and bridging header not found. Cannot import Branch." 40 | say "Please add use_frameworks! to your Podfile and/or enable modules in your project or use --no-patch-source." 41 | exit(-1) 42 | end 43 | 44 | begin 45 | bridging_header = File.read config.bridging_header_path 46 | return false if bridging_header =~ %r{^\s+#import\s+|^\s+@import\s+Branch\s*;} 47 | rescue RuntimeError => e 48 | say e.message 49 | say "Cannot read #{config.bridging_header_path}." 50 | say "Please correct this setting or use --no-patch-source." 51 | exit(-1) 52 | end 53 | 54 | say "Patching #{config.bridging_header_path}" 55 | 56 | if /^\s*(#import|#include|@import)/.match_file? config.bridging_header_path 57 | # Add among other imports 58 | patch(:objc_import).apply config.bridging_header_path 59 | elsif /\n\s*#ifndef\s+(\w+).*\n\s*#define\s+\1.*?\n/m.match_file? config.bridging_header_path 60 | # Has an include guard. Add inside. 61 | patch(:objc_import_include_guard).apply config.bridging_header_path 62 | else 63 | # No imports, no include guard. Add at the end. 64 | patch(:objc_import_at_end).apply config.bridging_header_path 65 | end 66 | helper.add_change config.bridging_header_path 67 | end 68 | 69 | def patch_app_delegate_swift(project) 70 | return false unless config.patch_source 71 | app_delegate_swift_path = config.app_delegate_swift_path 72 | 73 | return false if app_delegate_swift_path.nil? || 74 | swift_file_includes_branch?(app_delegate_swift_path) 75 | 76 | say "Patching #{app_delegate_swift_path}" 77 | 78 | unless config.bridging_header_required? 79 | patch(:swift_import).apply app_delegate_swift_path 80 | end 81 | 82 | patch_did_finish_launching_method_swift app_delegate_swift_path 83 | patch_continue_user_activity_method_swift app_delegate_swift_path 84 | patch_open_url_method_swift app_delegate_swift_path 85 | 86 | add_change app_delegate_swift_path 87 | true 88 | end 89 | 90 | def patch_app_delegate_objc(project) 91 | return false unless config.patch_source 92 | app_delegate_objc_path = config.app_delegate_objc_path 93 | 94 | return false unless app_delegate_objc_path 95 | 96 | app_delegate = File.read app_delegate_objc_path 97 | return false if app_delegate =~ %r{^\s+#import\s+|^\s+@import\s+Branch\s*;} 98 | 99 | say "Patching #{app_delegate_objc_path}" 100 | 101 | patch(:objc_import).apply app_delegate_objc_path 102 | 103 | patch_did_finish_launching_method_objc app_delegate_objc_path 104 | patch_continue_user_activity_method_objc app_delegate_objc_path 105 | patch_open_url_method_objc app_delegate_objc_path 106 | 107 | add_change app_delegate_objc_path 108 | true 109 | end 110 | 111 | def patch_did_finish_launching_method_swift(app_delegate_swift_path) 112 | app_delegate_swift = File.read app_delegate_swift_path 113 | 114 | is_new_method = app_delegate_swift !~ /didFinishLaunching[^\n]+?\{/m 115 | if is_new_method 116 | patch_name = :did_finish_launching_new_swift 117 | else 118 | patch_name = :did_finish_launching_swift 119 | end 120 | patch(patch_name).apply app_delegate_swift_path, binding: binding 121 | end 122 | 123 | def patch_did_finish_launching_method_objc(app_delegate_objc_path) 124 | app_delegate_objc = File.read app_delegate_objc_path 125 | 126 | is_new_method = app_delegate_objc !~ /didFinishLaunchingWithOptions/m 127 | if is_new_method 128 | patch_name = :did_finish_launching_new_objc 129 | else 130 | patch_name = :did_finish_launching_objc 131 | end 132 | patch(patch_name).apply app_delegate_objc_path, binding: binding 133 | end 134 | 135 | def patch_open_url_method_swift(app_delegate_swift_path) 136 | app_delegate_swift = File.read app_delegate_swift_path 137 | 138 | if app_delegate_swift =~ /application.*open\s+url.*options/ 139 | # Has application:openURL:options: 140 | patch_name = :open_url_swift 141 | elsif app_delegate_swift =~ /application.*open\s+url.*sourceApplication/ 142 | # Has application:openURL:sourceApplication:annotation: 143 | # TODO: This method is deprecated. 144 | patch_name = :open_url_source_application_swift 145 | else 146 | # Has neither 147 | patch_name = :open_url_new_swift 148 | end 149 | patch(patch_name).apply app_delegate_swift_path 150 | end 151 | 152 | def patch_continue_user_activity_method_swift(app_delegate_swift_path) 153 | app_delegate_swift = File.read app_delegate_swift_path 154 | 155 | if app_delegate_swift =~ /application:.*continue userActivity:.*restorationHandler:/ 156 | patch_name = :continue_user_activity_swift 157 | else 158 | patch_name = :continue_user_activity_new_swift 159 | end 160 | patch(patch_name).apply app_delegate_swift_path 161 | end 162 | 163 | def patch_open_url_method_objc(app_delegate_objc_path) 164 | app_delegate_objc = File.read app_delegate_objc_path 165 | 166 | if app_delegate_objc =~ /application:.*openURL:.*options/ 167 | # Has application:openURL:options: 168 | patch_name = :open_url_objc 169 | elsif app_delegate_objc =~ /application:.*openURL:.*sourceApplication/ 170 | # Has application:openURL:sourceApplication:annotation: 171 | patch_name = :open_url_source_annotation_objc 172 | # TODO: This method is deprecated. 173 | else 174 | # Has neither 175 | patch_name = :open_url_new_objc 176 | end 177 | patch(patch_name).apply app_delegate_objc_path 178 | end 179 | 180 | def patch_continue_user_activity_method_objc(app_delegate_objc_path) 181 | app_delegate_swift = File.read app_delegate_objc_path 182 | 183 | if app_delegate_swift =~ /application:.*continueUserActivity:.*restorationHandler:/ 184 | patch_name = :continue_user_activity_objc 185 | else 186 | patch_name = :continue_user_activity_new_objc 187 | end 188 | patch(patch_name).apply app_delegate_objc_path 189 | end 190 | 191 | def patch_messages_view_controller 192 | path = config.messages_view_controller_path 193 | 194 | patch_name = "messages_did_become_active_" 195 | case path 196 | when nil 197 | return false 198 | when /\.swift$/ 199 | return false if swift_file_includes_branch?(path) 200 | 201 | unless config.bridging_header_required? 202 | patch(:swift_import).apply path 203 | end 204 | 205 | is_new_method = !/didBecomeActive\(with.*?\{[^\n]*\n/m.match_file?(path) 206 | patch_name += "#{is_new_method ? 'new_' : ''}swift" 207 | else 208 | return false if %r{^\s+#import\s+|^\s+@import\s+Branch\s*;}.match_file?(path) 209 | 210 | patch(:objc_import).apply path 211 | 212 | is_new_method = !/didBecomeActiveWithConversation.*?\{[^\n]*\n/m.match_file?(path) 213 | patch_name += "#{is_new_method ? 'new_' : ''}objc" 214 | end 215 | 216 | say "Patching #{path}" 217 | 218 | patch(patch_name).apply path, binding: binding 219 | 220 | helper.add_change(path) 221 | true 222 | end 223 | 224 | def patch_podfile(podfile_path) 225 | target_definition = config.podfile.target_definitions[config.target.name] 226 | raise "Target #{config.target.name} not found in Podfile" unless target_definition 227 | 228 | # Podfile already contains the Branch pod, possibly just a subspec 229 | return false if target_definition.dependencies.any? { |d| d.name =~ %r{^(Branch|Branch-SDK)(/.*)?$} } 230 | 231 | say "Adding pod \"Branch\" to #{podfile_path}" 232 | 233 | # It may not be clear from the Pod::Podfile whether the target has a do block. 234 | # It doesn't seem to be possible to update the Podfile object and write it out. 235 | # So we patch. 236 | podfile = File.read config.podfile_path 237 | 238 | if podfile =~ /target\s+(["'])#{config.target.name}\1\s+do.*?\n/m 239 | # if there is a target block for this target: 240 | patch = PatternPatch::Patch.new( 241 | regexp: /\n(\s*)target\s+(["'])#{config.target.name}\2\s+do.*?\n/m, 242 | text: "\\1 pod \"Branch\"\n", 243 | mode: :append 244 | ) 245 | else 246 | # add to the abstract_target for this target 247 | patch = PatternPatch::Patch.new( 248 | regexp: /^(\s*)target\s+["']#{config.target.name}/, 249 | text: "\\1pod \"Branch\"\n", 250 | mode: :prepend 251 | ) 252 | end 253 | patch.apply podfile_path 254 | 255 | true 256 | end 257 | 258 | def patch_cartfile(cartfile_path) 259 | cartfile = File.read cartfile_path 260 | 261 | # Cartfile already contains the Branch framework 262 | return false if cartfile =~ /git.+Branch/ 263 | 264 | say "Adding \"Branch\" to #{cartfile_path}" 265 | 266 | patch(:cartfile).apply cartfile_path 267 | 268 | true 269 | end 270 | 271 | def patch_source(xcodeproj) 272 | # Patch the bridging header any time Swift imports are not available, 273 | # to make Branch available throughout the app, whether the AppDelegate 274 | # is in Swift or Objective-C. 275 | patch_bridging_header if config.bridging_header_required? 276 | patch_app_delegate_swift(xcodeproj) || patch_app_delegate_objc(xcodeproj) 277 | patch_messages_view_controller 278 | end 279 | end 280 | end 281 | end 282 | end 283 | -------------------------------------------------------------------------------- /lib/branch_io_cli/helper/report_helper.rb: -------------------------------------------------------------------------------- 1 | require "plist" 2 | require "shellwords" 3 | require_relative "../configuration/configuration" 4 | 5 | module BranchIOCLI 6 | module Helper 7 | class ReportHelper 8 | class << self 9 | include Methods 10 | 11 | def report_imports 12 | report = "Branch imports:\n" 13 | config.branch_imports.each_key do |path| 14 | report += " #{config.relative_path path}:\n" 15 | report += " #{config.branch_imports[path].join("\n ")}" 16 | report += "\n" 17 | end 18 | report 19 | end 20 | 21 | def config 22 | Configuration::Configuration.current 23 | end 24 | 25 | def env 26 | Configuration::Environment 27 | end 28 | 29 | def helper 30 | BranchHelper 31 | end 32 | 33 | def xcode_settings 34 | Configuration::XcodeSettings.settings 35 | end 36 | 37 | def base_xcodebuild_cmd 38 | if config.workspace_path 39 | ["xcodebuild", "-workspace", config.workspace_path] 40 | else 41 | ["xcodebuild", "-project", config.xcodeproj_path] 42 | end 43 | end 44 | 45 | def report_scheme 46 | report = "\nScheme #{config.scheme}:\n" 47 | report += " Configurations:\n" 48 | report += " #{config.configurations_from_scheme.join("\n ")}\n" 49 | report 50 | end 51 | 52 | # rubocop: disable Metrics/PerceivedComplexity 53 | def report_header 54 | header = "cocoapods-core: #{Pod::CORE_VERSION}\n" 55 | 56 | header += `xcodebuild -version` 57 | header += "SDK: #{xcode_settings['SDK_NAME']}\n" if xcode_settings 58 | 59 | header += report_scheme 60 | 61 | configuration = config.configuration || config.configurations_from_scheme.first 62 | configurations = config.configuration ? [config.configuration] : config.configurations_from_scheme 63 | 64 | bundle_identifier = config.target.expanded_build_setting "PRODUCT_BUNDLE_IDENTIFIER", configuration 65 | dev_team = config.target.expanded_build_setting "DEVELOPMENT_TEAM", configuration 66 | 67 | header += "\nTarget #{config.target.name}:\n" 68 | header += " Bundle identifier: #{bundle_identifier || '(none)'}\n" 69 | header += " Development team: #{dev_team || '(none)'}\n" 70 | header += " Deployment target: #{config.target.deployment_target}\n" 71 | header += " Modules #{config.modules_enabled? ? '' : 'not '}enabled\n" 72 | header += " Swift #{config.swift_version}\n" if config.swift_version 73 | header += " Bridging header: #{config.relative_path(config.bridging_header_path)}\n" if config.bridging_header_path 74 | 75 | header += " Info.plist\n" 76 | configurations.each do |c| 77 | header += " #{c}: #{config.target.expanded_build_setting 'INFOPLIST_FILE', c}\n" 78 | end 79 | 80 | header += " Entitlements file\n" 81 | configurations.each do |c| 82 | header += " #{c}: #{config.target.expanded_build_setting 'CODE_SIGN_ENTITLEMENTS', c}\n" 83 | end 84 | 85 | if config.podfile_path 86 | begin 87 | cocoapods_version = `pod --version`.chomp 88 | rescue Errno::ENOENT 89 | header += "\n(pod command not found)\n" 90 | end 91 | 92 | if File.exist?("#{config.podfile_path}.lock") 93 | podfile_lock = Pod::Lockfile.from_file Pathname.new "#{config.podfile_path}.lock" 94 | end 95 | 96 | if cocoapods_version || podfile_lock 97 | header += "\nUsing CocoaPods v. " 98 | if cocoapods_version 99 | header += "#{cocoapods_version} (CLI) " 100 | end 101 | if podfile_lock 102 | header += "#{podfile_lock.cocoapods_version} (Podfile.lock)" 103 | end 104 | header += "\n" 105 | end 106 | 107 | target_definition = config.podfile.target_definitions[config.target.name] 108 | if target_definition 109 | branch_deps = target_definition.dependencies.select { |p| p.name =~ %r{^(Branch|Branch-SDK)(/.*)?$} } 110 | header += "Podfile target #{target_definition.name}:" 111 | header += "\n use_frameworks!" if target_definition.uses_frameworks? 112 | header += "\n platform: #{target_definition.platform}" 113 | header += "\n build configurations: #{target_definition.build_configurations}" 114 | header += "\n inheritance: #{target_definition.inheritance}" 115 | branch_deps.each do |dep| 116 | header += "\n pod '#{dep.name}', '#{dep.requirement}'" 117 | header += ", #{dep.external_source}" if dep.external_source 118 | header += "\n" 119 | end 120 | else 121 | header += "Target #{config.target.name.inspect} not found in Podfile.\n" 122 | end 123 | 124 | header += "\npod install #{config.pod_install_required? ? '' : 'not '}required.\n" 125 | end 126 | 127 | if config.cartfile_path 128 | begin 129 | carthage_version = `carthage version`.chomp 130 | header += "\nUsing Carthage v. #{carthage_version}\n" 131 | rescue Errno::ENOENT 132 | header += "\n(carthage command not found)\n" 133 | end 134 | end 135 | 136 | cartfile_requirement = config.requirement_from_cartfile 137 | header += "\nFrom Cartfile:\n#{cartfile_requirement}\n" if cartfile_requirement 138 | 139 | version = config.branch_version 140 | if version 141 | header += "\nBranch SDK v. #{version}\n" 142 | else 143 | header += "\nBranch SDK not found.\n" 144 | end 145 | 146 | header += "\n#{report_branch}" 147 | 148 | header 149 | end 150 | # rubocop: enable Metrics/PerceivedComplexity 151 | 152 | # String containing information relevant to Branch setup 153 | def report_branch 154 | report = "Branch configuration:\n" 155 | 156 | configurations = config.configuration ? [config.configuration] : config.configurations_from_scheme 157 | 158 | configurations.each do |configuration| 159 | report += " #{configuration}:\n" 160 | infoplist_path = config.target.expanded_build_setting "INFOPLIST_FILE", configuration 161 | infoplist_path = File.expand_path infoplist_path, File.dirname(config.xcodeproj_path) 162 | 163 | begin 164 | info_plist = File.open(infoplist_path) { |f| Plist.parse_xml f } 165 | branch_key = info_plist["branch_key"] 166 | if config.branch_key_setting_from_info_plist(configuration) 167 | annotation = "[#{File.basename infoplist_path}:$(#{config.branch_key_setting_from_info_plist})]" 168 | else 169 | annotation = "(#{File.basename infoplist_path})" 170 | end 171 | 172 | report += " Branch key(s) #{annotation}:\n" 173 | if branch_key.kind_of? Hash 174 | branch_key.each_key do |key| 175 | resolved_key = config.target.expand_build_settings branch_key[key], configuration 176 | report += " #{key.capitalize}: #{resolved_key}\n" 177 | end 178 | elsif branch_key 179 | resolved_key = config.target.expand_build_settings branch_key, configuration 180 | report += " #{resolved_key}\n" 181 | else 182 | report += " (none found)\n" 183 | end 184 | 185 | branch_universal_link_domains = info_plist["branch_universal_link_domains"] 186 | if branch_universal_link_domains 187 | if branch_universal_link_domains.kind_of? Array 188 | report += " branch_universal_link_domains (Info.plist):\n" 189 | branch_universal_link_domains.each do |domain| 190 | report += " #{domain}\n" 191 | end 192 | else 193 | report += " branch_universal_link_domains (Info.plist): #{branch_universal_link_domains}\n" 194 | end 195 | end 196 | rescue StandardError => e 197 | report += " (Failed to open Info.plist: #{e.message})\n" 198 | end 199 | end 200 | 201 | unless config.target.extension_target_type? 202 | begin 203 | configurations = config.configuration ? [config.configuration] : config.configurations_from_scheme 204 | configurations.each do |configuration| 205 | domains = helper.domains_from_project configuration 206 | report += " Universal Link domains (entitlements:#{configuration}):\n" 207 | domains.each do |domain| 208 | report += " #{domain}\n" 209 | end 210 | end 211 | rescue StandardError => e 212 | report += " (Failed to get Universal Link domains from entitlements file: #{e.message})\n" 213 | end 214 | end 215 | 216 | report += report_imports 217 | 218 | report 219 | end 220 | end 221 | end 222 | end 223 | end 224 | -------------------------------------------------------------------------------- /lib/branch_io_cli/helper/task.rb: -------------------------------------------------------------------------------- 1 | require "tty/spinner" 2 | 3 | module BranchIOCLI 4 | module Helper 5 | class Task 6 | def initialize(use_spinner: true) 7 | @use_spinner = use_spinner 8 | end 9 | 10 | def use_spinner? 11 | @use_spinner 12 | end 13 | 14 | def begin(message) 15 | if use_spinner? 16 | @spinner = TTY::Spinner.new "[:spinner] #{message}", format: :flip 17 | @spinner.auto_spin 18 | end 19 | end 20 | 21 | def success(message) 22 | if use_spinner? 23 | @spinner.success message 24 | end 25 | end 26 | 27 | def error(message) 28 | if use_spinner? 29 | @spinner.error message 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/branch_io_cli/helper/util.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | module Helper 3 | class Util 4 | extend Methods 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/branch_io_cli/rake_task.rb: -------------------------------------------------------------------------------- 1 | require "rake" 2 | require "rake/tasklib" 3 | require_relative "../branch_io_cli" 4 | require "highline/import" 5 | 6 | module BranchIOCLI 7 | class RakeTask < Rake::TaskLib 8 | def initialize(name = :branch) 9 | namespace name do 10 | add_branch_task :report, "Generate a brief Branch report" 11 | add_branch_task :setup, "Set a project up with the Branch SDK" 12 | add_branch_task :validate, "Validate Universal Links in one or more projects" 13 | end 14 | end 15 | 16 | def add_branch_task(task_name, description) 17 | command_class = Command.const_get("#{task_name.to_s.capitalize}Command") 18 | configuration_class = Configuration.const_get("#{task_name.to_s.capitalize}Configuration") 19 | 20 | desc description 21 | task task_name, %i{paths options} do |task, args| 22 | paths = args[:paths] 23 | paths = [paths] unless paths.respond_to?(:each) 24 | options = args[:options] || {} 25 | 26 | paths.each do |path| 27 | Dir.chdir(path) do 28 | begin 29 | command_class.new(configuration_class.wrapper(options)).run! 30 | rescue StandardError => e 31 | say "Error from #{task_name} task in #{path}: #{e.message}" 32 | say e.backtrace if options[:trace] 33 | end 34 | end 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/branch_io_cli/version.rb: -------------------------------------------------------------------------------- 1 | module BranchIOCLI 2 | VERSION = "0.13.2" 3 | end 4 | -------------------------------------------------------------------------------- /spec/io_ext_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'IO extension' do 2 | describe 'command_from_args' do 3 | it 'returns the string when a string is passed' do 4 | command = IO.command_from_args "git commit -m 'A message'" 5 | expect(command).to eq "git commit -m 'A message'" 6 | end 7 | 8 | it 'raises when no argument passed' do 9 | expect do 10 | IO.command_from_args 11 | end.to raise_error ArgumentError 12 | end 13 | 14 | it 'ignores any trailing options hash' do 15 | command = IO.command_from_args "git commit -m 'A message'", chdir: "/tmp" 16 | expect(command).to eq "git commit -m 'A message'" 17 | end 18 | 19 | it 'shelljoins multiple args' do 20 | command = IO.command_from_args "git", "commit", "-m", "A message" 21 | expect(command).to eq 'git commit -m A\ message' 22 | end 23 | 24 | it 'adds an environment Hash at the beginning' do 25 | command = IO.command_from_args({ "PATH" => "/usr/local/bin" }, "git", "commit", "-m", "A message") 26 | expect(command).to eq 'PATH=/usr/local/bin git commit -m A\ message' 27 | end 28 | 29 | it 'shell-escapes environment variable values' do 30 | command = IO.command_from_args({ "PATH" => "/usr/my local/bin" }, "git", "commit", "-m", "A message") 31 | expect(command).to eq 'PATH=/usr/my\ local/bin git commit -m A\ message' 32 | end 33 | 34 | it 'recognizes an array as the only element of a command' do 35 | command = IO.command_from_args ["/usr/local/bin/git", "git"] 36 | expect(command).to eq "/usr/local/bin/git" 37 | end 38 | 39 | it 'recognizes an array as the first element of a command' do 40 | command = IO.command_from_args ["/usr/local/bin/git", "git"], "commit", "-m", "A message" 41 | expect(command).to eq '/usr/local/bin/git commit -m A\ message' 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/ios_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require "net/http" 2 | require "openssl" 3 | 4 | class ModuleInstance 5 | class << self 6 | attr_accessor :errors 7 | include BranchIOCLI::Helper::IOSHelper 8 | end 9 | end 10 | 11 | describe BranchIOCLI::Helper::IOSHelper do 12 | let (:instance) { ModuleInstance } 13 | 14 | before :each do 15 | instance.errors = [] 16 | instance.reset_aasa_cache 17 | end 18 | 19 | describe "constants" do 20 | it "defines APPLINKS" do 21 | expect(BranchIOCLI::Helper::IOSHelper::APPLINKS).to eq "applinks" 22 | end 23 | 24 | it "defines ASSOCIATED_DOMAINS" do 25 | expect(BranchIOCLI::Helper::IOSHelper::ASSOCIATED_DOMAINS).to eq "com.apple.developer.associated-domains" 26 | end 27 | 28 | it "defines CODE_SIGN_ENTITLEMENTS" do 29 | expect(BranchIOCLI::Helper::IOSHelper::CODE_SIGN_ENTITLEMENTS).to eq "CODE_SIGN_ENTITLEMENTS" 30 | end 31 | 32 | it "defines DEVELOPMENT_TEAM" do 33 | expect(BranchIOCLI::Helper::IOSHelper::DEVELOPMENT_TEAM).to eq "DEVELOPMENT_TEAM" 34 | end 35 | 36 | it "defines PRODUCT_BUNDLE_IDENTIFIER" do 37 | expect(BranchIOCLI::Helper::IOSHelper::PRODUCT_BUNDLE_IDENTIFIER).to eq "PRODUCT_BUNDLE_IDENTIFIER" 38 | end 39 | 40 | it "defines RELEASE_CONFIGURATION" do 41 | expect(BranchIOCLI::Helper::IOSHelper::RELEASE_CONFIGURATION).to eq "Release" 42 | end 43 | end 44 | 45 | describe "#app_ids_from_aasa_file" do 46 | it "parses the contents of an apple-app-site-assocation file" do 47 | mock_response = '{"applinks":{"apps":[],"details":[{"appID":"XYZPDQ.com.example.MyApp","paths":["NOT /e/*","*","/"]}]}}' 48 | 49 | expect(instance).to receive(:contents_of_aasa_file).with("myapp.app.link") { mock_response } 50 | 51 | expect(instance.app_ids_from_aasa_file("myapp.app.link")).to eq %w{XYZPDQ.com.example.MyApp} 52 | expect(instance.errors).to be_empty 53 | end 54 | 55 | it "raises if the file cannot be retrieved" do 56 | expect(instance).to receive(:contents_of_aasa_file).and_raise RuntimeError 57 | 58 | expect do 59 | instance.app_ids_from_aasa_file("myapp.app.link") 60 | end.to raise_error RuntimeError 61 | end 62 | 63 | it "returns nil in case of unparseable JSON" do 64 | # return value missing final } 65 | mock_response = '{"applinks":{"apps":[],"details":[{"appID":"XYZPDQ.com.example.MyApp","paths":["NOT /e/*","*","/"]}]}' 66 | expect(instance).to receive(:contents_of_aasa_file).with("myapp.app.link") { mock_response } 67 | 68 | expect(instance.app_ids_from_aasa_file("myapp.app.link")).to be_nil 69 | expect(instance.errors).not_to be_empty 70 | end 71 | 72 | it "returns nil if no applinks found in file" do 73 | mock_response = '{"webcredentials": {}}' 74 | expect(instance).to receive(:contents_of_aasa_file).with("myapp.app.link") { mock_response } 75 | 76 | expect(instance.app_ids_from_aasa_file("myapp.app.link")).to be_nil 77 | expect(instance.errors).not_to be_empty 78 | end 79 | 80 | it "returns nil if no details found for applinks" do 81 | mock_response = '{"applinks": {}}' 82 | expect(instance).to receive(:contents_of_aasa_file).with("myapp.app.link") { mock_response } 83 | 84 | expect(instance.app_ids_from_aasa_file("myapp.app.link")).to be_nil 85 | expect(instance.errors).not_to be_empty 86 | end 87 | 88 | it "returns nil if no appIDs found in file" do 89 | mock_response = '{"applinks":{"apps":[],"details":[]}}' 90 | expect(instance).to receive(:contents_of_aasa_file).with("myapp.app.link") { mock_response } 91 | 92 | expect(instance.app_ids_from_aasa_file("myapp.app.link")).to be_nil 93 | expect(instance.errors).not_to be_empty 94 | end 95 | end 96 | 97 | describe "#contents_of_aasa_file" do 98 | it "returns the contents of an unsigned AASA file" do 99 | mock_contents = "{}" 100 | mock_response = double "response", body: mock_contents, code: "200", message: "OK" 101 | expect(mock_response).to receive(:[]).with("Content-type") { "application/json" } 102 | 103 | mock_http_request mock_response 104 | 105 | expect(instance.contents_of_aasa_file("myapp.app.link")).to eq mock_contents 106 | end 107 | 108 | it "returns the contents of a signed AASA file" do 109 | mock_contents = "{}" 110 | mock_response = double "response", code: "200", message: "OK", body: "" 111 | expect(mock_response).to receive(:[]).with("Content-type") { "application/pkcs7-mime" } 112 | 113 | mock_signature = double "signature", data: mock_contents 114 | # just ensure verify doesn't raise 115 | expect(mock_signature).to receive(:verify) 116 | # and return the mock_contents as signature.data 117 | expect(OpenSSL::PKCS7).to receive(:new) { mock_signature } 118 | 119 | mock_http_request mock_response 120 | 121 | expect(instance.contents_of_aasa_file("myapp.app.link")).to eq mock_contents 122 | end 123 | 124 | it "returns nil if the file cannot be retrieved" do 125 | mock_response = double "response", code: "404", message: "Not Found" 126 | 127 | mock_http_request mock_response 128 | 129 | expect(instance.contents_of_aasa_file("myapp.app.link")).to be_nil 130 | expect(instance.errors).not_to be_empty 131 | end 132 | 133 | it "returns nil if the response does not contain a Content-type" do 134 | mock_contents = "{}" 135 | mock_response = double "response", body: mock_contents, code: "200", message: "OK" 136 | expect(mock_response).to receive(:[]).at_least(:once).with("Content-type") { nil } 137 | 138 | mock_http_request mock_response 139 | 140 | expect(instance.contents_of_aasa_file("myapp.app.link")).to be_nil 141 | expect(instance.errors).not_to be_empty 142 | end 143 | 144 | it "returns nil in case of a redirect" do 145 | mock_response = double "response", code: "302", message: "Moved Permanently" 146 | 147 | mock_http_request mock_response 148 | 149 | expect(STDOUT).to receive(:puts).with(/redirect/i).at_least(:once) 150 | expect(instance.contents_of_aasa_file("myapp.app.link")).to be_nil 151 | expect(instance.errors).not_to be_empty 152 | end 153 | end 154 | 155 | describe '#validate_team_and_bundle_ids_from_aasa_files' do 156 | it 'only succeeds if all domains are valid' do 157 | # No domains in project. Just validating what's passed in. 158 | expect(instance).to receive(:domains_from_project) { [] } 159 | # example.com is valid 160 | expect(instance).to receive(:validate_team_and_bundle_ids) 161 | .with("example.com", "Release") { true } 162 | # www.example.com is not valid 163 | expect(instance).to receive(:validate_team_and_bundle_ids) 164 | .with("www.example.com", "Release") { false } 165 | 166 | valid = instance.validate_team_and_bundle_ids_from_aasa_files( 167 | %w{example.com www.example.com} 168 | ) 169 | expect(valid).to be false 170 | end 171 | 172 | it 'succeeds if all domains are valid' do 173 | # No domains in project. Just validating what's passed in. 174 | expect(instance).to receive(:domains_from_project) { [] } 175 | # example.com is valid 176 | expect(instance).to receive(:validate_team_and_bundle_ids) 177 | .with("example.com", "Release") { true } 178 | # www.example.com is not valid 179 | expect(instance).to receive(:validate_team_and_bundle_ids) 180 | .with("www.example.com", "Release") { true } 181 | 182 | valid = instance.validate_team_and_bundle_ids_from_aasa_files( 183 | %w{example.com www.example.com} 184 | ) 185 | expect(valid).to be true 186 | end 187 | 188 | it 'fails if no domains specified and no domains in project' do 189 | # No domains in project. Just validating what's passed in. 190 | expect(instance).to receive(:domains_from_project) { [] } 191 | 192 | valid = instance.validate_team_and_bundle_ids_from_aasa_files( 193 | [] 194 | ) 195 | expect(valid).to be false 196 | end 197 | end 198 | 199 | def mock_http_request(mock_response) 200 | mock_http = double "http", peer_cert: nil 201 | expect(mock_http).to receive(:request).at_least(:once) { mock_response } 202 | expect(Net::HTTP).to receive(:start).at_least(:once).and_yield mock_http 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /spec/option_spec.rb: -------------------------------------------------------------------------------- 1 | describe BranchIOCLI::Configuration::Option do 2 | describe 'initialization' do 3 | OPTION_CLASS = BranchIOCLI::Configuration::Option 4 | 5 | it 'raises if both :validate_proc and :valid_values are passed' do 6 | expect do 7 | OPTION_CLASS.new valid_values_proc: -> {}, validate_proc: ->(x) {} 8 | end.to raise_error ArgumentError 9 | end 10 | 11 | it 'sets negatable to true by default if type is nil' do 12 | option = OPTION_CLASS.new({}) 13 | expect(option.negatable).to be true 14 | end 15 | 16 | it 'sets argument_optional to false if type is non-nil and negatable is false' do 17 | option = OPTION_CLASS.new type: String 18 | expect(option.argument_optional).to be false 19 | end 20 | 21 | it 'sets argument_optional to true if negatable is true' do 22 | option = OPTION_CLASS.new negatable: true 23 | expect(option.argument_optional).to be true 24 | end 25 | 26 | it 'sets confirm_symbol to name by default' do 27 | option = OPTION_CLASS.new name: :foo 28 | expect(option.confirm_symbol).to eq :foo 29 | end 30 | 31 | it 'sets aliases to an Array if a scalar is passed' do 32 | option = OPTION_CLASS.new aliases: "-x" 33 | expect(option.aliases).to eq %w(-x) 34 | end 35 | 36 | it 'accepts an Array for aliases' do 37 | option = OPTION_CLASS.new aliases: %w(-x) 38 | expect(option.aliases).to eq %w(-x) 39 | end 40 | 41 | it 'uses name to set env_name by default' do 42 | option = OPTION_CLASS.new name: :foo 43 | expect(option.env_name).to eq "BRANCH_FOO" 44 | end 45 | 46 | it 'sets the label to a default value' do 47 | option = OPTION_CLASS.new name: :pod_repo_update 48 | expect(option.label).to eq "Pod repo update" 49 | end 50 | end 51 | 52 | describe '#valid_values' do 53 | it 'calls the valid_values_proc if present' do 54 | option = OPTION_CLASS.new valid_values_proc: -> { %w(a b c) } 55 | expect(option.valid_values).to eq %w(a b c) 56 | end 57 | 58 | it 'returns nil for valid_values if valid_values_proc is nil' do 59 | option = OPTION_CLASS.new({}) 60 | expect(option.valid_values).to be_nil 61 | end 62 | end 63 | 64 | describe '#ui_type' do 65 | it 'returns "Comma-separated list" for Array type' do 66 | option = OPTION_CLASS.new type: Array 67 | expect(option.ui_type).to eq "Comma-separated list" 68 | end 69 | 70 | it 'returns "Boolean" for nil type' do 71 | option = OPTION_CLASS.new({}) 72 | expect(option.ui_type).to eq "Boolean" 73 | end 74 | 75 | it 'returns the name of the type for any other type' do 76 | option = OPTION_CLASS.new type: String 77 | expect(option.ui_type).to eq "String" 78 | end 79 | end 80 | 81 | describe '#env_value' do 82 | it 'returns the value of the named env. var. if set and valid' do 83 | ENV["FOO"] = "bar" 84 | option = OPTION_CLASS.new env_name: "FOO", type: String 85 | expect(option.env_value).to eq "bar" 86 | end 87 | 88 | it 'returns nil if env_name is falsy' do 89 | option = OPTION_CLASS.new env_name: false 90 | expect(option.env_value).to be_nil 91 | end 92 | 93 | it 'converts the value' do 94 | ENV["FOO"] = "YES" 95 | option = OPTION_CLASS.new env_name: "FOO" 96 | expect(option.env_value).to be true 97 | end 98 | end 99 | 100 | describe '#convert' do 101 | it 'calls a convert_proc if present' do 102 | option = OPTION_CLASS.new convert_proc: ->(value) { value * 3 } 103 | expect(option.convert("*")).to eq "***" 104 | end 105 | 106 | it 'splits a string using commas if the type is Array' do 107 | option = OPTION_CLASS.new type: Array 108 | expect(option.convert("a,b")).to eq %w(a b) 109 | end 110 | 111 | it 'recognizes yes/no true/false for Boolean types' do 112 | option = OPTION_CLASS.new({}) 113 | expect(option.convert("Yes")).to be true 114 | expect(option.convert("TRUE")).to be true 115 | expect(option.convert("yes")).to be true 116 | 117 | expect(option.convert("no")).to be false 118 | expect(option.convert("False")).to be false 119 | expect(option.convert("NO")).to be false 120 | end 121 | 122 | it 'strips strings' do 123 | option = OPTION_CLASS.new type: String 124 | expect(option.convert(" abc ")).to eq "abc" 125 | end 126 | 127 | it 'returns nil for an empty string' do 128 | option = OPTION_CLASS.new type: String 129 | expect(option.convert("")).to be_nil 130 | end 131 | end 132 | 133 | describe '#valid?' do 134 | it 'returns the result of a validate_proc if present' do 135 | expected = false 136 | option = OPTION_CLASS.new validate_proc: ->(value) { expected } 137 | expect(option.valid?(:foo)).to eq expected 138 | end 139 | 140 | it 'checks #valid_values if non-nil' do 141 | option = OPTION_CLASS.new valid_values_proc: -> { %w(a b) } 142 | expect(option.valid?("a")).to be true 143 | expect(option.valid?("b")).to be true 144 | expect(option.valid?("c")).to be false 145 | end 146 | 147 | it 'accepts nil for all types' do 148 | option = OPTION_CLASS.new type: String 149 | expect(option.valid?(nil)).to be true 150 | end 151 | 152 | it 'checks all values of an Array argument' do 153 | option = OPTION_CLASS.new type: Array, valid_values_proc: -> { %w(a b) } 154 | expect(option.valid?(%w(a))).to be true 155 | expect(option.valid?(%w(a b))).to be true 156 | expect(option.valid?(%w(a b c))).to be false 157 | end 158 | 159 | it 'checks type conformance if type is non-nil' do 160 | option = OPTION_CLASS.new type: Array 161 | expect(option.valid?("a")).to be false 162 | expect(option.valid?(%w(a))).to be true 163 | end 164 | 165 | it 'checks for Boolean values if type is nil' do 166 | option = OPTION_CLASS.new({}) 167 | expect(option.valid?(true)).to be true 168 | expect(option.valid?(false)).to be true 169 | expect(option.valid?("foo")).to be false 170 | end 171 | end 172 | 173 | describe '#display_value' do 174 | it 'returns yes and no when type is nil' do 175 | option = OPTION_CLASS.new({}) 176 | expect(option.display_value(true)).to eq "yes" 177 | expect(option.display_value(false)).to eq "no" 178 | end 179 | 180 | it 'converts to a string for other types' do 181 | option = OPTION_CLASS.new(type: Integer) 182 | expect(option.display_value(1)).to eq "1" 183 | end 184 | 185 | it 'returns "(none)" for nil' do 186 | option = OPTION_CLASS.new(type: String) 187 | expect(option.display_value(nil)).to eq "(none)" 188 | end 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /spec/option_wrapper_spec.rb: -------------------------------------------------------------------------------- 1 | describe BranchIOCLI::Configuration::OptionWrapper do 2 | WRAPPER_CLASS = BranchIOCLI::Configuration::OptionWrapper 3 | let (:options) do 4 | [ 5 | BranchIOCLI::Configuration::Option.new( 6 | name: :foo, 7 | default_value: "bar", 8 | env_name: "FOO", 9 | type: String 10 | ) 11 | ] 12 | end 13 | 14 | before :each do 15 | ENV["FOO"] = nil 16 | end 17 | 18 | it 'returns the value of any valid key in a hash supplied to the initializer as a method' do 19 | wrapper = WRAPPER_CLASS.new({ foo: "bar" }, options) 20 | 21 | expect(wrapper.foo).to eq "bar" 22 | expect do 23 | wrapper.bar 24 | end.to raise_error NoMethodError 25 | end 26 | 27 | it 'adds defaults if add_defaults is true' do 28 | wrapper = WRAPPER_CLASS.new({}, options) 29 | expect(wrapper.foo).to eq "bar" 30 | end 31 | 32 | it 'consults any env_value before default_value' do 33 | ENV["FOO"] = "y" 34 | wrapper = WRAPPER_CLASS.new({}, options) 35 | expect(wrapper.foo).to eq "y" 36 | end 37 | 38 | it 'does not consult the environment or default_value if add_defaults is false' do 39 | ENV["FOO"] = "y" 40 | wrapper = WRAPPER_CLASS.new({}, options, false) 41 | expect(wrapper.foo).to be_nil 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'rspec/simplecov' 3 | 4 | # SimpleCov.minimum_coverage 95 5 | SimpleCov.start 6 | 7 | require_relative "../lib/branch_io_cli" 8 | -------------------------------------------------------------------------------- /spec/xcodeproj_ext_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Xcodeproj extensions' do 2 | describe 'PBXNativeTarget#expanded_build_setting' do 3 | let (:project) do 4 | double :project 5 | end 6 | 7 | let (:target) do 8 | expect(project).to receive(:mark_dirty!) 9 | t = Xcodeproj::Project::Object::PBXNativeTarget.new(project, nil) 10 | t.name = "MyTarget" 11 | t 12 | end 13 | 14 | it "expands values delimited by $()" do 15 | expect(target).to receive(:resolved_build_setting).with("SETTING_WITH_NESTED_VALUE", true) { { "Release" => "$(SETTING_VALUE)" } } 16 | expect(target).to receive(:resolved_build_setting).with("SETTING_VALUE", true) { { "Release" => "value" } } 17 | expect(target.expanded_build_setting("SETTING_WITH_NESTED_VALUE", "Release")).to eq "value" 18 | end 19 | 20 | it "expands values delimited by ${}" do 21 | expect(target).to receive(:resolved_build_setting).with("SETTING_WITH_NESTED_VALUE", true) { { "Release" => "${SETTING_VALUE}" } } 22 | expect(target).to receive(:resolved_build_setting).with("SETTING_VALUE", true) { { "Release" => "value" } } 23 | expect(target.expanded_build_setting("SETTING_WITH_NESTED_VALUE", "Release")).to eq "value" 24 | end 25 | 26 | it "expands an all-caps word as a setting" do 27 | expect(target).to receive(:resolved_build_setting).with("SETTING_WITH_NESTED_VALUE", true) { { "Release" => "SETTING_VALUE" } } 28 | expect(target).to receive(:resolved_build_setting).with("SETTING_VALUE", true) { { "Release" => "value" } } 29 | expect(target.expanded_build_setting("SETTING_WITH_NESTED_VALUE", "Release")).to eq "value" 30 | end 31 | 32 | it "expands any component of a path as a setting if all-caps" do 33 | expect(target).to receive(:resolved_build_setting).with("SETTING_WITH_NESTED_VALUE", true) { { "Release" => "SETTING_VALUE/SETTING_VALUE/file.txt" } } 34 | expect(target).to receive(:resolved_build_setting).with("SETTING_VALUE", true).at_least(:once) { { "Release" => "value" } } 35 | expect(target.expanded_build_setting("SETTING_WITH_NESTED_VALUE", "Release")).to eq "value/value/file.txt" 36 | end 37 | 38 | it "resolves without an xcconfig if the xcconfig is not found" do 39 | expect(target).to receive(:resolved_build_setting).with("SETTING_WITH_NESTED_VALUE", true).and_raise(Errno::ENOENT) 40 | expect(target).to receive(:resolved_build_setting).with("SETTING_WITH_NESTED_VALUE", false) { { "Release" => "$(SETTING_VALUE)" } } 41 | expect(target).to receive(:resolved_build_setting).with("SETTING_VALUE", true) { { "Release" => "value" } } 42 | expect(target.expanded_build_setting("SETTING_WITH_NESTED_VALUE", "Release")).to eq "value" 43 | end 44 | 45 | it "returns nil if the setting is not present" do 46 | expect(target).to receive(:resolved_build_setting).with("NONEXISTENT_SETTING", true) { { "Release" => nil } } 47 | expect(BranchIOCLI::Configuration::XcodeSettings).to receive(:[]).with("Release") { {} } 48 | expect(target.expanded_build_setting("NONEXISTENT_SETTING", "Release")).to be_nil 49 | end 50 | 51 | it "substitutes . for $(SRCROOT)" do 52 | expect(target).to receive(:resolved_build_setting).with("SETTING_USING_SRCROOT", true) { { "Release" => "$(SRCROOT)/some.file" } } 53 | expect(target.expanded_build_setting("SETTING_USING_SRCROOT", "Release")).to eq "./some.file" 54 | end 55 | 56 | it "subsitutes the target name for $(TARGET_NAME)" do 57 | expect(target).to receive(:resolved_build_setting).with("SETTING_USING_TARGET_NAME", true) { { "Release" => "$(TARGET_NAME)" } } 58 | expect(target.expanded_build_setting("SETTING_USING_TARGET_NAME", "Release")).to eq target.name 59 | end 60 | 61 | it "returns the setting when no macro present" do 62 | expect(target).to receive(:resolved_build_setting).with("SETTING_WITHOUT_MACRO", true) { { "Release" => "setting" } } 63 | expect(target.expanded_build_setting("SETTING_WITHOUT_MACRO", "Release")).to eq "setting" 64 | end 65 | 66 | it "expands multiple instances of the same macro" do 67 | expect(target).to receive(:resolved_build_setting).with("SETTING_WITH_NESTED_VALUE", true) { { "Release" => "$(SETTING_VALUE).$(SETTING_VALUE)" } } 68 | expect(target).to receive(:resolved_build_setting).with("SETTING_VALUE", true) { { "Release" => "value" } } 69 | expect(target.expanded_build_setting("SETTING_WITH_NESTED_VALUE", "Release")).to eq "value.value" 70 | end 71 | 72 | it "expands multiple macros in a setting" do 73 | expect(target).to receive(:resolved_build_setting).with("SETTING_WITH_NESTED_VALUES", true) { { "Release" => "$(SETTING_VALUE1).$(SETTING_VALUE2)" } } 74 | expect(target).to receive(:resolved_build_setting).with("SETTING_VALUE1", true) { { "Release" => "value1" } } 75 | expect(target).to receive(:resolved_build_setting).with("SETTING_VALUE2", true) { { "Release" => "value2" } } 76 | expect(target.expanded_build_setting("SETTING_WITH_NESTED_VALUES", "Release")).to eq "value1.value2" 77 | end 78 | 79 | it "balances delimiters" do 80 | expect(target).to receive(:resolved_build_setting).with("SETTING_WITH_NESTED_VALUES", true) { { "Release" => "$(SETTING_VALUE1}.${SETTING_VALUE2)" } } 81 | expect(target.expanded_build_setting("SETTING_WITH_NESTED_VALUES", "Release")).to eq "$(SETTING_VALUE1}.${SETTING_VALUE2)" 82 | end 83 | 84 | it "expands recursively" do 85 | expect(target).to receive(:resolved_build_setting).with("SETTING_WITH_NESTED_VALUES", true) { { "Release" => "$(SETTING_VALUE1)" } } 86 | expect(target).to receive(:resolved_build_setting).with("SETTING_VALUE1", true) { { "Release" => "$(SETTING_VALUE2)" } } 87 | expect(target).to receive(:resolved_build_setting).with("SETTING_VALUE2", true) { { "Release" => "value2" } } 88 | expect(target.expanded_build_setting("SETTING_WITH_NESTED_VALUES", "Release")).to eq "value2" 89 | end 90 | 91 | it "returns the unexpanded macro for nonexistent settings" do 92 | expect(target).to receive(:resolved_build_setting).with("SETTING_WITH_BOGUS_VALUE", true) { { "Release" => "$(SETTING_VALUE1).$(SETTING_VALUE2)" } } 93 | expect(target).to receive(:resolved_build_setting).with("SETTING_VALUE1", true) { { "Release" => nil } } 94 | expect(target).to receive(:resolved_build_setting).with("SETTING_VALUE2", true) { { "Release" => "value2" } } 95 | expect(BranchIOCLI::Configuration::XcodeSettings).to receive(:[]).with("Release") { {} } 96 | expect(target.expanded_build_setting("SETTING_WITH_BOGUS_VALUE", "Release")).to eq "$(SETTING_VALUE1).value2" 97 | end 98 | 99 | it "recognizes :rfc1034identifier when expanding" do 100 | expect(target).to receive(:resolved_build_setting).with("PRODUCT_NAME", true) { { "Release" => "My App" } } 101 | expect(target).to receive(:resolved_build_setting).with("PRODUCT_BUNDLE_IDENTIFIER", true) { { "Release" => "com.example.$(PRODUCT_NAME:rfc1034identifier)" } } 102 | expect(target.expanded_build_setting("PRODUCT_BUNDLE_IDENTIFIER", "Release")).to eq "com.example.My-App" 103 | end 104 | 105 | it "ignores any other modifier" do 106 | expect(target).to receive(:resolved_build_setting).with("PRODUCT_NAME", true) { { "Release" => "My App" } } 107 | expect(target).to receive(:resolved_build_setting).with("PRODUCT_BUNDLE_IDENTIFIER", true) { { "Release" => "com.example.$(PRODUCT_NAME:foo)" } } 108 | expect(target.expanded_build_setting("PRODUCT_BUNDLE_IDENTIFIER", "Release")).to eq "com.example.My App" 109 | end 110 | 111 | it "substitutes - for special characters when :rfc1034identifier is present" do 112 | expect(target).to receive(:resolved_build_setting).with("PRODUCT_NAME", true) { { "Release" => "My .@*&'\\\"+%_App" } } 113 | expect(target).to receive(:resolved_build_setting).with("PRODUCT_BUNDLE_IDENTIFIER", true) { { "Release" => "com.example.$(PRODUCT_NAME:rfc1034identifier)" } } 114 | expect(target.expanded_build_setting("PRODUCT_BUNDLE_IDENTIFIER", "Release")).to eq "com.example.My-----------App" 115 | end 116 | 117 | it "expands against Xcode settings when setting not found for target" do 118 | expect(target).to receive(:resolved_build_setting).with("PROJECT_NAME", true) { { "Release" => nil } } 119 | expect(BranchIOCLI::Configuration::XcodeSettings).to receive(:[]).with("Release") { { "PROJECT_NAME" => "MyProject" } } 120 | expect(target.expanded_build_setting("PROJECT_NAME", "Release")).to eq "MyProject" 121 | end 122 | end 123 | end 124 | --------------------------------------------------------------------------------