From a5d4a4e59bcfc7e13df5c0cfcbffafafbb6c1083 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sat, 27 Oct 2018 09:42:30 +0200 Subject: [PATCH 1/8] Enable iTunes File Sharing --- Passepartout-iOS/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Passepartout-iOS/Info.plist b/Passepartout-iOS/Info.plist index 68d21360..a54a6ad4 100644 --- a/Passepartout-iOS/Info.plist +++ b/Passepartout-iOS/Info.plist @@ -43,6 +43,8 @@ LSSupportsOpeningDocumentsInPlace + UIFileSharingEnabled + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile From 25523b5f6132bf569bf012014a801a26c2c585a4 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sat, 27 Oct 2018 10:25:21 +0200 Subject: [PATCH 2/8] Add stubs for imported hosts --- .../Global/SwiftGen+Storyboards.swift | 1 + .../ImportedHostsViewController.swift | 39 ++++++++++ .../en.lproj/Organizer.storyboard | 74 ++++++++++++++++++- Passepartout.xcodeproj/project.pbxproj | 4 + .../Resources/en.lproj/Localizable.strings | 2 + Passepartout/Sources/SwiftGen+Strings.swift | 5 ++ 6 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift diff --git a/Passepartout-iOS/Global/SwiftGen+Storyboards.swift b/Passepartout-iOS/Global/SwiftGen+Storyboards.swift index a101983c..9b6efc0c 100644 --- a/Passepartout-iOS/Global/SwiftGen+Storyboards.swift +++ b/Passepartout-iOS/Global/SwiftGen+Storyboards.swift @@ -90,6 +90,7 @@ internal enum StoryboardSegue { case addProviderSegueIdentifier = "AddProviderSegueIdentifier" case creditsSegueIdentifier = "CreditsSegueIdentifier" case disclaimerSegueIdentifier = "DisclaimerSegueIdentifier" + case importHostSegueIdentifier = "ImportHostSegueIdentifier" case selectProfileSegueIdentifier = "SelectProfileSegueIdentifier" case versionSegueIdentifier = "VersionSegueIdentifier" } diff --git a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift new file mode 100644 index 00000000..4cee520b --- /dev/null +++ b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift @@ -0,0 +1,39 @@ +// +// ImportedHostsViewController.swift +// Passepartout-iOS +// +// Created by Davide De Rosa on 10/27/18. +// Copyright (c) 2018 Davide De Rosa. All rights reserved. +// +// https://github.com/keeshux +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// +import UIKit + +class ImportedHostsViewController: UITableViewController { + override func viewDidLoad() { + super.viewDidLoad() + + title = L10n.ImportedHosts.title + } + + // MARK: Actions + + @IBAction private func close() { + dismiss(animated: true, completion: nil) + } +} diff --git a/Passepartout-iOS/en.lproj/Organizer.storyboard b/Passepartout-iOS/en.lproj/Organizer.storyboard index d078b53a..2a100a63 100644 --- a/Passepartout-iOS/en.lproj/Organizer.storyboard +++ b/Passepartout-iOS/en.lproj/Organizer.storyboard @@ -161,6 +161,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -175,7 +245,7 @@ - + @@ -223,6 +293,7 @@ + @@ -496,5 +567,6 @@ + diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index eafffd55..bd81e9bc 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ 0EA068F4218475F800C320AD /* ParsedFile+Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA068F3218475F800C320AD /* ParsedFile+Alerts.swift */; }; 0EAAD71920E6669A0088754A /* GroupConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE8DED20C93E4C004C739C /* GroupConstants.swift */; }; 0EB60FDA2111136E00AD27F3 /* UITextView+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB60FD92111136E00AD27F3 /* UITextView+Search.swift */; }; + 0EB67D6B2184581E00BA6200 /* ImportedHostsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB67D6A2184581E00BA6200 /* ImportedHostsViewController.swift */; }; 0EBBE8F221822B4D00106008 /* ConnectionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBBE8F021822B4D00106008 /* ConnectionServiceTests.swift */; }; 0EBBE8F321822B4D00106008 /* ConnectionService.json in Resources */ = {isa = PBXBuildFile; fileRef = 0EBBE8F121822B4D00106008 /* ConnectionService.json */; }; 0EBBE8F52182361800106008 /* ConnectionService+Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBBE8F42182361700106008 /* ConnectionService+Migration.swift */; }; @@ -169,6 +170,7 @@ 0E8D97E421389276006FB4A0 /* pia.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = pia.json; sourceTree = ""; }; 0EA068F3218475F800C320AD /* ParsedFile+Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParsedFile+Alerts.swift"; sourceTree = ""; }; 0EB60FD92111136E00AD27F3 /* UITextView+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+Search.swift"; sourceTree = ""; }; + 0EB67D6A2184581E00BA6200 /* ImportedHostsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedHostsViewController.swift; sourceTree = ""; }; 0EBBE8F021822B4D00106008 /* ConnectionServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionServiceTests.swift; sourceTree = ""; }; 0EBBE8F121822B4D00106008 /* ConnectionService.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ConnectionService.json; sourceTree = ""; }; 0EBBE8F42182361700106008 /* ConnectionService+Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConnectionService+Migration.swift"; sourceTree = ""; }; @@ -344,6 +346,7 @@ isa = PBXGroup; children = ( 0EE3BBB1215ED3A900F30952 /* AboutViewController.swift */, + 0EB67D6A2184581E00BA6200 /* ImportedHostsViewController.swift */, 0EFD943F215BED8E00529B64 /* LabelViewController.swift */, 0EBE3A78213C4E5400BFA2F5 /* OrganizerViewController.swift */, 0E3DA370215CB5BF00B40FC9 /* VersionViewController.swift */, @@ -852,6 +855,7 @@ 0ED38AEA214054A50004D387 /* OptionViewController.swift in Sources */, 0EFD943E215BE10800529B64 /* IssueReporter.swift in Sources */, 0EB60FDA2111136E00AD27F3 /* UITextView+Search.swift in Sources */, + 0EB67D6B2184581E00BA6200 /* ImportedHostsViewController.swift in Sources */, 0E57F63E20C83FC5008323CF /* ServiceViewController.swift in Sources */, 0E39BCF0214B9EF10035E9DE /* WebServices.swift in Sources */, 0EDE8DE720C93945004C739C /* Credentials.swift in Sources */, diff --git a/Passepartout/Resources/en.lproj/Localizable.strings b/Passepartout/Resources/en.lproj/Localizable.strings index 67084496..fc3021a4 100644 --- a/Passepartout/Resources/en.lproj/Localizable.strings +++ b/Passepartout/Resources/en.lproj/Localizable.strings @@ -58,6 +58,8 @@ "parsed_file.alerts.parsing.message" = "Unable to parse the provided configuration file (%@)."; "parsed_file.alerts.buttons.report" = "Report an issue"; +"imported_hosts.title" = "Imported"; + "service.welcome.message" = "Welcome to Passepartout!\n\nUse the organizer to add a new profile."; "service.sections.general.header" = "General"; "service.sections.vpn.header" = "VPN"; diff --git a/Passepartout/Sources/SwiftGen+Strings.swift b/Passepartout/Sources/SwiftGen+Strings.swift index 29293221..b87019a4 100644 --- a/Passepartout/Sources/SwiftGen+Strings.swift +++ b/Passepartout/Sources/SwiftGen+Strings.swift @@ -300,6 +300,11 @@ internal enum L10n { internal static let ok = L10n.tr("Localizable", "global.ok") } + internal enum ImportedHosts { + /// Imported + internal static let title = L10n.tr("Localizable", "imported_hosts.title") + } + internal enum IssueReporter { /// The debug log of your latest connections is crucial to resolve your connectivity issues and is completely anonymous.\n\nThe .ovpn configuration file, if any, is attached stripped of any sensitive data.\n\nPlease double check the email attachments if unsure. internal static let message = L10n.tr("Localizable", "issue_reporter.message") From 0e7c0b6388d425df5d73a50ca52c65beee262c0d Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sat, 27 Oct 2018 11:14:06 +0200 Subject: [PATCH 3/8] List imported .ovpn if any, fall back to alert Use .formSheet presentation (iPad). --- .../ImportedHostsViewController.swift | 57 +++++++++++++++++++ .../Organizer/OrganizerViewController.swift | 7 +-- .../en.lproj/Organizer.storyboard | 4 +- .../Resources/en.lproj/Localizable.strings | 2 +- .../ConnectionService+Configurations.swift | 13 +++++ Passepartout/Sources/SwiftGen+Strings.swift | 2 +- 6 files changed, 75 insertions(+), 10 deletions(-) diff --git a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift index 4cee520b..f5cbd812 100644 --- a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift @@ -22,18 +22,75 @@ // You should have received a copy of the GNU General Public License // along with Passepartout. If not, see . // + import UIKit +import TunnelKit +import SwiftyBeaver + +private let log = SwiftyBeaver.self class ImportedHostsViewController: UITableViewController { + private lazy var pendingConfigurationURLs = TransientStore.shared.service.pendingConfigurationURLs() + + private var parsedFile: ParsedFile? + override func viewDidLoad() { super.viewDidLoad() title = L10n.ImportedHosts.title } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + guard !pendingConfigurationURLs.isEmpty else { + let alert = Macros.alert( + L10n.ImportedHosts.title, + L10n.Organizer.Alerts.AddHost.message + ) + alert.addCancelAction(L10n.Global.ok) { + self.close() + } + present(alert, animated: true, completion: nil) + return + } + } + // MARK: Actions + override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { + guard let cell = sender as? UITableViewCell, let indexPath = tableView.indexPath(for: cell) else { + return false + } + let url = pendingConfigurationURLs[indexPath.row] + guard let parsedFile = ParsedFile.from(url, withErrorAlertIn: self) else { + return false + } + self.parsedFile = parsedFile + return true + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + guard let wizard = segue.destination as? WizardHostViewController else { + return + } + wizard.parsedFile = parsedFile + } + @IBAction private func close() { dismiss(animated: true, completion: nil) } } + +extension ImportedHostsViewController { + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return pendingConfigurationURLs.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let url = pendingConfigurationURLs[indexPath.row] + let cell = Cells.setting.dequeue(from: tableView, for: indexPath) + cell.leftText = url.normalizedFilename + return cell + } +} diff --git a/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift b/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift index 9b7bb949..0785cd76 100644 --- a/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift @@ -174,12 +174,7 @@ class OrganizerViewController: UITableViewController, TableModelHost { } private func addNewHost() { - let alert = Macros.alert( - L10n.Organizer.Sections.Hosts.header, - L10n.Organizer.Alerts.AddHost.message - ) - alert.addCancelAction(L10n.Global.ok) - present(alert, animated: true, completion: nil) + perform(segue: StoryboardSegue.Organizer.importHostSegueIdentifier) } private func removeProfile(at indexPath: IndexPath) { diff --git a/Passepartout-iOS/en.lproj/Organizer.storyboard b/Passepartout-iOS/en.lproj/Organizer.storyboard index 2a100a63..f4006b7c 100644 --- a/Passepartout-iOS/en.lproj/Organizer.storyboard +++ b/Passepartout-iOS/en.lproj/Organizer.storyboard @@ -293,7 +293,7 @@ - + @@ -567,6 +567,6 @@ - + diff --git a/Passepartout/Resources/en.lproj/Localizable.strings b/Passepartout/Resources/en.lproj/Localizable.strings index fc3021a4..e3a06b30 100644 --- a/Passepartout/Resources/en.lproj/Localizable.strings +++ b/Passepartout/Resources/en.lproj/Localizable.strings @@ -43,7 +43,7 @@ "organizer.cells.about.caption" = "About %@"; "organizer.cells.uninstall.caption" = "Delete VPN profile"; "organizer.alerts.exhausted_providers.message" = "You have created profiles for any available network."; -"organizer.alerts.add_host.message" = "Open an URL to an .ovpn configuration file from Safari, Mail or another app to set up a host profile."; +"organizer.alerts.add_host.message" = "Open an URL to an .ovpn configuration file from Safari, Mail or another app to set up a host profile.\n\nYou can also import an .ovpn with iTunes File Sharing."; "organizer.alerts.delete_vpn_profile.message" = "Do you really want to delete the VPN profile from the device?"; "account.suggestion_footer.infrastructure.pia" = "Use your website credentials. Your username is usually numeric with a \"p\" prefix."; diff --git a/Passepartout/Sources/Model/ConnectionService+Configurations.swift b/Passepartout/Sources/Model/ConnectionService+Configurations.swift index 9a717fc9..d17ce951 100644 --- a/Passepartout/Sources/Model/ConnectionService+Configurations.swift +++ b/Passepartout/Sources/Model/ConnectionService+Configurations.swift @@ -24,6 +24,9 @@ // import Foundation +import SwiftyBeaver + +private let log = SwiftyBeaver.self extension ConnectionService { func save(configurationURL: URL, for profile: ConnectionProfile) throws -> URL { @@ -46,4 +49,14 @@ extension ConnectionService { let contextURL = ConnectionService.ProfileKey(profile).contextURL(in: self) return contextURL.appendingPathComponent(profile.id).appendingPathExtension("ovpn") } + + func pendingConfigurationURLs() -> [URL] { + do { + let list = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: []) + return list.filter { $0.pathExtension == "ovpn" } + } catch let e { + log.error("Could not list imported configurations: \(e)") + return [] + } + } } diff --git a/Passepartout/Sources/SwiftGen+Strings.swift b/Passepartout/Sources/SwiftGen+Strings.swift index b87019a4..7859a287 100644 --- a/Passepartout/Sources/SwiftGen+Strings.swift +++ b/Passepartout/Sources/SwiftGen+Strings.swift @@ -343,7 +343,7 @@ internal enum L10n { internal enum Alerts { internal enum AddHost { - /// Open an URL to an .ovpn configuration file from Safari, Mail or another app to set up a host profile. + /// Open an URL to an .ovpn configuration file from Safari, Mail or another app to set up a host profile.\n\nYou can also import an .ovpn with iTunes File Sharing. internal static let message = L10n.tr("Localizable", "organizer.alerts.add_host.message") } From 663764177afd872d2e5f05d218a66d4129024bf7 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sat, 27 Oct 2018 12:12:01 +0200 Subject: [PATCH 4/8] Forward wizard delegate after import Necessary to delegate adding to organizer. --- .../Scenes/Organizer/ImportedHostsViewController.swift | 3 +++ .../Scenes/Organizer/OrganizerViewController.swift | 2 ++ 2 files changed, 5 insertions(+) diff --git a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift index f5cbd812..e9cd0087 100644 --- a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift @@ -33,6 +33,8 @@ class ImportedHostsViewController: UITableViewController { private lazy var pendingConfigurationURLs = TransientStore.shared.service.pendingConfigurationURLs() private var parsedFile: ParsedFile? + + weak var wizardDelegate: WizardDelegate? override func viewDidLoad() { super.viewDidLoad() @@ -75,6 +77,7 @@ class ImportedHostsViewController: UITableViewController { return } wizard.parsedFile = parsedFile + wizard.delegate = wizardDelegate } @IBAction private func close() { diff --git a/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift b/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift index 0785cd76..f503499d 100644 --- a/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift @@ -139,6 +139,8 @@ class OrganizerViewController: UITableViewController, TableModelHost { providerVC.availableNames = availableProviderNames ?? [] } vc.delegate = self + } else if let vc = destination as? ImportedHostsViewController { + vc.wizardDelegate = self } } From 7b7804091bfb5a7b9e2747c748010dfe32aa26b2 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sat, 27 Oct 2018 12:15:19 +0200 Subject: [PATCH 5/8] Deselect profile row on parsing error --- .../Scenes/Organizer/ImportedHostsViewController.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift index e9cd0087..12df0aff 100644 --- a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift @@ -66,6 +66,9 @@ class ImportedHostsViewController: UITableViewController { } let url = pendingConfigurationURLs[indexPath.row] guard let parsedFile = ParsedFile.from(url, withErrorAlertIn: self) else { + if let selectedIP = tableView.indexPathForSelectedRow { + tableView.deselectRow(at: selectedIP, animated: true) + } return false } self.parsedFile = parsedFile From 5dcc9ff97034d10b68cf2513fc7fcc227a987902 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sat, 27 Oct 2018 12:23:17 +0200 Subject: [PATCH 6/8] Sort imported profiles alphabetically --- .../Scenes/Organizer/ImportedHostsViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift index 12df0aff..db86d754 100644 --- a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift @@ -30,8 +30,8 @@ import SwiftyBeaver private let log = SwiftyBeaver.self class ImportedHostsViewController: UITableViewController { - private lazy var pendingConfigurationURLs = TransientStore.shared.service.pendingConfigurationURLs() - + private lazy var pendingConfigurationURLs = TransientStore.shared.service.pendingConfigurationURLs().sorted { $0.normalizedFilename < $1.normalizedFilename } + private var parsedFile: ParsedFile? weak var wizardDelegate: WizardDelegate? From 4098a15172493c3732b40f42abaa435adda03f98 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sat, 27 Oct 2018 12:28:40 +0200 Subject: [PATCH 7/8] Allow deletion of imported profiles --- .../Organizer/ImportedHostsViewController.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift index db86d754..6a095b6d 100644 --- a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift +++ b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift @@ -99,4 +99,15 @@ extension ImportedHostsViewController { cell.leftText = url.normalizedFilename return cell } + + override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + return true + } + + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + let url = pendingConfigurationURLs[indexPath.row] + try? FileManager.default.removeItem(at: url) + pendingConfigurationURLs.remove(at: indexPath.row) + tableView.deleteRows(at: [indexPath], with: .top) + } } From d96d5728fddd7878318b3acbf75fca6938ed3084 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sat, 27 Oct 2018 12:29:17 +0200 Subject: [PATCH 8/8] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d475f196..62d9178d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Attach .ovpn when reporting a connectivity issue, stripped of sensitive data. [#13](https://github.com/keeshux/passepartout-ios/pull/13) +- iTunes File Sharing (skythedesu). [#14](https://github.com/keeshux/passepartout-ios/pull/14) ## 1.0 beta 1107 (2018-10-26)