iOS link two firebase account - ios

I'm trying to link two firebase accounts, usually the user would be logged in with social media or email or anonmus account and then a user would sign in with phone. I need to link these two accounts.
parent view
a function in the parent view would be called to sign up with phone number
class welcomeView: UIViewController, FUIAuthDelegate {
//stores current user
var previosUser = FIRUser()
override func viewDidLoad() {
super.viewDidLoad()
if let user = Auth.auth().currentUser {
previosUser = user
}else{
Auth.auth().signInAnonymously() { (user, error) in
if error != nil {
//handle error
}
previosUser = user!
}
}
}
func signUpWithPhone(){
let vc = signUpWithPhoneView()
vc.authUI?.delegate = self
vc.auth?.languageCode = "ar"
self.present(vc, animated: true)
}
}
in the child view (signUpWithPhoneView) I present the FUIPhoneAuth
phoneProvider.signIn(withPresenting: self)
child view
import UIKit
import FirebaseAuthUI
import FirebasePhoneAuthUI
class signUpWithPhoneView: UIViewController {
fileprivate(set) var authUI = FUIAuth.defaultAuthUI()
var didload = false
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
if !didload { // stops from looping
didload = !didload
guard let authUI = self.authUI else {return}
let phoneProvider = FUIPhoneAuth.init(authUI: authUI)
self.authUI?.providers = [phoneProvider]
>> phoneProvider.signIn(withPresenting: self)
}
}
}
when the user signs in, the child view will be dismissed automatically and I have didSignInWith function that will be Called in the parents view. I need to link the previous user account and the user phone account
parent view
func authUI(_ authUI: FUIAuth, didSignInWith user: FirebaseAuth.User?, error: Error?) {
if let user = user{
// link the the two accounts
}else{
}
}
I tried to link by using
let provider = PhoneAuthProvider.provider(auth: authUI.auth!)
let credential = PhoneAuthProvider.credential(provider)
previosUser.link(with: credential, completion: { (user, error) in
if error != nil {
print("this is linking two acounts error : " , error)
}
})
but there was error in credential
in ...previosUser.link(with: *credential*, completion: ...
Cannot convert value of type '(String, String) -> PhoneAuthCredential' to expected
argument type 'AuthCredential'
any help would be appreciated
thanks

You are getting the provider with authUI.auth and then not using it to get the credential and you are currently using the static reference of the instance method credential.
The instance method itself takes two strings and returns an AuthCredential which is why you're seeing (String, String) -> AuthCredential. You must create the credential using a verification ID and code that you must collect from the user.
guard let auth = authUI.auth else { return }
let provider = PhoneAuthProvider.provider(auth: auth)
let credential = provider.credential(
withVerificationID: "XXXXXXXXXXXX",
verificationCode: "XXXXXX"
)
// ...

Related

Current Firebase User Returning Nil

I have an app using FirebaseAuth to authenticate users. I do this in an AuthService class;
import Foundation
import FirebaseAuth
class AuthService: NSObject {
private override init() {}
static let shared = AuthService()
let currentUser = Auth.auth().currentUser
var uid = Auth.auth().currentUser?.uid
func checkAuthState(completion: #escaping(FirebaseAuth.User?) -> Void) {
// let listener = Auth.auth().addStateDidChangeListener { (auth, user) in
Auth.auth().addStateDidChangeListener { (auth, user) in
switch user {
case .none:
print("USER NOT FOUND IN CHECK AUTH STATE")
completion(nil)
case .some(let user):
print("USER FOUND WITH ID: \(user.uid)")
completion(user)
}
}
// Auth.auth().removeStateDidChangeListener(listener)
}
}
I then sign in a user anonymously using checkAuthState function above. Once this operation completes with some user I perform an action to switch the rootViewController.
func checkAuthState() {
authService.checkAuthState { (user) in
switch user {
case .none:
SceneDelegate.shared.rootController.showWelcomeController()
case .some(_):
SceneDelegate.shared.rootController.showTabBarController()
}
}
}
}
Here is my code within my RootViewController;
func showWelcomeController() {
let welcomeController = WelcomeController(authService: AuthService.shared)
let navigationController = UINavigationController(rootViewController: welcomeController)
addChild(navigationController)
navigationController.view.frame = view.frame
view.addSubview(navigationController.view)
navigationController.didMove(toParent: self)
currentController.willMove(toParent: nil)
currentController.view.removeFromSuperview()
currentController.removeFromParent()
currentController = navigationController
}
func showTabBarController() {
let tabBarController = TabBarController()
addChild(tabBarController)
tabBarController.view.frame = view.frame
view.addSubview(tabBarController.view)
tabBarController.didMove(toParent: self)
currentController.willMove(toParent: nil)
currentController.view.removeFromSuperview()
currentController.removeFromParent()
currentController = tabBarController
}
The above operates all as expected and shows my TabBarController, however, I then run a function in one of my ViewControllers in my TabBarController which is;
var uid = Auth.auth().currentUser?.uid
func readComments(query: Query, lastDocument: DocumentSnapshot?, completion: #escaping(Result<[CommentModel], NetworkError>) -> ()) {
guard let uid = auth.uid else { return completion(.failure(.userNotFound)) }
// Other code
}
However, I get the .userNotFound error? as it returns nil? I'm confused as checkAuthState is returning a user?
This typically means that the currentUser is not yet initialized by the time that code runs. Since restoring the user's sign-in state requires a call to the server, it is an asynchronous operation.
Any code that depends on the current user state being restored should be inside (or be called from) an addStateDidChangeListener as in your first snippet. So you may need a snippet like that in your other view controller(s) too.

