Get the currently signed-in user Firebase in swiftUI - 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.

Related

How to set userSession in AuthView from SignInWithAppleButton in LoginView in 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()
}

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.

StateHasChanged() does not reload page

Issue:
As mentioned in Title, StateHasChanged does not re-render the page
Objective:
I want to Refresh the page when a button is clicked
Current Code
<button #onclick="CreatePlayer">Create User</button>
#functions {
string username;
[CascadingParameter]
Task<AuthenticationState> authenticationStateTask { get; set; }
async Task CreatePlayer()
{
var authState = await authenticationStateTask;
var user = authState.User;
var player = await PlayerData.GetByEmail(user.Identity.Name);
if (player == null)
{
player = new Player()
{
Email = user.Identity.Name,
UserName = username
};
await PlayerData.Create(player);
}
await Task.Delay(50);
StateHasChanged();
}
}
Just for the record, I add my comment in an answer :
StateHasChanged just inform the component that something changes in is state, that doesn't rerender it. The component choose by itself if it has to rerender or not. You can override ShouldRender to force the component to rerender on state changed.
#code {
bool _forceRerender;
async Task CreatePlayer()
{
var authState = await authenticationStateTask;
var user = authState.User;
var player = await PlayerData.GetByEmail(user.Identity.Name);
if (player == null)
{
player = new Player()
{
Email = user.Identity.Name,
UserName = username
};
await PlayerData.Create(player);
}
_forceRerender = true;
StateHasChanged();
}
protected override bool ShouldRender()
{
if (_forceRerender)
{
_forceRerender = false;
return true;
}
return base.ShouldRender();
}
}
On the one hand, you tell the compiler that she should create an event handler for the click event, named CreatePlayer: #onclick="CreatePlayer . This attribute compiler directive, behind the scenes, creates an EventCallback<Task> handler for you, the implication of which is that you do not need to use StateHasChanged in your code at all, as this method ( StateHasChanged ) is automatically called after UI events take place.
On the other hand, you tell the compiler that the type of the button should be set to "submit". This is wrong of course... You can't have it both. Setting the type attribute to "submit", normally submit form data to the server, but In Blazor it is prevented to work that way by code in the JavaScript portion of Blazor. Do you want to submit a form data to the server ? Always recall Blazor is an SPA Application. No submit ?
Your code should be:
<button #onclick="CreatePlayer" >Create User</button>
Just for the records, ordinarily you should inject the AuthenticationStateProvider object into your components, like this:
#inject AuthenticationStateProvider AuthenticationStateProvider
and then retrieve the AuthenticationState object. This is how your code may be rewritten:
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;

UIWebView: ics and vcard-Links not handled

I do have a UIWebView included where a public URL is loaded; unfortunately, vcard and ical-Links are not handled, i.e. nothing happens when I click on them.
I tried to set all data detectors, no luck unfortunately.
In the Xcode-log, I get this here when clicking on such a link:
2017-07-14 13:43:00.982413+0200 xxx[2208:967973] WF: _userSettingsForUser mobile: {
filterBlacklist = (
);
filterWhitelist = (
);
restrictWeb = 1;
useContentFilter = 0;
useContentFilterOverrides = 0;
whitelistEnabled = 0;
}
In Safari, the same stuff works as expected.
If I use UIApplication.shared.openURL(icsOrVcardUrl) Safari gets opened and from there everything works as expected again, but I don't want the user to leave the app...
EDIT
This doesn't work either:
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
if let url = request.url {
if url.absoluteString.contains("=vcard&") || url.absoluteString.contains("/ical/") {
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:url)
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
DispatchQueue.main.async {
self.documentController.url = tempLocalUrl
self.documentController.presentPreview(animated: true)
}
}
}
task.resume()
return false
}
}
return true
}
Use a UIDocumentInteractionController to preview without leaving your app.
I tested it quickly with an .ics file and it works fine.
Implement the UIDocumentInteractionControllerDelegate protocol
extension MainViewController: UIDocumentInteractionControllerDelegate {
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return self;
}
}
Create an instance of the interaction controller:
let documentController = UIDocumentInteractionController()
Intercept the clicks in your UIWebView in shouldStartLoadWithRequest, return false for links you want to handle with the in-app preview and true for all the rest. And finally:
func previewDocument(_ url: URL) {
documentController.url = url
documentController.presentPreview(animated: true)
}
Here it is in the simulator
EDIT:
In response to the comment to this answer:
The reason it doesn't work for you is because the UIDocumentInteractionController depends on the file extension. The extension of the temp file is .tmp
Renaming the file after the download solves the problem. Quick and dirty example:
let task = session.downloadTask(with: url!) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
do {
let filemgr = FileManager.default
let newUrl = tempLocalUrl.appendingPathExtension("ics")
try filemgr.moveItem(at: tempLocalUrl, to: newUrl)
DispatchQueue.main.async {
self.documentController.url = newUrl
self.documentController.presentPreview(animated: true)
}
} catch let error {
print("Error!!!: \(error.localizedDescription)")
}
}
}
task.resume()
In this case it is advisable to clean after yourself, because the file won't be deleted after the task completes although the OS will delete it eventually, when space is needed. If you often access the same urls, Library/Caches/ may be a better place for this files, just come up with good naming schema, and check if the file doesn't exist already.

