I am using SwiftUI with the Firebase SDK. I am trying to implement Sign In with Microsoft/Azure in my app. I am following what the docs say but when I get to the part that is supposed to make the login window pop up, nothing shows. There are no errors printed to the console. Here is my login function:
func signInWithMicrosoft() {
print("Microsoft Login")
let auth = Auth.auth()
let provider = OAuthProvider(providerID: "microsoft.com", auth: auth)
provider.customParameters = [
"prompt": "consent",
"login_hint": "user#firstadd.onmicrosoft.com"
]
provider.scopes = ["mail.read", "openid.read"]
provider.getCredentialWith(_: nil) { credential, error in
if error != nil {
return
}
if credential != nil {
self.authenticateUserWithMicrosoft(for: credential)
}
}
}
and here is my auth function:
private func authenticateUserWithMicrosoft(for credential: AuthCredential?) {
Auth.auth().signIn(with: credential!) { [unowned self] (_, error) in
if let error = error {
print(error.localizedDescription)
} else {
state = .signedIn
}
}
}
I have tried many fixes such as putting my Auth code inside my AppDelegate class but nothing seems to have worked. Any help would be appreciated.
Related
Using SwiftUI, Xcode12.5.1, Swift5.4.2, iOS14.7.1,
My Firebase-Email/Password Login-page shall be extended with other Login possibilities such as Apple-Login (eventually Google-login, Facebook-login etc).
My steps:
log in with Email/Password to Firebase
log out
log in with "Sign in with Apple"
--> Then I get the following error:
Error Domain=FIRAuthErrorDomain Code=17007
"The email address is already in use by another account."
UserInfo={NSLocalizedDescription=The email address is already in use by another account.,
FIRAuthErrorUserInfoNameKey=ERROR_EMAIL_ALREADY_IN_USE}
What I intended to do is to link the existing Email/Password-Firebase-Account to the Sign in with Apple-Account (as described here and here).
But for doing that I would need the error FIRAuthErrorUserInfoUpdatedCredentialKey that allows to retrieve the old user eventually.
In my case, I get ERROR_EMAIL_ALREADY_IN_USE which does not lead to any old user to be linked.
What do I have to do ?
Here is my code:
let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce)
Auth.auth().signIn(with: credential) { (authResult, error) in
if (error != nil) {
print(error?.localizedDescription as Any)
return
}
print("signed in with Apple...")
do {
// if user did log in with Email/Password previously
if let email = try THKeychain.getEmail(),
let password = try THKeychain.getPassword() {
let credential = EmailAuthProvider.credential(withEmail: email, password: password)
if let user = authResult?.user {
// here I am trying to link the existing Firebase-Email/Password account to the just signed-in with Apple account
user.link(with: credential) { (result, linkError) in
print(linkError) // this is where I get FIRAuthErrorUserInfoNameKey=ERROR_EMAIL_ALREADY_IN_USE
// unfortunately, the two accounts are not linked as expected due to this error !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// What is missing ??????????????????
loginStatus = true
}
}
} else {
loginStatus = true
}
} catch {
print(error.localizedDescription)
}
}
On the Firebase-documentation it sais:
Sign in with Apple will not allow you to reuse an auth credential to link to an existing account. If you want to link a Sign in with Apple credential to another account, you must first attempt to link the accounts using the old Sign in with Apple credential and then examine the error returned to find a new credential. The new credential will be located in the error's userInfo dictionary and can be accessed via the FIRAuthErrorUserInfoUpdatedCredentialKey key.
What does the part "...If you want to link a Sign in with Apple credential to another account, you must first attempt to link the accounts using the old Sign in with Apple credential..." exactly mean ? WHAT IS THE old Sign in with Apple credential ????????
And how would I do that ?
In fact, at the linking-call, I actually expected some sort of linkError.userInfo with an updated user to sign in with. But the linkError in my example only gives me the ERROR_EMAIL_ALREADY_IN_USE error without further userInfo.
As Peter Friese mentions in his Blog, I should somehow be able to retrieve a AuthErrorUserInfoUpdatedCredentialKey from the error.userInfo. But in my case, the linkError does not have any kind of such information - unfortunately!
Here is an excerpt of Peter's example: (again not applicable in my case for some unknown reason?????)
currentUser.link(with: credential) { (result, error) in // (1)
if let error = error, (error as NSError).code == AuthErrorCode.credentialAlreadyInUse.rawValue { // (2)
print("The user you're signing in with has already been linked, signing in to the new user and migrating the anonymous users [\(currentUser.uid)] tasks.")
if let updatedCredential = (error as NSError).userInfo[AuthErrorUserInfoUpdatedCredentialKey] as? OAuthCredential {
print("Signing in using the updated credentials")
Auth.auth().signIn(with: updatedCredential) { (result, error) in
if let user = result?.user {
// TODO: handle data migration
self.doSignIn(appleIDCredential: appleIDCredential, user: user) // (3)
}
}
}
}
}
Reversing the order of linking made me advance a tiny bit.
If I press the Sign in with Apple button, my code now logs in with Firebase-Email/Password first (i.e. the necessary credentials are taken from the Keychain). And on a second step, links with the Apple-credentials. And by doing so, the linking finally gives me the desired AuthErrorUserInfoUpdatedCredentialKey in the link-callback.
There I retrieve the updatedCredential to log in with Apple.
See code below.
HOWEVER, I STILL DON'T KNOW WHY AFTER LOGIN THIS WAY, MY DATA IS STILL MISSING ???????
HOW DOES THIS DATA-MIGRATION STEP WORK ???
Shouldn't the user.link(with: appleCredentials) { ... } do the job ?
What do I need to do in order to get the very same Firebase-Data, no matter the login method ???
let appleCredentials = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce)
do {
// if user did log in with Email/Password anytime before
if let email = try THKeychain.getEmail(),
let password = try THKeychain.getPassword() {
let firebaseEmailCredentials = EmailAuthProvider.credential(withEmail: email, password: password)
Auth.auth().signIn(with: firebaseEmailCredentials) { (authResult, error) in
if let user = authResult?.user {
user.link(with: appleCredentials) { (result, linkError) in
if let linkError = linkError, (linkError as NSError).code == AuthErrorCode.credentialAlreadyInUse.rawValue {
print("The user you're signing in with has been linked.")
print("Signing in to Apple and migrating the email/pw-firebase-users [\(user.uid)]` data.")
if let updatedCredential = (linkError as NSError).userInfo[AuthErrorUserInfoUpdatedCredentialKey] as? OAuthCredential {
print("Signing in using the updated credentials")
Auth.auth().signIn(with: updatedCredential) { (result, error) in
if let _ = result?.user {
print("signed in with Apple...")
// TODO: handle data migration
print("Data-migration takes place now...")
loginStatus = true
}
}
}
}
else if let error = error {
print("Error trying to link user: \(error.localizedDescription)")
}
else {
if let _ = result?.user {
loginStatus = true
}
}
}
}
}
} else {
// case where user never logged in with firebase-Email/Password before
Auth.auth().signIn(with: appleCredentials) { (result, error) in
if let _ = result?.user {
print("signed in with Apple...")
loginStatus = true
}
}
}
} catch {
print(error.localizedDescription)
}
I can successfully log into facebook by calling this signIn() method.
private fun signIn() {
loginBtn?.registerCallback(callBackManager,object: FacebookCallback<LoginResult>{
override fun onSuccess(result: LoginResult?) {
handleFacebookAccessToken(result!!.accessToken)
}
override fun onCancel() {
}
override fun onError(error: FacebookException?) {
Log.d("MainActivity:", "onError "+error?.message)
}
})
}
private fun handleFacebookAccessToken(accessToken: AccessToken?) {
val authCredential = FacebookAuthProvider.getCredential(accessToken!!.token)
firebaseAuth?.signInWithCredential(authCredential)
?.addOnCompleteListener(this) { task ->
if (task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
Log.d("MainActivity:", "signInWithCredential:success")
} else {
// If sign in fails, display a message to the user.
Log.w("dsds", "signInWithCredential:failure ", task.exception)
Toast.makeText(baseContext, "Authentication failed.",
Toast.LENGTH_SHORT).show()
}
}
}
But I am not able to authenticate with firebase to sign into firebase by calling handleFacebookAccessToken(accessToken: AccessToken?) method because it is giving invalid access token.
If I manually get an access token from a test user by going to the app in facebook developer console and
hardcode it in the handleFacebookAccessToken(token: String?) method below then it works.
private fun handleFacebookAccessToken(token: String?) {
val authCredential = FacebookAuthProvider.getCredential(token)
firebaseAuth?.signInWithCredential(authCredential)
?.addOnCompleteListener(this) { task ->
if (task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
Log.d("MainActivity:", "signInWithCredential:success")
} else {
// If sign in fails, display a message to the user.
Log.w("dsds", "signInWithCredential:failure ", task.exception)
Toast.makeText(baseContext, "Authentication failed.",
Toast.LENGTH_SHORT).show()
}
}
}
I am getting this exception below.
com.google.firebase.auth.FirebaseAuthInvalidCredentialsException: The supplied auth credential is malformed or has expired. [ Remote site 5XX from facebook.com for VERIFY_CREDENTIAL ]
at com.google.android.gms.internal.firebase-auth-api.zztt.zza(com.google.firebase:firebase-auth##20.0.4:28)
at com.google.android.gms.internal.firebase-auth-api.zzvb.zza(com.google.firebase:firebase-auth##20.0.4:9)
at com.google.android.gms.internal.firebase-auth-api.zzvc.zzk(com.google.firebase:firebase-auth##20.0.4:1)
at com.google.android.gms.internal.firebase-auth-api.zzuz.zzh(com.google.firebase:firebase-auth##20.0.4:25)
at com.google.android.gms.internal.firebase-auth-api.zztq.zzk(com.google.firebase:firebase-auth##20.0.4:1)
at com.google.android.gms.internal.firebase-auth-api.zzpr.zza(com.google.firebase:firebase-auth##20.0.4:2)
at com.google.android.gms.internal.firebase-auth-api.zzvg.zza(com.google.firebase:firebase-auth##20.0.4:25)
at com.google.android.gms.internal.firebase-auth-api.zzuq.zzd(com.google.firebase:firebase-auth##20.0.4:4)
at com.google.android.gms.internal.firebase-auth-api.zzpy.zzc(com.google.firebase:firebase-auth##20.0.4:4)
at com.google.android.gms.internal.firebase-auth-api.zztu.zzd(com.google.firebase:firebase-auth##20.0.4:5)
at com.google.android.gms.internal.firebase-auth-api.zzsg.zzd(com.google.firebase:firebase-auth##20.0.4:3)
at com.google.android.gms.internal.firebase-auth-api.zzsf.accept(Unknown Source:6)
at com.google.android.gms.common.api.internal.zacj.doExecute(com.google.android.gms:play-services-base##17.1.0:2)
at com.google.android.gms.common.api.internal.zaf.zac(com.google.android.gms:play-services-base##17.1.0:6)
at com.google.android.gms.common.api.internal.GoogleApiManager$zaa.zac(com.google.android.gms:play-services-base##17.1.0:167)
at com.google.android.gms.common.api.internal.GoogleApiManager$zaa.zab(com.google.android.gms:play-services-base##17.1.0:139)
at com.google.android.gms.common.api.internal.GoogleApiManager$zaa.zaa(com.google.android.gms:play-services-base##17.1.0:105)
at com.google.android.gms.common.api.internal.GoogleApiManager.handleMessage(com.google.android.gms:play-services-base##17.1.0:145)
at android.os.Handler.dispatchMessage(Handler.java:103)
at com.google.android.gms.internal.base.zar.dispatchMessage(com.google.android.gms:play-services-base##17.1.0:8)
at android.os.Looper.loop(Looper.java:224)
at android.os.HandlerThread.run(HandlerThread.java:67)
Login to https://developers.facebook.com/apps/
Select app and confirm App Type is Consumer
Solution referred from https://github.com/FirebaseExtended/flutterfire/issues/4524#issuecomment-764610739
So I have push notifications implemented in my App and when the app first starts up, its asks users if they would like to allow push notifications (this implementation works fine as expected).
If this user disallows the push notifications, is it possible to have a button in the app which allows the user to click on and it would ask to allow permissions again?
This is what im trying to achieve:
SettingsView
//IF PUSH NOTIFICATIONS NOT ENABLED, SHOW THIS SECTION
Section (header: Text("Push Notifications")) {
HStack {
Image(systemName: "folder")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Enable Push Notifications").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
checkPushNotifications()
}) {
Text("View").font(.system(size:12))
}
}
}
In my Push Notification Function:
class PushNotificationService: NSObject, MessagingDelegate {
static let shared = PushNotificationService()
private let SERVER_KEY = "myserverkey"
private let NOTIFICATION_URL = URL(string: "https://fcm.googleapis.com/fcm/send")!
private let PROJECT_ID = "my project name"
private override init() {
super.init()
Messaging.messaging().delegate = self
}
func askForPermission() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted: Bool, error: Error?) in
if granted {
self.refreshFCMToken()
} else {
// Maybe tell the user to go to settings later and re-enable push notifications
}
}
}
func refreshFCMToken() {
InstanceID.instanceID().instanceID { (result, error) in
if let error = error {
print("Error fetching remote instance ID: \(error)")
} else if let result = result {
print("Remote instance ID token: \(result.token)")
self.updateFCMToken(result.token)
}
}
}
func updateFCMToken(_ token: String) {
guard let currentUser = Auth.auth().currentUser else { return }
let firestoreUserDocumentReference = Firestore.firestore().collection("users").document(currentUser.uid)
firestoreUserDocumentReference.updateData([
"fcmToken" : token
])
}
}
What im trying to achieve is if the user HAS NOT enabled notification only then ask them the option to reenable in SettingsView.
No you cannot. However, a good UI/UX design will be careful before burning the one-time chance of asking for permissions. Instead, use a user friendly UI to explain why you need certain permissions. For example, I often found it frustrating to implement a permission display view, and handle various async permission requests in a seperate view model. So I recently made a SwiftUI package:
PermissionsSwiftUI
PermissionSwiftUI is a package to beautifully display and handle permissions.
EmptyView()
.JMPermissions(showModal: $showModal, for: [.locationAlways, .photo, .microphone])
For a SINGLE line of code, you get a beautiful UI and the permission dialogs.
It already supports 7 OUT OF 12 iOS system permissions. More features coming 🙌
Full example
struct ContentView: View {
#State var showModal = false
var body: some View {
Button(action: {showModal=true},
label: {Text("Ask user for permissions")})
.JMPermissions(showModal: $showModal, for: [.locationAlways, .photo, .microphone])
}
}
To use PermissionsSwiftUI, simply add the JMPermission modifier to any view.
Pass in a Binding to show the modal view, and add whatever permissions you want to show.
The short answer is no, you can't ask the user again if he once disabled the push-notifications for your app.
What you can do, is navigating the user to the settings in their phone to allow push-notifications again.
The code snippet in SwiftUI for the button would be:
Button(action: {
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}, label: {
Text("Allow Push")
})
I would also refer to this question: How to ask notifications permissions if denied?
Below is the code for my signup page. I want to make it so that when someone creates an account on the sign up page, I create a document in the users collection and include uuid in the document. However, session.session?.uid ends up being nil. Does anyone know why this is?
struct SignUpView: View {
#State var email = ""
#State var password = ""
#State var name = ""
#State var error = ""
#EnvironmentObject var session: SessionStore
func signUp() {
let db = Firestore.firestore()
let user = db.collection("users").document()
let test = db.collection("users").document(user.documentID).collection("routines").document()
session.signUp(email: email, password: password) { (result, error) in
if let error = error {
self.error = error.localizedDescription
print("This is the error \(error)")
return
} else {
self.email = ""
self.password = ""
}
}
user.setData(["id": user.documentID, "email": email]) { (err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
}
print(session.session?.uid)
test.setData(["id:": test.documentID, "msg": "samwell Tarly", "uuid": session.session?.uid]) { (err) in
print("ummmmm test data?")
if err != nil {
print((err?.localizedDescription)!)
return
}
}
}
The Firebase APIs are asynchronous, simply because they access a remote system, across the internet, which takes a little time. The same applies for accessing the local disk, by the way. This blog post explains this in more detail.
Consequently, session.signUp is an asynchronous process. I.e. the call to print(session.session?.uid) is executed before session.signUp returns. Thus, session.session?.uid is still nil.
To work around this, you can nest your calls like this:
session.signUp(email: email, password: password) { (result, error) in
if let error = error {
self.error = error.localizedDescription
print("This is the error \(error)")
return
}
else {
self.email = ""
self.password = ""
user.setData(["id": user.documentID, "email": email]) { (err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
}
}
}
Generally speaking, I would strongly recommend to not perform so much logic in your views, but instead keep your views as anaemic as possible - meaning: put all your logic into view models, and bind the view to the view models by using Combine. This will make your code much cleaner, easier to test, and maintainable.
See https://peterfriese.dev/replicating-reminder-swiftui-firebase-part2/ for how to do this.
I am still having difficulty in checking whether the user is logged in with Google or Facebook to read and write on Firebase Database. I want to present a log in screen to a first time user and when the user authenticates, the log in screen is dismissed and it sent to the tabViewControllers. Here's my Swift 4 code below, which is placed in the AppDelegate, application(application:didFInishLaunchingWithOptions launchOptions:).
if Auth.auth().currentUser == nil {
print("NO USER") // this does print out in the console before the app crashes
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let loginVC = storyboard.instantiateViewController(withIdentifier: "loginVC") as! LogInViewController
self.window?.rootViewController = loginVC
} else {
let tabController = window!.rootViewController as! UITabBarController
if let tabViewControllers = tabController.viewControllers {
// First tab (only one so far...)
let navController = tabViewControllers[0] as! UINavigationController
let controller1 = navController.viewControllers.first as! UserProfileViewController
controller1.coreDataStack = coreDataStack
}
}
}
Please note the LogInViewController Scene is created in the Main.storyboard file and it has a Storyboard ID of "loginVC". When I try to run this, the program crashes at the part where the tabViewController[0] tries to fetch from the coreDataStack.
Hi you need to store UID of the user here is my code for login screen as you said if user open app for first time he have to login / authenticate and second time is automatically.
override func viewDidLoad() {
super.viewDidLoad()
if let uid = KeychainWrapper.standard.string(forKey: KEY_UID) {
autoLoginWithUID(uid: uid)
}
}
after app launches try it to auto login him if have stored his UID otherwise screen stays
func autoLoginWithUID(uid: String) {
KeychainWrapper.standard.set(uid, forKey: KEY_UID)
print(uid)
//Keep db and userRef as class constants shouldn't be here
let db = Firestore.firestore()
let userRef = db.collection("Users").document(uid)
userRef.getDocument { (document, error) in
if let document = document {
print("User data: \(document.data())")
self.performSegue(withIdentifier: "LogIn", sender: nil)
} else {
print("User does not exist")
}
}
}
Here I look in db if I have user with this UID if I got it its stored in global variable and continue. You can also store users credentials and log user by them. But dont know which way is more secure.
Auth.auth().signIn(with: credential) { (user, error) in
if let error = error {
// ...
return
}
// User is signed in
// ...
}
}
If you want more code from UserRequest let me know ;)