In Cognito on iOS, handling new password required doesn't ever reach didCompleteNewPasswordStepWithError

I'm trying to implement functionality to respond to FORCE_CHANGE_PASSWORD on my iOS app that uses AWS Cognito. I used this Stack Overflow question which references this sample code. Right now, my code opens a view controller like it's supposed to; however, once on that view controller, I can't get it to do anything. In the sample code, it seems that when you want to submit the password change request you call .set on an instance of AWSTaskCompletionSource<AWSCognitoIdentityNewPasswordRequiredDetails>, yet when I do this, the protocol function didCompleteNewPasswordStepWithError is never called. Interestingly, the other protocol function getNewPasswordDetails is called quickly after viewDidLoad and I can't tell why. I believe this shouldn't be called until the user has entered their new password, etc and should be in response to .set but I could be wrong.
My code is pretty identical to the sample code and that SO post, so I'm not sure what's going wrong here.
My relevant AppDelegate code is here:
extension AppDelegate: AWSCognitoIdentityInteractiveAuthenticationDelegate {
func startNewPasswordRequired() -> AWSCognitoIdentityNewPasswordRequired {
//assume we are presenting from login vc cuz where else would we be presenting that from
DispatchQueue.main.async {
let presentVC = UIApplication.shared.keyWindow?.visibleViewController
TransitionHelperFunctions.presentResetPasswordViewController(viewController: presentVC!)
print(1)
}
var vcToReturn: ResetPasswordViewController?
returnVC { (vc) in
vcToReturn = vc
print(2)
}
print(3)
return vcToReturn!
}
//put this into its own func so we can call it on main thread
func returnVC(completion: #escaping (ResetPasswordViewController) -> () ) {
DispatchQueue.main.sync {
let storyboard = UIStoryboard(name: "ResetPassword", bundle: nil)
let resetVC = storyboard.instantiateViewController(withIdentifier: "ResetPasswordViewController") as? ResetPasswordViewController
completion(resetVC!)
}
}
}
My relevant ResetPasswordViewController code is here:
class ResetPasswordViewController: UIViewController, UITextFieldDelegate {
#IBAction func resetButtonPressed(_ sender: Any) {
var userAttributes: [String:String] = [:]
userAttributes["given_name"] = firstNameField.text!
userAttributes["family_name"] = lastNameField.text!
let details = AWSCognitoIdentityNewPasswordRequiredDetails(proposedPassword: self.passwordTextField.text!, userAttributes: userAttributes)
self.newPasswordCompletion?.set(result: details)
}
}
extension ResetPasswordViewController: AWSCognitoIdentityNewPasswordRequired {
func getNewPasswordDetails(_ newPasswordRequiredInput: AWSCognitoIdentityNewPasswordRequiredInput, newPasswordRequiredCompletionSource: AWSTaskCompletionSource<AWSCognitoIdentityNewPasswordRequiredDetails>) {
self.newPasswordCompletion = newPasswordRequiredCompletionSource
}
func didCompleteNewPasswordStepWithError(_ error: Error?) {
DispatchQueue.main.async {
if let error = error as? NSError {
print("error")
print(error)
} else {
// Handle success, in my case simply dismiss the view controller
SCLAlertViewHelperFunctions.displaySuccessAlertView(timeoutValue: 5.0, title: "Success", subTitle: "You can now login with your new passowrd", colorStyle: Constants.UIntColors.emeraldColor, colorTextButton: Constants.UIntColors.whiteColor)
self.dismiss(animated: true, completion: nil)
}
}
}
}
Thank you so much for your help in advance and let me know if you need any more information.