Swift Siesta Get and post

I am new to Siesta. How can I get the entire array and pass into my model? Besides, how can I post with params? In their documentation I couldn't find any of this.
I'm new to siesta as well. I was able to find documentation about requests here http://bustoutsolutions.github.io/siesta/guide/requests/
Basically you'll setup your resource and then call:
resource.request(.post, json: ["foo": [1,2,3]])
Your question is far too complex so I will try to explain you in a simple way.
Siesta provides a great way to map your models by using Custom Transformers. One simple way to do this is to implement the Decodable protocol provided by Swift 4+:
Lets say we want to decode this JSON response:
{
"id": 1,
"name": "Oswaldo",
"email": "omaestra#gmail.com"
}
Into our great User class which implements the Decodable protocol:
class User: Decodable {
var id: Int
var name: String
var email: String
private enum CodingKeys: String, CodingKey {
case id, name, email
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id
name = try container.decode(String.self, forKey: .name)
email = try container.decode(String.self, forKey: .email)
}
}
Awesome! Now we can decode the JSON response from our server into our awesome User class.
Next, inside our Siesta.Service class we can configure our custom transformers for specific resources:
class API: Siesta.Service {
init() {
super.init(baseURL: "https://api.example.com")
// Some initial configuration
}
let myAPI = API()
// –––––– Global configuration ––––––
let jsonDecoder = JSONDecoder()
// –––––– Mapping from specific paths to models ––––––
// These all use Swift 4’s JSONDecoder, but you can configure arbitrary transforms on arbitrary data types.
configureTransformer("rest/user/") {
// Input type inferred because the from: param takes Data.
// Output type inferred because jsonDecoder.decode() will return User
try jsonDecoder.decode(User.self, from: $0.content)
}
// MARK: - Resources
var userResource: Siesta.Resource { return resource("rest/user/") }
}
Finally, we can implement our resource inside our ViewController:
class UserViewController: UIViewController {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var emailLabel: UILabel!!
override func viewDidLoad() {
super.viewDidLoad()
myAPI.userResource
.addObserver(self)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
myAPI.userResource.loadIfNeeded()
}
}
extension UserViewController: ResourceObserver {
func resourceChanged(_ resource: Resource, event: ResourceEvent) {
let user: User? = resource.typedContent()
nameLabel.text = user?.name
emailLabel.text = user?.email
}
}
Note: Siesta is very flexible and customisable framework, you can find multiple ways to configure your services and resources. This is just one simple way to implement what you are asking for.
To make a POST request with params you can do something like this inside your Service class:
Implement an update method that makes the POST request.
func update(user: User, newName: String) -> Siesta.Request {
return usersResource
.child(user.id)
.child("update")
.request(.post, json: ["name": newName])
}
Then, in your ViewController you can call the method to submit the POST request and evaluate its response:
myAPI.update(user: user, newName: "NEW NAME")
.onSuccess({ (_) in
print("Successfully updated user's name.")
})
.onFailure({ (error) in
print("Error trying to update user's name.")
})