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

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.

Related

Type of expression is ambiguous without more context GIDSignIn

I've been following this tutorial to implement Google Sign In: https://www.youtube.com/watch?v=M5LiqOBDeGg
Everything works except one line:
GIDSignIn.sharedInstance.signIn(with: config, presenting: presenting) {user, error in
The error is Type of expression is ambiguous without more context
This is the file:
//
// FirebAuth.swift
// TrippiTest
//
// Created by Dragos Catana on 12.01.2023.
//
import Foundation
import FirebaseAuth
import GoogleSignIn
import Firebase
struct FirebAuth {
static let share = FirebAuth()
private init() {}
func signinWithGoogle(presenting: UIViewController,
completion: #escaping (Error?) -> Void) {
guard let clientID = FirebaseApp.app()?.options.clientID else { return }
// Create Google Sign In configuration object.
let config = GIDConfiguration(clientID: clientID)
// Start the sign in flow!
GIDSignIn.sharedInstance.signIn(with: config, presenting: presenting) {user, error in
if let error = error {
completion(error)
return
}
guard
let authentication = user?.authentication,
let idToken = authentication.idToken
else {
return
}
let credential = GoogleAuthProvider.credential(withIDToken: idToken,
accessToken: authentication.accessToken)
Auth.auth().signIn(with: credential) { result, error in
guard error == nil else {
completion(error)
return
}
print("SIGN IN")
UserDefaults.standard.set(true, forKey: "signIn") // When this change to true, it will go to the home screen
}
}
}
}
Just for context, this is the line of code from the Login page
GoogleSiginBtn {
// TODO: - Call the sign method here
FirebAuth.share.signinWithGoogle(presenting: getRootViewController()) { error in
// TODO: Handle ERROR
}
} // GoogleSiginBtn
What should I do?

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()
}

Update a View after an in app purchase was done

In the meantime a try to implement in app purchases in my app that is in the AppStore.
So I done this with Glassfy and everything works fine.
final class IAPManager {
static let shared = IAPManager()
private init() {}
enum Product: String {
case comfirstmember
var sku: String {
"numberOne"
}
}
#AppStorage("boughtFirst") var boughtFirst = false
func configure() {
Glassfy.initialize(apiKey: "31876r5654fgz4f95f0e6e6bh5d")
}
func getProduct(completion: #escaping (Glassfy.Sku) -> Void) {
Glassfy.sku(id: "numberOne") { sku, error in
guard let sku = sku, error == nil else {
return
}
completion(sku)
}
}
func purchase(sku: Glassfy.Sku) {
Glassfy.purchase(sku: sku) { transaction, error in
guard let t = transaction, error == nil else {
return
}
if t.permissions["numberOne"]?.isValid == true {
NotificationCenter.default.post(
name: Notification.Name("numberOne"),
object: nil
)
self.boughtFirst = true
}
}
}
func getPermissions() {
Glassfy.permissions { permissions, error in
guard let permissions = permissions, error == nil else {
return
}
if permissions["numberOne"]?.isValid == true {
NotificationCenter.default.post(
name: Notification.Name("numberOne"),
object: nil
)
self.boughtFirst = true
}
}
}
func restorePurchases() {
Glassfy.restorePurchases { permissions, error in
guard let permissions = permissions, error == nil else {
return
}
if permissions["numberOne"]?.isValid == true {
NotificationCenter.default.post(
name: Notification.Name("numberOne"),
object: nil
)
self.boughtFirst = true
}
}
}
}
But now I need to update a View after the purchase was successfully done buy the user to display the Content that he purchased.
NavigationView {
VStack {
if boughtFirst == false {
BuyExtraContent()
}
if boughtFirst == true {
AllView()
}
}
}
I want do this as easy as possible just with AppStorage.
But if I place the Boolean in the func purchase to switch from false to true nothings changes.
So my question is how I can update a view after a successful purchase.
P.S. I´m a bit lost in SwiftUI so please explain it not to complex :)
use a state variable for this .
you used boughtFirst as a bool variable. make sure it's an observable .
try to call with observable object and when your variable will change the view will automatically notify and update by itself.
I am not sure you followed this strategy or not.

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.

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.")
})