I need to call a function in my SignScreen's onCreate method but there is not any applicable place for this. I can't call currentUserCheck function from anywhere.
What i tried :
Calling it in init block in viewModel. But the problem was it throwing nullPointerException for NavController here.
Calling it in MainActivity and MyTheme but i faced many weird issues in these scopes.
ViewModel:
#HiltViewModel
class SignViewModel (#ApplicationContext context: Context) : ViewModel() {
val auth = FirebaseAuth.getInstance()
init {
currentUserCheck(NavController(context))
}
fun signIn(email: String?, password: String?, context: Context, navController: NavController) {
if (email != null && email.isNotBlank() && password != null && password.isNotBlank()) {
auth.signInWithEmailAndPassword(email, password).addOnSuccessListener {
navController.navigate(ScreenHolder.ProfileScreen.route) {
popUpTo(ScreenHolder.SigningScreen.route) {
inclusive = true
}
}
}.addOnFailureListener {
Toast.makeText(context, it.localizedMessage, Toast.LENGTH_LONG).show()
}
} else {
Toast.makeText(
context,
"Lütfen email ve şifre alanlarını boş bırakmayınız.",
Toast.LENGTH_LONG
).show()
}
}
fun signUp(email: String?, password: String?, context: Context, navController: NavController) {
if (email != null && email.isNotBlank() && password != null && password.isNotBlank()) {
auth.createUserWithEmailAndPassword(email, password)
.addOnSuccessListener {
navController.navigate(ScreenHolder.ProfileScreen.route) {
popUpTo(ScreenHolder.ProfileScreen.route) {
inclusive = true
}
}
}
} else {
Toast.makeText(
context,
"Lütfen email ve şifre alanlarını boş bırakmayınız.",
Toast.LENGTH_LONG
).show()
}
}
fun currentUserCheck(navController: NavController) {
if (auth.currentUser != null) {
navController.navigate(ScreenHolder.ProfileScreen.route) {
popUpTo(ScreenHolder.ProfileScreen.route) {
inclusive = true
}
}
}
}
}
Solution
As #Pztar said i declare the authentication controlling operation as a state in viewModel and then i observed it in my Composable SignScreen with LaunchEffect and it is solved.
#HiltViewModel
class SignViewModel (#ApplicationContext context: Context) : ViewModel() {
val auth = FirebaseAuth.getInstance()
var currentUser = mutableStateOf(false)
//New currenUserCheck method that declare if state is true or not
fun currentUserCheck() {
if (auth.currentUser != null) {
currentUser.value = true
}
}
}
#Composable
fun SignScreen(viewModel: SignViewModel= hiltViewModel(),navController: NavController,context: Context) {
//Observing the state from my composable and routing user if state is true.
LaunchedEffect(key1 = Unit ){
viewModel.currentUserCheck()
if (viewModel.currentUser.value){
navController.navigate(ScreenHolder.ProfileScreen.route) {
popUpTo(ScreenHolder.SigningScreen.route) {
inclusive = true
}
}
}
}
}
Related
`const handleDateChange = (value: any, employeeId: string, i: number) =\> {
if (selectedDate?.filter((e: any) =\> e.employeeId === employeeId).length \> 0) {
setSelectedDate(selectedDate.map((e: any) =\> {
if (e.employeeId === employeeId) {
return { ...e, dateOfBirth: value }
}
else {
return { ...e }
}
}))
}
else {
setSelectedDate([...selectedDate, { employeeId: employeeId, dateOfBirth: value }])
}
setIsDisabled(false);
};`
If this datepicker contains functionality that allows you to change a button's state, develop a test for it.
I am building a sign in functionality using bloc pattern, if the entered credentials are invalid, bloc will return a authErrorState, so I will display a invalid credentials popup as soon as the bloc return a authError State
please check the code :
if (state is IsAuthLoadingState) {
return const LoadingSpinnerWidget();
} else if (state is IsAuthenticatedState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
stopTimer();
BlocProvider.of<AuthBloc>(context).add(LoadAuthStatus());
Navigator.pop(context, true);
});
} else if (state is AuthErrorState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
stopTimer();
showCustomPopUp(state.message);
});
}
Bloc code :
void _onLoginUser(LoginUser event, Emitter<AuthState> emit) async {
emit(IsAuthLoadingState());
final UserLoggedInResponse userDetails =
await authRepository.handleLoginUser(event.phoneNumber, event.otp);
if (userDetails.status == "success") {
for (var item in userDetails.wishlist) {
await _localRepo.addWishlistItem(item);
}
for (var item in userDetails.cart) {
await _localRepo.addCartItem(item);
}
for (var item in userDetails.recentSearches) {
await _localRepo.addRecentSearchTerm(item);
}
await _localRepo.addPurchasedItems(userDetails.purchasedItemIds);
await _localRepo.setIsAuthenticated(
userDetails.accessToken, userDetails.userId);
emit(IsAuthenticatedState());
} else {
emit(AuthErrorState(
message: userDetails.message, content: userDetails.content));
}
}
But, the invalid credentials popup written in authErrorState is getting called multiple times.
Any help is really appreciated. Thank you.
As I didn't found any alternative options, I someone tried to manage this for now like this,
I used a bool variable called isErrorShown, and it was set to false by default,
once the code in widgetsBinding is executed, it will set the isErrorShown to true, function is widgetsBinding checks the value of isErrorShown and executes only if it is false :
else if (state is AuthErrorState) {
print("error state");
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!isErrorShown) {
stopTimer();
if (state.message ==
"user does not exits, please create user") {
Navigator.pushReplacementNamed(context, '/create-user',
arguments: CreateUserPage(
showProfile: widget.showProfile,
phoneNumber: phoneNumberController.text,
otp: otpController.text,
));
// BlocProvider.of<AuthBloc>(context).add(LoadAuthStatus());
// Navigator.pushNamed(context, '/create-user');
} else {
showCustomPopUp(state.message);
}
isErrorShown = true;
}
});
I'm having trouble figuring out how to smoothly navigate from my SignInView() to my FirstView(). I have my FirstView() inside of a Navigation Stack, but the transition between the views is very abrupt and devoid of the transition that you normally get with the use of a NavigationLink. How can I get the transition to work?
Much appreciated!
Here is the relevant code...
struct ContentView: View {
#EnvironmentObject var viewModel: AppViewModel
var body: some View {
VStack{
NavigationView {
if viewModel.signedIn {
FirstView()
.transition(.slide)
} else {
//.onAppear method is used for keyboard management (See Misc Functions...)
SignInView()
.onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
.navigationBarHidden(true)
}
}
.onAppear {
viewModel.listen()
}
}
}
}
class AppViewModel: ObservableObject {
private var db = Firestore.firestore()
#Published var userInfo: User?
#Published var signedIn: Bool = false
var handle: AuthStateDidChangeListenerHandle?
let authRef = Auth.auth()
var authHandle : AuthStateDidChangeListenerHandle?
var rootInfoCollection : CollectionReference!
var userIdRef = ""
func fetchUserData(){
db.collection("Users").document("\(userIdRef)").getDocument { document, error in
// Check for error
if error == nil {
// Check that this document exists
if document != nil && document!.exists {
self.userInfo = document.map { (documentSnapshot) -> User in
let data = documentSnapshot.data()
let uid = data?["uid"] as? UUID ?? UUID()
let company = data?["company"] as? String ?? ""
let name = data?["name"] as? String ?? ""
let admin = data?["admin"] as? Bool ?? false
let photo = data?["photo"] as? String ?? ""
return User(uid: uid, company: company, name: name, admin: admin, photo: photo)
}
withAnimation {
self.signedIn = true
}
}
}
}
}
func listen(){
handle = authRef.addStateDidChangeListener({ auth, user in
print(user?.email ?? "No User Found")
if let user = auth.currentUser {
self.userIdRef = user.uid
self.rootInfoCollection = Firestore.firestore().collection("/Users/")
DispatchQueue.main.async {
self.fetchUserData()
}
} else {
self.signedIn = false
}
})
}
func signIn(email: String, password: String){
authRef.signIn(withEmail: email, password: password) { result, error in
guard result != nil, error == nil else {
return
}
}
}
}
struct SignInView: View {
#EnvironmentObject var viewModel: AppViewModel
#State private var username : String = ""
#State private var password : String = ""
#State private var shouldShowLoginAlert: Bool = false
#State var selectedImageArray : [Image] = []
var disableLoginButton : Bool {
return self.username.isEmpty || self.password.isEmpty
}
var body: some View {
VStack{
Image(uiImage: #imageLiteral(resourceName: "awText"))
.resizable()
.frame(width: 180, height: 100)
.padding(.bottom, 50)
TextField("Email", text: $username)
.padding(.leading)
.disableAutocorrection(true)
.autocapitalization(.none)
Rectangle().fill(Color.gray.opacity(0.25)).frame(height: 1, alignment: .center).padding(.bottom)
.padding(.bottom)
.onChange(of: self.username, perform: { value in
if value.count > 10 {
self.username = String(value.prefix(20)) //Max 10 Characters for Username.
}
})
SecureField("Password", text: $password)
.padding(.leading)
.disableAutocorrection(true)
.autocapitalization(.none)
Rectangle().fill(Color.gray.opacity(0.25)).frame(height: 1, alignment: .center)
.onChange(of: self.username, perform: { value in
if value.count > 10 {
self.username = String(value.prefix(10)) //Max 10 Characters for Password.
}
})
//SignIn Button
Button(action: {
viewModel.signIn(email: username, password: password)
}, label: {
Text("Sign In")
.disabled(disableLoginButton)
.frame(width: 300, height: 50)
.background(Color.green)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
.padding()
})
}
Replacing the default NavigationView behavior with your own animations isn't necessarily totally straightforward. I'll lay out one possibility, but another would be to use a real NavigationView transition, but just hide the back button once you're on FirstView.
To do the transition yourself, you'll need one root element to NavigationView, an if clause, a transition(.slide) and withAnimation. Here's a simplified version of your code showing just these elements:
class AppViewModel: ObservableObject {
#Published var signedIn = false
}
struct FirstView : View {
var body: some View {
Text("Signed in")
}
}
struct ContentView: View {
#StateObject var viewModel = AppViewModel()
var body: some View {
NavigationView {
VStack {
if viewModel.signedIn {
FirstView()
.transition(.slide)
} else {
Button("Sign me in") {
withAnimation {
viewModel.signedIn = true
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationBarHidden(true)
}
}
}
So i have a sessionStore:
class SessionStore: ObservableObject {
var handle: AuthStateDidChangeListenerHandle?
#Published var isLoggedIn = false
#Published var userInSession: User?
func listenAuthenticationState() {
handle = Auth.auth().addStateDidChangeListener({(auth, user) in
if let user = user {
let firestoreGetUser = Firestore.firestore().collection("users").document(user.uid)
firestoreGetUser.getDocument{(document, error) in
if let dict = document?.data() {
guard let decodedUser = try? User.init(fromDictionary: dict) else { return }
self.userInSession = decodedUser
print("decoded user = \(decodedUser)")
}
}
self.isLoggedIn = true
print("user logged in")
} else {
self.isLoggedIn = false
self.userInSession = nil
print("no one logged in")
}
})
}
func logout() {
do {
try Auth.auth().signOut()
} catch {
}
}
func unbind() {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
deinit {
unbind()
}
}
Its working as expected, I am able to sign in etc.
I have the following to pull the current user data:
import Foundation
import Firebase
import FirebaseAuth
import FirebaseFirestore
class ProfileViewModel: ObservableObject {
var uid: String = ""
var email: String = ""
var username: String = ""
var profileURL: String = ""
var bio: String = ""
var occupation: String = ""
var city: String = ""
func LoadAUser(userId: String) {
Firestore.firestore().collection("users").document(userId).getDocument{(snapshot, error) in
guard let snap = snapshot else {
print("error fetching data")
return
}
let dict = snap.data()
guard let decodedUser = try? User.init(fromDictionary: dict!) else { return }
print("decoded user - load user - \(decodedUser)")
}
}
}
In my view im trying to call it like:
import SwiftUI
struct ProfileView: View {
#EnvironmentObject var session: SessionStore
#ObservedObject var profileViewModel = ProfileViewModel()
func loadUserData() {
profileViewModel.LoadAUser(userId: session.userInSession!.uid)
}
var body: some View {
VStack {
Text("Edit Profile")
.fontWeight(.semibold)
.font(.system(.title, design: .rounded))
.foregroundColor(Color("startColor"))
Spacer()
VStack(alignment: .leading) {
Text("view")
}.padding()
.onAppear(perform: loadUserData)
}
}
struct ProfileView_Previews: PreviewProvider {
static var previews: some View {
ProfileView()
}
}
Im using .onAppear(perform: loadUserData) which is causing an issue - Thread1: Fatal error: Unexpectedly found nil while unwrapping
I also tried:
init() {
profileViewModel.LoadAUser(userId: session.userInSession!.uid)
}
But this also causes the same error.
The thing is I should only be able to get to this view if I'm logged in as this already works:
struct InitialView: View {
#EnvironmentObject var session: SessionStore
func listen() {
session.listenAuthenticationState()
}
var body: some View {
Group {
if session.isLoggedIn {
MainView()
} else {
NavigationView {
SignUpView()
}
}
}.onAppear(perform: listen)
}
}
I have an initialView()
struct InitialView: View {
#EnvironmentObject var session: SessionStore
func listen() {
session.listenAuthenticationState()
}
var body: some View {
Group {
if session.isLoggedIn {
MainView()
} else {
NavigationView {
SignUpView()
}
}
}.onAppear(perform: listen)
}
}
which takes you to the MainView() which has tabs to control which screen you can navigate to, then from here i can go to ProfileView()
Anyway by the logic of provided code it is more correct to activate isLoggedIn in
let firestoreGetUser = Firestore.firestore().collection("users").document(user.uid)
firestoreGetUser.getDocument{(document, error) in
if let dict = document?.data() {
guard let decodedUser = try? User.init(fromDictionary: dict) else { return }
self.userInSession = decodedUser
print("decoded user = \(decodedUser)")
self.isLoggedIn = true // << here !!
print("user logged in")
}
}
So whats worked for me is passing in Auth instead of session data:
func loadUserData() {
profileViewModel.LoadAUser(userId: Auth.auth().currentUser!.uid)
}
I want to write a function loggedIn() in file auth.service.ts to check the token from local storage, and then verify it with firebase/php-jwt in server side. But the code in Typescript gives an infinite loop. Here is my code:
auth.service.ts
loggedIn(){
const token: string = localStorage.getItem('id_token');
if (token == null) {
return false;
}
else {
const subs = this.http.post('http://localhost/url/to/myPHP.php', {"token":token})
.map(res=>res.json()).subscribe(data=>{
if(data.valid){
this.valid = true;
} else {
this.valid = false;
}
},
err=>console.log(err));
if (this.valid){
console.log("Valid");
return true;
} else {
console.log("Invalid");
return false;
}
}
}
Given token: valid token.
Result: give no error but infinite console.log of 'Valid' as well as return true, until the Apache down.
Given token: invalid token
Result: give no error but infinite console.log of 'Invalid' as well as return false, until the Apache down.
What I have tried:
loggedIn(){
const token: string = localStorage.getItem('id_token');
if (token == null) {
return false;
}
else {
const subs = this.http.post('http://localhost/url/to/myPHP.php', {"token":token})
.map(res=>res.json()).subscribe(data=>{
if(data.valid){
this.valid = true;
} else {
this.valid = false;
}
},
err=>console.log(err));
if (this.valid){
console.log("Valid");
console.log(this.valid);
return true;
} else {
console.log("Invalid");
console.log(this.valid);
return false;
}
subs.unsubscribe();
return true;
}
}
The line subs.unsubscribe(); did stop the loop, yet it will literally unsubscribe the Observable<Response> and the code inside .subscribe() will not run. Please help.
Edit: Usage of loggedIn()
*ngIf="authService.loggedIn()
for 4 times in navbar component.
Inside auth.guard.ts
canActivate(){
if (this.authService.validToken){
return true;
} else {
this.router.navigate(['/login']);
return false;
}
}
In app.module.ts
{path:'profile', component:ProfileComponent, canActivate:[AuthGuard]}
I finally solve the problem. The problem with my code: .subscribe() is an async call (which actually scheduled for last/later execution). This is the situation:ngOnInit() : which located in component < ts > file
ngOnInit(){
this.authService.loggedIn();
console.log("10");
}
loggedIn() : which located in auth.service.ts file
loggedIn(){
const token: string = localStorage.getItem('id_token');
if (token == null) {
return false;
}
else {
const subs = this.http.post('http://localhost/url/to/myPHP.php', {"token":token})
.map(res=>res.json()).subscribe(data=>{
if(data.valid){
console.log("1");
} else {
console.log("2");
}
console.log("3")
},
err=>console.log(err));
}
}
and then the result will be :
1013 which mean, anything you do inside the subscribe will change only after you need it (in many cases). So we need to do whatever we need to, inside the subscribe(), and fire it only when needed. In my case, I want to fire it if any changes apply to token inside local storage.
This is my solution
As I want it always check with the token. I used DoCheck()
Inside auth.service.ts
verifyToken(authToken) {
const body = {token:authToken};
return this.http.post('http://localhost/url/to/myPHP.php',body)
.map(res => res.json());
}
tokenExist(): boolean {
const token: string = localStorage.getItem('id_token');
if (token == null) {
return false;
} else {
return true;
}
}
Inside navbar.component.ts
ngOnInit() {
this.curToken = localStorage.getItem('id_token');
this.loggedIn(this.curToken);
}
ngDoCheck(){
if (this.curToken != localStorage.getItem('id_token')){
this.curToken = localStorage.getItem('id_token');
this.loggedIn(this.curToken);
}
}
loggedIn(token){
if(this.authService.tokenExist()){
this.authService.verifyToken(token).subscribe(data=>{
if(data.valid){
this.validLogin = true;
} else {
console.log("Invalid Token");
this.validLogin = false;
}
});
} else {
console.log("Token Missing");
this.validLogin = false;
}
}
Of course, don't forget to implement DoCheck in the line
export class NavbarComponent implements OnInit, DoCheck