diff --git a/Passepartout-iOS/AppDelegate.swift b/Passepartout-iOS/AppDelegate.swift
index 21fea37f..4fdf55e3 100644
--- a/Passepartout-iOS/AppDelegate.swift
+++ b/Passepartout-iOS/AppDelegate.swift
@@ -25,6 +25,10 @@
import UIKit
import TunnelKit
+import Intents
+import SwiftyBeaver
+
+private let log = SwiftyBeaver.self
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate {
@@ -153,3 +157,15 @@ extension UISplitViewController {
return nil
}
}
+
+extension AppDelegate {
+ func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
+ guard let interaction = userActivity.interaction else {
+ return false
+ }
+ if #available(iOS 12, *) {
+ InteractionsHandler.handleInteraction(interaction)
+ }
+ return true
+ }
+}
diff --git a/Passepartout-iOS/Info.plist b/Passepartout-iOS/Info.plist
index a282fe02..6e4d51c0 100644
--- a/Passepartout-iOS/Info.plist
+++ b/Passepartout-iOS/Info.plist
@@ -43,6 +43,16 @@
LSSupportsOpeningDocumentsInPlace
+ NSUserActivityTypes
+
+ ConnectVPNIntent
+ DisableVPNIntent
+ MoveToLocationIntent
+ TrustCellularNetworkIntent
+ TrustCurrentNetworkIntent
+ UntrustCellularNetworkIntent
+ UntrustCurrentNetworkIntent
+
UIFileSharingEnabled
UILaunchStoryboardName
diff --git a/Passepartout-iOS/Intents/Intents.intentdefinition b/Passepartout-iOS/Intents/Intents.intentdefinition
new file mode 100644
index 00000000..f4bdc2f3
--- /dev/null
+++ b/Passepartout-iOS/Intents/Intents.intentdefinition
@@ -0,0 +1,585 @@
+
+
+
+
+ INEnums
+
+ INIntentDefinitionModelVersion
+ 1.0
+ INIntentDefinitionSystemVersion
+ 17G3025
+ INIntentDefinitionToolsBuildVersion
+ 10B61
+ INIntentDefinitionToolsVersion
+ 10.1
+ INIntents
+
+
+ INIntentCategory
+ generic
+ INIntentDescriptionID
+ eXXb2z
+ INIntentLastParameterTag
+ 5
+ INIntentName
+ ConnectVPN
+ INIntentParameterCombinations
+
+ context,profileId
+
+ INIntentParameterCombinationIsPrimary
+
+ INIntentParameterCombinationSubtitle
+ On-demand VPN service
+ INIntentParameterCombinationSubtitleID
+ uMFvJW
+ INIntentParameterCombinationSupportsBackgroundExecution
+
+ INIntentParameterCombinationTitle
+ Connect to ${profileId}
+ INIntentParameterCombinationTitleID
+ U6o81V
+
+
+ INIntentParameters
+
+
+ INIntentParameterDisplayPriority
+ 1
+ INIntentParameterName
+ context
+ INIntentParameterSupportsMultipleValues
+
+ INIntentParameterTag
+ 5
+ INIntentParameterType
+ String
+
+
+ INIntentParameterDisplayPriority
+ 2
+ INIntentParameterName
+ profileId
+ INIntentParameterSupportsMultipleValues
+
+ INIntentParameterTag
+ 4
+ INIntentParameterType
+ String
+
+
+ INIntentResponse
+
+ INIntentResponseCodes
+
+
+ INIntentResponseCodeFormatString
+ Unable to connect
+ INIntentResponseCodeFormatStringID
+ uKU9RD
+ INIntentResponseCodeName
+ failure
+ INIntentResponseCodeSuccess
+
+
+
+ INIntentResponseCodeFormatString
+ Successfully connected!
+ INIntentResponseCodeFormatStringID
+ WNbRl5
+ INIntentResponseCodeName
+ success
+ INIntentResponseCodeSuccess
+
+
+
+ INIntentResponseLastParameterTag
+ 0
+ INIntentResponseParameters
+
+
+ INIntentRestrictions
+ 0
+ INIntentTitle
+ Connect to VPN
+ INIntentTitleID
+ LA99yM
+ INIntentType
+ Custom
+ INIntentUserConfirmationRequired
+
+ INIntentVerb
+ Do
+
+
+ INIntentCategory
+ generic
+ INIntentDescriptionID
+ BKxs8X
+ INIntentLastParameterTag
+ 0
+ INIntentName
+ TrustCurrentNetwork
+ INIntentParameterCombinations
+
+
+
+ INIntentParameterCombinationIsPrimary
+
+ INIntentParameterCombinationSubtitle
+
+ INIntentParameterCombinationSubtitleID
+ AxbXUn
+ INIntentParameterCombinationSupportsBackgroundExecution
+
+ INIntentParameterCombinationTitle
+
+ INIntentParameterCombinationTitleID
+ POyDPM
+
+
+ INIntentParameters
+
+ INIntentResponse
+
+ INIntentResponseCodes
+
+
+ INIntentResponseCodeFormatString
+
+ INIntentResponseCodeFormatStringID
+ x3pVKZ
+ INIntentResponseCodeName
+ failure
+ INIntentResponseCodeSuccess
+
+
+
+ INIntentResponseCodeFormatString
+
+ INIntentResponseCodeFormatStringID
+ qEDn4J
+ INIntentResponseCodeName
+ success
+ INIntentResponseCodeSuccess
+
+
+
+ INIntentResponseLastParameterTag
+ 0
+ INIntentResponseParameters
+
+
+ INIntentRestrictions
+ 0
+ INIntentTitle
+ Trust current network
+ INIntentTitleID
+ m2E7SI
+ INIntentType
+ Custom
+ INIntentUserConfirmationRequired
+
+ INIntentVerb
+ Do
+
+
+ INIntentCategory
+ generic
+ INIntentDescriptionID
+ eQ1yzr
+ INIntentLastParameterTag
+ 0
+ INIntentName
+ DisableVPN
+ INIntentParameterCombinations
+
+
+
+ INIntentParameterCombinationIsPrimary
+
+ INIntentParameterCombinationSubtitle
+
+ INIntentParameterCombinationSubtitleID
+ 85kxu8
+ INIntentParameterCombinationSupportsBackgroundExecution
+
+ INIntentParameterCombinationTitle
+
+ INIntentParameterCombinationTitleID
+ IeGsEq
+
+
+ INIntentParameters
+
+ INIntentResponse
+
+ INIntentResponseCodes
+
+
+ INIntentResponseCodeFormatString
+
+ INIntentResponseCodeFormatStringID
+ fnSNbT
+ INIntentResponseCodeName
+ failure
+ INIntentResponseCodeSuccess
+
+
+
+ INIntentResponseCodeFormatString
+
+ INIntentResponseCodeFormatStringID
+ oKHXZ3
+ INIntentResponseCodeName
+ success
+ INIntentResponseCodeSuccess
+
+
+
+ INIntentResponseLastParameterTag
+ 0
+ INIntentResponseParameters
+
+
+ INIntentRestrictions
+ 0
+ INIntentTitle
+ Disable VPN service
+ INIntentTitleID
+ 1ZRTCZ
+ INIntentType
+ Custom
+ INIntentUserConfirmationRequired
+
+ INIntentVerb
+ Do
+
+
+ INIntentCategory
+ generic
+ INIntentDescriptionID
+ 7eoAss
+ INIntentLastParameterTag
+ 0
+ INIntentName
+ UntrustCurrentNetwork
+ INIntentParameterCombinations
+
+
+
+ INIntentParameterCombinationIsPrimary
+
+ INIntentParameterCombinationSubtitle
+
+ INIntentParameterCombinationSubtitleID
+ pb3MGt
+ INIntentParameterCombinationSupportsBackgroundExecution
+
+ INIntentParameterCombinationTitle
+
+ INIntentParameterCombinationTitleID
+ 0Wu9nb
+
+
+ INIntentParameters
+
+ INIntentResponse
+
+ INIntentResponseCodes
+
+
+ INIntentResponseCodeFormatString
+
+ INIntentResponseCodeFormatStringID
+ r0ZjO4
+ INIntentResponseCodeName
+ failure
+ INIntentResponseCodeSuccess
+
+
+
+ INIntentResponseCodeFormatString
+
+ INIntentResponseCodeFormatStringID
+ rtaAzk
+ INIntentResponseCodeName
+ success
+ INIntentResponseCodeSuccess
+
+
+
+ INIntentResponseLastParameterTag
+ 0
+ INIntentResponseParameters
+
+
+ INIntentRestrictions
+ 0
+ INIntentTitle
+ Untrust current network
+ INIntentTitleID
+ rd1T8p
+ INIntentType
+ Custom
+ INIntentUserConfirmationRequired
+
+ INIntentVerb
+ Do
+
+
+ INIntentCategory
+ generic
+ INIntentDescriptionID
+ 9GpJt5
+ INIntentLastParameterTag
+ 0
+ INIntentName
+ TrustCellularNetwork
+ INIntentParameterCombinations
+
+
+
+ INIntentParameterCombinationIsPrimary
+
+ INIntentParameterCombinationSubtitle
+
+ INIntentParameterCombinationSubtitleID
+ vIPVA5
+ INIntentParameterCombinationSupportsBackgroundExecution
+
+ INIntentParameterCombinationTitle
+
+ INIntentParameterCombinationTitleID
+ NWWgCl
+
+
+ INIntentParameters
+
+ INIntentResponse
+
+ INIntentResponseCodes
+
+
+ INIntentResponseCodeFormatString
+
+ INIntentResponseCodeFormatStringID
+ nMRaxS
+ INIntentResponseCodeName
+ failure
+ INIntentResponseCodeSuccess
+
+
+
+ INIntentResponseCodeFormatString
+
+ INIntentResponseCodeFormatStringID
+ gSqy7Y
+ INIntentResponseCodeName
+ success
+ INIntentResponseCodeSuccess
+
+
+
+ INIntentResponseLastParameterTag
+ 0
+ INIntentResponseParameters
+
+
+ INIntentRestrictions
+ 0
+ INIntentTitle
+ Trust cellular network
+ INIntentTitleID
+ H4taev
+ INIntentType
+ Custom
+ INIntentUserConfirmationRequired
+
+ INIntentVerb
+ Do
+
+
+ INIntentCategory
+ generic
+ INIntentDescriptionID
+ 0jRWn5
+ INIntentLastParameterTag
+ 0
+ INIntentName
+ UntrustCellularNetwork
+ INIntentParameterCombinations
+
+
+
+ INIntentParameterCombinationIsPrimary
+
+ INIntentParameterCombinationSubtitle
+
+ INIntentParameterCombinationSubtitleID
+ qRkLSU
+ INIntentParameterCombinationSupportsBackgroundExecution
+
+ INIntentParameterCombinationTitle
+
+ INIntentParameterCombinationTitleID
+ ggzKA2
+
+
+ INIntentParameters
+
+ INIntentResponse
+
+ INIntentResponseCodes
+
+
+ INIntentResponseCodeFormatString
+
+ INIntentResponseCodeFormatStringID
+ YncGoj
+ INIntentResponseCodeName
+ failure
+ INIntentResponseCodeSuccess
+
+
+
+ INIntentResponseCodeFormatString
+
+ INIntentResponseCodeFormatStringID
+ BW8KLX
+ INIntentResponseCodeName
+ success
+ INIntentResponseCodeSuccess
+
+
+
+ INIntentResponseLastParameterTag
+ 0
+ INIntentResponseParameters
+
+
+ INIntentRestrictions
+ 0
+ INIntentTitle
+ Untrust cellular network
+ INIntentTitleID
+ wB1iYX
+ INIntentType
+ Custom
+ INIntentUserConfirmationRequired
+
+ INIntentVerb
+ Do
+
+
+ INIntentCategory
+ generic
+ INIntentDescriptionID
+ KjkCfU
+ INIntentLastParameterTag
+ 3
+ INIntentName
+ MoveToLocation
+ INIntentParameterCombinations
+
+ providerId,poolName,poolId
+
+ INIntentParameterCombinationIsPrimary
+
+ INIntentParameterCombinationSubtitle
+ With ${providerId} provider
+ INIntentParameterCombinationSubtitleID
+ 66bZBE
+ INIntentParameterCombinationSupportsBackgroundExecution
+
+ INIntentParameterCombinationTitle
+ Move to ${poolName}
+ INIntentParameterCombinationTitleID
+ WnTPFg
+
+
+ INIntentParameters
+
+
+ INIntentParameterDisplayPriority
+ 1
+ INIntentParameterName
+ providerId
+ INIntentParameterSupportsMultipleValues
+
+ INIntentParameterTag
+ 2
+ INIntentParameterType
+ String
+
+
+ INIntentParameterDisplayPriority
+ 2
+ INIntentParameterName
+ poolId
+ INIntentParameterSupportsMultipleValues
+
+ INIntentParameterTag
+ 3
+ INIntentParameterType
+ String
+
+
+ INIntentParameterDisplayPriority
+ 3
+ INIntentParameterName
+ poolName
+ INIntentParameterSupportsMultipleValues
+
+ INIntentParameterTag
+ 1
+ INIntentParameterType
+ String
+
+
+ INIntentResponse
+
+ INIntentResponseCodes
+
+
+ INIntentResponseCodeFormatString
+
+ INIntentResponseCodeFormatStringID
+ PmWNkC
+ INIntentResponseCodeName
+ failure
+ INIntentResponseCodeSuccess
+
+
+
+ INIntentResponseCodeFormatString
+
+ INIntentResponseCodeFormatStringID
+ O6nCLQ
+ INIntentResponseCodeName
+ success
+ INIntentResponseCodeSuccess
+
+
+
+ INIntentResponseLastParameterTag
+ 0
+ INIntentResponseParameters
+
+
+ INIntentRestrictions
+ 0
+ INIntentTitle
+ Move to provider location
+ INIntentTitleID
+ qo3Szz
+ INIntentType
+ Custom
+ INIntentUserConfirmationRequired
+
+ INIntentVerb
+ Go
+
+
+
+
diff --git a/Passepartout-iOS/Intents/InteractionsHandler.swift b/Passepartout-iOS/Intents/InteractionsHandler.swift
new file mode 100644
index 00000000..d0fa6b53
--- /dev/null
+++ b/Passepartout-iOS/Intents/InteractionsHandler.swift
@@ -0,0 +1,267 @@
+//
+// InteractionsHandler.swift
+// Passepartout-iOS
+//
+// Created by Davide De Rosa on 3/8/19.
+// Copyright (c) 2018 Davide De Rosa. All rights reserved.
+//
+// https://github.com/passepartoutvpn
+//
+// 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 Foundation
+import Intents
+import SwiftyBeaver
+
+private let log = SwiftyBeaver.self
+
+@available(iOS 12, *)
+class InteractionsHandler {
+ private class Groups {
+ static let vpn = "VPN"
+
+ static let trust = "Trust"
+ }
+
+ static func donateConnectVPN(with profile: ConnectionProfile) {
+ let profileKey = ProfileKey(profile)
+
+ let intent = ConnectVPNIntent()
+ intent.context = profileKey.context.rawValue
+ intent.profileId = profileKey.id
+
+ let interaction = INInteraction(intent: intent, response: nil)
+ interaction.groupIdentifier = profileKey.rawValue
+ interaction.donateAndLog()
+ }
+
+ static func donateDisableVPN() {
+ let intent = DisableVPNIntent()
+
+ let interaction = INInteraction(intent: intent, response: nil)
+ interaction.groupIdentifier = Groups.vpn
+ interaction.donateAndLog()
+ }
+
+ static func donateMoveToLocation(with profile: ProviderConnectionProfile, pool: Pool) {
+ let profileKey = ProfileKey(profile)
+
+ let intent = MoveToLocationIntent()
+ intent.providerId = profile.id
+ intent.poolId = pool.id
+ intent.poolName = pool.name
+
+ let interaction = INInteraction(intent: intent, response: nil)
+ interaction.groupIdentifier = profileKey.rawValue
+ interaction.donateAndLog()
+ }
+
+ static func donateTrustCurrentNetwork() {
+ let intent = TrustCurrentNetworkIntent()
+
+ let interaction = INInteraction(intent: intent, response: nil)
+ interaction.groupIdentifier = Groups.trust
+ interaction.donateAndLog()
+ }
+
+ static func donateUntrustCurrentNetwork() {
+ let intent = UntrustCurrentNetworkIntent()
+
+ let interaction = INInteraction(intent: intent, response: nil)
+ interaction.groupIdentifier = Groups.trust
+ interaction.donateAndLog()
+ }
+
+ static func donateTrustCellularNetwork() {
+ let intent = TrustCellularNetworkIntent()
+
+ let interaction = INInteraction(intent: intent, response: nil)
+ interaction.groupIdentifier = Groups.trust
+ interaction.donateAndLog()
+ }
+
+ static func donateUntrustCellularNetwork() {
+ let intent = UntrustCellularNetworkIntent()
+
+ let interaction = INInteraction(intent: intent, response: nil)
+ interaction.groupIdentifier = Groups.trust
+ interaction.donateAndLog()
+ }
+
+ //
+
+ static func handleInteraction(_ interaction: INInteraction) {
+ if let custom = interaction.intent as? ConnectVPNIntent {
+ handleConnectVPN(custom, interaction: interaction)
+ } else if let custom = interaction.intent as? DisableVPNIntent {
+ handleDisableVPN(custom, interaction: interaction)
+ } else if let custom = interaction.intent as? MoveToLocationIntent {
+ handleMoveToLocation(custom, interaction: interaction)
+ } else if let _ = interaction.intent as? TrustCurrentNetworkIntent {
+ handleCurrentNetwork(trust: true, interaction: interaction)
+ } else if let _ = interaction.intent as? UntrustCurrentNetworkIntent {
+ handleCurrentNetwork(trust: false, interaction: interaction)
+ } else if let _ = interaction.intent as? TrustCellularNetworkIntent {
+ handleCellularNetwork(trust: true, interaction: interaction)
+ } else if let _ = interaction.intent as? UntrustCellularNetworkIntent {
+ handleCellularNetwork(trust: false, interaction: interaction)
+ }
+ }
+
+ private static func handleConnectVPN(_ intent: ConnectVPNIntent, interaction: INInteraction) {
+ guard let contextValue = intent.context, let context = Context(rawValue: contextValue), let id = intent.profileId else {
+ INInteraction.delete(with: [interaction.identifier], completion: nil)
+ return
+ }
+ let profileKey = ProfileKey(context, id)
+ log.info("Connect to profile \(profileKey)")
+
+ let service = TransientStore.shared.service
+ let vpn = VPN.shared
+ guard !(service.isActiveProfile(profileKey) && (vpn.status == .connected)) else {
+ log.info("Profile is already active and connected")
+ return
+ }
+
+ guard let profile = service.profile(withContext: context, id: id) else {
+ return
+ }
+ service.activateProfile(profile)
+
+ // FIXME: ServiceViewController is not updated
+
+ let configuration: VPNConfiguration
+ do {
+ configuration = try service.vpnConfiguration()
+ } catch let e {
+ log.error("Unable to build VPN configuration: \(e)")
+ return
+ }
+
+ vpn.reconnect(configuration: configuration) { (error) in
+ if let error = error {
+ log.error("Unable to connect to \(profileKey): \(error)")
+ return
+ }
+ log.info("Connecting to \(profileKey)...")
+ }
+ }
+
+ private static func handleDisableVPN(_ intent: DisableVPNIntent, interaction: INInteraction) {
+ VPN.shared.disconnect(completionHandler: nil)
+ }
+
+ private static func handleMoveToLocation(_ intent: MoveToLocationIntent, interaction: INInteraction) {
+ guard let providerId = intent.providerId, let poolId = intent.poolId else {
+ return
+ }
+ let service = TransientStore.shared.service
+ guard let providerProfile = service.profile(withContext: .provider, id: providerId) as? ProviderConnectionProfile else {
+ return
+ }
+ log.info("Move to provider \(providerId) @ [\(poolId)]")
+
+ let vpn = VPN.shared
+ guard !(service.isActiveProfile(providerProfile) && (providerProfile.poolId == poolId) && (vpn.status == .connected)) else {
+ log.info("Profile is already active and connected to \(poolId)")
+ return
+ }
+
+ providerProfile.poolId = poolId
+ service.activateProfile(providerProfile)
+
+ let configuration: VPNConfiguration
+ do {
+ configuration = try service.vpnConfiguration()
+ } catch let e {
+ log.error("Unable to build VPN configuration: \(e)")
+ return
+ }
+
+ vpn.reconnect(configuration: configuration) { (error) in
+ if let error = error {
+ log.error("Unable to connect to \(providerId) @ [\(poolId)]: \(error)")
+ return
+ }
+ log.info("Connecting to \(providerId) @ [\(poolId)]...")
+ }
+ }
+
+ private static func handleCurrentNetwork(trust: Bool, interaction: INInteraction) {
+ guard let currentWifi = Utils.currentWifiNetworkName() else {
+ return
+ }
+ let service = TransientStore.shared.service
+ service.preferences.trustedWifis[currentWifi] = trust
+ TransientStore.shared.serialize(withProfiles: false)
+
+ reconnectVPN(service: service)
+ }
+
+ private static func handleCellularNetwork(trust: Bool, interaction: INInteraction) {
+ guard Utils.hasCellularData() else {
+ return
+ }
+ let service = TransientStore.shared.service
+ service.preferences.trustsMobileNetwork = trust
+ TransientStore.shared.serialize(withProfiles: false)
+
+ reconnectVPN(service: service)
+ }
+
+ private static func reconnectVPN(service: ConnectionService) {
+ let configuration: VPNConfiguration
+ do {
+ configuration = try service.vpnConfiguration()
+ } catch let e {
+ log.error("Unable to build VPN configuration: \(e)")
+ return
+ }
+
+ let vpn = VPN.shared
+ switch vpn.status {
+ case .connected:
+ vpn.reconnect(configuration: configuration, completionHandler: nil)
+
+ default:
+ vpn.install(configuration: configuration, completionHandler: nil)
+ }
+ }
+
+ //
+
+ static func forgetProfile(withKey profileKey: ProfileKey) {
+ INInteraction.delete(with: profileKey.rawValue) { (error) in
+ if let error = error {
+ log.error("Unable to forget interactions: \(error)")
+ return
+ }
+ log.debug("Removed profile \(profileKey) interactions")
+ }
+ }
+}
+
+private extension INInteraction {
+ func donateAndLog() {
+ donate { (error) in
+ if let error = error {
+ log.error("Unable to donate interaction: \(error)")
+ }
+ log.debug("Donated \(self.intent)")
+ }
+ }
+}
diff --git a/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift b/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift
index c59e434e..f7029e7f 100644
--- a/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift
+++ b/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift
@@ -223,6 +223,9 @@ class OrganizerViewController: UITableViewController, TableModelHost {
tableView.endUpdates()
service.removeProfile(rowProfile)
+ if #available(iOS 12, *) {
+ InteractionsHandler.forgetProfile(withKey: rowProfile)
+ }
}
private func confirmVpnProfileDeletion() {
diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj
index f6d58b94..6868b390 100644
--- a/Passepartout.xcodeproj/project.pbxproj
+++ b/Passepartout.xcodeproj/project.pbxproj
@@ -31,6 +31,8 @@
0E4FD7E120D3E4C5002221FF /* MockVPNProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E4FD7E020D3E4C5002221FF /* MockVPNProvider.swift */; };
0E4FD7EE20D539A0002221FF /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E4FD7ED20D539A0002221FF /* Utils.swift */; };
0E4FD7F120D58618002221FF /* Macros.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E4FD7F020D58618002221FF /* Macros.swift */; };
+ 0E50E7C322330E4900D5F76C /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 0E50E7C222330E4900D5F76C /* Intents.intentdefinition */; };
+ 0E50E7C6223318A500D5F76C /* InteractionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E50E7C5223318A500D5F76C /* InteractionsHandler.swift */; };
0E57F63C20C83FC5008323CF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E57F63B20C83FC5008323CF /* AppDelegate.swift */; };
0E57F63E20C83FC5008323CF /* ServiceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E57F63D20C83FC5008323CF /* ServiceViewController.swift */; };
0E57F64120C83FC5008323CF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E57F63F20C83FC5008323CF /* Main.storyboard */; };
@@ -152,6 +154,8 @@
0E4FD7ED20D539A0002221FF /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; };
0E4FD7F020D58618002221FF /* Macros.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Macros.swift; sourceTree = ""; };
0E4FD80420D683A2002221FF /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/NetworkExtension.framework; sourceTree = DEVELOPER_DIR; };
+ 0E50E7C222330E4900D5F76C /* Intents.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = Intents.intentdefinition; sourceTree = ""; };
+ 0E50E7C5223318A500D5F76C /* InteractionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionsHandler.swift; sourceTree = ""; };
0E57F63820C83FC5008323CF /* Passepartout.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Passepartout.app; sourceTree = BUILT_PRODUCTS_DIR; };
0E57F63B20C83FC5008323CF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
0E57F63D20C83FC5008323CF /* ServiceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceViewController.swift; sourceTree = ""; };
@@ -289,6 +293,15 @@
path = VPN;
sourceTree = "";
};
+ 0E50E7C422330E5100D5F76C /* Intents */ = {
+ isa = PBXGroup;
+ children = (
+ 0E50E7C222330E4900D5F76C /* Intents.intentdefinition */,
+ 0E50E7C5223318A500D5F76C /* InteractionsHandler.swift */,
+ );
+ path = Intents;
+ sourceTree = "";
+ };
0E57F62F20C83FC5008323CF = {
isa = PBXGroup;
children = (
@@ -319,6 +332,7 @@
0E1066CA20E0F85C004F98B7 /* Cells */,
0ECEE44C20E1120F00A6BB43 /* Tables */,
0EDE8DF120C93ED8004C739C /* Scenes */,
+ 0E50E7C422330E5100D5F76C /* Intents */,
0EDE8DE220C86A13004C739C /* Passepartout.entitlements */,
0E57F63B20C83FC5008323CF /* AppDelegate.swift */,
0E57F63F20C83FC5008323CF /* Main.storyboard */,
@@ -826,6 +840,7 @@
0E1066C920E0F84A004F98B7 /* Cells.swift in Sources */,
0EF56BBB2185AC8500B0C8AB /* SwiftGen+Segues.swift in Sources */,
0EBE3AA6213DC1B000BFA2F5 /* ProviderConnectionProfile.swift in Sources */,
+ 0E50E7C322330E4900D5F76C /* Intents.intentdefinition in Sources */,
0E3DA371215CB5BF00B40FC9 /* VersionViewController.swift in Sources */,
0EBBE8F52182361800106008 /* ConnectionService+Migration.swift in Sources */,
0E39BCF3214DA9310035E9DE /* AppConstants.swift in Sources */,
@@ -834,6 +849,7 @@
0E79D14121919F5600BB5FB2 /* ProfileKey.swift in Sources */,
0E89DFC5213DF7AE00741BA1 /* Preferences.swift in Sources */,
0E6BE13A20CFB76800A6DD36 /* ApplicationError.swift in Sources */,
+ 0E50E7C6223318A500D5F76C /* InteractionsHandler.swift in Sources */,
0EFD9440215BED8E00529B64 /* LabelViewController.swift in Sources */,
0ED31C2C20CF2D6F0027975F /* ProviderPoolViewController.swift in Sources */,
0E2B494020FCFF990094784C /* Theme+Titles.swift in Sources */,
diff --git a/Passepartout/Sources/Model/Profiles/ProfileKey.swift b/Passepartout/Sources/Model/Profiles/ProfileKey.swift
index fe27ce52..3ee02f5c 100644
--- a/Passepartout/Sources/Model/Profiles/ProfileKey.swift
+++ b/Passepartout/Sources/Model/Profiles/ProfileKey.swift
@@ -25,7 +25,7 @@
import Foundation
-struct ProfileKey: RawRepresentable, Hashable, Codable {
+struct ProfileKey: RawRepresentable, Hashable, Codable, CustomStringConvertible {
private static let separator: Character = "."
let context: Context
@@ -63,4 +63,10 @@ struct ProfileKey: RawRepresentable, Hashable, Codable {
let idEnd = rawValue.endIndex
id = String(rawValue[idStart..