Skip to content

Commit

Permalink
Merge pull request #499 from checkout/release/4.3.0
Browse files Browse the repository at this point in the history
Merge release/4.3.0 into main
  • Loading branch information
okhan-okbay-cko authored Nov 7, 2023
2 parents 10c3cd6 + 01422b0 commit 7a47fb1
Show file tree
Hide file tree
Showing 38 changed files with 1,594 additions and 276 deletions.
76 changes: 76 additions & 0 deletions .github/partial-readmes/SecurityCodeComponent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
## Make a payment with a hosted security code
Use our security code component to make a compliant saved card payment in regions where sending a security code is always mandatory.

Within this flow, we will securely tokenise the security code and return a security code token to your application layer, which you can then use to continue the payment flow with.

### Step 1: Initialise the configuration

```swift
var configuration = SecurityCodeComponentConfiguration(apiKey: "PUBLIC_KEY", // set your public key
environment: Frames.Environment.sandbox) // set the environment
```

### Step 2: Create a UIView

Either create a UIView on storyboard (or a nib file) and define the `Custom Class` and `Module` like below and create an `IBOutlet` in the code counterpart:

<img width="727" alt="Screenshot 2023-11-06 at 11 46 34" src="https://github.com/checkout/frames-ios/assets/125963311/ee19b1f8-f3eb-47ee-a20a-e328bdba7001">

Or, create it programmatically:

```swift
let securityCodeView = SecurityCodeComponent()
securityCodeView.frame = parentView.bounds
parentView.addSubview(securityCodeView)
```

### Step 3: Style the component

We are using a secure display view so it won't be possible to edit the properties of the inner text field. We provide the `SecurityCodeComponentStyle` to allow the component to be configured. Other than text style, all other attributes can be configured like any other `UIView`.

Note that security code view has a `clear` background by default.

```swift
let style = SecurityCodeComponentStyle(text: .init(),
font: UIFont.systemFont(ofSize: 24),
textAlignment: .natural,
textColor: .red,
tintColor: .red,
placeholder: "Enter here")
configuration.style = style
```

### Step 4: Inject an optional card scheme for granular security code validation

If you don't define a card scheme, then all 3 and 4 digit security codes are considered valid for all card schemes. If you don't use the SDKs front-end validation, you will get an error at the API level if you don't define a card scheme and the CVV is invalid. If the CVV is length 0, the SDK will throw a validation error when calling `createToken` independent from the injected card scheme.

```swift
configuration.cardScheme = Card.Scheme(rawValue: "VISA") // or you can directly use `Card.Scheme.visa`. You should be getting the scheme name string values from your backend.
```

### Step 5: Call the configure method

```swift
securityCodeView.configure(with: configuration) { [weak self] isSecurityCodeValid in
DispatchQueue.main.async {
self?.payButton.isEnabled = isSecurityCodeValid
}
 }
```

### Step 6: Create a security code token
```swift
securityCodeView.createToken { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let tokenDetails):
self?.showAlert(with: tokenDetails.token, title: "Success")

case .failure(let error):
self?.showAlert(with: error.localizedDescription, title: "Failure")
}
}
}
```

