iOS: Calling AWSMobileClient initialize() makes getUserAttributes() not invoking callback - ios

Here is a code snippet I am trying to get it work but without success so far. initialize() works fine but then getUserAttributes() is not triggering the callback. Not just getUserAttributes(), even other AWS calls such as getTokens() not triggering either. Believe, some where down inside AWS code, it is getting blocked. If I comment out initialize() then getUserAttributes() callback gets invoked. Tried various options with DispatchQueue/DispatchGroup, no help.
AWSMobileClient pod version 2.12.7.
import Foundation
import AWSMobileClient
struct AWSUser {
static let serialQueue = DispatchQueue(label: "serialQueue")
static let group = DispatchGroup()
static func initialize() -> Void {
DispatchQueue.global(qos: .background).async {
AWSInitialize()
getAWSUserAttributes()
}
}
static func AWSInitialize() -> Void {
group.enter()
AWSMobileClient.default().initialize { (userState, error) in
// error handling ...
switch userState {
case .signedIn:
//getAWSUserAttributes()
break
default:
break
}
group.leave()
}
}
static func getAWSUserAttributes() {
group.wait()
group.enter()
AWSMobileClient.default().getUserAttributes { (attrs, error) in
// NEVER REACHED!!!
// BUT WORKS IF AWSMobileClient.default().initialize() is commented out
group.leave()
}
}
}

For Getting Callback or trigger any event of AWSMobile Client, Make sure you have implemented below code in AppDelegate or respective view controller. If this method implement then function is trigger...
//Initialised Use Pool
func intializeUserPool() -> Void {
AWSDDLog.sharedInstance.logLevel = .verbose // TODO: Disable or reduce log level in production
AWSDDLog.add(AWSDDTTYLogger.sharedInstance) // TTY = Log everything to Xcode console
//Important for event handler
initializeAWSMobileClient()
}
// Add user state listener and initialize the AWSMobileClient
func initializeAWSMobileClient() {
AWSMobileClient.default().initialize { (userState, error) in
print("Initialise userstate:\(String(describing: userState)) and Info:\(String(describing: error))")
if let userState = userState {
switch(userState){
case .signedIn: // is Signed IN
print("Logged In")
print("Cognito Identity Id (authenticated): \(String(describing: AWSMobileClient.default().identityId))")
case .signedOut: // is Signed OUT
print("Logged Out")
print("Cognito Identity Id (unauthenticated): \(String(describing: AWSMobileClient.default().identityId))")
case .signedOutUserPoolsTokenInvalid: // User Pools refresh token INVALID
print("User Pools refresh token is invalid or expired.")
default:
self.signOut()
}
} else if let error = error {
print(error.localizedDescription)
}
}
//Register State
self.addUserStateListener() // Register for user state changes
}
// AWSMobileClient - a realtime notifications for user state changes
func addUserStateListener() {
AWSMobileClient.default().addUserStateListener(self) { (userState, info) in
print("Add useruserstate:\(userState) and Info:\(info)")
switch (userState) {
case .signedIn:
print("Listener status change: signedIn")
DispatchQueue.main.async {
self.getSession()
}
case .signedOut:
print("Listener status change: signedOut")
case .signedOutFederatedTokensInvalid:
print("Listener status change: signedOutFederatedTokensInvalid")
default:
print("Listener: unsupported userstate")
}
}
}

Related

Combine: converting one publisher to another in func

