How could I return a Bool from this sign in with apple function that uses the authorizationController? I have been looking at withCheckedThrowingContinuation but with no luck. I have also seen some examples of using completion handlers but I would like to use async.
func signInWithApple(withState state: SignInState) async -> Bool {
let request = signInWithAppleRequest(withState: state)
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.performRequests()
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
//...
let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce)
switch state {
case .signIn:
Task {
authenticationState = .authenticating
do {
let result = try await Auth.auth().signIn(with: credential)
if result.additionalUserInfo?.isNewUser == true {
self.isNewUser = true
}
return true
} catch {
self.authenticationState = .unauthenticated
print(error.localizedDescription)
return false
}
}
case .reauth:
Task {
if let user = Auth.auth().currentUser {
do {
try await user.reauthenticate(with: credential)
return true
} catch {
print(error.localizedDescription)
return false
}
}
}
}
}
}
I don't have all your types, so here is a simple example of a continuation which at least compiles.
The class must inherit from NSObject and adopt ASAuthorizationControllerDelegate.
The continuation is set in the same scope where the request is executed.
The most important rule is: The continuation must be resumed exactly once
import AuthenticationServices
class Authorization: NSObject, ASAuthorizationControllerDelegate {
private var continuation : CheckedContinuation<ASAuthorization,Error>?
func signInWithApple() async throws -> ASAuthorization {
return try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.performRequests()
}
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
continuation?.resume(returning: authorization)
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
continuation?.resume(throwing: error)
}
}
UPDATED: I have a project that is using Firebase Firestore. I have snapshot listeners set up to my model objects. I start the snapshot listeners when the app starts. My understanding is that if you start a snapshot listener once that's all you need.
My issue is that when I create a new object in the app I must restart the snapshot listener in order for it to see the changes. I am wondering if I am making a mistake somewhere. Here are the files I think are important. If anything else is needed please let me know.
Main
Main where I create the environment objects for the project and start the UserRepository snapshot listener.
#main
struct TheExchangeApp: App {
// #EnvironmentObjects
#StateObject private var userRepository = UserRepository()
#StateObject private var authListener = AuthSession(userRepository: UserRepository())
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(userRepository)
.environmentObject(authListener)
}
}
}
UserRepository.swift
This is the user repository were I create the snapshot listener.
class UserRepository: ObservableObject {
// Access to Firestore Database
let db = Firestore.firestore()
private var snapshotListener: ListenerRegistraion?
#Published var users = [User]()
init() {
startSnapshotListener()
}
func startSnapshotListener() {
// Add a SnapshotListener to the User Collection.
if snapshotListner == nil {
db.collection("users").addSnapshotListener { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
guard let documents = querySnapshot?.documents else {
print("No Users.")
return
}
self.users = documents.compactMap { user in
do {
return try user.data(as: User.self)
} catch {
print(error)
}
return nil
}
}
}
}
}
func stopSnapshotListener() {
if snapshotListener != nil {
snapshotListener?.remove()
snapshotListener = nil
}
}
func createNewUser(userUid: String, user: User) {
do {
let _ = try self.db.collection(FirestoreCollection.users).document(userUid).setData(from: user)
if snapshotListener == nil {
print("createNewUser snapshotListener is nil")
} else if snapshotListener != nil {
print("createNewUser snapshotListener is not nil")
}
} catch let error as NSError {
print("UserRepository - createNewUser Error: \(error)")
}
}
}
AuthSession
Here I start a combine publisher on the userRepository snapshot listener. The publisher is not getting an update when I create a new user in SignUpView below. If I restart the snapshot listener in UserRepository I get the published value.
class AuthSession: ObservableObject {
let db = Firestore.firestore()
var userRepository: UserRepository
#Published var currentUser: User?
private var cancellables = Set<AnyCancellable>()
init(userRepository: UserRepository) {
self.userRepository = userRepository
self.startCombine()
}
func startCombine() {
userRepository
.$users
.receive(on: RunLoop.main)
.map { users in
users
.first(where: { $0.id == self.currentUserUid})
}
.assign(to: \.currentUser, on: self)
.store(in: &cancellables)
}
}
SignUp View
Here is where I create a new user. The snapshot listener does not work unless I restart. I know because the combine publisher in AuthSession does not fire.
struct SignUpView: View {
#EnvironmentObject var authSession: AuthSession
let db = Firestore.firestore()
var body: some View {
Button(action: {
self.signUp()
}, label: {
Text("Sign Up")
})
}
func signUp() {
authSession.listen()
let newUser = User(email: myemail#email.com, name: "Hans")
self.authSession.userRepository.createNewUser(userUid: user.uid, user: newUser)
}
}
Any help here would be greatly appreciated.
I believe I've sorted out the issue. A developer I am working with changed the rules on Firestore. These new rules included the following for the User collection.
match /users/{userId} {
allow create: if request.auth.uid == userId;
allow read: if request.auth != null;
allow write: if request.auth.uid == userId;
Once I removed these rules everything works as expected. I hope this helps someone else out there.
In my project I'm using a trigger to create a user document in Firestore when the user signs in. And this is great - everything works perfect for Google Sign-In and Facebook Login.
Here is this trigger:
exports.createAccountDocument = functions.auth.user().onCreate(async user => {
const { uid, displayName } = user
const username = displayName;
const email = user.email || user.providerData[0].email;
const profileImageUrl = uid;
const status = "active";
const str = username;
const qwery = [];
for (let i = 0; i < str.length; ++i) {
qwery[i] = str.substring(0, i + 1).replace(/\s/g, "").toLowerCase();
}
const keywords = qwery;
const bio = "";
return await admin
.firestore()
.collection("users")
.doc(uid)
.set({ bio, email, keywords, profileImageUrl, status, uid, username })
})
For example - this is Facebook Login method:
import SwiftUI
import FBSDKLoginKit
import Firebase
struct FacebookAuthView: UIViewRepresentable {
#Binding var showAnimation: Bool
#Binding var showSheet: Bool
init(showAnimation: Binding<Bool>, showSheet: Binding<Bool>) {
self._showAnimation = showAnimation
self._showSheet = showSheet
}
func makeCoordinator() -> FacebookAuthView.Coordinator {
return FacebookAuthView.Coordinator(showAnimation: self.$showAnimation, showSheet: self.$showSheet)
}
class Coordinator: NSObject, LoginButtonDelegate {
#Binding var showAnimation: Bool
#Binding var showSheet: Bool
init(showAnimation: Binding<Bool>, showSheet: Binding<Bool>) {
self._showAnimation = showAnimation
self._showSheet = showSheet
}
func loginButton(_ loginButton: FBLoginButton, didCompleteWith result: LoginManagerLoginResult?, error: Error?) {
if let error = error {
print(error.localizedDescription)
return
}
guard let token = AccessToken.current else {
return
}
self.showAnimation = true
self.showSheet = false
let credential = FacebookAuthProvider.credential(withAccessToken: token.tokenString)
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error, (error as NSError).code == AuthErrorCode.credentialAlreadyInUse.rawValue {
Auth.auth().signIn(with: credential) { result, error in
// continue
print("signIn result: " + authResult!.user.email!)
if let token = firebaseRegistrationPushToken {
checkUserAuthSettings(pushToken: token)
}
}
} else {
// continue
print("Facebook Sign In")
if let token = firebaseRegistrationPushToken {
checkUserAuthSettings(pushToken: token)
}
}
}
}
func loginButtonDidLogOut(_ loginButton: FBLoginButton) {
try! Auth.auth().signOut()
}
}
func makeUIView(context: UIViewRepresentableContext<FacebookAuthView>) -> FBLoginButton {
let view = FBLoginButton()
view.permissions = ["email"]
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: FBLoginButton, context: UIViewRepresentableContext<FacebookAuthView>) { }
}
But when I try to create a document when user logs in using Swign in with Apple, this doesn't work. In the Firebase console under Firebase Authentication I can see new the user, but in Firestore, no document shows up at all.
Here is my Sign in with Apple method:
import Foundation
import SwiftUI
import AuthenticationServices
import CryptoKit
import Firebase
struct AppleAuthView: UIViewRepresentable {
#Binding var showAnimation: Bool
#Binding var showSheet: Bool
init(showAnimation: Binding<Bool>, showSheet: Binding<Bool>) {
self._showAnimation = showAnimation
self._showSheet = showSheet
}
func makeCoordinator() -> AppleAuthView.Coordinator {
return AppleAuthView.Coordinator(showAnimation: self.$showAnimation, showSheet: self.$showSheet)
}
class Coordinator: NSObject, ASAuthorizationControllerPresentationContextProviding, ASAuthorizationControllerDelegate {
#Binding var showAnimation: Bool
#Binding var showSheet: Bool
fileprivate var currentNonce: String?
init(showAnimation: Binding<Bool>, showSheet: Binding<Bool>) {
self._showAnimation = showAnimation
self._showSheet = showSheet
super.init()
}
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
let viewController = UIApplication.shared.windows.last?.rootViewController
return (viewController?.view.window!)!
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
guard let nonce = currentNonce else {
fatalError("Invalid state: A login callback was received, but no login request was sent.")
}
guard let appleIDToken = appleIDCredential.identityToken else {
print("Unable to fetch identity token")
return
}
guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
return
}
let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce)
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
print(error.localizedDescription)
return
}
print("Apple Sign In")
if let token = firebaseRegistrationPushToken {
checkUserAuthSettings(pushToken: token)
}
}
}
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
print("Sign in with Apple errored: \(error)")
}
#objc func startSignInWithAppleFlow() {
let nonce = randomNonceString()
currentNonce = nonce
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
request.nonce = sha256(nonce)
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.presentationContextProvider = self
authorizationController.performRequests()
}
private func randomNonceString(length: Int = 32) -> String {
precondition(length > 0)
let charset: Array<Character> = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
var result = ""
var remainingLength = length
while remainingLength > 0 {
let randoms: [UInt8] = (0 ..< 16).map { _ in
var random: UInt8 = 0
let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
if errorCode != errSecSuccess {
fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)")
}
return random
}
randoms.forEach { random in
if remainingLength == 0 {
return
}
if random < charset.count {
result.append(charset[Int(random)])
remainingLength -= 1
}
}
}
return result
}
#available(iOS 13, *)
private func sha256(_ input: String) -> String {
let inputData = Data(input.utf8)
let hashedData = SHA256.hash(data: inputData)
let hashString = hashedData.compactMap {
return String(format: "%02x", $0)
}.joined()
return hashString
}
}
func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
let button = ASAuthorizationAppleIDButton(type: .signIn, style: .black)
button.addTarget(context.coordinator,action: #selector(Coordinator.startSignInWithAppleFlow),for: .touchUpInside)
return button
}
func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {
}
}
I don't understand why the document cannot be created. In the console I can see nil.
Please help to fix this issue.
Updated.
Code below - is a simple function to creating user document in firestore in my app
func signup(username: String, email: String, password: String, imageData: Data, completed: #escaping(_ user: User) -> Void, onError: #escaping(_ errorMessage: String) -> Void) {
if !username.isEmpty && !email.isEmpty && !password.isEmpty && !imageData.isEmpty {
AuthService.signupUser(username: username, email: email, password: password, imageData: imageData, onSuccess: completed, onError: onError)
} else {
if username == "" {
errorString = "Please enter your name"
onError(errorString)
// showAlert = true
}
if email == "" {
errorString = "Please enter yor valid email"
onError(errorString)
// showAlert = true
}
if password == "" {
errorString = "Please create password"
onError(errorString)
// showAlert = true
}
if image == Image(IMAGE_USER_PLACEHOLDER) {
errorString = "Please upload your avatar"
onError(errorString)
// showAlert = true
}
}
}
and here is AuthService.signupUser method
static func signupUser(username: String, email: String, password: String, imageData: Data, onSuccess: #escaping(_ user: User) -> Void, onError: #escaping(_ errorMessage: String) -> Void) {
//Firebase.createAccount(username: username, email: email, password: password, imageData: imageData)
Auth.auth().createUser(withEmail: email, password: password) { (authData, error) in
if error != nil {
print(error!.localizedDescription)
onError(error!.localizedDescription)
return
}
guard let userId = authData?.user.uid else { return }
let storageAvatarUserId = Ref.STORAGE_AVATAR_USERID(userId: userId)
let metadata = StorageMetadata()
metadata.contentType = "image/jpg"
StorageService.saveAvatar(userId: userId, username: username, email: email, imageData: imageData, metadata: metadata, storageAvatarRef: storageAvatarUserId, onSuccess: onSuccess, onError: onError)
}
}
I know that apple sign in cannot take users image & it's ok - my cloud trigger fill this field in document users uid & in my app if user avatar = nil - it's takes universal clipart, It's ok, newer mind.
And this is User file
import Foundation
struct User: Encodable, Decodable {
var uid: String
var email: String
var profileImageUrl: String
var username: String
var bio: String
var keywords: [String]
var status: String?
}
my security rules in firebase are simple, here they are
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
This issue is - that Apple don’t get username.
Issue is solved.
I want to gather information about user with firebase firestore, I have in a Class a function which do that, but I want share this information with my struct ContentView.
class UserData : ObservableObject {
// Use Firebase library to configure APIs
#Published var db = Firestore.firestore()
#Published var WholeDocument : Array<String> = []
func GetAllData(completion: #escaping (Array<String>) -> ()) {
db.collection("Users").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
completion([""])
} else {
for document in querySnapshot!.documents {
//print("\(document.documentID) => \(document.data())")
self.WholeDocument.append(document.documentID)
}
completion(self.WholeDocument)
}
}
I try to gather this array in my contentView
func Connection()
{
self.Database.GetAllData{(tab) in
if tab.count > 0 {
self.WholeDocumentContentView = tab
}
else {
print("Not found")
}
}
}
How to retrieve WholeDocument array with closure ?
Thank you for the help.
If I'm understanding the structure/question correctly, your goal is to access the WholeDocument array in another SwiftUI view. The way to do that could be:
class UserData : ObservableObject {
// Use Firebase library to configure APIs
private var db = Firestore.firestore()
#Published var WholeDocument : Array<String> = []
init() {
db.collection("Users").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
completion([""])
} else {
for document in querySnapshot!.documents {
// print("\(document.documentID) => \(document.data())")
self.WholeDocument.append(document.documentID)
}
}
}
}
}
struct MyContentView : View {
#ObservedObject var database: UserData
var body: some View {
// access database.WholeDocument
List(database.WholeDocument, id: \.self) {
// do something
}
}
That way, when you initialize MyContentView with an instance of UserData, you will be able to observe WholeDocument in that view.
I'm trying to avoid using the .then((u) { return u.uid }) function in all my code where I need to access the current user's UID, instead just by calling getCurrentUser().uid for a much faster access. However, it gives me an error The getter 'uid' was called on null. but it's not null because it does print in the console but only after showing that it's null and the error at the end for some reason. I'm not well knowledge in the Future/Async/Await logic so any help would be greatly appreciated!
class UsersAPI {
final DatabaseReference usersRef = FirebaseDatabase.instance.reference().child(Config.users);
Future<FirebaseUser> currentUser() async {
return await FirebaseAuth.instance.currentUser();
}
FirebaseUser getCurrentUser() {
FirebaseUser user;
this.currentUser().then((u) {
user = u;
print('USER 1 $user'); // Prints after 'USER 2'
});
print('USER 2 $user'); // Prints first
if (user != null) {
return user;
} else {
return null;
}
}
DatabaseReference getCurrentUserRef() {
return this.usersRef.child(this.getCurrentUser().uid); // GIVES THE 'uid' WAS CALLED ON NULL ERROR
}
observeCurrentUser(Function onSuccess(User u)) {
this.usersRef.child(this.getCurrentUser().uid).onValue.listen( (event) { // GIVES THE 'uid' WAS CALLED ON NULL ERROR
DataSnapshot snapshot = event.snapshot;
if (snapshot.value != null) {
User user = User().transform(snapshot.key, snapshot.value);
onSuccess(user);
}
});
}
observeUser(String userID, Function onSuccess(User u), Function onFailure(String e)) {
this.usersRef.child(userID).onValue.listen( (e) {
DataSnapshot snapshot = e.snapshot;
if (snapshot.value != null) {
User user = User().transform(snapshot.key, snapshot.value);
onSuccess(user);
} else {
onFailure("User Not Found...");
}
});
}
}
Example Usage - WORKS:
APIs().usersAPI.currentUser().then((u) {
APIs().usersAPI.observeUser(u.uid, (u) {
onSuccess(u);
}, (e) {
print(e);
});
});
DOESN'T WORK:
APIs().usersAPI.observeCurrentUser((u) {
onSuccess(u);
});
DatabaseReference getCurrentUserRef() async {
return this.usersRef.child((await this.getCurrentUser()).uid); =
}
than call
var ref = await getCurrentUserRef()
Little bit more pretty
DatabaseReference getCurrentUserRef() async {
var firebaseUser = await this.getCurrentUser();
return this.usersRef.child(firebaseUser.uid);
}
EDIT: to clarify some question on asynchronicity
How would you call now this function to get the reference?
Lets say you want to update the data on the user, you can do
Firestore.instance.runTransaction((transaction) async {
var reference = await getCurrentUserRef();
await transaction.set(reference, someData);
});
Or you would like to read the data from that reference
readAndProcessData() async {
var reference = await getCurrentUserRef();
DocumentSnapshot user = await reference.get();
print(user.data.toString);
}