Commit ad974441 authored by Jose Blaya's avatar Jose Blaya
Browse files

DIP logic

parent ba3cd1f5
Pod::Spec.new do |s|
s.name = "PIALibrary"
s.version = "2.8.1"
s.version = "2.8.2"
s.summary = "PIA client library in Swift."
s.homepage = "https://www.privateinternetaccess.com/"
......
......@@ -240,6 +240,8 @@
8221921F24CEBABA00C24F1C /* NMTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8221921E24CEBABA00C24F1C /* NMTType.swift */; };
8221922024CEBABA00C24F1C /* NMTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8221921E24CEBABA00C24F1C /* NMTType.swift */; };
822BC1D024BF20C90041BF9A /* UIControl+Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = 822BC1CF24BF20C90041BF9A /* UIControl+Action.swift */; };
829EB63F2535C432003E74DD /* DedicatedIP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 829EB63E2535C432003E74DD /* DedicatedIP.swift */; };
829EB6402535C432003E74DD /* DedicatedIP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 829EB63E2535C432003E74DD /* DedicatedIP.swift */; };
82C374F82514DE7D00E391EE /* EndpointManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82C374F42514DC6D00E391EE /* EndpointManagerTests.swift */; };
82C374F92514DE8200E391EE /* server.json in Resources */ = {isa = PBXBuildFile; fileRef = 82C374F62514DC7200E391EE /* server.json */; };
82C374FB2514DEC700E391EE /* EndpointManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82C374FA2514DEC700E391EE /* EndpointManager.swift */; };
......@@ -559,6 +561,7 @@
8221921B24CEBA3800C24F1C /* NMTRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NMTRules.swift; sourceTree = "<group>"; };
8221921E24CEBABA00C24F1C /* NMTType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NMTType.swift; sourceTree = "<group>"; };
822BC1CF24BF20C90041BF9A /* UIControl+Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+Action.swift"; sourceTree = "<group>"; };
829EB63E2535C432003E74DD /* DedicatedIP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DedicatedIP.swift; sourceTree = "<group>"; };
82C374F42514DC6D00E391EE /* EndpointManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EndpointManagerTests.swift; sourceTree = "<group>"; };
82C374F62514DC7200E391EE /* server.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = server.json; sourceTree = "<group>"; };
82C374FA2514DEC700E391EE /* EndpointManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EndpointManager.swift; sourceTree = "<group>"; };
......@@ -858,6 +861,7 @@
DD56E3F5225F5D77002EDFB2 /* Product.swift */,
DD6768E222FAB19D00B9FDD0 /* AppStoreInformation.swift */,
82E20B1524F6AA110065EFE3 /* RegionData.swift */,
829EB63E2535C432003E74DD /* DedicatedIP.swift */,
);
path = WebServices;
sourceTree = "<group>";
......@@ -1526,7 +1530,11 @@
"${BUILT_PRODUCTS_DIR}/FXPageControl/FXPageControl.framework",
"${BUILT_PRODUCTS_DIR}/Gloss/Gloss.framework",
"${PODS_ROOT}/OpenSSL-Apple/frameworks/iPhone/openssl.framework",
"${PODS_ROOT}/../../account/build/cocoapods/framework/PIAAccount.framework",
"${PODS_ROOT}/../../account/build/cocoapods/framework/PIAAccount.framework.dSYM",
"${BUILT_PRODUCTS_DIR}/PIAAccountModule/PIAAccountModule.framework",
"${PODS_ROOT}/PIARegions/regions/build/cocoapods/framework/Regions.framework",
"${PODS_ROOT}/PIARegions/regions/build/cocoapods/framework/Regions.framework.dSYM",
"${BUILT_PRODUCTS_DIR}/PIARegions/PIARegions.framework",
"${BUILT_PRODUCTS_DIR}/PIAWireguard/PIAWireguard.framework",
"${BUILT_PRODUCTS_DIR}/PopupDialog/PopupDialog.framework",
......@@ -1545,7 +1553,11 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FXPageControl.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Gloss.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PIAAccount.framework",
"${DWARF_DSYM_FOLDER_PATH}/PIAAccount.framework.dSYM",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PIAAccountModule.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Regions.framework",
"${DWARF_DSYM_FOLDER_PATH}/Regions.framework.dSYM",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PIARegions.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PIAWireguard.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PopupDialog.framework",
......@@ -1574,7 +1586,11 @@
"${BUILT_PRODUCTS_DIR}/FXPageControl/FXPageControl.framework",
"${BUILT_PRODUCTS_DIR}/Gloss/Gloss.framework",
"${PODS_ROOT}/OpenSSL-Apple/frameworks/iPhone/openssl.framework",
"${PODS_ROOT}/../../account/build/cocoapods/framework/PIAAccount.framework",
"${PODS_ROOT}/../../account/build/cocoapods/framework/PIAAccount.framework.dSYM",
"${BUILT_PRODUCTS_DIR}/PIAAccountModule/PIAAccountModule.framework",
"${PODS_ROOT}/PIARegions/regions/build/cocoapods/framework/Regions.framework",
"${PODS_ROOT}/PIARegions/regions/build/cocoapods/framework/Regions.framework.dSYM",
"${BUILT_PRODUCTS_DIR}/PIARegions/PIARegions.framework",
"${BUILT_PRODUCTS_DIR}/PIAWireguard/PIAWireguard.framework",
"${BUILT_PRODUCTS_DIR}/PopupDialog/PopupDialog.framework",
......@@ -1593,7 +1609,11 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FXPageControl.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Gloss.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PIAAccount.framework",
"${DWARF_DSYM_FOLDER_PATH}/PIAAccount.framework.dSYM",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PIAAccountModule.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Regions.framework",
"${DWARF_DSYM_FOLDER_PATH}/Regions.framework.dSYM",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PIARegions.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PIAWireguard.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PopupDialog.framework",
......@@ -1679,6 +1699,7 @@
0E3D13DA1F9E273300434A48 /* GlossSignup.swift in Sources */,
0E392D961FE316860002160D /* DebugLog.swift in Sources */,
0E7BC6EB1F96B1040035C8B2 /* PIAWebServices.swift in Sources */,
829EB6402535C432003E74DD /* DedicatedIP.swift in Sources */,
0E245C9E1FECF0C20010DEF2 /* ClientAccess.swift in Sources */,
DD2683EA24617F0300C65DAA /* PingTask.swift in Sources */,
0E9D62D21FDEBBC8009A90CF /* GlossServersBundle.swift in Sources */,
......@@ -1852,6 +1873,7 @@
0EBBC6DC1F9F64E700B8BD21 /* Client+Environment.swift in Sources */,
0EB3D9821FF02FE5005B11F4 /* VPNAction.swift in Sources */,
0E2ADD371FE14F0000BB170C /* DefaultVPNProvider.swift in Sources */,
829EB63F2535C432003E74DD /* DedicatedIP.swift in Sources */,
0EA8073220A2F50A0033EC1A /* SignupMetadata.swift in Sources */,
0E4D4E9F1FA4CA7A007DA6DA /* Restylable.swift in Sources */,
0EB8C06E1F9CD38B005857E4 /* SignupInternetUnreachableViewController.swift in Sources */,
......
......@@ -46,5 +46,13 @@ protocol SecureStore: class {
func tokenKey(for username: String) -> String
func dipTokens() -> [String]?
func setDIPToken(_ dipToken: String)
func remove(_ dipToken: String)
func removeDIPTokens()
func clear(for username: String)
}
......@@ -46,6 +46,9 @@ public protocol ServerProvider: class {
var regionStaticData: RegionData! { get }
/// The array of DIP tokens stored in the keychain, or `nil` if logged out.
var dipTokens: [String]? { get }
/**
Loads this provider with a local JSON, as seen on the /servers web client API.
......@@ -94,4 +97,31 @@ public protocol ServerProvider: class {
Reset the currentServers object
*/
func resetCurrentServers()
/**
Activates the dedicated IP tokens.
- Precondition: `isLoggedIn` is `true`.
- Parameter tokens: The `String` array of DIP token to activate.
- Parameter callback: Returns the status of the DIP region `Server` array.
*/
func activateDIPTokens(_ tokens: [String], _ callback: LibraryCallback<[Server]>?)
/**
Activates the dedicated IP token.
- Precondition: `isLoggedIn` is `true`.
- Parameter tokens: The `String` DIP token to activate.
- Parameter callback: Returns the status of the DIP region `Server`.
*/
func activateDIPToken(_ token: String, _ callback: LibraryCallback<Server?>?)
/**
Removes the dedicated IP region.
- Precondition: `isLoggedIn` is `true`.
- Parameter dipToken: The `String` DIP token to remove.
*/
func removeDIPToken(_ dipToken: String)
}
//
// DedicatedIP.swift
// PIALibrary
//
// Created by Jose Blaya on 13/10/2020.
// Copyright © 2020 Private Internet Access, Inc.
//
// This file is part of the Private Internet Access iOS Client.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
public enum DedicatedIPStatus {
case active
case expired
case invalid
}
......@@ -145,6 +145,12 @@ public class Server: Hashable {
/// The meta IP.
public let meta: ServerAddressIP?
public let dipExpire: Date?
public let dipToken: String?
public let dipStatus: DedicatedIPStatus?
var isAutomatic: Bool
/// :nodoc:
......@@ -164,6 +170,9 @@ public class Server: Hashable {
serverNetwork: ServersNetwork? = .legacy,
geo: Bool = false,
meta: ServerAddressIP? = nil,
dipExpire: Date? = nil,
dipToken: String? = nil,
dipStatus: DedicatedIPStatus? = nil,
regionIdentifier: String) {
self.serial = serial
......@@ -185,6 +194,10 @@ public class Server: Hashable {
self.pingAddress = pingAddress
self.serverNetwork = serverNetwork
self.dipExpire = dipExpire
self.dipToken = dipToken
self.dipStatus = dipStatus
isAutomatic = true
}
......
......@@ -40,6 +40,10 @@ protocol WebServices: class {
func loginLink(email: String, _ callback: SuccessLibraryCallback?)
// MARK: DIP Token
func activateDIPToken(tokens: [String], _ callback: LibraryCallback<[Server]>?)
/**
Invalidates the access token.
- Parameter callback: Returns an `Bool` if the token was expired.
......
......@@ -544,6 +544,7 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
accessedDatabase.secure.clear(for: username)
accessedDatabase.secure.setToken(nil, for: accessedDatabase.secure.tokenKey(for: username))
}
accessedDatabase.secure.removeDIPTokens()
accessedDatabase.secure.setPublicUsername(nil)
accessedDatabase.plain.accountInfo = nil
accessedDatabase.plain.visibleTiles = AvailableTiles.defaultTiles()
......
......@@ -117,3 +117,23 @@ extension KeychainStore {
}
}
extension KeychainStore {
func dipTokens() -> [String]? {
return try? backend.dipTokens()
}
func setDIPToken(_ dipToken: String) {
try? backend.set(dipToken: dipToken)
}
func remove(_ dipToken: String) {
try? backend.remove(dipToken: dipToken)
}
func removeDIPTokens() {
try? backend.removeDIPTokens()
}
}
......@@ -117,6 +117,10 @@ class DefaultServerProvider: ServerProvider, ConfigurationAccess, DatabaseAccess
return server
}
var dipTokens: [String]? {
return accessedDatabase.secure.dipTokens()
}
public var regionStaticData: RegionData!
func loadLocalJSON(fromJSON jsonData: Data) {
......@@ -153,8 +157,20 @@ class DefaultServerProvider: ServerProvider, ConfigurationAccess, DatabaseAccess
if let configuration = bundle.configuration {
self.accessedDatabase.transient.serversConfiguration = configuration
}
self.currentServers = bundle.servers
callback?(bundle.servers, error)
if let tokens = self.accessedDatabase.secure.dipTokens() {
self.webServices.activateDIPToken(tokens: tokens) { (servers, error) in
if let servers = servers {
self.currentServers.append(contentsOf: servers)
}
callback?(self.currentServers, error)
}
} else {
callback?(self.currentServers, error)
}
}
}
......@@ -167,6 +183,49 @@ class DefaultServerProvider: ServerProvider, ConfigurationAccess, DatabaseAccess
}
}
func activateDIPToken(_ token: String, _ callback: LibraryCallback<Server?>?) {
guard Client.providers.accountProvider.isLoggedIn else {
preconditionFailure()
}
webServices.activateDIPToken(tokens: [token]) { (servers, error) in
if let servers = servers,
let first = servers.first {
if !self.currentServers.contains(where: {$0.dipToken == first.dipToken}) {
self.currentServers.append(contentsOf: servers)
}
callback?(first, error)
} else {
callback?(nil, error)
}
}
}
func activateDIPTokens(_ tokens: [String], _ callback: LibraryCallback<[Server]>?) {
guard Client.providers.accountProvider.isLoggedIn else {
preconditionFailure()
}
webServices.activateDIPToken(tokens: tokens) { (servers, error) in
if let servers = servers {
for server in servers {
if !self.currentServers.contains(where: {$0.dipToken == server.dipToken}) {
self.currentServers.append(server)
}
}
callback?(servers, error)
} else {
callback?([], error)
}
}
}
func removeDIPToken(_ dipToken: String) {
guard Client.providers.accountProvider.isLoggedIn else {
preconditionFailure()
}
accessedDatabase.secure.remove(dipToken)
self.currentServers = self.currentServers.filter({$0.dipToken != dipToken})
}
func find(withIdentifier identifier: String) -> Server? {
return currentServers.first { $0.identifier == identifier }
}
......
......@@ -175,7 +175,7 @@ class PIAWebServices: WebServices, ConfigurationAccess {
callback?(nil)
}
} else {
callback?(ClientError.invalidParameter)
callback?(ClientError.unauthorized)
}
} else {
//We use the email and the password returned by the signup endpoint in the previous step, we don't update the password
......@@ -217,11 +217,41 @@ class PIAWebServices: WebServices, ConfigurationAccess {
callback?(true, nil)
}
} else {
callback?(false, ClientError.invalidParameter)
callback?(false, ClientError.unauthorized)
}
}
func activateDIPToken(tokens: [String], _ callback: LibraryCallback<[Server]>?) {
if let token = Client.providers.accountProvider.token {
self.accountAPI.dedicatedIPs(token: token, ipTokens: tokens) { (dedicatedIps, error) in
if let _ = error {
callback?([], ClientError.invalidParameter)
return
}
var dipRegions = [Server]()
for dipServer in dedicatedIps {
if dipServer.status == DedicatedIPInformationResponse.Status.active {
//Replace ES with dipServer.id
var firstServer = Client.providers.serverProvider.currentServers.first(where: {$0.country == "ES"})
let dipRegion = Server(serial: firstServer!.serial, name: firstServer!.name, country: firstServer!.country, hostname: firstServer!.hostname, bestOpenVPNAddressForTCP: firstServer!.bestOpenVPNAddressForTCP, bestOpenVPNAddressForUDP: firstServer!.bestOpenVPNAddressForUDP, openVPNAddressesForTCP: [Server.ServerAddressIP(ip: dipServer.ip!, cn: dipServer.cn!)], openVPNAddressesForUDP: [Server.ServerAddressIP(ip: dipServer.ip!, cn: dipServer.cn!)], wireGuardAddressesForUDP: [Server.ServerAddressIP(ip: dipServer.ip!, cn: dipServer.cn!)], iKEv2AddressesForUDP: [Server.ServerAddressIP(ip: dipServer.ip!, cn: dipServer.cn!)], pingAddress: firstServer!.pingAddress, serverNetwork: ServersNetwork.gen4, geo: false, meta: nil, dipExpire: Date(timeIntervalSince1970: TimeInterval(dipServer.dip_expire!)), dipToken: dipServer.dipToken, dipStatus: DedicatedIPStatus.active, regionIdentifier: firstServer!.regionIdentifier)
dipRegions.append(dipRegion)
Client.database.secure.setDIPToken(dipServer.dipToken)
}
}
callback?(dipRegions, nil)
}
}else {
callback?([], ClientError.unauthorized)
}
}
#if os(iOS)
func signup(with request: Signup, _ callback: ((Credentials?, Error?) -> Void)?) {
......
......@@ -183,7 +183,7 @@ public class MockAccountProvider: AccountProvider, WebServicesConsumer {
public var currentPasswordReference: Data? {
return delegate.currentPasswordReference
}
#if os(iOS)
/// :nodoc:
public var lastSignupRequest: SignupRequest? {
......
......@@ -122,6 +122,10 @@ public class MockServerProvider: ServerProvider, DatabaseAccess, WebServicesCons
return RegionData(translations: [String : [String : String]](), geolocations: [String : [String]]())
}
public var dipTokens: [String]? {
return []
}
/// :nodoc:
public func load(fromJSON jsonData: Data) {
return delegate.load(fromJSON: jsonData)
......@@ -151,4 +155,16 @@ public class MockServerProvider: ServerProvider, DatabaseAccess, WebServicesCons
public func resetCurrentServers() {
}
public func removeDIPToken(_ dipToken: String) {
delegate.removeDIPToken(dipToken)
}
public func activateDIPToken(_ token: String, _ callback: LibraryCallback<Server?>?) {
delegate.activateDIPToken(token, callback)
}
public func activateDIPTokens(_ tokens: [String], _ callback: LibraryCallback<[Server]>?) {
delegate.activateDIPTokens(tokens, callback)
}
}
......@@ -68,6 +68,10 @@ class MockWebServices: WebServices {
callback?(true, nil)
}
func activateDIPToken(tokens: [String], _ callback: LibraryCallback<[Server]>?) {
callback?([], nil)
}
func signup(with request: Signup, _ callback: ((Credentials?, Error?) -> Void)?) {
let result = credentials?()
let error: ClientError? = (result == nil) ? .unsupported : nil
......
......@@ -302,6 +302,41 @@ extension Macros {
SwiftEntryKit.display(entry: contentView,
using: attributes)
}
/**
Shortcut to display a warning `EKImageNoteMessageView`.
- Parameter image: The note image
- Parameter message: The note message
- Parameter duration: Optional duration of the note
*/
public static func displayWarningImageNote(withImage image: UIImage,
message: String,
andDuration duration: Double? = nil) {
var attributes = EKAttributes()
attributes = .topToast
attributes.hapticFeedbackType = .success
attributes.entryBackground = .color(color: UIColor.piaOrange)
attributes.positionConstraints.size = .init(width: EKAttributes.PositionConstraints.Edge.fill,
height: EKAttributes.PositionConstraints.Edge.constant(value: bannerHeight))
if let duration = duration {
attributes.displayDuration = duration
}
let labelContent = EKProperty.LabelContent(text: message,
style: .init(font: TextStyle.textStyle7.font!,
color: .white))
let imageContent = EKProperty.ImageContent(image: image)
let contentView = EKImageNoteMessageView(with: labelContent,
imageContent: imageContent)
SwiftEntryKit.display(entry: contentView,
using: attributes)
}
/**
Shortcut to display an infinite `EKImageNoteMessageView`.
......
......@@ -371,6 +371,10 @@ class EphemeralAccountProvider: AccountProvider, ProvidersAccess, InAppAccess {
fatalError("Not implemented")
}
func activateDIPTokens(_ dipToken: String, _ callback: LibraryCallback<DedicatedIPStatus>?) {
fatalError("Not implemented")
}
func cleanDatabase() {
fatalError("Not implemented")
}
......
......@@ -43,6 +43,7 @@ public class Keychain {
private let usernameKey = "USERNAME_KEY"
private let publicUsernameKey = "PUBLIC_USERNAME_KEY"
private let dipTokensKey = "DIP_TOKENS_KEY"
/**
Default initializer. Uses the default keychain associated with the main bundle identifier.
......@@ -397,3 +398,96 @@ extension Keychain {
}
}
extension Keychain {
// MARK: DIP Region
/// :nodoc:
public func set(dipToken: String) throws {
var tokens = [String]()
if let storedTokens = try? dipTokens() {
removeDIPTokens()
if !storedTokens.contains(where: {$0 == dipToken }){
tokens.append(contentsOf: storedTokens)
}
}
tokens.append(dipToken)
var query = [String: Any]()
setScope(query: &query)
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrAccount as String] = dipTokensKey
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
let encoder = JSONEncoder()
query[kSecValueData as String] = try? encoder.encode(tokens)
let status = SecItemAdd(query as CFDictionary, nil)
guard (status == errSecSuccess) else {
throw KeychainError.add
}
}
/// :nodoc:
@discardableResult public func remove(dipToken: String) throws {
var tokens = [String]()
if let storedTokens = try? dipTokens() {
removeDIPTokens()
tokens = storedTokens.filter({ $0 != dipToken })
}
var query = [String: Any]()
setScope(query: &query)
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrAccount as String] = dipTokensKey
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
let encoder = JSONEncoder()
query[kSecValueData as String] = try? encoder.encode(tokens)
let status = SecItemAdd(query as CFDictionary, nil)
guard (status == errSecSuccess) else {
throw KeychainError.add
}
}
/// :nodoc:
@discardableResult public func removeDIPTokens() -> Bool {
var query = [String: Any]()
setScope(query: &query)
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrAccount as String] = dipTokensKey
let status = SecItemDelete(query as CFDictionary)
return (status == errSecSuccess)
}
/// :nodoc:
public func dipTokens() throws -> [String] {