Subscribing to Amplify.Auth Publisher - swift

I am using Amplify Auth for iOS and attempting to implement the signUp functionality:
public class UserSessionRepository {
func signUp(email: String, password: String) -> AnyPublisher<AuthSignUpResult, AuthError> {
let userAttributes = [AuthUserAttribute(.email, value: email)]
let options = AuthSignUpRequest.Options(userAttributes: userAttributes)
return Amplify.Auth.signUp(username: email, password: password, options: options).resultPublisher
}
}
However, when I attempt to subscribe to the Publisher and remap the return values, the subscribe does not seem to function correctly:
class SignUpEmailService {
#Injected private var userSessionRepository: UserSessionRepository
func signupEmailRequest(to email: String, password: String) -> AnyPublisher<SignupComplete, Error> {
return userSessionRepository.signUp(email: email, password: password)
.map { signupResult in
print("SignUpEmailService.signupEmailRequest: \(signupResult)")
return SignupComplete(result: signupResult.isSignupComplete)
}
.mapError { error in
print("SignUpEmailService.signupEmailRequest: \(error)")
return FooError.authError
}
.eraseToAnyPublisher()
}
}
Neither the map or mapError is actually called.
Thoughts on what I am missing in terms of setting up the Combine publish/subscribe mechanism?

Related

Registration with Gigya Swift SDK code verification email not being sent

Working on writing a bridge function for registering a new user in Gigya. Or registration flow is set to send a code verification email on registering a new user. This seems to not be firing when registering via the Swift SDK. Am I missing something in the way we are sending the request?
swift...
#objc(registerAccount:password:params:resolve:rejecter:)
func registerAccount(email: String, password: String, params: [String : Any],
_ resolve: #escaping RCTPromiseResolveBlock,
rejecter reject: #escaping RCTPromiseRejectBlock) -> Void {
self.gigya.register(email: email, password: password, params: params) { result in
switch result {
case .success(let data):
resolve(data);
case .failure(let error):
switch error.error {
case .gigyaError(let data):
resolve(data.toDictionary())
default:
reject("E_REGISTER_ACCOUNT", "Registration failed", error.error)
}
}
}
}
objc...
RCT_EXTERN_METHOD(registerAccount:(NSString *)email
password:(NSString *)password
params:(NSDictionary *)params
resolve:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
and lastly this gets called by JS...
export async function register() {
const email = 'test-registration#purple.com';
const password = '234567';
const req = await GIGYA_MANAGER.registerAccount(
email,
password,
{
finalizeRegistration: true,
profile: {
email: email,
firstName: 'Stella',
lastName: 'Beaglesmith'
}
}
).catch((err) => console.log(err));
return req;
}
I am able to see new users created in the sites identity access tab, but no code verification email seems to be sent.
For anyone coming across this in the future. When registering via the API, you need to send the verification manually so in Swift you will have a method that looks like this...
#objc(resendVerificationCode:regToken:resolve:rejecter:)
func resendVerificationCode(UID: String, regToken: String,
_ resolve: #escaping RCTPromiseResolveBlock,
rejecter reject: #escaping RCTPromiseRejectBlock) -> Void {
self.gigya.send(api: "accounts.resendVerificationCode", params: [
"UID": UID,
"regToken": regToken
]) { result in
switch result {
case .success(let data):
print(data.values)
case .failure(let error):
print(error)
}
}
}

flatMap doesn't get invoked

I'm trying to validate user's email and password using two separate function calls.
Both functions return AnyPublisher publishers, and I use combineLatest to collect the returned values (each validate call returns the string it's validating) into a tuple.
Then I'm using flatMap to make a network request to sign the user up using the values returned by combineLatest, however the flatMap operator never gets called.
validator.validate(text: email, with: [.validEmail])
.combineLatest(validator.validate(text: password, with: [.notEmpty]))
.flatMap { credentials in
return self.userSessionRepository.signUp(email: credentials.0, password: credentials.1)
}
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print(error)
self.indicateErrorSigningIn(error)
case .finished:
self.goToSignInNavigator.navigateToOtp()
}
}, receiveValue: { _ in })
.store(in: &subscriptions)
signUp(email:password:) returns AnyPublisher
Here's the validator function:
public func validate(text: String, with rules: [Rule]) -> AnyPublisher<String, ErrorMessage> {
rules.publisher
.compactMap { $0.check(text) }
.setFailureType(to: ErrorMessage.self)
.flatMap {
Fail<Void, ErrorMessage>(error: ErrorMessage(title: "Error", message: $0.description))
}
.map { text }
.eraseToAnyPublisher()
}
And the signUp function:
public func signUp(email: String, password: String) -> AnyPublisher<Void, ErrorMessage> {
remoteAPI.signUp(email: email, password: password)
.flatMap(dataStore.save)
.mapError { error -> ErrorMessage in
return ErrorMessage(title: "Error", message: error.description)
}
.eraseToAnyPublisher()
}
It calls these two functions:
public func signUp(email: String, password: String) -> AnyPublisher<Confirmation, RemoteError> {
guard email == "john.doe#email.com" else {
return Fail<Confirmation, RemoteError>(error: .invalidCredentials)
.eraseToAnyPublisher()
}
return Just(Confirmation(otp: "", nonce: "abcd"))
.setFailureType(to: RemoteError.self)
.eraseToAnyPublisher()
}
public func save(confirmation: Confirmation) -> AnyPublisher<Void, RemoteError> {
self.nonce = confirmation.nonce
return Empty().eraseToAnyPublisher()
}
I'm not sure what's wrong, though it's likely my not understanding Combine enough, as I've just started learning it recently.
I have figured it out.
The problem was with the validate(text:with:) function.
In the event of an error, the function behaved correctly, but when there was no error the function wasn't emitting any value, and that's why flatMap or any other operator in the pipeline wasn't being invoked.
The reason it wasn't emitting any values boils down to how the check(_:) function, which is called in compactMap, works. It returns an optional string, which is an error message. But if there's no error, there's no string, and thus no value is emitted.
As a result, the call to .map { text } doesn't get evaluated, and the credential doesn't get returned.
I've changed the code to this and now the program behaves correctly:
public func validate(text: String, with rules: [Rule]) -> AnyPublisher<String, ErrorMessage> {
rules.publisher
.setFailureType(to: ErrorMessage.self)
.tryMap { rule -> String in
if let error = rule.check(text) {
throw ErrorMessage(title: "Error", message: error)
}
return text
}
.mapError { error -> ErrorMessage in
return error as! ErrorMessage
}
.eraseToAnyPublisher()
}

