From f7626641035326bdf171a1bac9da47fbada5461b Mon Sep 17 00:00:00 2001 From: Alessio Moiso Date: Sun, 19 Apr 2020 22:58:37 +0200 Subject: [PATCH 1/7] Delete .travis.yml --- .travis.yml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 26c7f2d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -# references: -# * https://www.objc.io/issues/6-build-tools/travis-ci/ -# * https://github.com/supermarin/xcpretty#usage - -osx_image: xcode7.3 -language: objective-c -# cache: cocoapods -# podfile: Example/Podfile -# before_install: -# - gem install cocoapods # Since Travis is not always on latest version -# - pod install --project-directory=Example -script: -- set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/RxFireAuth.xcworkspace -scheme RxFireAuth-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty -- pod lib lint From 2b9533b76c0d86272c6a4dfb7bf6d37b9033b131 Mon Sep 17 00:00:00 2001 From: Alessio Moiso Date: Sat, 25 Apr 2020 21:09:02 +0200 Subject: [PATCH 2/7] Add loginWithCredentials to UsersManagerType to support split flow in Sign in with Apple --- Example/Podfile.lock | 4 +- Example/RxFireAuth.xcodeproj/project.pbxproj | 13 ++- Example/RxFireAuth/Base.lproj/Main.storyboard | 32 ++++++- Example/RxFireAuth/ViewController.swift | 90 +++++++++++-------- Example/RxFireAuth_Example.entitlements | 10 +++ RxFireAuth/Classes/LoginCredentials.swift | 26 ++++++ RxFireAuth/Classes/UserManager+Apple.swift | 79 +++------------- RxFireAuth/Classes/UserManager.swift | 74 +++++++++++++++ RxFireAuth/Classes/UserManagerType.swift | 7 ++ 9 files changed, 225 insertions(+), 110 deletions(-) create mode 100644 Example/RxFireAuth_Example.entitlements create mode 100644 RxFireAuth/Classes/LoginCredentials.swift diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 44e270d..cde920e 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -51,7 +51,7 @@ PODS: - RxCocoa (5.1.1): - RxRelay (~> 5) - RxSwift (~> 5) - - RxFireAuth (1.0.0): + - RxFireAuth (1.2.0): - Firebase/Auth (~> 6.5) - JWTDecode (~> 2.4) - RxCocoa (~> 5) @@ -98,7 +98,7 @@ SPEC CHECKSUMS: JWTDecode: 4bd4da0cdf0f8154fd467fa48499f34ec057fb88 nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601 - RxFireAuth: 2fdfec4d2f2214019924986354875a53cae81d52 + RxFireAuth: d2ecd67892ffc2112d9160b6135a85e75bffaf81 RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9 RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 diff --git a/Example/RxFireAuth.xcodeproj/project.pbxproj b/Example/RxFireAuth.xcodeproj/project.pbxproj index e2151d7..813d8da 100644 --- a/Example/RxFireAuth.xcodeproj/project.pbxproj +++ b/Example/RxFireAuth.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 66774B58D2973FB029D59734 /* Pods_RxFireAuth_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E44A537D04B4AD25BCA4E8B /* Pods_RxFireAuth_Tests.framework */; }; C4EDED199E5D6FAD9D6BD92C /* Pods_RxFireAuth_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93D1018E205EC59B01C0D604 /* Pods_RxFireAuth_Example.framework */; }; FA3AA21A244B52F400363BCE /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = FA3AA219244B52F400363BCE /* GoogleService-Info.plist */; }; + FAC78B532454B46800D2F391 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FAC78B522454B46800D2F391 /* AuthenticationServices.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -49,6 +50,8 @@ EE4839DFF9311DA585A57B1D /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; F2D9CFE62573321CD826326E /* Pods-RxFireAuth_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RxFireAuth_Example.debug.xcconfig"; path = "Target Support Files/Pods-RxFireAuth_Example/Pods-RxFireAuth_Example.debug.xcconfig"; sourceTree = ""; }; FA3AA219244B52F400363BCE /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + FAC78B512454B3F400D2F391 /* RxFireAuth_Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RxFireAuth_Example.entitlements; sourceTree = ""; }; + FAC78B522454B46800D2F391 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -56,6 +59,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + FAC78B532454B46800D2F391 /* AuthenticationServices.framework in Frameworks */, C4EDED199E5D6FAD9D6BD92C /* Pods_RxFireAuth_Example.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -74,6 +78,7 @@ 0F5A69D35E045B4D7E2C8E19 /* Frameworks */ = { isa = PBXGroup; children = ( + FAC78B522454B46800D2F391 /* AuthenticationServices.framework */, 93D1018E205EC59B01C0D604 /* Pods_RxFireAuth_Example.framework */, 6E44A537D04B4AD25BCA4E8B /* Pods_RxFireAuth_Tests.framework */, ); @@ -83,6 +88,7 @@ 607FACC71AFB9204008FA782 = { isa = PBXGroup; children = ( + FAC78B512454B3F400D2F391 /* RxFireAuth_Example.entitlements */, 607FACF51AFB993E008FA782 /* Podspec Metadata */, 607FACD21AFB9204008FA782 /* Example for RxFireAuth */, 607FACE81AFB9204008FA782 /* Tests */, @@ -215,6 +221,7 @@ TargetAttributes = { 607FACCF1AFB9204008FA782 = { CreatedOnToolsVersion = 6.3.1; + DevelopmentTeam = 43C9YP2S4Y; LastSwiftMigration = 1140; }; 607FACE41AFB9204008FA782 = { @@ -500,7 +507,8 @@ baseConfigurationReference = F2D9CFE62573321CD826326E /* Pods-RxFireAuth_Example.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; + CODE_SIGN_ENTITLEMENTS = RxFireAuth_Example.entitlements; + DEVELOPMENT_TEAM = 43C9YP2S4Y; INFOPLIST_FILE = RxFireAuth/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -517,7 +525,8 @@ baseConfigurationReference = E5F34834E8AAEBED9D6B955D /* Pods-RxFireAuth_Example.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; + CODE_SIGN_ENTITLEMENTS = RxFireAuth_Example.entitlements; + DEVELOPMENT_TEAM = 43C9YP2S4Y; INFOPLIST_FILE = RxFireAuth/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; diff --git a/Example/RxFireAuth/Base.lproj/Main.storyboard b/Example/RxFireAuth/Base.lproj/Main.storyboard index 1c2810b..9b3da63 100644 --- a/Example/RxFireAuth/Base.lproj/Main.storyboard +++ b/Example/RxFireAuth/Base.lproj/Main.storyboard @@ -220,9 +220,35 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -250,7 +276,7 @@ - + @@ -305,7 +331,7 @@ - + diff --git a/Example/RxFireAuth/ViewController.swift b/Example/RxFireAuth/ViewController.swift index 01bca72..c503ab1 100644 --- a/Example/RxFireAuth/ViewController.swift +++ b/Example/RxFireAuth/ViewController.swift @@ -29,7 +29,7 @@ class ViewController: UITableViewController { @IBOutlet weak var resetAnononymousSwitch: UISwitch! - private var userManager: UserManagerType = UserManager() + private var userManager: UserManagerType & LoginProviderManagerType = UserManager() private var disposeBag = DisposeBag() private var progressDialog: UIAlertController? @@ -75,6 +75,20 @@ class ViewController: UITableViewController { .disposed(by: self.disposeBag) } + private var migrationAllowance: Bool? { + let allowMigration: Bool? + switch self.dataMigrationControl.selectedSegmentIndex { + case 1: + allowMigration = true + case 2: + allowMigration = false + default: + allowMigration = nil + } + + return allowMigration + } + @IBAction func signIn(sender: AnyObject) { self.toggleProgress(true) if self.loginField.text?.count == 0 { @@ -84,40 +98,19 @@ class ViewController: UITableViewController { }, onError: self.show(error:)) .disposed(by: self.disposeBag) } else { - let allowMigration: Bool? - switch self.dataMigrationControl.selectedSegmentIndex { - case 1: - allowMigration = true - case 2: - allowMigration = false - default: - allowMigration = nil - } - - self.userManager.login(email: self.loginField.text!, password: self.passwordField.text!, allowMigration: allowMigration) - .subscribe(onSuccess: { [unowned self] (descriptor) in - self.loginField.text = nil - self.passwordField.text = nil - self.toggleProgress(false) { [unowned self] in - let alertController = UIAlertController(title: "You are now logged in!", message: nil, preferredStyle: .alert) - var messages = [String]() - if descriptor.performMigration { - messages += ["Migration is required!"] - } else { - messages += ["Migration is not required."] - } - if let oldUserId = descriptor.oldUserId { - messages += ["Your old User ID is: \(oldUserId)."] - } - if let newUserId = descriptor.newUserId { - messages += ["Your new User ID is: \(newUserId)."] - } - alertController.message = messages.joined(separator: " ") - alertController.addAction(UIAlertAction(title: "Cool!", style: .default, handler: nil)) - self.present(alertController, animated: true, completion: nil) - } - }, onError: self.show(error:)) + self.userManager.login(email: self.loginField.text!, password: self.passwordField.text!, allowMigration: self.migrationAllowance) + .subscribe(onSuccess: self.handleLoggedIn(_:), onError: self.show(error:)) + .disposed(by: self.disposeBag) + } + } + + @IBAction func signInWithApple(sender: AnyObject) { + if #available(iOS 13.0, *) { + self.userManager.signInWithApple(in: self, updateUserDisplayName: true, allowMigration: self.migrationAllowance) + .subscribe(onSuccess: self.handleLoggedIn(_:), onError: self.show(error:)) .disposed(by: self.disposeBag) + } else { + self.show(title: "Sign in with Apple is not available on iOS 12 or earlier.", message: "Use a device running iOS 13 or later to test this feature.") } } @@ -155,9 +148,36 @@ class ViewController: UITableViewController { } } + private func handleLoggedIn(_ descriptor: LoginDescriptor) { + self.loginField.text = nil + self.passwordField.text = nil + self.toggleProgress(false) { [unowned self] in + let alertController = UIAlertController(title: "You are now logged in!", message: nil, preferredStyle: .alert) + var messages = [String]() + if descriptor.performMigration { + messages += ["Migration is required!"] + } else { + messages += ["Migration is not required."] + } + if let oldUserId = descriptor.oldUserId { + messages += ["Your old User ID is: \(oldUserId)."] + } + if let newUserId = descriptor.newUserId { + messages += ["Your new User ID is: \(newUserId)."] + } + alertController.message = messages.joined(separator: " ") + alertController.addAction(UIAlertAction(title: "Cool!", style: .default, handler: nil)) + self.present(alertController, animated: true, completion: nil) + } + } + private func show(error: Error) { + self.show(title: "An error occurred!", message: error.localizedDescription) + } + + private func show(title: String, message: String) { self.toggleProgress(false) { [unowned self] in - let alertController = UIAlertController(title: "An error occurred!", message: error.localizedDescription, preferredStyle: .alert) + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) self.present(alertController, animated: true, completion: nil) } diff --git a/Example/RxFireAuth_Example.entitlements b/Example/RxFireAuth_Example.entitlements new file mode 100644 index 0000000..a812db5 --- /dev/null +++ b/Example/RxFireAuth_Example.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.applesignin + + Default + + + diff --git a/RxFireAuth/Classes/LoginCredentials.swift b/RxFireAuth/Classes/LoginCredentials.swift new file mode 100644 index 0000000..099e438 --- /dev/null +++ b/RxFireAuth/Classes/LoginCredentials.swift @@ -0,0 +1,26 @@ +// +// LoginCredentials.swift +// Pods +// +// Created by Alessio Moiso on 25/04/2020. +// + +import Foundation + +public struct LoginCredentials { + + public enum Provider: String { + case apple = "apple.com" + } + + var idToken: String + + var fullName: String? + + var email: String + + var provider: Provider + + var nonce: String + +} diff --git a/RxFireAuth/Classes/UserManager+Apple.swift b/RxFireAuth/Classes/UserManager+Apple.swift index 6946312..59b8a80 100644 --- a/RxFireAuth/Classes/UserManager+Apple.swift +++ b/RxFireAuth/Classes/UserManager+Apple.swift @@ -14,7 +14,7 @@ extension UserManager: LoginProviderManagerType { @available(iOS 13.0, *) public func signInWithApple(in viewController: UIViewController, updateUserDisplayName: Bool, allowMigration: Bool?) -> Single { - return Single.create { [unowned self] (observer) -> Disposable in + return Single.create { [unowned self] (observer) -> Disposable in let disposable = Disposables.create { [unowned self] in self.loginHandler = nil } @@ -25,82 +25,25 @@ extension UserManager: LoginProviderManagerType { appleSignInHandler.signIn { (idToken, nonce, fullName, email, error) in guard !disposable.isDisposed else { return } - if let error = error { - observer(.error(error)) + guard error == nil else { + observer(.error(error!)) return } guard let email = email else { observer(.error(UserError.invalidEmail)); return } - let credentials = OAuthProvider.credential(withProviderID: "apple.com", idToken: idToken!, rawNonce: nonce) - - var oldUserId: String? - let signInCompletionHandler: (Error?) -> Void = { (error) in - guard !disposable.isDisposed else { return } - if let error = error { - observer(.error(error)) - } else if let newUser = Auth.auth().currentUser { - observer( - .success( - LoginDescriptor(fullName: fullName, performMigration: allowMigration!, oldUserId: oldUserId, newUserId: newUser.uid) - ) - ) - } else { - observer(.error(UserError.noUser)) - } - } - - /// Get if this user already exists - Auth.auth().fetchSignInMethods(forEmail: email) { (methods, error) in - guard !disposable.isDisposed else { return } - guard error == nil else { observer(.error(error!)); return } - - if let methods = methods, methods.count > 0, let currentUser = Auth.auth().currentUser { - /// This user exists. - /// There is a currently logged-in user. - if currentUser.isAnonymous { - if allowMigration == nil { - observer(.error(UserError.migrationRequired)) - return - } - - oldUserId = currentUser.uid - - /// The currently logged-in user is anonymous - /// We'll delete the anonymous account and login with the new account. - currentUser.delete { (error) in - guard !disposable.isDisposed else { return } - if let error = error { - observer(.error(error)) - } else { - self.signIn(with: credentials, in: disposable, completionHandler: signInCompletionHandler) - } - } - } else { - /// The logged-in user is not anonymous. - /// We'll try to link this authentication method to the existing account. - currentUser.link(with: credentials) { (_, error) in - signInCompletionHandler(error) - } - } - } else if let currentUser = Auth.auth().currentUser { - /// This user does not exist. - /// There is a logged-in user. - /// We'll try to link the new authentication method to the existing account. - currentUser.link(with: credentials) { (_, error) in - signInCompletionHandler(error) - } - } else { - /// This user does not exist. - /// There's nobody logged-in. - /// We'll go ahead and sign in with the authentication method. - self.signIn(with: credentials, in: disposable, completionHandler: signInCompletionHandler) - } - } + observer( + .success( + LoginCredentials(idToken: idToken ?? "", fullName: fullName, email: email, provider: .apple, nonce: nonce ?? "") + ) + ) } return disposable } + .flatMap { [unowned self] credentials in + self.continueSigninIn(with: credentials, allowMigration: allowMigration) + } .flatMap { (loginDescriptor) -> Single in if updateUserDisplayName, let fullName = loginDescriptor.fullName, fullName.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 { return self.update(user: UserData(id: nil, email: nil, displayName: fullName, isAnonymous: false)) diff --git a/RxFireAuth/Classes/UserManager.swift b/RxFireAuth/Classes/UserManager.swift index 4c76a78..0a3ac9c 100644 --- a/RxFireAuth/Classes/UserManager.swift +++ b/RxFireAuth/Classes/UserManager.swift @@ -250,6 +250,80 @@ public class UserManager: UserManagerType { } } + public func continueSigninIn(with credentials: LoginCredentials, allowMigration: Bool? = nil) -> Single { + return Single.create { [unowned self] observer -> Disposable in + let disposable = Disposables.create { } + + let firebaseCredentials = OAuthProvider.credential(withProviderID: credentials.provider.rawValue, idToken: credentials.idToken, rawNonce: credentials.nonce) + + var oldUserId: String? + let signInCompletionHandler: (Error?) -> Void = { (error) in + guard !disposable.isDisposed else { return } + if let error = error { + observer(.error(error)) + } else if let newUser = Auth.auth().currentUser { + observer( + .success( + LoginDescriptor(fullName: credentials.fullName, performMigration: allowMigration ?? false, oldUserId: oldUserId, newUserId: newUser.uid) + ) + ) + } else { + observer(.error(UserError.noUser)) + } + } + + /// Get if this user already exists + Auth.auth().fetchSignInMethods(forEmail: credentials.email) { (methods, error) in + guard !disposable.isDisposed else { return } + guard error == nil else { observer(.error(error!)); return } + + if let methods = methods, methods.count > 0, let currentUser = Auth.auth().currentUser { + /// This user exists. + /// There is a currently logged-in user. + if currentUser.isAnonymous { + if allowMigration == nil { + observer(.error(UserError.migrationRequired)) + return + } + + oldUserId = currentUser.uid + + /// The currently logged-in user is anonymous + /// We'll delete the anonymous account and login with the new account. + currentUser.delete { (error) in + guard !disposable.isDisposed else { return } + if let error = error { + observer(.error(error)) + } else { + self.signIn(with: firebaseCredentials, in: disposable, completionHandler: signInCompletionHandler) + } + } + } else { + /// The logged-in user is not anonymous. + /// We'll try to link this authentication method to the existing account. + currentUser.link(with: firebaseCredentials) { (_, error) in + signInCompletionHandler(error) + } + } + } else if let currentUser = Auth.auth().currentUser { + /// This user does not exist. + /// There is a logged-in user. + /// We'll try to link the new authentication method to the existing account. + currentUser.link(with: firebaseCredentials) { (_, error) in + signInCompletionHandler(error) + } + } else { + /// This user does not exist. + /// There's nobody logged-in. + /// We'll go ahead and sign in with the authentication method. + self.signIn(with: firebaseCredentials, in: disposable, completionHandler: signInCompletionHandler) + } + } + + return disposable + } + } + /// Sign in with the passed credentials in the specified disposable /// and calls the completion handler when done. /// diff --git a/RxFireAuth/Classes/UserManagerType.swift b/RxFireAuth/Classes/UserManagerType.swift index 20cb1ac..cb35a29 100644 --- a/RxFireAuth/Classes/UserManagerType.swift +++ b/RxFireAuth/Classes/UserManagerType.swift @@ -90,6 +90,13 @@ public protocol UserManagerType { /// - returns: A Single to observe for result. func loginWithoutChecking(email: String, password: String, allowMigration: Bool?) -> Single + /// Sign in with the passed credentials on a provider. + /// + /// Use this function to sign in with a provider credentials. In a normal flow, + /// you'll use this function with credentials obtained by one of the `signInWith…` methods + /// provided by implementations of `LoginProviderManagerType`. + func login(with credentials: LoginCredentials, allowMigration: Bool?) -> Single + /// Logout the currently logged-in user. /// /// Using the `resetToAnonymous` parameter, you can make sure From f826ca4acd13ce9e86835bb45023448c67c9c1ed Mon Sep 17 00:00:00 2001 From: Alessio Moiso Date: Sat, 25 Apr 2020 21:09:38 +0200 Subject: [PATCH 3/7] Rename login(with:) --- RxFireAuth/Classes/UserManager+Apple.swift | 2 +- RxFireAuth/Classes/UserManager.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RxFireAuth/Classes/UserManager+Apple.swift b/RxFireAuth/Classes/UserManager+Apple.swift index 59b8a80..792b122 100644 --- a/RxFireAuth/Classes/UserManager+Apple.swift +++ b/RxFireAuth/Classes/UserManager+Apple.swift @@ -42,7 +42,7 @@ extension UserManager: LoginProviderManagerType { return disposable } .flatMap { [unowned self] credentials in - self.continueSigninIn(with: credentials, allowMigration: allowMigration) + self.login(with: credentials, allowMigration: allowMigration) } .flatMap { (loginDescriptor) -> Single in if updateUserDisplayName, let fullName = loginDescriptor.fullName, fullName.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 { diff --git a/RxFireAuth/Classes/UserManager.swift b/RxFireAuth/Classes/UserManager.swift index 0a3ac9c..706e2a8 100644 --- a/RxFireAuth/Classes/UserManager.swift +++ b/RxFireAuth/Classes/UserManager.swift @@ -250,7 +250,7 @@ public class UserManager: UserManagerType { } } - public func continueSigninIn(with credentials: LoginCredentials, allowMigration: Bool? = nil) -> Single { + public func login(with credentials: LoginCredentials, allowMigration: Bool? = nil) -> Single { return Single.create { [unowned self] observer -> Disposable in let disposable = Disposables.create { } From 913a425ef8f04b0a2ccbbba993b21f787e4b37bb Mon Sep 17 00:00:00 2001 From: Alessio Moiso Date: Sat, 25 Apr 2020 21:51:10 +0200 Subject: [PATCH 4/7] Update README, showcasing new split flow, add credentials to migrationRequired error --- Example/RxFireAuth/ViewController.swift | 62 ++++++++++++++++++++-- README.md | 31 ++++++----- RxFireAuth/Classes/UserError.swift | 3 +- RxFireAuth/Classes/UserManager+Apple.swift | 9 +--- RxFireAuth/Classes/UserManager.swift | 15 ++++-- RxFireAuth/Classes/UserManagerType.swift | 9 +++- 6 files changed, 99 insertions(+), 30 deletions(-) diff --git a/Example/RxFireAuth/ViewController.swift b/Example/RxFireAuth/ViewController.swift index c503ab1..55c2822 100644 --- a/Example/RxFireAuth/ViewController.swift +++ b/Example/RxFireAuth/ViewController.swift @@ -11,6 +11,8 @@ import RxFireAuth import RxSwift import RxCocoa +/// This class shows you an example of almost all the features that +/// RxFireAuth supports. class ViewController: UITableViewController { @IBOutlet weak var welcomeLabel: UILabel! @@ -37,6 +39,12 @@ class ViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() + /// Registering to `autoupdatingUser` gives us + /// an observable that emits a new value every time something + /// on the user changes. + /// + /// You should use this to bind your UI to make sure that + /// everything is always updated. self.userManager.autoupdatingUser .observeOn(MainScheduler.instance) .subscribe(onNext: { (user) in @@ -89,6 +97,8 @@ class ViewController: UITableViewController { return allowMigration } + /// Login with email and password or anonymously based + /// on if there is something written in the email field. @IBAction func signIn(sender: AnyObject) { self.toggleProgress(true) if self.loginField.text?.count == 0 { @@ -99,21 +109,35 @@ class ViewController: UITableViewController { .disposed(by: self.disposeBag) } else { self.userManager.login(email: self.loginField.text!, password: self.passwordField.text!, allowMigration: self.migrationAllowance) - .subscribe(onSuccess: self.handleLoggedIn(_:), onError: self.show(error:)) + .subscribe(onSuccess: self.handleLoggedIn(_:), onError: { [unowned self] error in + if case UserError.migrationRequired(let credentials) = error { + self.handleMigration(credentials: credentials) + } else { + self.show(error: error) + } + }) .disposed(by: self.disposeBag) } } + /// Start the Sign in with Apple flow. @IBAction func signInWithApple(sender: AnyObject) { if #available(iOS 13.0, *) { self.userManager.signInWithApple(in: self, updateUserDisplayName: true, allowMigration: self.migrationAllowance) - .subscribe(onSuccess: self.handleLoggedIn(_:), onError: self.show(error:)) + .subscribe(onSuccess: self.handleLoggedIn(_:), onError: { [unowned self] error in + if case UserError.migrationRequired(let credentials) = error { + self.handleMigration(credentials: credentials) + } else { + self.show(error: error) + } + }) .disposed(by: self.disposeBag) } else { self.show(title: "Sign in with Apple is not available on iOS 12 or earlier.", message: "Use a device running iOS 13 or later to test this feature.") } } + /// Sign out. @IBAction func signOut(sender: AnyObject) { self.toggleProgress(true) self.userManager.logout(resetToAnonymous: self.resetAnononymousSwitch.isOn) @@ -123,6 +147,8 @@ class ViewController: UITableViewController { .disposed(by: self.disposeBag) } + /// Update the user display name using the + /// value of the name field. @IBAction func updateProfile(sender: AnyObject) { self.toggleProgress(true) self.userManager.update { (userData) -> UserData in @@ -139,12 +165,42 @@ class ViewController: UITableViewController { self.loginField.becomeFirstResponder() } + /// This function is called when a login operation has failed because of a `UserError.migrationRequired` error. + /// + /// After having asked to the user what they would like to do with their existing data, you can continue the flow + /// using `login(with credentials:updateUserDisplayName:allowMigration:)`. No credentials are + /// passed when the `UserError.migrationRequired` error is thrown during a sign in with email and password, because + /// a new login attempt can be made seamlessly without asking anything to the user. + private func handleMigration(credentials: LoginCredentials?) { + let migrationAlert = UIAlertController(title: "Migration Required", message: "You are trying to login into an existing account while being logged-in with an anonymous account. When doing this in a real app, you should check if the user has data in the anonymous account and, if so, offer the option to merge the anonymous account with the one that the user is trying to sign into.", preferredStyle: .actionSheet) + migrationAlert.addAction(UIAlertAction(title: "Migrate", style: .destructive, handler: { [unowned self] _ in + if let credentials = credentials { + self.userManager.login(with: credentials, updateUserDisplayName: true, allowMigration: true) + .subscribe(onSuccess: self.handleLoggedIn(_:), onError: self.show(error:)) + .disposed(by: self.disposeBag) + } else { + self.userManager.login(email: self.loginField.text!, password: self.passwordField.text!, allowMigration: true) + .subscribe(onSuccess: self.handleLoggedIn(_:), onError: self.show(error:)) + .disposed(by: self.disposeBag) + } + })) + migrationAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + + self.present(migrationAlert, animated: true, completion: nil) + } + + // MARK: - Utilities + private func toggleProgress(_ show: Bool, completionHandler: (() -> Void)? = nil) { if show { self.progressDialog = UIAlertController(title: "Working…", message: nil, preferredStyle: .alert) self.present(self.progressDialog!, animated: true, completion: completionHandler) } else { - self.progressDialog?.dismiss(animated: true, completion: completionHandler) + if let progressDialog = self.progressDialog { + progressDialog.dismiss(animated: true, completion: completionHandler) + } else { + completionHandler?() + } } } diff --git a/README.md b/README.md index 892137e..df76e22 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ RxFireAuth is a wrapper around the [Firebase Authentication](https://firebase.google.com/docs/auth) SDK that exposes the most common use cases through [RxSwift](https://github.com/ReactiveX/RxSwift) objects. -Firebase Authentication is a great way to support user authentication in your app easily. This library builds on top of that to simplify even further the process with pre-built algorithms that support registering, logging-in, linking accounts with other providers and more. +Firebase Authentication is a great way to support user authentication in your app easily. This library builds on top of that to simplify even further the process with pre-built algorithms that support registering, logging-in, linking accounts with other providers, and more. ## Installation @@ -24,7 +24,7 @@ To find out the latest version, look at the Releases tab of this repository. To get started with RxFireAuth, you can download the example project or dive right into the [documentation](https://mrasterisco.github.io/RxFireAuth/). ## Example Project -This library includes a sample project that shows how to support a user login, including anonymous accounts. +This library includes a sample project that shows how to support a user log in, including anonymous accounts. To see it in action, follow these steps: @@ -57,15 +57,15 @@ One of the things that RxFireAuth aims to simplify is the ability to build a Reg #### Anonymous Accounts Flow Modern applications should always try to delay sign-in as long as possible. From Apple Human Interface Guidelines: -> Delay sign-in as long as possible. People often abandon apps when they're forced to sign in before doing anything useful. Give them a chance to familiarize themselves with your app before making a commitment. For example, a live-streaming app could let people explore available content before signing in to stream something. +> Delay sign-in as long as possible. People often abandon apps when they're forced to sign in before doing anything useful. Give them a chance to familiarize themselves with your app before committing. For example, a live-streaming app could let people explore available content before signing in to stream something. -Anonymous Accounts are Firebase's way to support this situation: when you first launch the app, you create an anonymous account that can then be converted to a new account when the user is ready to actually sign-in. This works flawlessly for new accounts, but has a few catches when dealing with returning users. +Anonymous Accounts are Firebase's way to support this situation: when you first launch the app, you create an anonymous account that can then be converted to a new account when the user is ready to sign-in. This works flawlessly for new accounts but has a few catches when dealing with returning users. Consider the following situation: -- Mike is a new user of your app. Since you've strictly followed Apple's guidelines, when Mike opens your app, he's taken directly to the main screen. +- Mike is a new user of your app. Since you've strictly followed Apple's guidelines when Mike opens your app, he's taken directly to the main screen. - All the data that Mike builds in your app is linked to an anonymous account that you have created automatically while starting the app for the first time. -- At some point, Mike decides to sign-in in order to sync his data with another device. He registers a new account with his email and a password. +- At some point, Mike decides to sign-in to sync his data with another device. He registers a new account with his email and a password. - Everything's looking good until now with the normal Firebase SDK, **unless you're super into RxSwift and you want all the Firebase methods to be wrapped into Rx components; if that's the case, skip the next points and go directly to "Code Showcase" paragraph.** - Now, Mike wants to use his shiny new account to sign-in into another device. He downloads the app once again and he finds himself on the Home screen. - He goes directly into the Sign-in screen and enters his account credentials: at this point, using the Firebase SDK, you'll try to link the anonymous account that has been created while opening the app to Mike's credential, but you'll get an error saying that those credentials are already in use. **Here's where this library will help you: when logging-in, the `UserManager` class will automatically check if the specified credentials already exist and will use those to login; it'll also delete the anonymous account that is no longer needed.** @@ -83,7 +83,7 @@ The `allowMigration` parameter is useful in the situation that we've just descri When the user has made a choice, pass either `true` or `false` to get the same value circled back to your code after the login procedure has completed successfully. -To support the migration, all login methods return an instance of `LoginDescriptor` which gives you the `allowMigration` parameter that you've passed, the User ID of the anonymous account and the User ID of the account that is now logged-in. With this information, you can go ahead and migrate the data from the anonymous account to the newly logged-in account. +To support the migration, all login methods return an instance of `LoginDescriptor` which gives you the `allowMigration` parameter that you've passed, the User ID of the anonymous account, and the User ID of the account that is now logged-in. With this information, you can go ahead and migrate the data from the anonymous account to the newly logged-in account. #### Sign-in with Apple If you are thinking of providing alternatives ways to login into your app, you have to consider Sign-in with Apple as an option, *[mainly because you have to](https://developer.apple.com/app-store/review/guidelines/#4.8), but also because it provides a great experience for people using your app in the Apple ecosystem*. @@ -93,14 +93,14 @@ When signing in with an external provider, it is always good to just let the use Let's use the same short story from before, but Mike is now going to use Sign-in with Apple. - On the first device, nothing changes: with the standard Firebase SDK, we can link the anonymous account with Mike's Apple ID. -- On the second device, a lot of different things will happen: first of all, Apple has a different flow for apps that have already used Sign-in with Apple; this means that if the user registers and then deletes their account in your app, they'll still get a different sign-in flow in the case they return to the app and Sign-in with Apple once again (further on this [here](https://forums.developer.apple.com/thread/119826)). Secondly, you'll have to handle a lot of different cases. +- On the second device, two things will happen: first of all, Apple has a different flow for apps that have already used Sign-in with Apple; and this is not controllable by you, so if the user registers and then deletes their account in your app, they'll still get a different sign-in flow in the case they return to the app and Sign-in with Apple once again (further on this [here](https://forums.developer.apple.com/thread/119826)). Secondly, you'll have to handle various cases. When using Sign-in with Apple *(or any other provider, such as Google or Facebook)*, the following situations may apply: - There is an anonymous user logged-in and the Apple ID is not linked with any existing account: that's fantastic! We'll just link the Apple ID with the anonymous user and we're done. -- There is an anonymous user logged-in, but the Apple ID is already linked with another account: we'll have to go through the migration and then login into the existing account. +- There is an anonymous user logged-in, but the Apple ID is already linked with another account: we'll have to go through the migration and then log in to the existing account. - There is a normal user logged-in and the Apple ID is not already linked with another account: the user is trying to link their Apple ID with an existing account, let's go ahead and do that. -- There is a normal user logged-in, but the Apple ID is already linked with another account: we'll throw an error, because the user has to choose what to do. +- There is a normal user logged-in, but the Apple ID is already linked with another account: we'll throw an error because the user has to choose what to do. - There is nobody logged-in and the Apple ID is either already linked or not: we'll sign in into the existing or new account. ##### Code Showcase @@ -113,9 +113,14 @@ func signInWithApple(in viewController: UIViewController, updateUserDisplayName: *This function is available in implementations of `LoginProviderManagerType`, such as the `UserManager` class that we're already using.* -You can use the `updateUserDisplayName` parameter to automatically set the Firebase User `displayName` property to the full name associated to the Apple ID. *Keep in mind that the user can change this information while signing-in with Apple for the first time.* +You can use the `updateUserDisplayName` parameter to automatically set the Firebase User `displayName` property to the full name associated with the Apple ID. *Keep in mind that the user can change this information while signing-in with Apple for the first time.* -This function will behave as the normal login, returning `UserError.migrationRequired`, if an anonymous account is going to be deleted and `allowMigration` is not set. +This function will behave as the normal login, returning `UserError.migrationRequired`, if an anonymous account is going to be deleted and `allowMigration` is not set. When this happens, you can use the following function to continue signing in after having asked the user what they would like to do: + +```swift +func login(with credentials: LoginCredentials, updateUserDisplayName: Bool, allowMigration: Bool?) -> Single +``` +The login credentials are embedded in the `migrationRequired` error and, except for particular cases, you shouldn't need to inspect them. #### Standard Flow If you don't want to support the anonymous authentication, you can use this library anyway as all of the methods are built to work even when no account is logged-in. @@ -157,7 +162,7 @@ RxFireAuth targets **iOS 9.0 or later** and has the following dependencies: Compatibility with macOS is **planned**. Don't hesitate to open an issue to prioritize it. ## Contributions -All contributions to expand the library are welcome. Fork the repo, make the changes you want and open a Pull Request. +All contributions to expand the library are welcome. Fork the repo, make the changes you want, and open a Pull Request. If you make changes to the codebase, I am not enforcing a coding style, but I may ask you to make changes based on how the rest of the library is made. diff --git a/RxFireAuth/Classes/UserError.swift b/RxFireAuth/Classes/UserError.swift index d83d0cf..7948206 100644 --- a/RxFireAuth/Classes/UserError.swift +++ b/RxFireAuth/Classes/UserError.swift @@ -21,7 +21,8 @@ public enum UserError: LocalizedError { /// The provided email is not valid. case invalidEmail /// The action would require to migrate the current user data to a new account. - case migrationRequired + /// Use the passed login credentials to continue signing-in when ready by calling `login(with credentials:updateUserDisplayName:allowMigration:)` + case migrationRequired(LoginCredentials?) /// The requested action cannot be performed because there is already an anonymous user logged-in. case alreadyAnonymous /// The specified user cannot be found. diff --git a/RxFireAuth/Classes/UserManager+Apple.swift b/RxFireAuth/Classes/UserManager+Apple.swift index 792b122..ed70096 100644 --- a/RxFireAuth/Classes/UserManager+Apple.swift +++ b/RxFireAuth/Classes/UserManager+Apple.swift @@ -42,14 +42,7 @@ extension UserManager: LoginProviderManagerType { return disposable } .flatMap { [unowned self] credentials in - self.login(with: credentials, allowMigration: allowMigration) - } - .flatMap { (loginDescriptor) -> Single in - if updateUserDisplayName, let fullName = loginDescriptor.fullName, fullName.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 { - return self.update(user: UserData(id: nil, email: nil, displayName: fullName, isAnonymous: false)) - .andThen(Single.just(loginDescriptor)) - } - return Single.just(loginDescriptor) + self.login(with: credentials, updateUserDisplayName: updateUserDisplayName, allowMigration: allowMigration) } } diff --git a/RxFireAuth/Classes/UserManager.swift b/RxFireAuth/Classes/UserManager.swift index 706e2a8..0f712d2 100644 --- a/RxFireAuth/Classes/UserManager.swift +++ b/RxFireAuth/Classes/UserManager.swift @@ -229,7 +229,7 @@ public class UserManager: UserManagerType { if let currentUser = Auth.auth().currentUser, currentUser.isAnonymous { if allowMigration == nil { - observer(.error(UserError.migrationRequired)) + observer(.error(UserError.migrationRequired(nil))) return disposable } @@ -250,8 +250,8 @@ public class UserManager: UserManagerType { } } - public func login(with credentials: LoginCredentials, allowMigration: Bool? = nil) -> Single { - return Single.create { [unowned self] observer -> Disposable in + public func login(with credentials: LoginCredentials, updateUserDisplayName: Bool, allowMigration: Bool? = nil) -> Single { + return Single.create { [unowned self] observer -> Disposable in let disposable = Disposables.create { } let firebaseCredentials = OAuthProvider.credential(withProviderID: credentials.provider.rawValue, idToken: credentials.idToken, rawNonce: credentials.nonce) @@ -282,7 +282,7 @@ public class UserManager: UserManagerType { /// There is a currently logged-in user. if currentUser.isAnonymous { if allowMigration == nil { - observer(.error(UserError.migrationRequired)) + observer(.error(UserError.migrationRequired(credentials))) return } @@ -322,6 +322,13 @@ public class UserManager: UserManagerType { return disposable } + .flatMap { (loginDescriptor) -> Single in + if updateUserDisplayName, let fullName = loginDescriptor.fullName, fullName.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 { + return self.update(user: UserData(id: nil, email: nil, displayName: fullName, isAnonymous: false)) + .andThen(Single.just(loginDescriptor)) + } + return Single.just(loginDescriptor) + } } /// Sign in with the passed credentials in the specified disposable diff --git a/RxFireAuth/Classes/UserManagerType.swift b/RxFireAuth/Classes/UserManagerType.swift index cb35a29..9e46815 100644 --- a/RxFireAuth/Classes/UserManagerType.swift +++ b/RxFireAuth/Classes/UserManagerType.swift @@ -95,7 +95,14 @@ public protocol UserManagerType { /// Use this function to sign in with a provider credentials. In a normal flow, /// you'll use this function with credentials obtained by one of the `signInWith…` methods /// provided by implementations of `LoginProviderManagerType`. - func login(with credentials: LoginCredentials, allowMigration: Bool?) -> Single + /// + /// - since: version 1.3.0 + /// + /// - parameters: + /// - credentials: Credentials to use to login. + /// - updateUserDisplayName: If the passed credentials result in a successful login and this is set to `true`, this function will attempt to update the user display name by reading it from the resulting `LoginDescriptor`. + /// - returns: A Single to observe for result. + func login(with credentials: LoginCredentials, updateUserDisplayName: Bool, allowMigration: Bool?) -> Single /// Logout the currently logged-in user. /// From cb5d403cb42b3e34bc8ade6240898faba6e6ba3c Mon Sep 17 00:00:00 2001 From: Alessio Moiso Date: Sat, 25 Apr 2020 21:54:28 +0200 Subject: [PATCH 5/7] Add documentation on LoginCredentials --- RxFireAuth/Classes/LoginCredentials.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/RxFireAuth/Classes/LoginCredentials.swift b/RxFireAuth/Classes/LoginCredentials.swift index 099e438..81dd09a 100644 --- a/RxFireAuth/Classes/LoginCredentials.swift +++ b/RxFireAuth/Classes/LoginCredentials.swift @@ -7,20 +7,34 @@ import Foundation +/// An instance of this class is returned by +/// methods of `LoginProviderManagerType` +/// when a `UserError.migrationRequired` error occurs. +/// +/// You shouldn't need to inspect the content of this struct. +/// Its main purpose is to temporary store credentials in order +/// to continue the login action when clients have handled the error. public struct LoginCredentials { + /// A provider represent a supported login provider. public enum Provider: String { + /// Sign in with Apple. case apple = "apple.com" } + /// Get or set the ID token. var idToken: String + /// Get or set the user full name. var fullName: String? + /// Get or set the user email. var email: String + /// Get or set the login provider. var provider: Provider + /// Get or set the nonce. var nonce: String } From a6476527062b06cb4d1ea965140dd2c55708396a Mon Sep 17 00:00:00 2001 From: Alessio Moiso Date: Sat, 25 Apr 2020 22:03:09 +0200 Subject: [PATCH 6/7] Add error mapping to login(with credentials:), improve README --- README.md | 6 +++--- RxFireAuth/Classes/UserError.swift | 2 +- RxFireAuth/Classes/UserManager.swift | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index df76e22..97f04da 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,11 @@ To see it in action, follow these steps: - Download this repository. - Navigate to your [Firebase Console](https://console.firebase.google.com/) and create a new project using `io.mrasterisco.github.RxFireAuth-Example` as bundle identifier *(or change the bundle identifier to match the one of a project you already have)*. - Download the `GoogleService-Info.plist` and place it in the `Example/RxFireAuth` folder. -- In the Firebase Console, navigate to Authentication and enable the "Email/Password" and "Anonymous" sign-in methods. +- In the Firebase Console, navigate to Authentication and enable the "Email/Password", "Anonymous" and "Apple" sign-in methods. - Run `pod install` inside the `Example/RxAuth` folder. -- Open the `RxFireAuth.xcworkspace`, build and run. +- Open the `RxFireAuth.xcworkspace`, select a valid Signing Identity, build and run. -*By default, the app will run on simulators only. To run it on an actual device, make sure to select a Team under Signing & Capabilities, in the Xcode settings for the "RxFireAuth_Example" target.* +*To test Sign in with Apple, you need a valid signing identity. If you don't have one now, you can turn off Sign in with Apple under the "Signing & Capabilities" tab of the Xcode project.* ## Usage The whole library is built around the `UserManagerType` protocol. The library provides the default implementation of it through the `UserManager` class, that you can instantiate directly or get through Dependency Injection. diff --git a/RxFireAuth/Classes/UserError.swift b/RxFireAuth/Classes/UserError.swift index 7948206..e64d4dc 100644 --- a/RxFireAuth/Classes/UserError.swift +++ b/RxFireAuth/Classes/UserError.swift @@ -95,7 +95,7 @@ public enum UserError: LocalizedError { case .providerAlreadyLinked: return "This login provider is already linked." case .configurationError: - return "There is an error in your Firebase Console configuration." + return "There is an error in your Firebase Console configuration. The requested login provider may be disabled." case .invalidConfiguration: return "There is an error in your app configuration." case .keychainError(let error): diff --git a/RxFireAuth/Classes/UserManager.swift b/RxFireAuth/Classes/UserManager.swift index 0f712d2..9ee264c 100644 --- a/RxFireAuth/Classes/UserManager.swift +++ b/RxFireAuth/Classes/UserManager.swift @@ -260,7 +260,7 @@ public class UserManager: UserManagerType { let signInCompletionHandler: (Error?) -> Void = { (error) in guard !disposable.isDisposed else { return } if let error = error { - observer(.error(error)) + observer(.error(self.map(error: error))) } else if let newUser = Auth.auth().currentUser { observer( .success( @@ -293,7 +293,7 @@ public class UserManager: UserManagerType { currentUser.delete { (error) in guard !disposable.isDisposed else { return } if let error = error { - observer(.error(error)) + observer(.error(self.map(error: error))) } else { self.signIn(with: firebaseCredentials, in: disposable, completionHandler: signInCompletionHandler) } From 2074d0b04b193b83ba3f8c12727af22ce1c2a1dd Mon Sep 17 00:00:00 2001 From: Alessio Moiso Date: Sat, 25 Apr 2020 22:10:57 +0200 Subject: [PATCH 7/7] Update version, generate docs --- RxFireAuth.podspec | 2 +- docs/Classes.html | 8 +- docs/Classes/SignInWithAppleHandler.html | 8 +- docs/Classes/UserManager.html | 34 +++- docs/Enums.html | 8 +- docs/Enums/UserError.html | 19 ++- docs/Protocols.html | 8 +- docs/Protocols/LoginProviderManagerType.html | 10 +- docs/Protocols/UserManagerType.html | 81 +++++++++- docs/Structs.html | 42 ++++- docs/Structs/LoginCredentials.html | 149 ++++++++++++++++++ docs/Structs/LoginCredentials/Provider.html | 142 +++++++++++++++++ docs/Structs/LoginDescriptor.html | 8 +- docs/Structs/UserData.html | 8 +- docs/Typealiases.html | 8 +- .../Contents/Resources/Documents/Classes.html | 8 +- .../Classes/SignInWithAppleHandler.html | 8 +- .../Documents/Classes/UserManager.html | 34 +++- .../Contents/Resources/Documents/Enums.html | 8 +- .../Resources/Documents/Enums/UserError.html | 19 ++- .../Resources/Documents/Protocols.html | 8 +- .../Protocols/LoginProviderManagerType.html | 10 +- .../Documents/Protocols/UserManagerType.html | 81 +++++++++- .../Contents/Resources/Documents/Structs.html | 42 ++++- .../Documents/Structs/LoginCredentials.html | 149 ++++++++++++++++++ .../Structs/LoginCredentials/Provider.html | 142 +++++++++++++++++ .../Documents/Structs/LoginDescriptor.html | 8 +- .../Resources/Documents/Structs/UserData.html | 8 +- .../Resources/Documents/Typealiases.html | 8 +- .../Contents/Resources/Documents/index.html | 46 +++--- .../Contents/Resources/Documents/search.json | 2 +- .../Contents/Resources/docSet.dsidx | Bin 36864 -> 36864 bytes docs/docsets/RxFireAuth.tgz | Bin 64570 -> 66601 bytes docs/index.html | 46 +++--- docs/search.json | 2 +- 35 files changed, 1087 insertions(+), 77 deletions(-) create mode 100644 docs/Structs/LoginCredentials.html create mode 100644 docs/Structs/LoginCredentials/Provider.html create mode 100644 docs/docsets/RxFireAuth.docset/Contents/Resources/Documents/Structs/LoginCredentials.html create mode 100644 docs/docsets/RxFireAuth.docset/Contents/Resources/Documents/Structs/LoginCredentials/Provider.html diff --git a/RxFireAuth.podspec b/RxFireAuth.podspec index 10c35e3..d957328 100644 --- a/RxFireAuth.podspec +++ b/RxFireAuth.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'RxFireAuth' - s.version = '1.2.0' + s.version = '1.3.0' s.summary = 'A smart Rx wrapper around Firebase Authentication SDK' # This description is used to generate tags and improve search results. diff --git a/docs/Classes.html b/docs/Classes.html index 98b07b2..c7b466b 100644 --- a/docs/Classes.html +++ b/docs/Classes.html @@ -60,6 +60,12 @@