I'm using the Amplify framework for my auth on the combine framework.
I want to check if the user is currently logged in. This is done by the following function
Amplify.Auth.fetchAuthSession()
This returns:
AnyPublisher<AuthSession,AuthError>
I've put it in a function, so I can call it from my AuthenticationViewModel, which deals with the business login for auth.
enum AuthenticationsFunctions {
static func fetchCurrentAuthSession() -> AnyPublisher<AuthSession, AuthError> {
Amplify.Auth.fetchAuthSession().resultPublisher
}
}
For my view model, it has states and events. In the code below, I want to call the authentication function and return the appropriate events. Such as .onAlreadyLoggedIn. Because the auth function returns a different publisher, I can't figure out how to return the appropriate event to AnyPublisher<Event, Never>
AuthSession has a function .isSignedIn which is a boolean.
static func fetchCurrentLogin() -> Feedback<State, Event> {
Feedback { (state: State) -> AnyPublisher<Event, Never> in
guard case .loading = state else { return Empty().eraseToAnyPublisher() } //Checks if the state is loading (When the app first opens)
AuthenticationsFunctions.fetchCurrentAuthSession().allSatisfy { (AuthSession) -> Bool in
if (AuthSession.isSignedIn) {
return true
}
else {
return false
}
}
}
}
Amazon on their docs provide this:
func fetchCurrentAuthSession() -> AnyCancellable {
Amplify.Auth.fetchAuthSession().resultPublisher
.eraseToAnyPublisher()
.sink {
if case let .failure(authError) = $0 {
print("Fetch session failed with error \(authError)")
}
}
receiveValue: { session in
print("Is user signed in - \(session.isSignedIn)")
}
}

RealmSwift: Implementing one time login with MongoDB Realm in swift

