From c59d2220b7496b26d538942117126481354688a6 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Wed, 27 Jul 2016 14:16:27 -0700 Subject: [PATCH 01/18] create auth screen --- samples/swift/uidemo/AuthViewController.swift | 18 ++++++++++++++++++ .../swift/uidemo/Base.lproj/Main.storyboard | 14 ++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/samples/swift/uidemo/AuthViewController.swift b/samples/swift/uidemo/AuthViewController.swift index 6dcbb79cb71..68d00dafbea 100644 --- a/samples/swift/uidemo/AuthViewController.swift +++ b/samples/swift/uidemo/AuthViewController.swift @@ -15,9 +15,27 @@ // import UIKit +import Firebase +import FirebaseAuthUI class AuthViewController: UIViewController { + + private(set) var auth: FIRAuth? = nil + + @IBOutlet var signInButton: UIButton! + @IBOutlet var signOutButton: UIButton! + + + static func fromStoryboard(storyboard: UIStoryboard = AppDelegate.mainStoryboard) -> AuthViewController { return storyboard.instantiateViewControllerWithIdentifier("AuthViewController") as! AuthViewController } + + override func viewDidAppear(animated: Bool) { + super.viewDidAppear(animated) + self.auth = FIRAuth.auth() + if let user = self.auth?.currentUser { + print("logged in! \(user.displayName ?? "")") + } + } } diff --git a/samples/swift/uidemo/Base.lproj/Main.storyboard b/samples/swift/uidemo/Base.lproj/Main.storyboard index 15671a660b9..36338e8c762 100644 --- a/samples/swift/uidemo/Base.lproj/Main.storyboard +++ b/samples/swift/uidemo/Base.lproj/Main.storyboard @@ -201,8 +201,22 @@ + + + + + + + + From 7931b5299e0c8f34a45610c9d65a8172af16d6b5 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Wed, 27 Jul 2016 17:15:56 -0700 Subject: [PATCH 02/18] some kind of wip --- samples/swift/uidemo/AuthViewController.swift | 21 +++++++++++++------ .../swift/uidemo/Base.lproj/Main.storyboard | 19 ++++++++--------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/samples/swift/uidemo/AuthViewController.swift b/samples/swift/uidemo/AuthViewController.swift index 68d00dafbea..8d7616fd286 100644 --- a/samples/swift/uidemo/AuthViewController.swift +++ b/samples/swift/uidemo/AuthViewController.swift @@ -21,11 +21,7 @@ import FirebaseAuthUI class AuthViewController: UIViewController { private(set) var auth: FIRAuth? = nil - - @IBOutlet var signInButton: UIButton! - @IBOutlet var signOutButton: UIButton! - - + private(set) var authUI: FIRAuthUI? = nil static func fromStoryboard(storyboard: UIStoryboard = AppDelegate.mainStoryboard) -> AuthViewController { return storyboard.instantiateViewControllerWithIdentifier("AuthViewController") as! AuthViewController @@ -35,7 +31,20 @@ class AuthViewController: UIViewController { super.viewDidAppear(animated) self.auth = FIRAuth.auth() if let user = self.auth?.currentUser { - print("logged in! \(user.displayName ?? "")") + print("logged in! \(user.uid)") + } else { + self.authUI = FIRAuthUI.authUI() + + let controller = FIRAuthUI.authViewController(self.authUI!)() // wat? + self.presentViewController(controller, animated: true, completion: nil) + } + } + + @IBAction func signOutPressed(sender: AnyObject) { + do { + try self.auth?.signOut() + } catch let error { + fatalError("Could not sign out: \(error)") } } } diff --git a/samples/swift/uidemo/Base.lproj/Main.storyboard b/samples/swift/uidemo/Base.lproj/Main.storyboard index 36338e8c762..1bd8a205bd3 100644 --- a/samples/swift/uidemo/Base.lproj/Main.storyboard +++ b/samples/swift/uidemo/Base.lproj/Main.storyboard @@ -202,21 +202,20 @@ - - + + + + - - - - From 8219bfaf8ed7bb7aa6f138ecf2e934cea9279049 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Thu, 28 Jul 2016 12:15:26 -0700 Subject: [PATCH 03/18] finish ultra basic auth flow --- .../swift/uidemo.xcodeproj/project.pbxproj | 36 +++++++++ samples/swift/uidemo/AppDelegate.swift | 10 +++ samples/swift/uidemo/AuthViewController.swift | 80 +++++++++++++++---- .../swift/uidemo/Base.lproj/Main.storyboard | 56 ++++++++++--- samples/swift/uidemo/Info.plist | 11 +++ 5 files changed, 170 insertions(+), 23 deletions(-) diff --git a/samples/swift/uidemo.xcodeproj/project.pbxproj b/samples/swift/uidemo.xcodeproj/project.pbxproj index e3980f13fc6..73b2ba2fc4a 100644 --- a/samples/swift/uidemo.xcodeproj/project.pbxproj +++ b/samples/swift/uidemo.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 5EC17CA937C19E0275523781 /* Pods_uidemoTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F456367642D92FB11C51802 /* Pods_uidemoTests.framework */; }; 8D16073E1D492B200069E4F5 /* AuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D16073D1D492B200069E4F5 /* AuthViewController.swift */; }; 8DABC9891D3D82D600453807 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DABC9881D3D82D600453807 /* AppDelegate.swift */; }; 8DABC98B1D3D82D600453807 /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DABC98A1D3D82D600453807 /* MenuViewController.swift */; }; @@ -18,6 +19,7 @@ 8DABC9AB1D3D947300453807 /* SampleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DABC9AA1D3D947300453807 /* SampleCell.swift */; }; 8DABC9AD1D3D9EAF00453807 /* ChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DABC9AC1D3D9EAF00453807 /* ChatViewController.swift */; }; 8DDF1AE51D3FF67D001F1160 /* ChatCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DDF1AE41D3FF67D001F1160 /* ChatCollectionViewCell.swift */; }; + A388401780D3B9B087EBD615 /* Pods_uidemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2FEDC84E3065303945D2713 /* Pods_uidemo.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -31,6 +33,10 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 02D92AEBCB35A7E01087732B /* Pods-uidemoTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-uidemoTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-uidemoTests/Pods-uidemoTests.debug.xcconfig"; sourceTree = ""; }; + 0C0ACF9F4D032D424C30C6CE /* Pods-uidemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-uidemo.release.xcconfig"; path = "Pods/Target Support Files/Pods-uidemo/Pods-uidemo.release.xcconfig"; sourceTree = ""; }; + 2F456367642D92FB11C51802 /* Pods_uidemoTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_uidemoTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3700755CBBEDF26A6490FFD2 /* Pods-uidemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-uidemo.debug.xcconfig"; path = "Pods/Target Support Files/Pods-uidemo/Pods-uidemo.debug.xcconfig"; sourceTree = ""; }; 8D16073D1D492B200069E4F5 /* AuthViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthViewController.swift; sourceTree = ""; }; 8DABC9851D3D82D600453807 /* uidemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = uidemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 8DABC9881D3D82D600453807 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -46,6 +52,8 @@ 8DABC9AA1D3D947300453807 /* SampleCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleCell.swift; sourceTree = ""; }; 8DABC9AC1D3D9EAF00453807 /* ChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatViewController.swift; sourceTree = ""; }; 8DDF1AE41D3FF67D001F1160 /* ChatCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatCollectionViewCell.swift; sourceTree = ""; }; + 9398BDB04D6420F5194FC978 /* Pods-uidemoTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-uidemoTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-uidemoTests/Pods-uidemoTests.release.xcconfig"; sourceTree = ""; }; + D2FEDC84E3065303945D2713 /* Pods_uidemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_uidemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -53,6 +61,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A388401780D3B9B087EBD615 /* Pods_uidemo.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -60,6 +69,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5EC17CA937C19E0275523781 /* Pods_uidemoTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -81,6 +91,8 @@ 8DABC9871D3D82D600453807 /* uidemo */, 8DABC99C1D3D82D600453807 /* uidemoTests */, 8DABC9861D3D82D600453807 /* Products */, + D0AB0A2867A40B9A167F17E2 /* Pods */, + 919F9A40606F1D64F69E31B0 /* Frameworks */, ); sourceTree = ""; }; @@ -119,6 +131,26 @@ path = uidemoTests; sourceTree = ""; }; + 919F9A40606F1D64F69E31B0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D2FEDC84E3065303945D2713 /* Pods_uidemo.framework */, + 2F456367642D92FB11C51802 /* Pods_uidemoTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + D0AB0A2867A40B9A167F17E2 /* Pods */ = { + isa = PBXGroup; + children = ( + 3700755CBBEDF26A6490FFD2 /* Pods-uidemo.debug.xcconfig */, + 0C0ACF9F4D032D424C30C6CE /* Pods-uidemo.release.xcconfig */, + 02D92AEBCB35A7E01087732B /* Pods-uidemoTests.debug.xcconfig */, + 9398BDB04D6420F5194FC978 /* Pods-uidemoTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -454,6 +486,7 @@ }; 8DABC9A31D3D82D600453807 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 3700755CBBEDF26A6490FFD2 /* Pods-uidemo.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -468,6 +501,7 @@ }; 8DABC9A41D3D82D600453807 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 0C0ACF9F4D032D424C30C6CE /* Pods-uidemo.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -481,6 +515,7 @@ }; 8DABC9A61D3D82D600453807 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 02D92AEBCB35A7E01087732B /* Pods-uidemoTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = uidemoTests/Info.plist; @@ -493,6 +528,7 @@ }; 8DABC9A71D3D82D600453807 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9398BDB04D6420F5194FC978 /* Pods-uidemoTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = uidemoTests/Info.plist; diff --git a/samples/swift/uidemo/AppDelegate.swift b/samples/swift/uidemo/AppDelegate.swift index e665a0a21ab..9c56c6c68c8 100644 --- a/samples/swift/uidemo/AppDelegate.swift +++ b/samples/swift/uidemo/AppDelegate.swift @@ -16,6 +16,7 @@ import UIKit import Firebase +import FirebaseAuthUI @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -32,5 +33,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { FIRApp.configure() return true } + + func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool { + // Seems like an oversight that `sourceApplication` should be force-unwrapped. + if FIRAuthUI.authUI()?.handleOpenURL(url, sourceApplication: sourceApplication!) ?? false { + return true + } + // other URL handling goes here. + return false + } } diff --git a/samples/swift/uidemo/AuthViewController.swift b/samples/swift/uidemo/AuthViewController.swift index 8d7616fd286..380c06808af 100644 --- a/samples/swift/uidemo/AuthViewController.swift +++ b/samples/swift/uidemo/AuthViewController.swift @@ -17,34 +17,86 @@ import UIKit import Firebase import FirebaseAuthUI +import FirebaseGoogleAuthUI +import FirebaseFacebookAuthUI +let kFirebaseTermsOfService = NSURL(string: "https://www.firebase.com/terms/terms-of-service.html")! + +// Your Google app's client ID, which can be found in the GoogleService-Info.plist file. +// Firebase Google auth is built on top of Google sign-in, so you'll have to add a URL +// scheme to your project as outlined at the bottom of this reference: +// https://developers.google.com/identity/sign-in/ios/start-integrating +// +// Make sure you don't accidentally check in your client ID in a public repo! +let kGoogleAppClientID = "your client ID here" + +// Your Facebook App ID, which can be found on developers.facebook.com. +let kFacebookAppID = "your fb app ID here" + +/// A view controller displaying a basic sign-in flow using FIRAuthUI. class AuthViewController: UIViewController { + // Before running this sample, make sure you've correctly configured + // the appropriate authentication methods in Firebase console. For more + // info, see https://firebase.google.com/docs/auth/ - private(set) var auth: FIRAuth? = nil - private(set) var authUI: FIRAuthUI? = nil + private var authStateDidChangeHandle: FIRAuthStateDidChangeListenerHandle? - static func fromStoryboard(storyboard: UIStoryboard = AppDelegate.mainStoryboard) -> AuthViewController { - return storyboard.instantiateViewControllerWithIdentifier("AuthViewController") as! AuthViewController + private(set) var auth: FIRAuth? = FIRAuth.auth() + private(set) var authUI: FIRAuthUI? = FIRAuthUI.authUI() + + @IBOutlet private var signOutButton: UIButton! + @IBOutlet private var startButton: UIButton! + + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + + // If you haven't set up your authentications correctly these buttons + // will still appear in the UI, but they'll crash the app when tapped. + let providers: [FIRAuthProviderUI] = [ + FIRGoogleAuthUI(clientID: kGoogleAppClientID)!, + FIRFacebookAuthUI(appID: kFacebookAppID)!, + ] + self.authUI?.signInProviders = providers + + // Strangely this is listed as TOSURL in the objc source and isn't + // given a swift name that would otherwise make it import as termsOfServiceURL. + self.authUI?.termsOfServiceURL = kFirebaseTermsOfService + + self.authStateDidChangeHandle = self.auth?.addAuthStateDidChangeListener { (auth, user) in + if let _ = user { + self.signOutButton.enabled = true + self.startButton.enabled = false + } else { + self.signOutButton.enabled = false + self.startButton.enabled = true + } + } } - override func viewDidAppear(animated: Bool) { - super.viewDidAppear(animated) - self.auth = FIRAuth.auth() - if let user = self.auth?.currentUser { - print("logged in! \(user.uid)") - } else { - self.authUI = FIRAuthUI.authUI() - - let controller = FIRAuthUI.authViewController(self.authUI!)() // wat? - self.presentViewController(controller, animated: true, completion: nil) + override func viewWillDisappear(animated: Bool) { + super.viewWillDisappear(animated) + if let handle = self.authStateDidChangeHandle { + self.auth?.removeAuthStateDidChangeListener(handle) } } + @IBAction func startPressed(sender: AnyObject) { + let controller = FIRAuthUI.authViewController(self.authUI!)() // wat? + self.presentViewController(controller, animated: true, completion: nil) + } + @IBAction func signOutPressed(sender: AnyObject) { do { try self.auth?.signOut() } catch let error { + // Again, fatalError is not a graceful way to handle errors. + // This error is most likely a network error, so retrying here + // makes sense. fatalError("Could not sign out: \(error)") } } + + static func fromStoryboard(storyboard: UIStoryboard = AppDelegate.mainStoryboard) -> AuthViewController { + return storyboard.instantiateViewControllerWithIdentifier("AuthViewController") as! AuthViewController + } } diff --git a/samples/swift/uidemo/Base.lproj/Main.storyboard b/samples/swift/uidemo/Base.lproj/Main.storyboard index 1bd8a205bd3..15646f336fe 100644 --- a/samples/swift/uidemo/Base.lproj/Main.storyboard +++ b/samples/swift/uidemo/Base.lproj/Main.storyboard @@ -202,20 +202,58 @@ - + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + diff --git a/samples/swift/uidemo/Info.plist b/samples/swift/uidemo/Info.plist index 40c6215d906..b4789d70968 100644 --- a/samples/swift/uidemo/Info.plist +++ b/samples/swift/uidemo/Info.plist @@ -18,6 +18,17 @@ 1.0 CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + com.googleusercontent.apps.whatever + + + CFBundleVersion 1 LSRequiresIPhoneOS From 8d4b15d9c73842cb96d67ca87edef73d5aceba83 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Thu, 28 Jul 2016 15:09:57 -0700 Subject: [PATCH 04/18] document strange behavior --- samples/swift/uidemo/AuthViewController.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/samples/swift/uidemo/AuthViewController.swift b/samples/swift/uidemo/AuthViewController.swift index 380c06808af..1e99ffa0c54 100644 --- a/samples/swift/uidemo/AuthViewController.swift +++ b/samples/swift/uidemo/AuthViewController.swift @@ -81,6 +81,9 @@ class AuthViewController: UIViewController { } @IBAction func startPressed(sender: AnyObject) { + // The function signature says it returns a view controller, + // but when called it actually returns a closure returning a view controller. + // Maybe this is a swift-objc interoperability bug. let controller = FIRAuthUI.authViewController(self.authUI!)() // wat? self.presentViewController(controller, animated: true, completion: nil) } From 4c4bc3d8d2d232fdd77bcf808c268b46c22c0655 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Thu, 28 Jul 2016 16:36:36 -0700 Subject: [PATCH 05/18] add more stuff to auth ui sample --- samples/swift/uidemo/AuthViewController.swift | 43 ++++++++++--- .../swift/uidemo/Base.lproj/Main.storyboard | 64 +++++++++++++++++-- 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/samples/swift/uidemo/AuthViewController.swift b/samples/swift/uidemo/AuthViewController.swift index 1e99ffa0c54..a7674abaace 100644 --- a/samples/swift/uidemo/AuthViewController.swift +++ b/samples/swift/uidemo/AuthViewController.swift @@ -47,6 +47,13 @@ class AuthViewController: UIViewController { @IBOutlet private var signOutButton: UIButton! @IBOutlet private var startButton: UIButton! + @IBOutlet private var signedInLabel: UILabel! + @IBOutlet private var nameLabel: UILabel! + @IBOutlet private var emailLabel: UILabel! + @IBOutlet private var uidLabel: UILabel! + + @IBOutlet var topConstraint: NSLayoutConstraint! + override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) @@ -62,15 +69,8 @@ class AuthViewController: UIViewController { // given a swift name that would otherwise make it import as termsOfServiceURL. self.authUI?.termsOfServiceURL = kFirebaseTermsOfService - self.authStateDidChangeHandle = self.auth?.addAuthStateDidChangeListener { (auth, user) in - if let _ = user { - self.signOutButton.enabled = true - self.startButton.enabled = false - } else { - self.signOutButton.enabled = false - self.startButton.enabled = true - } - } + self.authStateDidChangeHandle = + self.auth?.addAuthStateDidChangeListener(self.updateUI(auth:user:)) } override func viewWillDisappear(animated: Bool) { @@ -99,6 +99,31 @@ class AuthViewController: UIViewController { } } + // Boilerplate + func updateUI(auth auth: FIRAuth, user: FIRUser?) { + if let user = user { + self.signOutButton.enabled = true + self.startButton.enabled = false + + self.signedInLabel.text = "Signed in" + self.nameLabel.text = "Name: " + (user.displayName ?? "(null)") + self.emailLabel.text = "Email: " + (user.email ?? "(null)") + self.uidLabel.text = "UID: " + user.uid + } else { + self.signOutButton.enabled = false + self.startButton.enabled = true + + self.signedInLabel.text = "Not signed in" + self.nameLabel.text = "Name" + self.emailLabel.text = "Email" + self.uidLabel.text = "UID" + } + } + + override func viewWillLayoutSubviews() { + self.topConstraint.constant = self.topLayoutGuide.length + } + static func fromStoryboard(storyboard: UIStoryboard = AppDelegate.mainStoryboard) -> AuthViewController { return storyboard.instantiateViewControllerWithIdentifier("AuthViewController") as! AuthViewController } diff --git a/samples/swift/uidemo/Base.lproj/Main.storyboard b/samples/swift/uidemo/Base.lproj/Main.storyboard index 15646f336fe..59742f0628a 100644 --- a/samples/swift/uidemo/Base.lproj/Main.storyboard +++ b/samples/swift/uidemo/Base.lproj/Main.storyboard @@ -202,11 +202,52 @@ + + + + + + + + + + + + + + + + + + + + + - +