diff --git a/FitKit/Assets.xcassets/AppIcon.appiconset/Contents.json b/FitKit/Assets.xcassets/AppIcon.appiconset/Contents.json index d8db8d6..c3bd516 100644 --- a/FitKit/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/FitKit/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -31,13 +31,15 @@ "scale" : "3x" }, { - "idiom" : "iphone", "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon-120.png", "scale" : "2x" }, { - "idiom" : "iphone", "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon-180.png", "scale" : "3x" }, { diff --git a/FitKit/Assets.xcassets/AppIcon.appiconset/icon-120.png b/FitKit/Assets.xcassets/AppIcon.appiconset/icon-120.png new file mode 100644 index 0000000..ae6b3b9 Binary files /dev/null and b/FitKit/Assets.xcassets/AppIcon.appiconset/icon-120.png differ diff --git a/FitKit/Assets.xcassets/AppIcon.appiconset/icon-180.png b/FitKit/Assets.xcassets/AppIcon.appiconset/icon-180.png new file mode 100644 index 0000000..07f1998 Binary files /dev/null and b/FitKit/Assets.xcassets/AppIcon.appiconset/icon-180.png differ diff --git a/FitKit/Assets.xcassets/Contents.json b/FitKit/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/FitKit/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FitKit/Base.lproj/Main.storyboard b/FitKit/Base.lproj/Main.storyboard index 2ba3971..8ac3174 100644 --- a/FitKit/Base.lproj/Main.storyboard +++ b/FitKit/Base.lproj/Main.storyboard @@ -27,14 +27,14 @@ - - + + @@ -44,7 +44,7 @@ - + diff --git a/FitKit/FitbitClient.swift b/FitKit/FitbitClient.swift index 12e75e2..7f638e9 100644 --- a/FitKit/FitbitClient.swift +++ b/FitKit/FitbitClient.swift @@ -15,6 +15,7 @@ import OAuthSwiftAlamofire class FitbitClient { private enum FitKitRequestError: Error { case unableToParseResults + case invalidAuthorization } private var oauthClient: OAuth2Swift @@ -24,6 +25,7 @@ class FitbitClient { private let authorizeUrl = "https://www.fitbit.com/oauth2/authorize" private let accessTokenUrl = "https://api.fitbit.com/oauth2/token" private var keychainWrapper: KeychainWrapper + private let keychainCredKey = "credential" init() { // Init inner client @@ -44,10 +46,11 @@ class FitbitClient { self.keychainWrapper = KeychainWrapper.standard // Attempt to load tokens - self.loadStoredTokens() + self.maybeLoadStoredCredential() } - func loadStoredTokens() { + /// Attempts to load and use stored credentials + func maybeLoadStoredCredential() { if let credential = self.readCredential() { NSLog("Using credential from Keychain") self.oauthClient.client.credential.oauthToken = credential.oauthToken @@ -55,8 +58,11 @@ class FitbitClient { } } + /// Attempts to read OAuthSwiftCredential from the keychain + /// + /// - Returns: If a valid credential is found, it will be returned func readCredential() -> OAuthSwiftCredential? { - if let credential = self.keychainWrapper.object(forKey: "credential") as? OAuthSwiftCredential { + if let credential = self.keychainWrapper.object(forKey: keychainCredKey) as? OAuthSwiftCredential { NSLog("Found credential in keychain") return credential } @@ -64,10 +70,21 @@ class FitbitClient { return nil } + /// Persists an OAuthSwiftCredential into the keychain + /// + /// - Parameter credential: credential to be persisted + /// - Returns: TRUE if persisted successfully private func storeCredential(_ credential: OAuthSwiftCredential) -> Bool { - return self.keychainWrapper.set(credential, forKey: "credential") + return self.keychainWrapper.set(credential, forKey: keychainCredKey) } - + + /// Clears credential token and expiration + func clearCredential() { + self.oauthClient.client.credential.oauthToken = "" + self.oauthClient.client.credential.oauthTokenExpiresAt = nil + self.keychainWrapper.removeObject(forKey: keychainCredKey) + } + /// Check if the current client is already authorized /// /// - Returns: True if the current client can make requests @@ -90,12 +107,13 @@ class FitbitClient { /// - viewController: the source controller to create the new Safari view /// - success: Callback to be executed on success /// - failure: Callback to be executed on failure - func authorize(viewController: UIViewController, success: @escaping () -> Void, failure: @escaping () -> Void) { + func authorize(viewController: UIViewController, callback: @escaping (String?, Error?) -> Void) { // Set webview handler self.oauthClient.authorizeURLHandler = SafariURLHandler( viewController: viewController, oauthSwift: self.oauthClient ) + // Authorize let _ = self.oauthClient.authorize( withCallbackURL: URL(string: self.callbackUrl)!, @@ -108,19 +126,18 @@ class FitbitClient { let scope = parameters["scope"] as? String else { NSLog("Invalid authorization response") - failure() + callback(nil, FitKitRequestError.invalidAuthorization) return } NSLog("Authorized scope: \(scope)") NSLog("Succesfully authenticated with credentials \(credential.oauthToken)") let _ = self.storeCredential(credential) - success() + callback(scope, nil) }, failure: { error in - // TODO: Maybe return the error - NSLog(error.localizedDescription) - failure() + NSLog("Error authorizing \(error.localizedDescription)") + callback(nil, error) } ) } diff --git a/FitKit/ViewController.swift b/FitKit/ViewController.swift index 6687dcf..c1a06bf 100644 --- a/FitKit/ViewController.swift +++ b/FitKit/ViewController.swift @@ -23,7 +23,7 @@ class ViewController: UIViewController { } override func viewDidAppear(_ animated: Bool) { - authorize() + ensureAuthorized(callback: self.updateView) } override func didReceiveMemoryWarning() { @@ -34,35 +34,53 @@ class ViewController: UIViewController { fileprivate func updateAuthorizedLabel() { if self.client.isAuthorized() { self.loggedIn.text = "Logged In" + //self.loginButton.currentTitle = "Log Out" } else { self.loggedIn.text = "Not Logged In" + //self.loginButton.currentTitle = "Log In" } } - fileprivate func authorize() { - HealthKitHelper.authorizeHealthKit() { - result, error in - // TODO: Do something with the result and error - if !self.client.isAuthorized() { - self.client.authorize(viewController: self, success: self.authorized, failure: self.unauthroized) - } else { - // TODO: this should be made significantly more clear - self.displayWeight() + /// Ensure that the client is authorized and then continue + fileprivate func ensureAuthorized(callback: @escaping (Bool) -> Void) { + // If already authroized, we can short circuit + if self.client.isAuthorized() { + callback(true) + return + } + // Authorize the client + self.client.authorize(viewController: self) { + scope, error in + if error != nil { + NSLog("Error encountered when attempting authorization: \(error.debugDescription)") + } + guard let scope = scope else { + NSLog("Did not retreive any authorized scope") + callback(false) + return + } + NSLog("Authorized for scope: \(scope). Requesting HealthKit access") + HealthKitHelper.authorizeHealthKit() { + result, error in + // TODO: Do something with the result and error + if error != nil { + callback(false) + } else { + callback(true) + } } } } - fileprivate func authorized() { - NSLog("We have Fitbit auth!!!") - updateAuthorizedLabel() - self.displayWeight() - } - - fileprivate func unauthroized() { - NSLog("No auth. Booo!!!") + fileprivate func updateView(authSuccess: Bool) { + self.updateAuthorizedLabel() + if authSuccess { + self.syncWeights() + } } - func displayWeight() { + /// Synchronizes weights from Fitbit to HealthKit + func syncWeights() { let today = Date() let lastMonth = Calendar.current.date(byAdding: .day, value: -31, to: today)! self.client.getWeight(start: lastMonth, end: today) { @@ -70,6 +88,7 @@ class ViewController: UIViewController { fbWeights, error in if let error = error { NSLog("Unable to get weights: \(error)") + self.outputText.text = "Error getting weights" return }