Checking user authentication using Google Sign In and SwiftUI

I've successfully set up authentication within my app using Google Sign-In to where I am able to return a Firebase User. I am attempting to set up a Sign-In screen that is only shown when there is no authenticated Firebase User, however with my current code the Sign-In screen is always visible even though I am consistently returning an authenticated user.
I've implemented the didSignInFor function in AppDelegate
func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error?) {
// ...
if let error = error {
print(error.localizedDescription)
return
}
guard let authentication = user.authentication else { return }
let credential = GoogleAuthProvider.credential(withIDToken: authentication.idToken,
accessToken: authentication.accessToken)
// ...
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
print(error.localizedDescription)
return
}
let session = FirebaseSession.shared
if let user = Auth.auth().currentUser {
session.user = User(uid: user.uid, displayName: user.displayName, email: user.email)
print("User sign in successful: \(user.email!)")
}
}
}
as well as a few lines in didFinishLaunchingWithOptions that sets the isLoggedIn property of my ObservableObject FirebaseSession
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
let auth = Auth.auth()
if auth.currentUser != nil {
FirebaseSession.shared.isLoggedIn = true
print(auth.currentUser?.email!)
} else {
FirebaseSession.shared.isLoggedIn = false
}
//Cache
let settings = FirestoreSettings()
settings.isPersistenceEnabled = false
GIDSignIn.sharedInstance().clientID = FirebaseApp.app()?.options.clientID
GIDSignIn.sharedInstance().delegate = self
return true
}
My ObservableObject
class FirebaseSession: ObservableObject {
static let shared = FirebaseSession()
init () {}
//MARK: Properties
#Published var user: User?
#Published var isLoggedIn: Bool?
#Published var items: [Thought] = []
var ref: DatabaseReference = Database.database().reference(withPath: "\(String(describing: Auth.auth().currentUser?.uid ?? "Error"))")
//MARK: Functions
func listen() {
_ = Auth.auth().addStateDidChangeListener { (auth, user) in
if auth.currentUser != nil {
self.isLoggedIn = true
}
if let user = user {
self.user = User(uid: user.uid, displayName: user.displayName, email: user.email)
} else {
self.user = nil
}
}
}
}
Finally, I perform my authentication check in the main view of my app here accessing FirebaseSession via my ObservedObject
struct AppView: View {
#ObservedObject var session = FirebaseSession.shared
#State var modalSelection = 1
#State var isPresentingAddThoughtModal = false
var body: some View {
NavigationView {
Group {
if session.isLoggedIn == true {
ThoughtsView()
} else {
SignInView()
}
}
}
}
}
As mentioned above my check doesn't seem to work. Even though my user is authenticated, SignInView is always visible.
How can I successfully check my user authentication each time my app loads?
UPDATE
I am now able to check authentication when the app loads, but after implementing Sohil's solution I am not observing realtime changes to my ObservableObject FirebaseSession. I want to observe changes to FirebaseSession so that after a new user signs in, the body of AppView will be redrawn and present ThoughtsView instead of SignInView. Currently I have to reload the app in order for the check to occur after authentication.
How do I observe changes to FirestoreSession from AppView?
You need to do something like this. I didn't try running this so I'm not sure if there are any typos...
class SessionStore : ObservableObject {
#Published var session: FIRUser?
var isLoggedIn: Bool { session != nil}
var handle: AuthStateDidChangeListenerHandle?
init () {
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
self.session = user
} else {
self.session = nil
}
}
}
deinit {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
}
in your component:
struct AppView: View {
#ObservedObject var session = SessionStore()
var body: some View {
Group {
if session.isLoggedIn {
...
} else {
...
}
}
}
}
Note the important thing here is that the object that is changing is #Published. That's how you will receive updates in your view.
Your problem is in accessing the objects and it's value. Means, in AppDelegate.swift file you are creating an object of FirebaseSession and assigning the values, but then in your AppView you are again creating a new object of FirebaseSession which creates a new instance of the class and all the values are replaced to default.
So, you need to use the same object throughout our application lifecycle, which can be done by defining the let session = FirebaseSession() globally or by creating a Singleton Class like below.
class FirebaseSession: ObservableObject {
static let shared = FirebaseSession()
private init () {}
//Properties...
//Functions...
}
Then you can access the shared object like this:
FirebaseSession.shared.properties
This way your assigned values will be preserved during the app lifecycle.
I don't know if it should be useful for you, but I read now your question because I was finding a solution for a similar issue for me, so I found it and I'm going to share with you.
I thought about using a delegate. So I created a protocol with the name ApplicationLoginDelegate in my AppDelegate class.
I define the protocol in this way:
protocol ApplicationLoginDelegate: AnyObject {
func loginDone(userDisplayName: String)
}
And in the AppDelegate class I define the loginDelegate:
weak var loginDelegate: ApplicationLoginDelegate?
You can call the delegate func in your didSignIn func
Auth.auth().signIn(with: credential) { (res, error) in
if let err = error {
print(err.localizedDescription)
return
} else {
self.loginDelegate?.loginDone(userDisplayName: (res?.user.displayName)!)
}
}
So in the SceneDelegate you use your delegate as I show you:
class SceneDelegate: UIResponder, UIWindowSceneDelegate, ApplicationLoginDelegate {
var window: UIWindow?
var eventsVM: EventsVM?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Set the app login delegate
(UIApplication.shared.delegate as! AppDelegate).loginDelegate = self
// Environment Objects
let eventsVM = EventsVM()
self.eventsVM = eventsVM
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
.environmentObject(eventsVM)
// Use a UIHostingController as window root view controller.
[...]
}
func loginDone(userDisplayName: String) {
guard let eventsVM = self.eventsVM else { return }
eventsVM.mainUser = User(displayName: userDisplayName)
}
So when you have updated your Environment Object, that has itself a #Publisced object (for example an User object), you receive your updates everywhere you define and call the Environment Object!
That's it!

