Skip to content

Commit

Permalink
Merge pull request #88 from commercetools/issues/#87-add-AnonymousCar…
Browse files Browse the repository at this point in the history
…tSignInMode

Closes #87 Add anonymous cart sign in mode
  • Loading branch information
nikola-mladenovic authored Oct 31, 2016
2 parents ae9b960 + 930444b commit 6875533
Show file tree
Hide file tree
Showing 14 changed files with 239 additions and 75 deletions.
4 changes: 4 additions & 0 deletions Commercetools.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
210BEA1170C392AA36907B63 /* StateRole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210BE9449BBF4A8DD0B7B09E /* StateRole.swift */; };
210BEA16377561C1FC741F28 /* CreateEndpointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210BEF918E99658EE26EA2EE /* CreateEndpointTests.swift */; };
210BEA9A3DDB3216E94A6152 /* Zone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210BEF391627F9DE1730CC16 /* Zone.swift */; };
210BEAB55406B7D50666F3D5 /* AnonymousCartSignInMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210BEA8C191A3D896D2DF133 /* AnonymousCartSignInMode.swift */; };
210BEAC43656B07788455DBF /* ReturnInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210BEAAA0DE6D66C5D909149 /* ReturnInfo.swift */; };
210BEAF3789CFF3898152D79 /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210BE4E18D39AAC4D717A979 /* Location.swift */; };
210BEB5CFFA25C9B2F702751 /* PaymentMethodInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210BEBF406AB4C5BABCAA58B /* PaymentMethodInfo.swift */; };
Expand Down Expand Up @@ -208,6 +209,7 @@
210BEA38F1A3754058F16FDC /* TaxMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaxMode.swift; sourceTree = "<group>"; };
210BEA757C72C7352C15B276 /* CartDraft.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CartDraft.swift; sourceTree = "<group>"; };
210BEA79B4FA8CDFF3BA78D5 /* DiscountCodeState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscountCodeState.swift; sourceTree = "<group>"; };
210BEA8C191A3D896D2DF133 /* AnonymousCartSignInMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnonymousCartSignInMode.swift; sourceTree = "<group>"; };
210BEA9FB39CC0B4C382E087 /* Endpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = "<group>"; };
210BEAAA0DE6D66C5D909149 /* ReturnInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReturnInfo.swift; sourceTree = "<group>"; };
210BEAABCBDF178FC0D01D3C /* Channel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -407,6 +409,7 @@
210BEEA9D57C44CBFA8EA866 /* ExternalLineItemTotalPrice.swift */,
210BE61CBA50D35A74FCD59B /* ResourceIdentifier.swift */,
210BE4D26AAC72DDCA7C600D /* CustomerUpdateAction.swift */,
210BEA8C191A3D896D2DF133 /* AnonymousCartSignInMode.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -808,6 +811,7 @@
210BE59FD6B10BFCB06869D9 /* ExternalLineItemTotalPrice.swift in Sources */,
210BE324CAFE1123C16146FD /* ResourceIdentifier.swift in Sources */,
210BEC81E5A96322CBA1BAA4 /* CustomerUpdateAction.swift in Sources */,
210BEAB55406B7D50666F3D5 /* AnonymousCartSignInMode.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ If at some point you wish to login the user, that can be achieved using `loginUs
let username = "[email protected]"
let password = "password"

Commercetools.loginUser(username, password: password, completionHandler: { error in
if let error = error as? CTError, case .accessTokenRetrievalFailed(let reason) = error {
Commercetools.loginCustomer(username, password: password, completionHandler: { result in
if let error = result.errors?.first as? CTError, case .accessTokenRetrievalFailed(let reason) = error {
// Handle error, and possibly get some more information from reason.message
}
})
Expand All @@ -106,7 +106,7 @@ Commercetools.loginUser(username, password: password, completionHandler: { error
Similarly, after logging out, all further interactions continue to use new anonymous user token.

```swift
Commercetools.logoutUser()
Commercetools.logoutCustomer()
```

Access and refresh tokens are being preserved across app launches. In order to inspect whether it's currently handling authenticated or anonymous user, `authState` property should be used:
Expand All @@ -118,13 +118,14 @@ if Commercetools.authState == .plainToken {
```
In order for your app to support anonymous session, you should set the `anonymousSession` bool property in your configuration `.plist` file to `true`. Additionally, it is possible to override this setting, and also provide optional custom `anonymous_id` (for metrics and tracking purposes) by invoking:
```swift
if Commercetools.obtainAnonymousToken(usingSession: true, anonymousId: "some-custom-id", completionHandler: { error in
if error == nil {
Commercetools.obtainAnonymousToken(usingSession: true, anonymousId: "some-custom-id", completionHandler: { error in
if error == nil {
// It is possible for token retrieval to fail, e.g custom token ID has already been taken,
// in which case reason.message from the returned CTError instance is set to the anonymousId is already in use.
}
})
}
})
```
When an anonymous sessions ends with a sign up or a login, carts and orders are migrated to the customer, and `CustomerSignInResult` is returned, providing access to both customer profile, and the currently active cart. For the login operation, you can define how to migrate line items from the currently active cart, by explicitly specifying one of two `AnonymousCartSignInMode` values: `.mergeWithExistingCustomerCart` or `.useAsNewActiveCustomerCart`.

## Consuming Commercetools Endpoints

Expand Down
27 changes: 14 additions & 13 deletions Source/AuthManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ open class AuthManager {
private var loginUrl: String? {
if let config = Config.currentConfig, let baseAuthUrl = config.authUrl, let projectKey = config.projectKey, config.validate() {
return "\(baseAuthUrl)oauth/\(projectKey)/customers/token"

}
return nil
}
Expand Down Expand Up @@ -139,15 +138,15 @@ open class AuthManager {
- parameter password: The user's password.
- parameter completionHandler: The code to be executed once the token fetching completes.
*/
open func loginUser(_ username: String, password: String, completionHandler: @escaping (Error?) -> Void) {
open func loginCustomer(username: String, password: String, completionHandler: @escaping (Error?) -> Void) {
// Process all token requests using private serial queue to avoid issues with race conditions
// when multiple credentials / login requests can lead auth manager in an unpredictable state
serialQueue.async(execute: {
let semaphore = DispatchSemaphore(value: 0)
if self.state != .plainToken {
self.logoutUser()
self.logoutCustomer()
}
self.processLoginUser(username, password: password, completionHandler: { token, error in
self.processLogin(username: username, password: password, completionHandler: { token, error in
completionHandler(error)
semaphore.signal()
})
Expand All @@ -159,7 +158,7 @@ open class AuthManager {
This method will clear all tokens both from memory and persistent storage.
Most common use case for this method is user logout.
*/
open func logoutUser() {
open func logoutCustomer() {
clearAllTokens()

Log.debug("Getting new anonymous access token after user logout")
Expand Down Expand Up @@ -229,7 +228,7 @@ open class AuthManager {

if (state == .anonymousToken && !usingAnonymousSession) ||
(state == .plainToken && usingAnonymousSession) {
logoutUser()
logoutCustomer()
}
}

Expand Down Expand Up @@ -259,13 +258,15 @@ open class AuthManager {
}
}

private func processLoginUser(_ username: String, password: String, completionHandler: @escaping (String?, Error?) -> Void) {
private func processLogin(username: String, password: String, completionHandler: @escaping (String?, Error?) -> Void) {
if let loginUrl = loginUrl, let authHeaders = authHeaders, let scope = Config.currentConfig?.scope {
Alamofire.request(loginUrl, method: .post, parameters: ["grant_type": "password", "scope": scope, "username": username, "password": password], encoding: URLEncoding.queryString, headers: authHeaders)
.responseJSON(queue: DispatchQueue.global(), completionHandler: { response in
self.state = .customerToken
self.handleAuthResponse(response, completionHandler: completionHandler)
})
Alamofire.request(loginUrl, method: .post,
parameters: ["grant_type": "password", "scope": scope, "username": username, "password": password],
encoding: URLEncoding.queryString, headers: authHeaders)
.responseJSON(queue: DispatchQueue.global(), completionHandler: { response in
self.state = .customerToken
self.handleAuthResponse(response, completionHandler: completionHandler)
})
}
}

Expand Down Expand Up @@ -331,7 +332,7 @@ open class AuthManager {
let statusCode = response.response?.statusCode, statusCode > 299 {
// In case we got an error while using refresh token, we want to clear token storage - there's no way
// to recover from this
logoutUser()
logoutCustomer()
completionHandler(nil, CTError.accessTokenRetrievalFailed(reason: CTError.FailureReason(message: failureReason, details: responseDict["error_description"] as? String)))

} else {
Expand Down
4 changes: 2 additions & 2 deletions Source/CTError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ public enum CTError: Error {
public struct FailureReason {

/// The error message returned by the API.
let message: String?
public let message: String?

/// The detailed description when returned as a value of the `detailedErrorMessage` response field.
let details: String?
public let details: String?
}

case configurationValidationFailed
Expand Down
74 changes: 63 additions & 11 deletions Source/Commercetools.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Copyright (c) 2016 Commercetools. All rights reserved.
//

import Foundation
import ObjectMapper

// MARK: - Configuration

Expand Down Expand Up @@ -33,25 +33,77 @@ public var authState: AuthManager.TokenState {
}

/**
This method should be used for user login. After successful login the new auth token is used for all
This method should be used for customer login. After successful login the new auth token is used for all
further requests with Commercetools services.
In case this method is called before previously logging user out, it will automatically logout (i.e remove
In case this method is called before previously logging customer out, it will automatically logout (i.e remove
previously stored tokens).

- parameter username: The user's username.
- parameter password: The user's password.
- parameter completionHandler: The code to be executed once the token fetching completes.
- parameter username: The user's username.
- parameter password: The user's password.
- parameter activeCartSignInMode: Optional sign in mode, specifying whether the cart line items should be merged.
- parameter completionHandler: The code to be executed once the token fetching completes.
*/
public func loginUser(_ username: String, password: String, completionHandler: @escaping (Error?) -> Void) {
AuthManager.sharedInstance.loginUser(username, password: password, completionHandler: completionHandler)
public func loginCustomer(username: String, password: String, activeCartSignInMode: AnonymousCartSignInMode? = nil,
result: @escaping (Result<CustomerSignInResult>) -> Void) {
if authState == .customerToken {
logoutCustomer()
}

// If the user is logging after an anonymous session, `/me/login` endpoint is triggered before obtaining
// access and refresh tokens, so that carts and orders can be migrated
Customer.login(username: username, password: password, activeCartSignInMode: activeCartSignInMode) { loginResult in
if loginResult.isFailure {
result(loginResult)
} else {
AuthManager.sharedInstance.loginCustomer(username: username, password: password) { error in
if let error = error {
result(.failure(nil, [error]))
} else {
result(loginResult)
}
}
}
}
}

/**
Creates new customer with specified profile.

- parameter profile: Draft of the customer profile to be created.
- parameter result: The code to be executed after processing the response.
*/
public func signUpCustomer(_ profile: CustomerDraft, result: @escaping (Result<CustomerSignInResult>) -> Void) {
signUpCustomer(Mapper<CustomerDraft>().toJSON(profile), result: result)
}

/**
Creates new customer with specified profile.

- parameter profile: Dictionary representation of the draft customer profile to be created.
- parameter result: The code to be executed after processing the response.
*/
public func signUpCustomer(_ profile: [String: Any], result: @escaping (Result<CustomerSignInResult>) -> Void) {
Customer.signUp(profile, result: { signUpResult in
if signUpResult.isFailure {
result(signUpResult)
} else if let username = signUpResult.model?.customer?.email, let password = profile["password"] as? String {
AuthManager.sharedInstance.loginCustomer(username: username, password: password) { error in
if let error = error {
result(.failure(nil, [error]))
} else {
result(signUpResult)
}
}
}
})
}

/**
This method will clear all tokens both from memory and persistent storage.
Most common use case for this method is user logout.
Most common use case for this method is customer logout.
*/
public func logoutUser() {
AuthManager.sharedInstance.logoutUser()
public func logoutCustomer() {
AuthManager.sharedInstance.logoutCustomer()
}

/**
Expand Down
27 changes: 20 additions & 7 deletions Source/Endpoints/Customer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,26 @@ open class Customer: Endpoint, Mappable {
}

/**
Creates new customer with specified profile.
Performs login operation in order to migrate carts and orders for anonymous user, when the login credentials
have been supplied.

- parameter profile: Draft of the customer profile to be created.
- parameter result: The code to be executed after processing the response.
- parameter username: The user's username.
- parameter password: The user's password.
- parameter activeCartSignInMode: Optional sign in mode, specifying whether the cart line items should be merged.
- parameter completionHandler: The code to be executed once the token fetching completes.
*/
open static func signup(_ profile: CustomerDraft, result: @escaping (Result<CustomerSignInResult>) -> Void) {
signup(Mapper<CustomerDraft>().toJSON(profile), result: result)
static func login(username: String, password: String, activeCartSignInMode: AnonymousCartSignInMode?,
result: @escaping (Result<CustomerSignInResult>) -> Void) {
var userDetails = ["email": username, "password": password]
if let activeCartSignInMode = activeCartSignInMode {
userDetails["activeCartSignInMode"] = activeCartSignInMode.rawValue
}
requestWithTokenAndPath(result, { token, path in
Alamofire.request("\(path)login", method: .post, parameters: userDetails, encoding: JSONEncoding.default, headers: self.headers(token))
.responseJSON(queue: DispatchQueue.global(), completionHandler: { response in
handleResponse(response, result: result)
})
})
}

/**
Expand All @@ -42,7 +55,7 @@ open class Customer: Endpoint, Mappable {
- parameter profile: Dictionary representation of the draft customer profile to be created.
- parameter result: The code to be executed after processing the response.
*/
open static func signup(_ profile: [String: Any], result: @escaping (Result<CustomerSignInResult>) -> Void) {
open static func signUp(_ profile: [String: Any], result: @escaping (Result<CustomerSignInResult>) -> Void) {
requestWithTokenAndPath(result, { token, path in
Alamofire.request("\(path)signup", method: .post, parameters: profile, encoding: JSONEncoding.default, headers: self.headers(token))
.responseJSON(queue: DispatchQueue.global(), completionHandler: { response in
Expand Down Expand Up @@ -100,7 +113,7 @@ open class Customer: Endpoint, Mappable {
"newPassword": newPassword, "version": version], encoding: JSONEncoding.default, result: { changePasswordResult in

if let response = changePasswordResult.json, let email = response["email"] as? String, changePasswordResult.isSuccess {
AuthManager.sharedInstance.loginUser(email, password: newPassword, completionHandler: { error in
AuthManager.sharedInstance.loginCustomer(username: email, password: newPassword, completionHandler: { error in
if let error = error as? CTError {
Log.error("Could not login automatically after password change "
+ (error.errorDescription ?? ""))
Expand Down
12 changes: 12 additions & 0 deletions Source/Models/AnonymousCartSignInMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// Copyright (c) 2016 Commercetools. All rights reserved.
//

import Foundation

public enum AnonymousCartSignInMode: String {

case mergeWithExistingCustomerCart = "MergeWithExistingCustomerCart"
case useAsNewActiveCustomerCart = "UseAsNewActiveCustomerCart"

}
Loading

0 comments on commit 6875533

Please sign in to comment.