Skip to content

Commit 84feb95

Browse files
authored
Merge pull request #503 from LoopKit/share-server
Updates to allow multiple share server credentials to be stored
2 parents 7d5e1e5 + ac3f5e1 commit 84feb95

File tree

8 files changed

+250
-60
lines changed

8 files changed

+250
-60
lines changed

Loop/Managers/KeychainManager+Loop.swift

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,10 @@
99
import Foundation
1010

1111

12-
private let DexcomShareURL = URL(string: "https://share1.dexcom.com")!
1312
private let NightscoutAccount = "NightscoutAPI"
1413

1514

1615
extension KeychainManager {
17-
func setDexcomShareUsername(_ username: String?, password: String?) throws {
18-
let credentials: InternetCredentials?
19-
20-
if let username = username, let password = password {
21-
credentials = InternetCredentials(username: username, password: password, url: DexcomShareURL)
22-
} else {
23-
credentials = nil
24-
}
25-
26-
try replaceInternetCredentials(credentials, forURL: DexcomShareURL)
27-
}
28-
29-
func getDexcomShareCredentials() -> (username: String, password: String)? {
30-
do {
31-
let credentials = try getInternetCredentials(url: DexcomShareURL)
32-
33-
return (username: credentials.username, password: credentials.password)
34-
} catch {
35-
return nil
36-
}
37-
}
38-
3916
func setNightscoutURL(_ url: URL?, secret: String?) {
4017
let credentials: InternetCredentials?
4118

Loop/Managers/KeychainManager.swift

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ struct KeychainManager {
5555
return query
5656
}
5757

58-
private func queryForInternetPassword(account: String? = nil, url: URL? = nil) -> Query {
58+
private func queryForInternetPassword(account: String? = nil, url: URL? = nil, label: String? = nil) -> Query {
5959
var query = self.query(by: kSecClassInternetPassword)
6060

6161
if let account = account {
@@ -68,6 +68,10 @@ struct KeychainManager {
6868
}
6969
}
7070

71+
if let label = label {
72+
query[kSecAttrLabel as String] = label as NSObject?
73+
}
74+
7175
return query
7276
}
7377

@@ -135,8 +139,8 @@ struct KeychainManager {
135139

136140
// MARK – Internet Passwords
137141

138-
func setInternetPassword(_ password: String, forAccount account: String, atURL url: URL) throws {
139-
var query = try updatedQuery(queryForInternetPassword(account: account, url: url), withPassword: password)
142+
func setInternetPassword(_ password: String, account: String, atURL url: URL, label: String? = nil) throws {
143+
var query = try updatedQuery(queryForInternetPassword(account: account, url: url, label: label), withPassword: password)
140144

141145
query[kSecAttrAccount as String] = account as NSObject?
142146

@@ -146,6 +150,10 @@ struct KeychainManager {
146150
}
147151
}
148152

153+
if let label = label {
154+
query[kSecAttrLabel as String] = label as NSObject?
155+
}
156+
149157
let statusCode = SecItemAdd(query as CFDictionary, nil)
150158

151159
guard statusCode == errSecSuccess else {
@@ -159,7 +167,17 @@ struct KeychainManager {
159167
try delete(query)
160168

161169
if let credentials = credentials {
162-
try setInternetPassword(credentials.password, forAccount: credentials.username, atURL: credentials.url)
170+
try setInternetPassword(credentials.password, account: credentials.username, atURL: credentials.url)
171+
}
172+
}
173+
174+
func replaceInternetCredentials(_ credentials: InternetCredentials?, forLabel label: String) throws {
175+
let query = queryForInternetPassword(label: label)
176+
177+
try delete(query)
178+
179+
if let credentials = credentials {
180+
try setInternetPassword(credentials.password, account: credentials.username, atURL: credentials.url, label: label)
163181
}
164182
}
165183

@@ -169,12 +187,12 @@ struct KeychainManager {
169187
try delete(query)
170188

171189
if let credentials = credentials {
172-
try setInternetPassword(credentials.password, forAccount: credentials.username, atURL: credentials.url)
190+
try setInternetPassword(credentials.password, account: credentials.username, atURL: credentials.url)
173191
}
174192
}
175193

176-
func getInternetCredentials(account: String? = nil, url: URL? = nil) throws -> InternetCredentials {
177-
var query = queryForInternetPassword(account: account, url: url)
194+
func getInternetCredentials(account: String? = nil, url: URL? = nil, label: String? = nil) throws -> InternetCredentials {
195+
var query = queryForInternetPassword(account: account, url: url, label: label)
178196

179197
query[kSecReturnData as String] = kCFBooleanTrue
180198
query[kSecReturnAttributes as String] = kCFBooleanTrue

Loop/Managers/RemoteDataManager.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,17 @@ final class RemoteDataManager {
2525

2626
var shareService: ShareService {
2727
didSet {
28-
try! keychain.setDexcomShareUsername(shareService.username, password: shareService.password)
28+
try! keychain.setDexcomShareUsername(shareService.username, password: shareService.password, url: shareService.url)
2929
}
3030
}
3131

3232
private let keychain = KeychainManager()
3333

3434
init() {
35-
if let (username, password) = keychain.getDexcomShareCredentials() {
36-
shareService = ShareService(username: username, password: password)
35+
if let (username, password, url) = keychain.getDexcomShareCredentials() {
36+
shareService = ShareService(username: username, password: password, url: url)
3737
} else {
38-
shareService = ShareService(username: nil, password: nil)
38+
shareService = ShareService(username: nil, password: nil, url: nil)
3939
}
4040

4141
if let (siteURL, APISecret) = keychain.getNightscoutCredentials() {

Loop/Models/ServiceAuthentication/ServiceCredential.swift

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,32 @@
99
import UIKit
1010

1111

12-
// Represents a credential for a service, including its text input traits
12+
/// Represents a credential for a service, including its text input traits
1313
struct ServiceCredential {
14-
// The localized title of the credential (e.g. "Username")
14+
/// The localized title of the credential (e.g. "Username")
1515
let title: String
1616

17-
// The localized placeholder text to assist text input
17+
/// The localized placeholder text to assist text input
1818
let placeholder: String?
1919

20-
// Whether the credential is considered secret. Correponds to the `secureTextEntry` trait.
20+
/// Whether the credential is considered secret. Correponds to the `secureTextEntry` trait.
2121
let isSecret: Bool
2222

23-
// The type of keyboard to use to enter the credential
23+
/// The type of keyboard to use to enter the credential
2424
let keyboardType: UIKeyboardType
2525

26-
// The credential value
26+
/// The credential value
2727
var value: String?
28+
29+
/// A set of valid values for presenting a selection. The first item is the default.
30+
var options: [(title: String, value: String)]?
31+
32+
init(title: String, placeholder: String? = nil, isSecret: Bool, keyboardType: UIKeyboardType = .asciiCapable, value: String?, options: [(title: String, value: String)]? = nil) {
33+
self.title = title
34+
self.placeholder = placeholder
35+
self.isSecret = isSecret
36+
self.keyboardType = keyboardType
37+
self.value = value
38+
self.options = options
39+
}
2840
}

Loop/Models/ServiceAuthentication/ShareService.swift

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,32 @@ class ShareService: ServiceAuthentication {
1616

1717
let title: String = NSLocalizedString("Dexcom Share", comment: "The title of the Dexcom Share service")
1818

19-
init(username: String?, password: String?) {
19+
init(username: String?, password: String?, url: URL?) {
2020
credentials = [
2121
ServiceCredential(
2222
title: NSLocalizedString("Username", comment: "The title of the Dexcom share username credential"),
23-
placeholder: nil,
2423
isSecret: false,
2524
keyboardType: .asciiCapable,
2625
value: username
2726
),
2827
ServiceCredential(
2928
title: NSLocalizedString("Password", comment: "The title of the Dexcom share password credential"),
30-
placeholder: nil,
3129
isSecret: true,
3230
keyboardType: .asciiCapable,
3331
value: password
32+
),
33+
ServiceCredential(
34+
title: NSLocalizedString("Server", comment: "The title of the Dexcom share server URL credential"),
35+
isSecret: false,
36+
value: url?.absoluteString,
37+
options: [
38+
(title: NSLocalizedString("US", comment: "U.S. share server option title"),
39+
value: DexcomShareURL.absoluteString)
40+
]
3441
)
3542
]
3643

37-
if let username = username, let password = password {
44+
if let username = username, let password = password, url != nil {
3845
isAuthorized = true
3946
client = ShareClient(username: username, password: password)
4047
}
@@ -51,10 +58,18 @@ class ShareService: ServiceAuthentication {
5158
return credentials[1].value
5259
}
5360

61+
var url: URL? {
62+
guard let urlString = credentials[2].value else {
63+
return nil
64+
}
65+
66+
return URL(string: urlString)
67+
}
68+
5469
var isAuthorized: Bool = false
5570

5671
func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) {
57-
guard let username = username, let password = password else {
72+
guard let username = username, let password = password, url != nil else {
5873
completion(false, nil)
5974
return
6075
}
@@ -69,7 +84,49 @@ class ShareService: ServiceAuthentication {
6984
func reset() {
7085
credentials[0].value = nil
7186
credentials[1].value = nil
87+
credentials[2].value = nil
7288
isAuthorized = false
7389
client = nil
7490
}
7591
}
92+
93+
94+
private let DexcomShareURL = URL(string: "https://share1.dexcom.com")!
95+
private let DexcomShareServiceLabel = "DexcomShare1"
96+
97+
98+
extension KeychainManager {
99+
func setDexcomShareUsername(_ username: String?, password: String?, url: URL?) throws {
100+
let credentials: InternetCredentials?
101+
102+
if let username = username, let password = password, let url = url {
103+
credentials = InternetCredentials(username: username, password: password, url: url)
104+
} else {
105+
credentials = nil
106+
}
107+
108+
// Replace the legacy URL-keyed credentials
109+
try replaceInternetCredentials(nil, forURL: DexcomShareURL)
110+
111+
try replaceInternetCredentials(credentials, forLabel: DexcomShareServiceLabel)
112+
}
113+
114+
func getDexcomShareCredentials() -> (username: String, password: String, url: URL)? {
115+
do { // Silence all errors and return nil
116+
do {
117+
let credentials = try getInternetCredentials(label: DexcomShareServiceLabel)
118+
119+
return (username: credentials.username, password: credentials.password, url: credentials.url)
120+
} catch KeychainManagerError.copy {
121+
// Fetch and replace the legacy URL-keyed credentials
122+
let credentials = try getInternetCredentials(url: DexcomShareURL)
123+
124+
try setDexcomShareUsername(credentials.username, password: credentials.password, url: credentials.url)
125+
126+
return (username: credentials.username, password: credentials.password, url: credentials.url)
127+
}
128+
} catch {
129+
return nil
130+
}
131+
}
132+
}

Loop/View Controllers/AuthenticationViewController.swift

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ final class AuthenticationViewController<T: ServiceAuthentication>: UITableViewC
5959
}
6060
}
6161

62+
var credentials: [ServiceCredential] {
63+
switch state {
64+
case .authorized:
65+
return authentication.credentials.filter({ !$0.isSecret })
66+
default:
67+
return authentication.credentials
68+
}
69+
}
70+
6271
init(authentication: T) {
6372
self.authentication = authentication
6473

@@ -89,12 +98,7 @@ final class AuthenticationViewController<T: ServiceAuthentication>: UITableViewC
8998
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
9099
switch Section(rawValue: section)! {
91100
case .credentials:
92-
switch state {
93-
case .authorized:
94-
return authentication.credentials.filter({ !$0.isSecret }).count
95-
default:
96-
return authentication.credentials.count
97-
}
101+
return credentials.count
98102
case .button:
99103
return 1
100104
}
@@ -126,16 +130,23 @@ final class AuthenticationViewController<T: ServiceAuthentication>: UITableViewC
126130
case .credentials:
127131
let cell = tableView.dequeueReusableCell(withIdentifier: AuthenticationTableViewCell.className, for: indexPath) as! AuthenticationTableViewCell
128132

129-
let credential = authentication.credentials[indexPath.row]
133+
let credentials = self.credentials
134+
let credential = credentials[indexPath.row]
130135

131136
cell.titleLabel.text = credential.title
132-
cell.textField.tag = indexPath.row
133137
cell.textField.keyboardType = credential.keyboardType
134138
cell.textField.isSecureTextEntry = credential.isSecret
135-
cell.textField.returnKeyType = (indexPath.row < authentication.credentials.count - 1) ? .next : .done
139+
cell.textField.returnKeyType = (indexPath.row < credentials.count - 1) ? .next : .done
136140
cell.textField.text = credential.value
137141
cell.textField.placeholder = credential.placeholder ?? NSLocalizedString("Required", comment: "The default placeholder string for a credential")
138142

143+
if let options = credential.options {
144+
let picker = CredentialOptionPicker(options: options)
145+
picker.value = credential.value
146+
147+
cell.credentialOptionPicker = picker
148+
}
149+
139150
cell.textField.delegate = self
140151

141152
switch state {
@@ -149,7 +160,7 @@ final class AuthenticationViewController<T: ServiceAuthentication>: UITableViewC
149160
}
150161
}
151162

152-
private func validate() {
163+
fileprivate func validate() {
153164
state = .verifying
154165
}
155166

@@ -173,7 +184,16 @@ final class AuthenticationViewController<T: ServiceAuthentication>: UITableViewC
173184
// MARK: - UITextFieldDelegate
174185

175186
func textFieldDidEndEditing(_ textField: UITextField) {
176-
authentication.credentials[textField.tag].value = textField.text
187+
let point = tableView.convert(textField.frame.origin, from: textField.superview)
188+
189+
guard case .unauthorized = state,
190+
let indexPath = tableView.indexPathForRow(at: point),
191+
let cell = tableView.cellForRow(at: IndexPath(row: indexPath.row, section: indexPath.section)) as? AuthenticationTableViewCell
192+
else {
193+
return
194+
}
195+
196+
authentication.credentials[indexPath.row].value = cell.value
177197
}
178198

179199
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
@@ -191,6 +211,10 @@ final class AuthenticationViewController<T: ServiceAuthentication>: UITableViewC
191211

192212
return true
193213
}
214+
215+
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
216+
return textField.inputView == nil
217+
}
194218
}
195219

196220

0 commit comments

Comments
 (0)