Vapor 3 - How to check for similar email before saving object

I would like to create a route to let users update their data (e.g. changing their email or their username). To make sure a user cannot use the same username as another user, I would like to check if a user with the same username already exists in the database.
I have already made the username unique in the migrations.
I have a user model that looks like this:
struct User: Content, SQLiteModel, Migration {
var id: Int?
var username: String
var name: String
var email: String
var password: String
var creationDate: Date?
// Permissions
var staff: Bool = false
var superuser: Bool = false
init(username: String, name: String, email: String, password: String) {
self.username = username
self.name = name
self.email = email
self.password = password
self.creationDate = Date()
}
}
This is the piece of code where I want to use it:
func create(_ req: Request) throws -> EventLoopFuture<User> {
return try req.content.decode(UserCreationRequest.self).flatMap { userRequest in
// Check if `userRequest.email` already exists
// If if does -> throw Abort(.badRequest, reason: "Email already in use")
// Else -> Go on with creation
let digest = try req.make(BCryptDigest.self)
let hashedPassword = try digest.hash(userRequest.password)
let persistedUser = User(name: userRequest.name, email: userRequest.email, password: hashedPassword)
return persistedUser.save(on: req)
}
}
I could do it like this (see next snippet) but it seems a strange option as it requires a lot of nesting when more checks for e.g. uniqueness would have to be performed (for instance in the case of updating a user).
func create(_ req: Request) throws -> EventLoopFuture<User> {
return try req.content.decode(UserCreationRequest.self).flatMap { userRequest in
let userID = userRequest.email
return User.query(on: req).filter(\.userID == userID).first().flatMap { existingUser in
guard existingUser == nil else {
throw Abort(.badRequest, reason: "A user with this email already exists")
}
let digest = try req.make(BCryptDigest.self)
let hashedPassword = try digest.hash(userRequest.password)
let persistedUser = User(name: userRequest.name, email: userRequest.email, password: hashedPassword)
return persistedUser.save(on: req)
}
}
}
As one of the answers suggested I've tried to add Error middleware (see next snippet) but this does not correctly catch the error (maybe I am doing something wrong in the code - just started with Vapor).
import Vapor
import FluentSQLite
enum InternalError: Error {
case emailDuplicate
}
struct EmailDuplicateErrorMiddleware: Middleware {
func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response> {
let response: Future<Response>
do {
response = try next.respond(to: request)
} catch is SQLiteError {
response = request.eventLoop.newFailedFuture(error: InternalError.emailDuplicate)
}
return response.catchFlatMap { error in
if let response = error as? ResponseEncodable {
do {
return try response.encode(for: request)
} catch {
return request.eventLoop.newFailedFuture(error: InternalError.emailDuplicate)
}
} else {
return request.eventLoop.newFailedFuture(error: error)
}
}
}
}
The quick way of doing it is to do something like User.query(on: req).filter(\.email == email).count() and check that equals 0 before attempting the save.
However, whilst this will work fine for almost everyone, you still risk edge cases where two users try to register with the same username at the exact same time - the only way to handle this is to catch the save failure, check if it was because the unique constraint on the email and return the error to the user. However the chances of you actually hitting that are pretty rare, even for big apps.
I would make the field unique in the model using a Migration such as:
extension User: Migration {
static func prepare(on connection: SQLiteConnection) -> Future<Void> {
return Database.create(self, on: connection) { builder in
try addProperties(to: builder)
builder.unique(on: \.email)
}
}
}
If you use a default String as the field type for email, then you will need to reduce it as this creates a field VARCHAR(255) which is too big for a UNIQUE key. I would then use a bit of custom Middleware to trap the error that arises when a second attempt to save a record is made using the same email.
struct DupEmailErrorMiddleware: Middleware
{
func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response>
{
let response: Future<Response>
do {
response = try next.respond(to: request)
} catch is MySQLError {
// needs a bit more sophistication to check the specific error
response = request.eventLoop.newFailedFuture(error: InternalError.dupEmail)
}
return response.catchFlatMap
{
error in
if let response = error as? ResponseEncodable
{
do
{
return try response.encode(for: request)
}
catch
{
return request.eventLoop.newFailedFuture(error: InternalError.dupEmail)
}
} else
{
return request.eventLoop.newFailedFuture(error: error )
}
}
}
}
EDIT:
Your custom error needs to be something like:
enum InternalError: Debuggable, ResponseEncodable
{
func encode(for request: Request) throws -> EventLoopFuture<Response>
{
let response = request.response()
let eventController = EventController()
//TODO make this return to correct view
eventController.message = reason
return try eventController.index(request).map
{
html in
try response.content.encode(html)
return response
}
}
case dupEmail
var identifier:String
{
switch self
{
case .dupEmail: return "dupEmail"
}
}
var reason:String
{
switch self
{
case .dupEmail: return "Email address already used"
}
}
}
In the code above, the actual error is displayed to the user by setting a value in the controller, which is then picked up in the view and an alert displayed. This method allows a general-purpose error handler to take care of displaying the error messages. However, in your case, it might be that you could just create the response in the catchFlatMap.

