Skip to content

Commit

Permalink
Merge pull request #7 from checkout/release/0.1.2-stub
Browse files Browse the repository at this point in the history
Stub release for 0.2.0
  • Loading branch information
melting-snowman authored Feb 23, 2023
2 parents 8e4852b + 2c32370 commit 82f3799
Show file tree
Hide file tree
Showing 31 changed files with 8,158 additions and 1,107 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "CheckoutCardManagement-iOS",
platforms: [
.iOS(.v11),
.iOS(.v12),
],
products: [
.library(
Expand Down
61 changes: 47 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,26 +59,19 @@ cardManager.getCards { result in
}
}
```
Note: card states. There are 4 different card states, which apply to both virtual and physical cards. They are:
| State | Meaning |
| ----------- | ----------- |
| Active | Card is active and can be used as normal. |
| Suspended | Card has been manually suspended by the cardholder, and can be reactivated to be used normally. Reactivation would return it to the Active state. |
| Inactive | Physical cards are created as inactive by default. Virtual cards are created as active by default. No matter the card's status, you cannot deactivate a card. |
| Revoked | Deleted. At this point, you cannot re-active the card. It is forever deleted. |



### Retrieve a PIN for one of the cards
<sub>The flow is similar for other sensitive information, like the long card number (PAN), or the security code (CVV)</sub>
### Retrieve SecureData for one of the cards
<sub>Example covers PIN, but the same flow and similar APIs are valid for PAN, CVV, PAN + CVV</sub>

These calls are subject to unique Strong Customer Authentication flow prior to every individual call. Only on completion of such a specific authentication, a single use token may be requested to continue executing the request.

```swift
// Authenticate your user for this request and on success, request a sensitive data specific token from your backend
// Not relevant when using Stub
let singleUseToken = retrieveSensitiveDataToken(generatedFor: reauthenticatedUser)
let singleUseToken = retrieveSingleUseToken(generatedFor: reauthenticatedUser)

// Using an index from the list of cards (for example from user UI selection), request the PIN for that card
cards[selectedIndex].getPin(singleUseToken: singleUseToken) { result in
// Request sensitive data via the card object
card.getPin(singleUseToken: singleUseToken) { result in
switch result {
case .success(let pinView):
// received an UI component we can now display to the user
Expand All @@ -89,6 +82,46 @@ cards[selectedIndex].getPin(singleUseToken: singleUseToken) { result in
```



### Perform a card lifecycle change
<sub>Used to activate, suspend or revoke cards (change the card state)</sub>

```swift
// We would recommend when looking to change the state of a card to validate
// the new state to be requested is possible for the current card.
let canActivateCard = card.possibleStateChanges.contains(.active)

// You can then call the respective associated method for the new state
card.activate { result in
switch result {
case .success:
// The call was successful and the `state` is now `active`
case .failure(let error):
// Failed to change card state, check the error object
}
}
// Other card lifecycle APIs are `.suspend` and `.revoke`,
// each receiving optional reasons for the requested operation
```

Note: card states. There are 4 different card states, which apply to both virtual and physical cards. They are:
| State | Meaning |
| ----------- | ----------- |
| Active | Card is active and can be used as normal. |
| Suspended | Card has been manually suspended by the cardholder, and can be reactivated to be used normally. Reactivation would return it to the Active state. |
| Inactive | Physical cards are created as inactive by default. Virtual cards are created as active by default. No matter the card's status, you cannot deactivate a card. |
| Revoked | Deleted. At this point, you cannot re-active the card. It is forever deleted. |



### Push Provisioning

The Push Provisioning feature adds the desired card to a digital wallet, in this case the Apple Wallet, from the mobile application.

This feature can be accessed by `card.provision(/* required parameters */)`. Note that this action is not possible using our Stub environment, but the API is expected to be be kept once releasing a Sandbox & Production eligible version.



### Moving to Sandbox and Production

Changing environments
Expand Down
20 changes: 15 additions & 5 deletions Sources/CheckoutCardManagement/CheckoutCardManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ protocol CardManager: AnyObject {
var cardService: CardService { get }
/// Analytics logger enabling tracking events
var logger: CheckoutEventLogging? { get }
/// Generic token used for non sensitive calls
var sessionToken: String? { get }
/// Design system to guide any secure view UI
var designSystem: CardManagementDesignSystem { get }

}

/// Access gateway into the Card Management functionality
Expand All @@ -41,15 +44,18 @@ public final class CheckoutCardManager: CardManager {
let designSystem: CardManagementDesignSystem
/// Analytics logger and dispatcher for tracked events
let logger: CheckoutEventLogging?

/// Generic token used for non sensitive calls
private var sessionToken: String?
var sessionToken: String?

/// Enable the functionality using the provided design system for secure UI components
public init(designSystem: CardManagementDesignSystem, environment: CardManagerEnvironment) {
let logger = CheckoutEventLogger(productName: Constants.productName)
let service = CheckoutCardService(environment: environment.networkEnvironment())
service.logger = logger

self.designSystem = designSystem
self.cardService = CheckoutCardService(environment: environment.networkEnvironment())
self.logger = CheckoutEventLogger(productName: Constants.productName)
self.cardService = service
self.logger = logger

setupRemoteLogging(environment: environment)
logInitialization()
Expand All @@ -66,8 +72,12 @@ public final class CheckoutCardManager: CardManager {
}

/// Store provided token to use on network calls
public func logInSession(token: String) {
public func logInSession(token: String) -> Bool {
guard cardService.isTokenValid(token) else {
return false
}
sessionToken = token
return true
}

/// Remove current token from future calls
Expand Down
2 changes: 1 addition & 1 deletion Sources/CheckoutCardManagement/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation

enum Constants {
/// Version of the SDK in use
static let productVersion = "0.1.1"
static let productVersion = "0.2.0"

/// Product name for logging
static let productName = "issuing-ios-sdk"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation
import CheckoutEventLoggerKit
import CheckoutCardNetwork

extension CheckoutEventLogging {

Expand All @@ -16,3 +17,11 @@ extension CheckoutEventLogging {
}

}

extension CheckoutEventLogger: NetworkLogger {

public func log(error: Error, additionalInfo: [String : String]) {
log(event: LogFormatter.build(event: .failure(source: additionalInfo["source"] ?? "", error: error),
extraProperties: additionalInfo))
}
}
6 changes: 6 additions & 0 deletions Sources/CheckoutCardManagement/LoggingSupport/LogEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ enum LogEvent {
/// Describe a successful call to retrieve a pan and a security code
case getPanCVV(idLast4: String, cardState: CardState)

/// Describe a successfull event where a card state change was completed
case stateManagement(idLast4: String, originalState: CardState, requestedState: CardState, reason: String?)

/// Describe a Push Provisioning event
case pushProvisioning(last4CardID: String, last4CardholderID: String)

/// Describe an unexpected but non critical failure
case failure(source: String, error: Error)
}
20 changes: 19 additions & 1 deletion Sources/CheckoutCardManagement/LoggingSupport/LogFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ enum LogFormatter {
case .getPan: return "card_pan"
case .getCVV: return "card_cvv"
case .getPanCVV: return "card_pan_cvv"
case .stateManagement: return "card_state_change"
case .pushProvisioning: return "push_provisioning"
case .failure: return "failure"
}
}
Expand All @@ -48,7 +50,9 @@ enum LogFormatter {
.getPin,
.getPan,
.getCVV,
.getPanCVV:
.getPanCVV,
.stateManagement,
.pushProvisioning:
return .info
case .failure:
return .warn
Expand All @@ -74,6 +78,20 @@ enum LogFormatter {
"suffix_id": AnyCodable(idLast4),
"card_state": AnyCodable(state.rawValue)
]
case .stateManagement(let idLast4, let originalState, let requestedState, let reason):
dictionary = [
"suffix_id": AnyCodable(idLast4),
"from": AnyCodable(originalState.rawValue),
"to": AnyCodable(requestedState.rawValue)
]
if let reason {
dictionary["reason"] = AnyCodable(reason)
}
case .pushProvisioning(let last4CardID, let last4CardholderID):
dictionary = [
"card": AnyCodable(last4CardID),
"cardholder": AnyCodable(last4CardholderID)
]
case .failure(let source, let error):
dictionary = [
"source": AnyCodable(source),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
//
// Card+Management.swift
//
//
// Created by Alex Ioja-Yang on 19/01/2023.
//

import Foundation
import CheckoutCardNetwork

public extension Card {

/// Possible Card State changes from the current state
var possibleStateChanges: [CardState] {
switch self.state {
case .inactive, .suspended:
return [.active, .revoked]
case .active:
return [.suspended, .revoked]
case .revoked:
return []
}
}

/// Add the card object to the Apple Wallet
///
/// - Parameters:
/// - cardhodlerID: Identifier for the cardholder owning the card
/// - configuration: Specialised object used for Push Provisioning, received during Onboarding
/// - provisioningToken: Push Provisioning token
/// - completionHandler: Completion Handler returning the outcome of the provisioning operation
func provision(cardholderID: String,
configuration: ProvisioningConfiguration,
provisioningToken: String,
completionHandler: @escaping ((CheckoutCardManager.OperationResult) -> Void)) {
let startTime = Date()
manager?.cardService.addCardToAppleWallet(cardID: self.id,
cardholderID: cardholderID,
configuration: configuration,
token: provisioningToken) { [weak self] result in
switch result {
case .success:
let event = LogEvent.pushProvisioning(last4CardID: self?.partIdentifier ?? "",
last4CardholderID: String(cardholderID.suffix(4)))
self?.manager?.logger?.log(event, startedAt: startTime)
completionHandler(.success)
case .failure(let networkError):
self?.manager?.logger?.log(.failure(source: "Push Provisioning", error: networkError), startedAt: startTime)
completionHandler(.failure(.from(networkError)))
}
}
}

/// Request to activate the card
func activate(completionHandler: @escaping ((CheckoutCardManager.OperationResult) -> Void)) {
guard possibleStateChanges.contains(.active) else {
completionHandler(.failure(.invalidStateRequested))
return
}
guard let sessionToken = manager?.sessionToken else {
completionHandler(.failure(.unauthenticated))
return
}
let startTime = Date()

manager?.cardService.activateCard(cardID: id,
sessionToken: sessionToken) { [weak self] result in
self?.handleCardOperationResult(result: result,
newState: .active,
startTime: startTime,
operationSource: "Activate Card",
completionHandler: completionHandler)
}
}

/// Request to suspend the card, with option to provide a reason for change
func suspend(reason: CardSuspendReason?,
completionHandler: @escaping ((CheckoutCardManager.OperationResult) -> Void)) {
guard possibleStateChanges.contains(.suspended) else {
completionHandler(.failure(.invalidStateRequested))
return
}
guard let sessionToken = manager?.sessionToken else {
completionHandler(.failure(.unauthenticated))
return
}
let startTime = Date()

manager?.cardService.suspendCard(cardID: id,
reason: reason,
sessionToken: sessionToken) { [weak self] result in
self?.handleCardOperationResult(result: result,
newState: .suspended,
reasonString: reason?.rawValue,
startTime: startTime,
operationSource: "Suspend Card",
completionHandler: completionHandler)
}
}

/// Request to revoke the card, with option to provide a reason for change
func revoke(reason: CardRevokeReason?,
completionHandler: @escaping ((CheckoutCardManager.OperationResult) -> Void)) {
guard possibleStateChanges.contains(.revoked) else {
completionHandler(.failure(.invalidStateRequested))
return
}
guard let sessionToken = manager?.sessionToken else {
completionHandler(.failure(.unauthenticated))
return
}
let startTime = Date()

manager?.cardService.revokeCard(cardID: id,
reason: reason,
sessionToken: sessionToken) { [weak self] result in
self?.handleCardOperationResult(result: result,
newState: .revoked,
reasonString: reason?.rawValue,
startTime: startTime,
operationSource: "Revoke Card",
completionHandler: completionHandler)
}
}

private func handleCardOperationResult(result: OperationResult,
newState: CardState,
reasonString: String? = nil,
startTime: Date,
operationSource: String,
completionHandler: @escaping ((CheckoutCardManager.OperationResult) -> Void)) {
switch result {
case .success:
let event = LogEvent.stateManagement(idLast4: partIdentifier,
originalState: state,
requestedState: newState,
reason: reasonString)
manager?.logger?.log(event, startedAt: startTime)
state = newState
completionHandler(.success)
case .failure(let networkError):
manager?.logger?.log(.failure(source: operationSource, error: networkError), startedAt: startTime)
completionHandler(.failure(.from(networkError)))
}
}

}
Loading

0 comments on commit 82f3799

Please sign in to comment.