How to set userSession in AuthView from SignInWithAppleButton in LoginView in SwiftUI - swiftui

LoginView
SignInWithAppleButton(
onRequest: { request in
let nonce = randomNonceString()
currentNonce = nonce
request.requestedScopes = [.fullName, .email]
request.nonce = sha256(nonce)
},
onCompletion: { result in
switch result {
case .success(let authResults):
switch authResults.credential {
case let appleIDCredential 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 {
fatalError("Invalid state: A login callback was received, but no login request was sent.")
}
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 (error != nil) {
print(error?.localizedDescription as Any)
return
}
guard let user = authResult?.user else {return}
viewModel.userSession = user
I cannot put viewModel.userSession = user either without compile-time error and on some lucky occasions, on app crashing. So the app crashes on apple login and on opening the app, everything works fine.
AuthViewModel
#Published var userSession: FirebaseAuth.User?
#Published var currentUser: User?
Normal Login -->
func login(withEmail email: String, password: String) {
Auth.auth().signIn(withEmail: email, password: password) {result, error in
if let error = error {
self.hapticFeedback.notificationOccurred(.error)
self.errorMessage = "\(error.localizedDescription)"
self.errorOccurred.toggle()
print("DEBUG: Failed to Signin with error \(error.localizedDescription)")
return
}
guard let user = result?.user else { return }
self.userSession = user
self.fetchUser()
self.writeUserData()
}

Related

Get the currently signed-in user Firebase in swiftUI

I want to switch to a new view when the login is successful And it will save the login state, when I close the App and open it again, no need to login again. I use combine . library .Hope you can help me. Thanks
Here is my full code. Hope you will help me
Firebase will tell you... you just need to ask.
This is how you ask.
let user = Auth.auth().currentUser
if let user = user {
// User is logged in
}
These are the FirebaseAuth iOS docs (for further information)
Your should probably call this in the init of your LoginViewModel
init() {
let user = Auth.auth().currentUser
if let user = user {
// User is logged in
session = User(uid: user.uid,
email: user.email)
} else {
session = nil
}
}
Make the LoginViewModel variable session #Published like this
class LoginViewModel: ObservableObject {
...
#Published var session: User?
...
}
read about the Published type here
Update
You should add this block to your signIn() func in the LoginView in the else instead of session.isLoggedIn.toggle().
I also recommended removing the isLoggedin var you should look at the user object in session to check if it is nil or not.
func signIn () {
loading = true
error = false
session.signIn(email: email, password: password) { (result, error) in
self.loading = false
if error != nil {
self.error = true
self.loading = false
} else {
guard let uid = result?.user.uid,
let email = result?.user.email else {
return
}
session.session = User(uid: uid, email: email)
}
}
}
This is the signUp() I added. Although I recommend making it on a separate page.
func signUp (email: String,
password: String,
handler: #escaping AuthDataResultCallback) {
Auth.auth().createUser(withEmail: email, password: password, completion: handler)
}
Finally I would recommend to move all the login logic to the viewModel instead of the view... that's why viewModels exist.

Where in Firebase login process, do I check/migrate existing content?

I have a SwiftUI/Firebase project, where I allow users to create and upload content while logged in with anonymous. I also have a Firebase rule that prevent editing data that isn't tagged with the same UID as you're logged in with.
My problem is that, when users log in with Google or Apple login, I don't know where to insert any logic for migrating their content from their old anonymous UID to their Apple/Google UID. (Update: Yes, I can link accounts, but that only works if they haven't previously used their account on a different device).
As far as I can tell, I don't get their new Apple/Google UID until after they're authenticated, and by then, they can no longer modify data tagged with the Anonymous UID.
I've tried linking the accounts, but I get an "Account is already linked" error, so I'm assuming that approach is a dead end?
As an example, here is my code for the Google login with a note where I'm trying to insert my migration logic:
import SwiftUI
import Firebase
import GoogleSignIn
struct GoogleSignInButton: View {
#EnvironmentObject var viewModel: GoogleSignInViewModel
var body: some View {
Button("Sign in with Google") {
viewModel.signIn()
}
.foregroundColor(Color.greyZ)
.padding()
.frame(maxWidth: .infinity)
.background(Color.greyB)
.cornerRadius(5)
.padding()
}
}
struct GoogleSignInButton_Previews: PreviewProvider {
static var previews: some View {
GoogleSignInButton()
}
}
class GoogleSignInViewModel: NSObject, ObservableObject {
enum SignInState {
case signedIn
case signedOut
}
#Published var state: SignInState = .signedOut
override init() {
super.init()
setupGoogleSignIn()
}
func signIn() {
if GIDSignIn.sharedInstance().currentUser == nil {
GIDSignIn.sharedInstance().presentingViewController = UIApplication.shared.windows.first?.rootViewController
GIDSignIn.sharedInstance().signIn()
}
}
func signOut() {
GIDSignIn.sharedInstance().signOut()
do {
try Auth.auth().signOut()
state = .signedOut
} catch let signOutError as NSError {
print(signOutError.localizedDescription)
}
}
private func setupGoogleSignIn() {
GIDSignIn.sharedInstance().delegate = self
}
}
extension GoogleSignInViewModel: GIDSignInDelegate {
func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {
if error == nil {
// Get UID of existing user
if let previousUID:String = Auth.auth().currentUser?.uid {
// migrate Firestore data for old uid to new uid
// Firebase rule prevent modifying data if you're logged in with different uid so it has to be before logging in with Google
// But I don't seem to have the new Google UID yet, so what do I migrate it to?
}
// Log in with new user
firebaseAuthentication(withUser: user)
} else {
print(error.debugDescription)
}
}
private func firebaseAuthentication(withUser user: GIDGoogleUser) {
if let authentication = user.authentication {
let credential = GoogleAuthProvider.credential(withIDToken: authentication.idToken, accessToken: authentication.accessToken)
Auth.auth().signIn(with: credential) { (_, error) in
if let error = error {
print(error.localizedDescription)
self.state = .signedOut
} else {
self.state = .signedIn
}
}
}
}
}
UPDATE: As requested, here is the Link-function that invariably results in a "This credential is already associated with a different user account" error. I have checked the account in Firebase, and the account already exists, so that is why I assumed the "link" approach is a dead end, and tried migrating the data instead.
private func firebaseAuthentication(withUser user: GIDGoogleUser) {
if let authentication = user.authentication {
let credential = GoogleAuthProvider.credential(withIDToken: authentication.idToken, accessToken: authentication.accessToken)
if let currentUser = Auth.auth().currentUser {
// User already logged in
currentUser.link(with: credential) { result, error in
if let error = error {
print(error.localizedDescription)
} else {
print(result ?? "Success")
}
}
} else {
// User not logged in (shouldn't happen as they're always anonymous
Auth.auth().signIn(with: credential) { (_, error) in
if let error = error {
print(error.localizedDescription)
self.state = .signedOut
} else {
self.state = .signedIn
}
}
}
}
}
Instead of migrating the data, consider linking the user's new Google or Apple credentials to their existing Firebase account by filling the process outlines in linking multiple Auth providers to an account on iOS.

SwiftUI - Publish Background Thread Not Allowed - on code that does not update ui

New to swiftui and don't understand why the JSONDecoder() line in the first code throws
[SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
This to me is not updating ui so why is this showing?
do {
// pass the request type, bearer token, and email / password ( which are sent as POST body params )
let request = L.getRequest(requestType:"POST", token: token, email: self.username, password: self.psw)
L.fetchData(from: request) { result in
switch result {
case .success(let data):
// covert the binary data to a swift dictionary
do {
let response = try JSONDecoder().decode(WpJson.self, from: data)
for (key, title) in response.allowedReadings {
let vimeoId = Int( key )!
let vimeoUri = self.buildVimeoUri(vimeoId: key)
self.addReadingEntity(vimeoUri: vimeoUri, vimeoId: vimeoId, title: title)
}
self.writeToKeychain(jwt:response.jwt, displayName: response.displayName)
readings = self.fetchReadings()
}
catch {
self.error = error.localizedDescription
}
case .failure(let error):
self.error = error.localizedDescription
}
}
}
I tried wrapping a main queue around the do-catch in the L.fetchData(from: request) { result in but this did not help
DispatchQueue.main.async { [weak self] in
Here is the Login protocol, again without any ui work:
import Foundation
import SwiftUI
struct Login: Endpoint {
var url: URL?
init(url: URL?) {
self.url = url
}
}
protocol Endpoint {
var url: URL? { get set }
init(url: URL?)
}
extension Endpoint {
func getRequestUrl() -> URLRequest {
guard let requestUrl = url else { fatalError() }
// Prepare URL Request Object
return URLRequest(url: requestUrl)
}
func getRequest(requestType:String="POST", token:String, email:String="", password:String="") -> URLRequest {
var request = self.getRequestUrl()
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
if ( "" != email && "" != password && requestType == "POST") {
let parameters:[String:String?] = [
"email": email,
"password": password
]
// Run the request
do {
// pass dictionary to nsdata object and set it as request body
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
} catch let error {
print(error.localizedDescription)
}
}
return request;
}
func fetchData(from request: URLRequest, completion: #escaping (Result<Data, NetworkError>) -> Void) {
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
completion(.success(data))
} else if error != nil {
// any sort of network failure
completion(.failure(.requestFailed))
} else {
// this ought not to be possible, yet here we are
completion(.failure(.unknown))
}
}.resume()
}
}
extension URLSession {
func dataTask(with request: URLRequest, completionHandler: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> URLSessionDataTask {
return dataTask(with: request, completionHandler: { (data, urlResponse, error) in
if let error = error {
completionHandler(.failure(error))
} else if let data = data, let urlResponse = urlResponse as? HTTPURLResponse {
completionHandler(.success((data, urlResponse)))
}
})
}
}
Do you have any idea on how to fix this?
Wrap it right in place of assignment
catch {
DispatchQueue.main.async {
self.error = error.localizedDescription
}
}
case .failure(let error):
DispatchQueue.main.async {
self.error = error.localizedDescription
}
}

facebook integration Error 2500 OAuthException xcode 8 Swift 3

I am working on facebook integration in xcode 8 Swift 3.
i have used the following code
let parameters = ["fields": "email, first_name, last_name, picture.type(large)"]
FBSDKGraphRequest.init(graphPath: "me", parameters: parameters).start { (connection, result, error) in
if error != nil{
print(error)
return
}
But I am getting below error.
Optional(Error Domain=com.facebook.sdk.core Code=8 "(null)" UserInfo={com.facebook.sdk:FBSDKGraphRequestErrorCategoryKey=0, com.facebook.sdk:FBSDKGraphRequestErrorHTTPStatusCodeKey=400, com.facebook.sdk:FBSDKErrorDeveloperMessageKey=An active access token must be used to query information about the current user., com.facebook.sdk:FBSDKGraphRequestErrorGraphErrorCode=2500, com.facebook.sdk:FBSDKGraphRequestErrorParsedJSONResponseKey={
body = {
error = {
code = 2500;
"fbtrace_id" = "FmK/8QACfhe";
message = "An active access token must be used to query information about the current user.";
type = OAuthException;
};
};
code = 400;
}})
can anyone help me out this ??
func loginButton(_ loginButton: FBSDKLoginButton!, didCompleteWith result: FBSDKLoginManagerLoginResult!, error: Error!) {
print("Login buttoon clicked")
let graphRequest:FBSDKGraphRequest = FBSDKGraphRequest(graphPath: "me", parameters: ["fields":"first_name, gender, last_name, email, picture.type(large)"])
graphRequest.start(completionHandler: { (connection, result, error) -> Void in
if ((error) != nil)
{
print("Error: \(error)")
}
else
{
let data:[String:AnyObject] = result as! [String : AnyObject]
print(data["first_name"]!)
print(data["last_name"]!)
print(data["email"]!)
print(data["id"]!)
print(data["gender"]!)
}
})
}

URLRequest gets HTTP error 502 and "connection reset by peer"

Got these errors with this code:
if let url = URL(string: "<valid web service url string>") {
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Basic \(base64Authorization)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request, completionHandler: {(data, response, error) in
if error == nil {
The same code returns no error with Xcode 7.3 but errors in Xcode 8 after converting to Swift 3.
This happened because of the Swift 3 proposal SE-0054.
base64Authorization was declared this way:
static var base64Authorization:String! {
get {
if base64Auth == nil {
let keyString = "<my key string>"
let plainTextData = keyString.data(using: .utf8, allowLossyConversion: false) as Data!
base64Auth = plainTextData!.base64EncodedString(options: .endLineWithLineFeed) as String!
}
return base64Auth
}
}
base64Authorization returned an Optional which messed up the "Basic \(base64Authorization)" HTTP setting.
This declaration of base64Authorization fixed the problem:
static var base64Authorization:String {
get {
if base64Auth == nil {
let keyString = "<my key string>"
let plainTextData = keyString.data(using: .utf8, allowLossyConversion: false) as Data!
base64Auth = plainTextData!.base64EncodedString(options: .endLineWithLineFeed) as String
}
return base64Auth!
}
}