sign up with firebase and swift 4 with two ViewController, one for email and one for password

i want create a sign up with firebase and swift 4, first a view controller asks me the email, after entering the email, I click on next, and another view controller will ask me for the password, then click on register. How do I save the first Viewcontroller's email, pass it to the second ViewController for the password, and then send it all to firebase? sorry for my english. In practice it must work like the instagram sign up.
This is the code of the first view controller:
import Foundation
import Firebase
import FirebaseAuth
import UITextField_Shake
internal class SignUpSegue0 : UIViewController {
#IBOutlet weak var labelwrongpass: UILabel!
#IBOutlet weak var Emailsign: UITextField!
func isValidEmail(testStr:String) -> Bool {
print("validate emilId: \(testStr)")
let emailRegEx = "[A-Z0-9a-z._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}"
let emailTest = NSPredicate(format:"SELF MATCHES %#", emailRegEx)
let result = emailTest.evaluate(with: testStr)
return result
}
#IBAction func Nextsign1(_ sender: UIButton) {
guard let email = Emailsign.text else { return }
Auth.auth().fetchProviders(forEmail: email, completion: {
(providers, error) in
if error != nil {
print("Email disponibile")
self.labelwrongpass.text = "Email inserita correttamente"
self.labelwrongpass.textColor = UIColor.green
} else if providers != nil {
print("L'email inserita è già presente")
self.labelwrongpass.text = nil
self.labelwrongpass.text = "L'email inserita è già presente"
self.Emailsign.shake(10,withDelta: 5.0)
}
})
}
override func viewDidLoad() {
super.viewDidLoad()
let userDefaults = UserDefaults.standard
let Emailsign = userDefaults.string(forKey: "emailsign")
}
}
If you want to pass the value from one VC to another you shouldn’t use UserDefaults. You may as well use a global variable if you do that !
You create a Custom ViewController
In the second VC create a property called email.
Then in the first Vc
let vc2 = SecondViewController()
vc2.email = theEmail
Push / Seque to vc2

