├── .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 |
--------------------------------------------------------------------------------