PromiseKit 6 error in cannot convert error

Firstly, I'm aware that the implementation has changed in v6 and and I using the seal object as intended, the problem I'm having is that even when following the example to the letter it still gives me the old Cannot convert value of type '(_) -> CustomerLoginResponse' to expected argument type '(_) -> _' error.
Here is my function that returns the promise:
static func makeCustomerLoginRequest(userName: String, password: String) -> Promise<CustomerLoginResponse>
{
return Promise
{ seal in
Alamofire.request(ApiProvider.buildUrl(), method: .post, parameters: ApiObjectFactory.Requests.createCustomerLoginRequest(userName: userName, password: password).toXML(), encoding: XMLEncoding.default, headers: Constants.Header)
.responseXMLObject { (resp: DataResponse<CustomerLoginResponse>) in
if let error = resp.error
{
seal.reject(error)
}
guard let Xml = resp.result.value else {
return seal.reject(ApiError.credentialError)
}
seal.fulfill(Xml)
}
}
}
and here is the function that is consuming it:
static func Login(userName: String, password: String) {
ApiClient.makeCustomerLoginRequest(userName: userName, password: password).then { data -> CustomerLoginResponse in
}
}
You might have to provide more information if you want to chain more than one promises. In v6, you need to use .done if you don't want to continue the promises chain. If you have only one promise with this request then below is the correct implementation.
static func Login(userName: String, password: String) {
ApiClient.makeCustomerLoginRequest(userName: userName, password: password)
.done { loginResponse in
print(loginResponse)
}.catch { error in
print(error)
}
}
Remember, you have to return a promise if you are using .then until you break the chain by using .done. If you want to chain multiple promises then your syntax should look like this,
ApiClient.makeCustomerLoginRequest(userName: userName, password: password)
.then { loginResponse -> Promise<CustomerLoginResponse> in
return .value(loginResponse)
}.then { loginResponse -> Promise<Bool> in
print(loginResponse)
return .value(true)
}.then { bool -> Promise<String> in
print(bool)
return .value("hello world")
}.then { string -> Promise<Int> in
print(string)
return .value(100)
}.done { int in
print(int)
}.catch { error in
print(error)
}