Cannot set value conforming to protocol to property with protocol type

I'm trying to create a fake authenticator for my unit tests that can manually set the user as logged in or logged out and bypass the API my code actually uses where I'd need a real accessToken to log the user in.
I've wrapped the Authentication API my app uses in to the following class:
API Wrapper
import OIDC
protocol Authenticator {
func getValidAccessToken(completionHandler: #escaping (Error?, String?) -> Void)
}
struct OIDCAuthenticator: Authenticator {
func getValidAccessToken(completionHandler: #escaping (Error?, String?) -> Void) {
//API call
OIDCHelper.getValidAccessToken { (error, accessToken) in
DispatchQueue.main.async {
completionHandler( error, accessToken)
}
}
}
}
Then I create a Fake Authenticator using the same protocol for testing purposes
Fake/Testing Authenticator
import OIDC
import Foundation
///Mocks the user being logged in our logged out for Testing purposes
struct FakeAuthenticator: Authenticator {
let error: OIDCError?
let accessToken: String?
func getValidAccessToken(completionHandler: #escaping (Error?, String?) -> Void) {
completionHandler(error, accessToken)
}
init(loggedIn: Bool) {
if loggedIn {
error = .tokenNotFoundInKeychainData
accessToken = nil
} else {
error = nil
accessToken = "abcdefg"
}
}
}
Settings the OIDCAuthenticator API Wrapper works fine when settings the authenticator in the ViewController subclass.
TableViewController Implementation
import UIKit
import OIDC
class SettingsPageTableViewController: UITableViewController{
// MARK: - Outlets and variables
var authenticator: Authenticator!
private var isUserLoggedIn = false
// MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
authenticator = OIDCAuthenticator()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
authenticator.getValidAccessToken { [weak self] (error, _) in
self?.isUserLoggedIn = (error == nil)
self?.setLogginStatus()
}
}
}
However when I try to do the same thing with the FakeAuthenticator in my Unit tests I get the following error:
SettingsViewController Test Class
import XCTest
import UIKit
#testable import MyProject
class SettingsViewControllerTests: XCTestCase {
var viewController: SettingsPageTableViewController!
override func setUp() {
super.setUp()
configureViewControllerForTesting()
}
private func configureViewControllerForTesting() {
let storyboard = UIStoryboard(name: "SettingsPage", bundle: nil)
let navigationController = storyboard.instantiateInitialViewController() as! UINavigationController
viewController = navigationController.topViewController as! SettingsPageTableViewController
_ = viewController.view
}
func testSignInButtonIsAvailableWhenUnauthenticated() {
viewController.authenticator = FakeAuthenticator(loggedIn: false)
}
}
The same things happens when I swap out FakeAuthenticator with OIDCAuthenticator. I've also attempted to cast the FakeAuthenticator to Authenticator but this merely alters the Error to Cannot assign value of type 'Authenticator' to type 'Authenticator!'.
Why am I getting this error and what is the best approach to fixing this?
You need to remove the files from your test target since you're already importing your whole project with #testable.

Resources