require 'spaceship' module Fastlane class SetupIos < Setup # the tools that are already enabled attr_accessor :tools attr_accessor :project attr_accessor :apple_id attr_accessor :portal_ref attr_accessor :itc_ref attr_accessor :dev_portal_team attr_accessor :itc_team attr_accessor :app_identifier attr_accessor :app_name attr_accessor :is_swift_fastfile def run(user: nil, is_swift_fastfile: false) self.is_swift_fastfile = is_swift_fastfile self.apple_id = user show_infos FastlaneCore::FastlaneFolder.create_folder! unless Helper.is_test? is_manual_setup = false setup_project react_native_pre_checks ask_for_apple_id begin if self.project.mac? UI.important("Generating apps on the Apple Developer Portal and iTunes Connect is not currently available for Mac apps") else detect_if_app_is_available end print_config_table if self.project.schemes.count > 1 UI.important("Note: If the values above are incorrect, it is possible the wrong scheme was selected") end if UI.confirm("Please confirm the above values") default_setup else is_manual_setup = true UI.message("Falling back to manual onboarding") manual_setup end UI.success('Successfully finished setting up fastlane') rescue Spaceship::Client::InsufficientPermissions, Spaceship::Client::ProgramLicenseAgreementUpdated => ex # We don't want to fallback to manual onboarding for this # as the user needs to first accept the agreement / get more permissions # Let's re-raise the exception to properly show the error message raise ex rescue => ex # this will also be caused by Ctrl + C UI.message("Ran into error while trying to connect to iTunes Connect / Dev Portal: #{ex}") UI.message("Falling back to manual onboarding") if is_manual_setup handle_exception(exception: ex) else UI.error(ex.to_s) UI.error('An error occurred during the setup process. Falling back to manual setup!') try_manual_setup end end end def handle_exception(exception: nil) # Something went wrong with the setup, clear the folder again # and restore previous files UI.error('Error occurred with the setup program! Reverting changes now!') restore_previous_state raise exception end def try_manual_setup manual_setup rescue => ex handle_exception(exception: ex) end # React Native specific code # Make it easy for people to onboard def react_native_pre_checks return unless self.class.project_uses_react_native? if app_identifier.to_s.length == 0 error_message = [] error_message << "Could not detect bundle identifier of your react-native app." error_message << "Make sure to open the Xcode project and update the bundle identifier" error_message << "in the `General` section of your project settings." error_message << "Restart `fastlane init` once you're done!" UI.user_error!(error_message.join(" ")) end end def self.project_uses_react_native?(path: Dir.pwd) package_json = File.join(path, "..", "package.json") return false unless File.basename(path) == "ios" return false unless File.exist?(package_json) package_content = File.read(package_json) return true if package_content.include?("react-native") false end def default_setup copy_existing_files generate_appfile(manually: false) detect_installed_tools # after copying the existing files if !self.project.mac? && self.itc_ref.nil? && self.portal_ref.nil? create_app_if_necessary end enable_deliver generate_fastfile(manually: false) if self.is_swift_fastfile update_swift_runner end show_analytics end def manual_setup copy_existing_files generate_appfile(manually: true) detect_installed_tools # after copying the existing files ask_to_enable_other_tools generate_fastfile(manually: true) if self.is_swift_fastfile update_swift_runner end show_analytics end def ask_to_enable_other_tools if self.itc_ref.nil? && self.portal_ref.nil? wants_to_create_app = UI.confirm('Would you like to create your app on iTunes Connect and the Developer Portal?') if wants_to_create_app create_app_if_necessary detect_if_app_is_available # check if the app was, in fact, created. end end if self.itc_ref && self.portal_ref wants_to_setup_deliver = UI.confirm("Do you want to setup 'deliver', which is used to upload app screenshots, app metadata and app updates to the App Store? This requires the app to be in the App Store already") enable_deliver if wants_to_setup_deliver end end def setup_project config = {} FastlaneCore::Project.detect_projects(config) self.project = FastlaneCore::Project.new(config) self.project.select_scheme(preferred_to_include: self.project.project_name) self.app_identifier = self.project.default_app_identifier # These two vars need to be accessed in order to be set self.app_name = self.project.default_app_name # They are set as a side effect, this could/should be changed down the road end def print_config_table rows = [] rows << ["Apple ID", self.apple_id] rows << ["App Name", self.app_name] rows << ["App Identifier", self.app_identifier] rows << [(self.project.is_workspace ? "Workspace" : "Project"), self.project.path] require 'terminal-table' puts "" puts Terminal::Table.new(rows: FastlaneCore::PrintTable.transform_output(rows), title: "Detected Values") puts "" unless self.itc_ref || self.project.mac? UI.important "This app identifier doesn't exist on iTunes Connect yet, it will be created for you" end unless self.portal_ref || self.project.mac? UI.important "This app identifier doesn't exist on the Apple Developer Portal yet, it will be created for you" end end def show_infos UI.success('This setup will help you get up and running in no time.') UI.success("fastlane will check what tools you're already using and set up") UI.success('the tool automatically for you. Have fun! ') end def files_to_copy ['Deliverfile', 'deliver', 'screenshots', 'metadata'] end def copy_existing_files files_to_copy.each do |current| current = File.join(File.expand_path('..', FastlaneCore::FastlaneFolder.path), current) next unless File.exist?(current) file_name = File.basename(current) to_path = File.join(folder, file_name) UI.success("Moving '#{current}' to '#{to_path}'") FileUtils.mv(current, to_path) end end def ask_for_apple_id self.apple_id ||= UI.input("Your Apple ID (e.g. fastlane@krausefx.com): ") end def ask_for_app_identifier self.app_identifier = UI.input("App Identifier (com.krausefx.app): ") end def generate_appfile(manually: false) template = File.read(appfile_template_path) if manually ask_for_app_identifier ask_for_apple_id end template.gsub!('[[DEV_PORTAL_TEAM_ID]]', self.dev_portal_team) if self.dev_portal_team template.gsub!('[[APP_IDENTIFIER]]', self.app_identifier) template.gsub!('[[APPLE_ID]]', self.apple_id) if self.is_swift_fastfile itc_team = self.itc_team ? "\"#{self.itc_team}\"" : "nil" path = File.join(folder, 'Appfile.swift') else itc_team = self.itc_team ? "itc_team_id \"#{self.itc_team}\" # iTunes Connect Team ID\n" : "" path = File.join(folder, 'Appfile') end template.gsub!('[[ITC_TEAM]]', itc_team) File.write(path, template) UI.success("Created new file '#{path}'. Edit it to manage your preferred app metadata information.") end # Detect if the app was created on the Dev Portal / iTC def detect_if_app_is_available UI.important "Verifying that app is available on the Apple Developer Portal and iTunes Connect..." UI.message "Starting login with user '#{self.apple_id}'" Spaceship.login(self.apple_id, nil) self.dev_portal_team = Spaceship.select_team self.portal_ref = Spaceship::App.find(self.app_identifier) Spaceship::Tunes.login(@apple_id, nil) self.itc_team = Spaceship::Tunes.select_team self.itc_ref = Spaceship::Application.find(self.app_identifier) end def create_app_if_necessary UI.important "Creating the app on iTunes Connect and the Apple Developer Portal" require 'produce' config = {} # this has to be done like this FastlaneCore::Project.detect_projects(config) project = FastlaneCore::Project.new(config) produce_options_hash = { app_name: project.app_name, app_identifier: self.app_identifier } Produce.config = FastlaneCore::Configuration.create(Produce::Options.available_options, produce_options_hash) begin ENV['PRODUCE_APPLE_ID'] = Produce::Manager.start_producing rescue => exception if exception.to_s.include?("The App Name you entered has already been used") UI.important("It looks like that #{project.app_name} has already been taken by someone else, please enter an alternative.") Produce.config[:app_name] = UI.input("App Name: ") Produce.config[:skip_devcenter] = true # since we failed on iTC ENV['PRODUCE_APPLE_ID'] = Produce::Manager.start_producing end end end def detect_installed_tools self.tools = {} self.tools[:snapshot] = File.exist?(File.join(folder, 'Snapfile')) self.tools[:snapshot] ||= File.exist?(File.join(folder, 'Snapfile.swift')) self.tools[:cocoapods] = File.exist?(File.join(File.expand_path('..', folder), 'Podfile')) self.tools[:carthage] = File.exist?(File.join(File.expand_path('..', folder), 'Cartfile')) end def enable_deliver UI.message("Loading up 'deliver', this might take a few seconds") require 'deliver' require 'deliver/setup' options = FastlaneCore::Configuration.create(Deliver::Options.available_options, {}) options[:run_precheck_before_submit] = false # precheck doesn't need to run during init options[:username] = self.apple_id if self.apple_id options[:app_identifier] = self.app_identifier if self.app_identifier Deliver::Runner.new(options) # to login... Deliver::Setup.new.run(options, is_swift: self.is_swift_fastfile) end def generate_fastfile(manually: false) scheme = self.project.schemes.first unless manually template = File.read(fastfile_template_path) scheme = UI.input("Optional: The scheme name of your app (If you don't need one, just hit Enter): ") unless scheme if scheme.length > 0 if self.is_swift_fastfile template.gsub!('[[SCHEME]]', "scheme: \"#{scheme}\"") else template.gsub!('[[SCHEME]]', "(scheme: \"#{scheme}\")") end else template.gsub!('[[SCHEME]]', "") end template.gsub!('[[FASTLANE_VERSION]]', Fastlane::VERSION) if self.is_swift_fastfile template.gsub!('snapshot()', '// snapshot') unless self.tools[:snapshot] template.gsub!('cocoapods()', '// cocoapods()') unless self.tools[:cocoapods] template.gsub!('carthage()', '// carthage()') unless self.tools[:carthage] path = File.join(folder, 'Fastfile.swift') else template.gsub!('snapshot', '# snapshot') unless self.tools[:snapshot] template.gsub!('cocoapods', '# cocoapods') unless self.tools[:cocoapods] template.gsub!('carthage', '# cocoapods') unless self.tools[:carthage] path = File.join(folder, 'Fastfile') end self.tools.each do |key, value| UI.message("'#{key}' enabled.".magenta) if value UI.important("'#{key}' not enabled.") unless value end File.write(path, template) UI.success("Created new file '#{path}'. Edit it to manage your own deployment lanes.") end def folder FastlaneCore::FastlaneFolder.path end def appfile_template_path if self.is_swift_fastfile return "#{Fastlane::ROOT}/lib/assets/AppfileTemplate.swift" else return "#{Fastlane::ROOT}/lib/assets/AppfileTemplate" end end def fastfile_template_path if self.is_swift_fastfile return "#{Fastlane::ROOT}/lib/assets/DefaultFastfileTemplate.swift" else return "#{Fastlane::ROOT}/lib/assets/DefaultFastfileTemplate" end end def update_swift_runner runner_source_resources = "#{Fastlane::ROOT}/swift/." destination_path = File.expand_path('swift', FastlaneCore::FastlaneFolder.path) FileUtils.cp_r(runner_source_resources, destination_path) UI.success("Copied Swift fastlane runner project to '#{destination_path}'.") end def restore_previous_state # Move all moved files back files_to_copy.each do |current| from_path = File.join(folder, current) to_path = File.basename(current) if File.exist?(from_path) UI.important("Moving '#{from_path}' to '#{to_path}'") FileUtils.mv(from_path, to_path) end end UI.important("Deleting the 'fastlane' folder") FileUtils.rm_rf(folder) end end end