I want user to login once and not have to reenter their login info everytime they open app unless they logout in the last session.
Login screen is currently displayed everytime the app is open. This is my rootview
struct AppRootView: View {
var body: some View {
AnyView {
// check if user has already logged in here and then route them accordingly
if auth.token != nil {
homeMainView()
} else {
LoginController()
}
}
}
}
currently this is what I use to login users
#objc func signUp() {
setLoading(true);
app.usernamePasswordProviderClient().registerEmail(username!, password: password!, completion: {[weak self](error) in
// Completion handlers are not necessarily called on the UI thread.
// This call to DispatchQueue.main.sync ensures that any changes to the UI,
// namely disabling the loading indicator and navigating to the next page,
// are handled on the UI thread:
DispatchQueue.main.sync {
self!.setLoading(false);
guard error == nil else {
print("Signup failed: \(error!)")
self!.errorLabel.text = "Signup failed: \(error!.localizedDescription)"
return
}
print("Signup successful!")
// Registering just registers. Now we need to sign in, but we can reuse the existing username and password.
self!.errorLabel.text = "Signup successful! Signing in..."
self!.signIn()
}
})
}
#objc func signIn() {
print("Log in as user: \(username!)");
setLoading(true);
app.login(withCredential: AppCredentials(username: username!, password: password!)) { [weak self](maybeUser, error) in
DispatchQueue.main.sync {
self!.setLoading(false);
guard error == nil else {
// Auth error: user already exists? Try logging in as that user.
print("Login failed: \(error!)");
self!.errorLabel.text = "Login failed: \(error!.localizedDescription)"
return
}
guard let user = maybeUser else {
fatalError("Invalid user object?")
}
print("Login succeeded!");
//
let hostingController = UIHostingController(rootView: ContentView())
self?.navigationController?.pushViewController(hostingController, animated: true)
}
how could I implement one time login so that users do have to login each time they open the app?
A correctly configured and initialized RealmApp class will persist the session information for you between app restarts, you can check for an existing session using the .currentUser() method from this class. So in your case something like:
if app.currentUser() != nil {
homeMainView()
} else {
LoginController()
}
While using Realm to persist login is a good idea, but I would highly
advice against using it for managing user authentication credentials such
as passwords. A better approach if you want to save sensitive information is
using KeyChain just like what Apple and password manager apps do. With a light
weight keyChain wrapper library such as SwiftKeychainWrapper You can easily
save your login credentials in the most secure way.
Here is a sample using a keyChain wrapper linked above.
With simple modification you can use this helper class to manage your sign in credentials anywhere in your app.
import SwiftKeychainWrapper
class KeyChainService {
// Make a singleton
static let shared = KeyChainService()
// Strings which will be used to map data in keychain
private let passwordKey = "passwordKey"
private let emailKey = "emailKey"
private let signInTokenKey = "signInTokenKey"
// Saving sign in info to keyChain
func saveUserSignInInformation(
email: String,
password: String,
token: String
onError: #escaping() -> Void,
onSuccess: #escaping() -> Void
) {
DispatchQueue.global(qos: .default).async {
let passwordIsSaved: Bool = KeychainWrapper.standard.set(password, forKey: self.passwordKey)
let emailIsSaved: Bool = KeychainWrapper.standard.set(email, forKey: self.emailKey)
let tokenIsSaved: Bool = KeychainWrapper.standard.set(token, forKey: self.signInTokenKey)
DispatchQueue.main.async {
// Verify that everything is saved as expected.
if passwordIsSaved && emailIsSaved && tokenIsSaved {
onSuccess()
}else {
onError()
}
}
}
}
// Retrieve signIn information for auto login
func retrieveSignInInfo(onError: #escaping() -> Void, onSuccess: #escaping(UserModel) -> Void) {
DispatchQueue.main.async {
let retrievedPassword: String? = KeychainWrapper.standard.string(forKey: self.passwordKey)
let retrievedEmail: String? = KeychainWrapper.standard.string(forKey: self.emailKey)
let retrievedToken: String? = KeychainWrapper.standard.string(forKey: self.signInTokenKey)
if let password = retrievedPassword,
let email = retrievedEmail,
let token = retrievedToken {
// Assuming that you have a custom user model named "UserModel"
let user = UserModel(email: email, password: password,token: token)
// Here is your user info which you can use to verify with server if needed and auto login user.
onSuccess(user)
}else {
onError()
}
}
}
}

Combine Future block called multiple times when using Flatmap and multiple subscribers

I've been successfully using BrightFutures in my apps mainly for async network requests. I decided it was time to see if I could migrate to Combine. However what I find is that when I combine two Futures using flatMap with two subscribers my second Future code block is executed twice. Here's some example code which will run directly in a playground:
import Combine
import Foundation
extension Publisher {
func showActivityIndicatorWhileWaiting(message: String) -> AnyCancellable {
let cancellable = sink(receiveCompletion: { _ in Swift.print("Hide activity indicator") }, receiveValue: { (_) in })
Swift.print("Busy: \(message)")
return cancellable
}
}
enum ServerErrors: Error {
case authenticationFailed
case noConnection
case timeout
}
func authenticate(username: String, password: String) -> Future<Bool, ServerErrors> {
Future { promise in
print("Calling server to authenticate")
DispatchQueue.main.async {
promise(.success(true))
}
}
}
func downloadUserInfo(username: String) -> Future<String, ServerErrors> {
Future { promise in
print("Downloading user info")
DispatchQueue.main.async {
promise(.success("decoded user data"))
}
}
}
func authenticateAndDownloadUserInfo(username: String, password: String) -> some Publisher {
return authenticate(username: username, password: password).flatMap { (isAuthenticated) -> Future<String, ServerErrors> in
guard isAuthenticated else {
return Future {$0(.failure(.authenticationFailed)) }
}
return downloadUserInfo(username: username)
}
}
let future = authenticateAndDownloadUserInfo(username: "stack", password: "overflow")
let cancellable2 = future.showActivityIndicatorWhileWaiting(message: "Please wait downloading")
let cancellable1 = future.sink(receiveCompletion: { (completion) in
switch completion {
case .finished:
print("Completed without errors.")
case .failure(let error):
print("received error: '\(error)'")
}
}) { (output) in
print("received userInfo: '\(output)'")
}
The code simulates making two network calls and flatmaps them together as a unit which either succeeds or fails.
The resulting output is:
Calling server to authenticate
Busy: Please wait downloading
Downloading user info
Downloading user info <---- unexpected second network call
Hide activity indicator
received userInfo: 'decoded user data'
Completed without errors.
The problem is downloadUserInfo((username:) appears to be called twice. If I only have one subscriber then downloadUserInfo((username:) is only called once. I have an ugly solution that wraps the flatMap in another Future but feel I missing something simple. Any thoughts?
When you create the actual publisher with let future, append the .share operator, so that your two subscribers subscribe to a single split pipeline.
EDIT: As I've said in my comments, I'd make some other changes in your pipeline. Here's a suggested rewrite. Some of these changes are stylistic / cosmetic, as an illustration of how I write Combine code; you can take it or leave it. But other things are pretty much de rigueur. You need Deferred wrappers around your Futures to prevent premature networking (i.e. before the subscription happens). You need to store your pipeline or it will go out of existence before networking can start. I've also substituted a .handleEvents for your second subscriber, though if you use the above solution with .share you can still use a second subscriber if you really want to. This is a complete example; you can just copy and paste it right into a project.
class ViewController: UIViewController {
enum ServerError: Error {
case authenticationFailed
case noConnection
case timeout
}
var storage = Set<AnyCancellable>()
func authenticate(username: String, password: String) -> AnyPublisher<Bool, ServerError> {
Deferred {
Future { promise in
print("Calling server to authenticate")
DispatchQueue.main.async {
promise(.success(true))
}
}
}.eraseToAnyPublisher()
}
func downloadUserInfo(username: String) -> AnyPublisher<String, ServerError> {
Deferred {
Future { promise in
print("Downloading user info")
DispatchQueue.main.async {
promise(.success("decoded user data"))
}
}
}.eraseToAnyPublisher()
}
func authenticateAndDownloadUserInfo(username: String, password: String) -> AnyPublisher<String, ServerError> {
let authenticate = self.authenticate(username: username, password: password)
let pipeline = authenticate.flatMap { isAuthenticated -> AnyPublisher<String, ServerError> in
if isAuthenticated {
return self.downloadUserInfo(username: username)
} else {
return Fail<String, ServerError>(error: .authenticationFailed).eraseToAnyPublisher()
}
}
return pipeline.eraseToAnyPublisher()
}
override func viewDidLoad() {
super.viewDidLoad()
authenticateAndDownloadUserInfo(username: "stack", password: "overflow")
.handleEvents(
receiveSubscription: { _ in print("start the spinner!") },
receiveCompletion: { _ in print("stop the spinner!") }
).sink(receiveCompletion: {
switch $0 {
case .finished:
print("Completed without errors.")
case .failure(let error):
print("received error: '\(error)'")
}
}) {
print("received userInfo: '\($0)'")
}.store(in: &self.storage)
}
}
Output:
start the spinner!
Calling server to authenticate
Downloading user info
received userInfo: 'decoded user data'
stop the spinner!
Completed without errors.

handle nested observables in RxSwift

I'm trying to combine facebook login with a rest call, so when the user is logged in it should make an authenticate call to the server, where the server makes the graph calls, however I'm a bit confused to how I nest the calls with RxSwift? so far I have a FacebookProvider class with following method
func login() -> Observable<String> {
return Observable.create({ observer in
let loginManager = LoginManager()
//LogOut before
loginManager.logOut()
//Set Login Method
loginManager.loginBehavior = .native
//Login Closure
loginManager.logIn([ .publicProfile, .userFriends, .email], viewController: self.parentController) { loginResult in
switch loginResult {
case .failed(let error):
print(error)
observer.onError(FacebookError.NoConnection(L10n.networkError))
case .cancelled:
print("User cancelled login.")
case .success(_, let declinedPermissions, let accessToken):
print("Logged in!")
guard declinedPermissions.count > 0 else {
observer.onError(FacebookError.DeclinedPermission(L10n.declinedPermission))
return
}
observer.onNext(accessToken.authenticationToken)
observer.onCompleted()
}
}
return Disposables.create()
})
}
Then I have a LoginViewModel with this model
public func retrieveUserData() -> Observable<User> {
return Network.provider
.request(.auth(fbToken: Globals.facebookToken)).retry(5).debug().mapObject(User.self)
}
then I in my UIViewController do this
facebookProvider.validate().subscribe({ [weak self] response in
switch response {
case .error(_):
// User is not logged in push to loginController
break
case .next():
//user is logged in retrieveUserData before proceeding
self?.loginViewModel.retrieveUserData().subscribe { event in
switch event {
case .next(let response):
print(response)
case .error(let error):
print(error)
case .completed:
print("completed")
}
}.addDisposableTo(self?.disposeBag)
break
case .completed:
//data is retrieved and can now push to app
break
}
}).addDisposableTo(disposeBag)
Validate
public func rx_validate() -> Observable<String> {
return Observable.create({ observer in
//Check if AccessToken exist
if AccessToken.current == nil {
observer.onError(FacebookError.NotLoggedIn)
} else {
observer.onNext(Globals.accessToken)
}
observer.onCompleted()
return Disposables.create()
})
}
You will want to use flatMap
The closure passed to flatMap will return an observable. flatMap will then take care of un-nesting it, meaning if the closure returns a value of type Observable<T>, and you call flatMap on a value of type Observable<U>, the resulting observable will be Observable<T> (an not Observable<Observable<T>>
In this particular case, the code would look like this:
facebookProvider.validate().flatMap { [weak self] _ in
return self?.loginViewModel.retrieveUserData()
}.subscribe { event in
switch event {
// ...
}
}.addDisposableTo(disposeBag)
On a side note, you should probably update func retrieveUserData() to accept the token as a parameter, instead of fetching it from your Globals structure.
The resulting code would look similar to this
public func retrieveUserData(token: String) -> Observable<User> {
return Network.provider
.request(.auth(fbToken: token)).retry(5).debug().mapObject(User.self)
}
in viewController
facebookProvider.validate().flatMap { [weak self] token in
return self?.loginViewModel.retrieveUserData(token: token)
}.subscribe { event in
switch event {
// ...
}
}.addDisposableTo(disposeBag)

Can't prevent user from cancelling Touch ID

When the Touch ID alert is displayed, there is also a "Cancel" button. I would prefer to NOT allow the user to cancel because they are prohibited from continuing any further. 1. Is there a way to remove the "Cancel" button. 2. If the "Cancel" button is required, how can I force the user to re-authenticate with a fingerprint? If authenticate() is called a second time, the Touch ID API just lets them in. There is no alternative passcode and I'd hate to have to code up yet another view controller for it.
func authenticate() {
let myContext:LAContext = LAContext()
let authError:NSErrorPointer = nil
if (myContext.canEvaluatePolicy(.DeviceOwnerAuthenticationWithBiometrics, error: authError)) {
myContext.evaluatePolicy(.DeviceOwnerAuthenticationWithBiometrics, localizedReason: "Press fingerprint", reply: { (success:Bool, error:NSError?) -> Void in
if success == true {
log.debug("SUCCESSFUL AUTHENTICATION")
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.performSegueWithIdentifier("showUI", sender: self)
})
}
else {
log.debug("FAILED AUTHENTICATION")
self.authenticate()
}
})
}
}
You need to dispatch your failure call to self.authenticate on the main queue;
func authenticate() {
let myContext:LAContext = LAContext()
let authError:NSErrorPointer = nil
if (myContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: authError)) {
myContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Press fingerprint", reply: { (success:Bool, error:NSError?) -> Void in
if success {
log.debug("SUCCESSFUL AUTHENTICATION")
DispatchQueue.main.async {
self.performSegueWithIdentifier("showUI", sender: self)
}
}
else {
log.debug("FAILED AUTHENTICATION")
DispatchQueue.main.async {
self.authenticate()
}
}
})
}
}

Resources