Best practice for Swift methods that can return or error [closed]

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 1 year ago.
Improve this question
I’m practicing Swift and have a scenario (and a method) where the result could either be successful or a failure.
It’s a security service class. I have a method where I can authenticate with an email address and password, and want to either return a User instance if the credentials are correct, or throw some form of false value.
I’m a bit confused as my understanding of Swift methods is you need to specify a return type, so I have:
class SecurityService {
static func loginWith(email: String, password: String) -> User {
// Body
}
}
I’ve seen in Go and Node.js methods that return a “double” value where the first represents any errors, and the second is the “success” response. I also know that Swift doesn’t have things like errors or exceptions (but that may have changed since as I was learning an early version of Swift).
What would be the appropriate thing to do in this scenario?
If you want to handle errors that can happen during login process than use the power of Swift error handling:
struct User {
}
enum SecurityError: Error {
case emptyEmail
case emptyPassword
}
class SecurityService {
static func loginWith(email: String, password: String) throws -> User {
if email.isEmpty {
throw SecurityError.emptyEmail
}
if password.isEmpty {
throw SecurityError.emptyPassword
}
return User()
}
}
do {
let user = try SecurityService.loginWith1(email: "", password: "")
} catch SecurityError.emptyEmail {
// email is empty
} catch SecurityError.emptyPassword {
// password is empty
} catch {
print("\(error)")
}
Or convert to optional:
guard let user = try? SecurityService.loginWith(email: "", password: "") else {
// error during login, handle and return
return
}
// successful login, do something with `user`
If you just want to get User or nil:
class SecurityService {
static func loginWith(email: String, password: String) -> User? {
if !email.isEmpty && !password.isEmpty {
return User()
} else {
return nil
}
}
}
if let user = SecurityService.loginWith(email: "", password: "") {
// do something with user
} else {
// error
}
// or
guard let user = SecurityService.loginWith(email: "", password: "") else {
// error
return
}
// do something with user
Besides the standard way to throw errors you can use also an enum with associated types as return type
struct User {}
enum LoginResult {
case success(User)
case failure(String)
}
class SecurityService {
static func loginWith(email: String, password: String) -> LoginResult {
if email.isEmpty { return .failure("Email is empty") }
if password.isEmpty { return .failure("Password is empty") }
return .success(User())
}
}
And call it:
let result = SecurityService.loginWith("Foo", password: "Bar")
switch result {
case .Success(let user) :
print(user)
// do something with the user
case .Failure(let errormessage) :
print(errormessage)
// handle the error
}
Returning a result enum with associated values, throwing exception, and using a callback with optional error and optional user, although valid make an assumption of login failure being an error. However thats not necessarily always the case.
Returning an enum with cases for success and failure containing result and error respectively is almost identical as returning an optional User?. More like writing a custom optional enum, both end up cluttering the caller. In addition, it only works if the login process is synchronous.
Returning result through a callBack, looks better as it allows for the operation to be async. But there is still error handling right in front of callers face.
Throwing is generally preferred than returning an error as long as the scope of the caller is the right place to handle the error, or at least the caller has access to an object/method that can handle this error.
Here is an alternative:
func login(with login: Login, failure: ((LoginError) -> ())?, success: (User) -> ()?) {
if successful {
success?(user)
} else {
failure?(customError)
}
}
// Rename with exactly how this handles the error if you'd have more handlers,
// Document the existence of this handler, so caller can pass it along if they wish to.
func handleLoginError(_ error: LoginError) {
// Error handling
}
Now caller can; simply decide to ignore the error or pass a handler function/closure.
login(with: Login("email", "password"), failure: nil) { user in
// Ignores the error
}
login(with: Login("email", "password"), failure: handleLoginError) { user in
// Lets the error be handled by the "default" handler.
}
PS, Its a good idea to create a data structure for related fields; Login email and password, rather individually setting the properties.
struct Login {
typealias Email = String
typealias Password = String
let email: Email
let password: Password
}
To add an answer to this question (five years later), there’s a dedicated Result type for this exact scenario. It can return the type you want on success, or type an error on failure.
It does mean re-factoring some code to instead accept a completion handler, and then enumerating over the result in that callback:
class SecurityService {
static func loginWith(email: String, password: String, completionHandler: #escaping (Result<User, SecurityError>) -> Void) {
// Body
}
}
Then in a handler:
securityService.loginWith(email: email, password: password) { result in
switch result {
case .success(let user):
// Do something with user
print("Authenticated as \(user.name)")
case .failure(let error):
// Do something with error
print(error.localizedDescription)
}
}
I think that the result of calling loginWith could be derived from a network request, here is code that I could do in the scenario you presented:
Helper classes:
struct User {
var name: String
var email: String
}
class HTTP {
static func request(URL: String, method: String, params: [String: AnyObject], callback: (error: NSError?, result: [String:AnyObject]?) -> Void) -> Void {
// network request
}
}
class SecurityService {
static func loginWith(email: String, password: String, callback: (error: NSError?, user: User?) -> Void) -> Void {
let URL = ".."
let params = [
"email": email,
"password": password
]
HTTP.request(URL, method: "POST", params: params) { (error, result) in
if let error = error {
callback(error: error, user: nil)
} else {
guard let JSON = result else {
let someDomain = "some_domain"
let someCode = 100
let someInfo = [NSLocalizedDescriptionKey: "No results were sent by the server."]
let error = NSError(domain: someDomain, code: someCode, userInfo: someInfo)
callback(error: error, user: nil)
return
}
guard let name = JSON["name"] as? String, email = JSON["email"] as? String else {
let someDomain = "some_domain"
let someCode = 100
let someInfo = [NSLocalizedDescriptionKey: "No user properties were sent by the server."]
let error = NSError(domain: someDomain, code: someCode, userInfo: someInfo)
callback(error: error, user: nil)
return
}
let user = User(name: name, email: email)
callback(error: nil, user: user)
}
}
}
}
Using the SecurityService class:
SecurityService.loginWith("someone#email.com", password: "123456") { (error, user) in
if let error = error {
print(error)
} else {
guard let user = user else {
print("no user found.")
return
}
print(user)
}
}