You can then continue the payment flow with this `token` by passing into a field name as `cvv` in the payment request.
2 changes: 1 addition & 1 deletion .github/scripts/lintEditedFiles.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ if EDITED_FILES=$(git diff HEAD origin/main --name-only --diff-filter=d | grep "
if [ -z "$EDITED_FILES" ]; then
echo "No edited .swift files found."
else
swiftlint lint $EDITED_FILES | sed -E 's/^(.*):([0-9]+):([0-9]+): (warning|error|[^:]+): (.*)/::\4 title=Lint error,file=\1,line=\2,col=\3::\5\n\1:\2:\3/'
swiftlint lint $EDITED_FILES | sed -E -n 's/^(.*):([0-9]+):([0-9]+): error: (.*)/::error file=\1,line=\2,col=\3::\4\n\1:\2:\3/p'
fi
else
echo "No changes in .swift files found."
Expand Down
2 changes: 1 addition & 1 deletion Checkout.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'Checkout'
s.version = '4.2.1'
s.version = '4.3.0'
s.summary = 'Checkout SDK for iOS'

s.description = <<-DESC
Expand Down
5 changes: 5 additions & 0 deletions Checkout/Source/Logging/CheckoutLogEvent+Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ extension CheckoutLogEvent {
let publicKey: String
}

struct SecurityCodeTokenRequestData: Equatable {
let tokenType: SecurityCodeTokenType?
let publicKey: String
}

struct TokenResponseData: Equatable {
let tokenID: String?
let scheme: String?
Expand Down
28 changes: 24 additions & 4 deletions Checkout/Source/Logging/CheckoutLogEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ enum CheckoutLogEvent: Equatable {
case validateExpiryString
case validateExpiryInteger
case validateCVV
case cvvRequested(SecurityCodeTokenRequestData)
case cvvResponse(SecurityCodeTokenRequestData, TokenResponseData)

func event(date: Date) -> Event {
Event(
Expand All @@ -39,9 +41,9 @@ enum CheckoutLogEvent: Equatable {

private var typeIdentifier: String {
switch self {
case .tokenRequested:
case .tokenRequested, .cvvRequested:
return "token_requested"
case .tokenResponse:
case .tokenResponse, .cvvResponse:
return "token_response"
case .cardValidator:
return "card_validator"
Expand All @@ -63,9 +65,10 @@ enum CheckoutLogEvent: Equatable {
.validateCardNumber,
.validateExpiryString,
.validateExpiryInteger,
.validateCVV:
.validateCVV,
.cvvRequested:
return .info
case .tokenResponse(_, let tokenResponseData):
case .tokenResponse(_, let tokenResponseData), .cvvResponse(_, let tokenResponseData):
return level(from: tokenResponseData.httpStatusCode)
}
}
Expand Down Expand Up @@ -102,6 +105,23 @@ enum CheckoutLogEvent: Equatable {
[.httpStatusCode: tokenResponseData.httpStatusCode],
[.serverError: tokenResponseData.serverError]
)
case .cvvRequested(let tokenRequestData):
return [
.tokenType: tokenRequestData.tokenType?.rawValue.lowercased(),
.publicKey: tokenRequestData.publicKey
].compactMapValues { $0 }

case let .cvvResponse(tokenRequestData, tokenResponseData):
return mergeDictionaries(
[
.tokenType: tokenRequestData.tokenType?.rawValue.lowercased(),
.publicKey: tokenRequestData.publicKey,
.tokenID: tokenResponseData.tokenID,
.scheme: tokenResponseData.scheme
],
[.httpStatusCode: tokenResponseData.httpStatusCode],
[.serverError: tokenResponseData.serverError]
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// SecurityCodeError.swift
//
//
// Created by Okhan Okbay on 05/10/2023.
//

import Foundation

extension TokenisationError {
public enum SecurityCodeError: CheckoutError {
case missingAPIKey
case couldNotBuildURLForRequest
case networkError(NetworkError)
case serverError(TokenisationError.ServerError)
case invalidSecurityCode

public var code: Int {
switch self {
case .missingAPIKey:
return 4001
case .couldNotBuildURLForRequest:
return 3007
case .networkError(let networkError):
return networkError.code
case .serverError(let serverError):
return serverError.code
case .invalidSecurityCode:
return 3006
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// SecurityCodeRequest.swift
//
//
// Created by Okhan Okbay on 05/10/2023.
//

import Foundation

struct SecurityCodeRequest: Encodable, Equatable {
let type: String = "cvv"
let tokenData: TokenData
}

struct TokenData: Encodable, Equatable {
let securityCode: String

enum CodingKeys: String, CodingKey {
case securityCode = "cvv"
}
}

// For logging purposes only
enum SecurityCodeTokenType: String, Codable, Equatable {
case cvv
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// SecurityCodeResponse.swift
//
//
// Created by Okhan Okbay on 05/10/2023.
//

import Foundation

public struct SecurityCodeResponse: Decodable, Equatable {
/// Type of the tokenisation. In SecurityCodeResponse, it's always `cvv`
public let type: String

/// Reference token
public let token: String

/// Date/time of the token expiration. The format is `2023-11-01T13:36:16.2003858Z`
public let expiresOn: String
}
15 changes: 9 additions & 6 deletions Checkout/Source/Network/RequestFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,22 @@ final class RequestFactory: RequestProviding {
}

enum Request: Equatable {
case token(tokenRequest: TokenRequest, publicKey: String)
case cardToken(tokenRequest: TokenRequest, publicKey: String)
case securityCodeToken(request: SecurityCodeRequest, publicKey: String)

var httpMethod: NetworkManager.RequestParameters.Method {
switch self {
case .token:
case .cardToken, .securityCodeToken:
return .post
}
}

func httpBody(encoder: Encoding) -> Data? {
switch self {
case .token(let tokenRequest, _):
return try? encoder.encode(tokenRequest)
case .cardToken(let request, _):
return try? encoder.encode(request)
case .securityCodeToken(let request, _):
return try? encoder.encode(request)
}
}

Expand All @@ -58,7 +61,7 @@ final class RequestFactory: RequestProviding {

var additionalHeaders: [String: String] {
switch self {
case let .token(_, publicKey):
case let .cardToken(_, publicKey), let .securityCodeToken(_, publicKey):
return [
"Authorization": "Bearer \(publicKey)",
"User-Agent": Constants.Product.userAgent
Expand All @@ -76,7 +79,7 @@ final class RequestFactory: RequestProviding {
}

switch self {
case .token:
case .cardToken, .securityCodeToken:
urlComponents.path += "tokens"
}

Expand Down
80 changes: 79 additions & 1 deletion Checkout/Source/Tokenisation/CheckoutAPIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import CheckoutEventLoggerKit

public protocol CheckoutAPIProtocol {
func createToken(_ paymentSource: PaymentSource, completion: @escaping (Result<TokenDetails, TokenisationError.TokenRequest>) -> Void)
func createSecurityCodeToken(securityCode: String, completion: @escaping (Result<SecurityCodeResponse, TokenisationError.SecurityCodeError>) -> Void)
var correlationID: String { get }
}

Expand Down Expand Up @@ -102,7 +103,7 @@ final public class CheckoutAPIService: CheckoutAPIProtocol {

private func createToken(tokenRequest: TokenRequest, completion: @escaping (Result<TokenDetails, TokenisationError.TokenRequest>) -> Void) {
let requestParameterResult = requestFactory.create(
request: .token(tokenRequest: tokenRequest, publicKey: publicKey)
request: .cardToken(tokenRequest: tokenRequest, publicKey: publicKey)
)

switch requestParameterResult {
Expand Down Expand Up @@ -170,3 +171,80 @@ final public class CheckoutAPIService: CheckoutAPIProtocol {
}
}
}

// MARK: Security Code Request

extension CheckoutAPIService {
public func createSecurityCodeToken(securityCode: String, completion: @escaping (Result<SecurityCodeResponse, TokenisationError.SecurityCodeError>) -> Void) {
guard !publicKey.isEmpty else {
completion(.failure(.missingAPIKey))
return
}

let request = SecurityCodeRequest(tokenData: TokenData(securityCode: securityCode))
let requestParameterResult = requestFactory.create(request: .securityCodeToken(request: request, publicKey: publicKey))

switch requestParameterResult {
case .success(let requestParameters):
logManager.queue(event: .cvvRequested(CheckoutLogEvent.SecurityCodeTokenRequestData(
tokenType: .cvv,
publicKey: publicKey
)))
createSecurityCodeToken(requestParameters: requestParameters, completion: completion)
case .failure(let error):
switch error {
case .baseURLCouldNotBeConvertedToComponents, .couldNotBuildURL:
completion(.failure(.couldNotBuildURLForRequest))
}
}
}

private func createSecurityCodeToken(requestParameters: NetworkManager.RequestParameters, completion: @escaping (Result<SecurityCodeResponse, TokenisationError.SecurityCodeError>) -> Void) {
requestExecutor.execute(
requestParameters,
responseType: SecurityCodeResponse.self,
responseErrorType: TokenisationError.ServerError.self
) { [logManager, logSecurityCodeTokenResponse] tokenResponseResult, httpURLResponse in
logSecurityCodeTokenResponse(tokenResponseResult, httpURLResponse)

switch tokenResponseResult {
case .response(let tokenResponse):
completion(.success(tokenResponse))
case .errorResponse(let errorResponse):
completion(.failure(.serverError(errorResponse)))
case .networkError(let networkError):
completion(.failure(.networkError(networkError)))
}

logManager.resetCorrelationID()
}
}

private func logSecurityCodeTokenResponse(tokenResponseResult: NetworkRequestResult<SecurityCodeResponse, TokenisationError.ServerError>, httpURLResponse: HTTPURLResponse?) {
switch tokenResponseResult {
case .response(let tokenResponse):
let tokenRequestData = CheckoutLogEvent.SecurityCodeTokenRequestData(tokenType: .cvv, publicKey: publicKey)
let tokenResponseData = CheckoutLogEvent.TokenResponseData(
tokenID: tokenResponse.token,
scheme: nil,
httpStatusCode: httpURLResponse?.statusCode,
serverError: nil
)

logManager.queue(event: .cvvResponse(tokenRequestData, tokenResponseData))
case .errorResponse(let errorResponse):
let tokenRequestData = CheckoutLogEvent.SecurityCodeTokenRequestData(tokenType: nil, publicKey: publicKey)
let tokenResponseData = CheckoutLogEvent.TokenResponseData(
tokenID: nil,
scheme: nil,
httpStatusCode: httpURLResponse?.statusCode,
serverError: errorResponse
)

logManager.queue(event: .cvvResponse(tokenRequestData, tokenResponseData))
case .networkError:
// we received no response, so nothing to log
break
}
}
}
2 changes: 1 addition & 1 deletion Checkout/Source/Validation/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public enum Constants {
}

enum Product {
static let version = "4.2.1"
static let version = "4.3.0"
static let name = "checkout-ios-sdk"
static let userAgent = "checkout-sdk-ios/\(version)"
}
Expand Down
Loading

0 comments on commit 7a47fb1

Please sign in to comment.