Skip to content

With this example you will be able to quickly connect with the PlacetoPay Checkout Services.

Notifications You must be signed in to change notification settings

AddsDev/checkout-ios

Repository files navigation

PlacetoPay Redirection

iOS Swift PlacetoPay

With this example you will be able to quickly connect with the PlacetoPay Checkout Services.

In order to see more comprehensive examples of how it works, please refer to the examples and the docs page


Table of Contents

Integrating Checkout

Sample App

Language


Authentication

The auth parameter must be sent in all API requests and contains the set of properties needed to verify authentication.

Requirements

To integrate with Checkout you must have your login and secretKey credentials.

  • login: Site identifier, can be considered public as it travels as plain data in API requests.
  • secretKey: Secret key of the site, it must be private, from this data a new tranKey will be generated that will be sent in the requests.

These credentials are unique to your site and should be treated securely.

Do not share your credentials in public accessible areas such as Github, client-side code or other places easily to third parties.

You must know and prepare the following information:

Parameter Description
login Site identifier, credential provided when starting your integration.
secretKey Site secret key, credential provided when starting your integration.
seed This is the date the authentication was generated. Date must be in ISO 8601 format.
nonce Random string value that identifies a request as unique. It is generated and used to create tranKey without base64 encoding. When sending it, it must be base64 encoded.
tranKey Generated on every request programmatically. It is generated with the following formulas Base64(SHA-256(nonce + seed + secretKey)).

For this example, a Colombia environment is being used. For more information, see the directory of environments


The CheckoutAuth example class is used as a request parameter in authentication operations. The class includes private methods for cryptographic computations, ensuring the security and confidentiality of the authentication.

class CheckoutAuth: Codable {
    var login: String
    private(set) var nonce: String
    private(set) var seed: String
    private(set) var tranKey: String
    
    enum CodingKeys: String, CodingKey {
        case login, nonce, seed, tranKey
    }
    
    init(login: String, secretKey: String) {
        self.login = login
        self.seed = ""
        self.nonce = ""
        self.tranKey = ""
        self.generateAuth(secretKey: secretKey)
    }
    
    private func generateAuth(secretKey: String) {
        let rawNonce = UInt64.random(in: 0..<1000000)
        self.seed = ISO8601DateFormatter.string(from: Date(), timeZone: .current, formatOptions: .withInternetDateTime)
        let hashedData = SHA256.hash(data: "\(rawNonce)\(self.seed)\(secretKey)".data(using: .utf8)!)
        self.tranKey = Data(hashedData).base64EncodedString()
        self.nonce = Data(String(rawNonce).utf8).base64EncodedString()
    }
    
}

CheckoutAuth as a parameter in the request body in JSON.

{
    "login": "c51ce410c124a10e0db5e4b97fc2af39",
    "tranKey": "VQOcRcVH2DfL6Y4B4SaK6yhoH/VOUveZ3xT16OQnvxE=",
    "nonce": "NjE0OWVkODgwYjNhNw==",
    "seed": "2023-10-17T16:22-0500"
}

Frequent errors, see the possible errors


Setup services

Create the service interface with Retrofit for the Create Session and Query Session endpoints.

The implementation of the services must have as baseUrl the environment according to the app version.

The request Body must contain the auth parameter of type CheckoutAuth or the struct/class responsible for authentication.

protocol CheckoutRepository {
    func createSession(package: WelcomePackage, buyer: Buyer, ipAddress: String, handler: @escaping (Result) -> Void) async
    func searchSession(sessionId: Int, handler: @escaping (Result) -> Void) async
    ...
}

class CheckoutPaymentRepository: CheckoutRepository {
    var baseUrl: String
    
    init(baseUrl: String) {
        self.baseUrl = baseUrl
    }
    
    func createSession(package: WelcomePackage, buyer: Buyer, ipAddress: String, handler: @escaping (Result) -> Void) async {
        do {
            let paymentRequest:CheckoutPaymentRequest = .init(package: package, buyer: buyer, ipAddress: ipAddress)
            let body = try JSONEncoder().encode(paymentRequest)
            
            try await RequestManager().request(
                from: baseUrl + "/api/session",
                decodeType: CheckoutPaymentResponse.self,
                method: .post,
                body: body,
                completionHandler: handler
            )
        } catch _ {
            handler(ResultError(error: .networkError("Error connecting to server", 500)))
        }
    }
    
    func searchSession(sessionId: Int, handler: @escaping (Result) -> Void) async {
        ...
    }
    
    ...
}

Creating a new payment request to obtain a session payment Url.

Just provide the information of the payment needed, and you will get a process url if its successful, for this example we are using the Basic Information that needs to be provided, to see the full structure refer to the docs page.

The CheckoutPaymentRequest example class is used as a body request. The class includes the payment, buyer, shipping and other information.

struct CheckoutPaymentRequest: Codable {
    
    var auth: CheckoutAuth?
    var buyer: CheckoutBuyer?
    var cancelUrl: String?
    var captureAddress:Bool? = false
    var expiration: String
    var fields: [CheckoutField]?
    var locale: String
    var noBuyerFill: Bool? = false
    var payment: CheckoutPayment?
    var returnUrl: String
    var ipAddress: String
    var skipResult: Bool? = false
    var userAgent = USER_AGENT
    
    enum CodingKeys: String, CodingKey {
        case auth, buyer, cancelUrl, captureAddress, expiration, fields, locale, noBuyerFill, payment, returnUrl, ipAddress, skipResult, userAgent
    }
    
    init(package: WelcomePackage, buyer: Buyer, ipAddress: String) {
        self.auth = .init(login: LOGIN_CHECKOUT, secretKey: SECRET_KEY_CHECKOUT)
        self.buyer = .init(buyer: buyer)
        self.cancelUrl = CANCEL_URL
        self.expiration = Calendar.autoupdatingCurrent.date(byAdding: .day, value: 1, to: Date())!.toString()
        self.fields = nil
        self.locale = "es_CO"
        self.payment = .init(package: package, buyer: buyer)
        self.returnUrl = RETURN_URL
        self.ipAddress = ipAddress
    }
}

CheckoutPaymentRequest as a request body in JSON.

See full basic payment

{
  "auth": {
    "login": "c51ce410c124a10e0db5e4b97fc2af39",
    ...
  },
  "expiration": "2023-10-17T23:13-0500",
  "locale": "es_CO",
  "payment": {
    "amount": {
      "currency": "COP",
      "details": [
        {
          "amount": 150000.0,
          "kind": "subtotal"
        },
        ...
      ],
      "total": 165000.0,
      ...
    },
    "buyer": {
      ...
    },
    "items": [
      {
        "category": "Books",
        "name": "Libro antiguo",
        "price": 80000,
        "qty": 1,
        "sku": 302,
        "tax": 15200.0
      },
      ...
    ],
    "reference": "3"
  },
  "returnUrl": "redirection://receipt?return",
  "userAgent": "Mozilla/5.0 (X11; Linux x86_64) Chrome/52.0.2743.116 Safari/537.36 ...",
  ...
}

The CheckoutPaymentResponse example class is used as a response body. The class includes the status, request id and payment Url.

class CheckoutPaymentResponse: Decodable, Result {
    
    var status: CheckoutStatus
    var requestId: Int?
    var processUrl: String?
    var date: String?
}

CheckoutPaymentResponse as a response body in JSON.

{
  "status": {
    "status": "OK",
    "reason": "PC",
    "message": "La petición se ha procesado correctamente",
    "date": "2023-10-18T15:02:20-05:00"
  },
  "requestId": 2630139,
  "processUrl": "https://checkout-test.placetopay.com/spa/session/2630139/7149a8cf3..."
}

This endpoint allows you to obtain the information of the session, if there are transactions in the session, the details of the same are shown.

The CheckoutInformationRequest example class is used as a body request. The class include auth information.

struct CheckoutInformationRequest: Codable {
    var auth: CheckoutAuth
    
    enum CodingKeys: String, CodingKey {
        case auth
    }
    
    init() {
        self.auth = .init(login: LOGIN_CHECKOUT, secretKey: SECRET_KEY_CHECKOUT)
    }
}

CheckoutInformationRequest as a request body in JSON.

{
  "auth": {
    "login": "c51ce410c124a10e0db5e4b97fc2af39",
    "nonce": "NjE0OWVkODgwYjNhNw==",
    "seed": "2023-10-17T17:13-0500",
    "tranKey": "VQOcRcVH2DfL6Y4B4SaK6yhoH/VOUveZ3xT16OQnvxE="
  }
}

The CheckoutInformationResponse example class is used as a response body. The class includes the status, request id and payment information.

struct CheckoutInformationResponse: Decodable, Result {
    var status: CheckoutStatus
    var requestId: Int?
    var request: CheckoutPaymentRequest?
    var payment: [CheckoutPaymentInformation]?
}

CheckoutInformationResponse as a response body in JSON.

See full information response

{
  "requestId": 2630139,
  "status": {
    "status": "APPROVED",
    "reason": "00",
    "message": "La petición ha sido aprobada exitosamente",
    "date": "2023-10-18T15:02:42-05:00"
  },
  "request": {
    "locale": "es_CO",
    "payer": {
      "document": "1040035001",
      ...
    },
    "payment": {
      "reference": "2",
      "amount": {
        "details": [
          {
            "kind": "shipping",
            "amount": 15000
          },
          ...
        ],
        "currency": "COP",
        "total": 65000
      },
      "items": [
        ...
      ],
      ...
    },
    "fields": [
      {
        "keyword": "_processUrl_",
        "value": "https://checkout-test.placetopay.com/spa/session/2630139/7149...",
        "displayOn": "none"
      },
      ...
    ],
    "returnUrl": "redirection://receipt?return",
    "userAgent": "Mozilla/5.0 (X11; Linux x86_64) Chrome/52.0.2743.116 Safari/537.36 ...",
    ...
  },
  "payment": [
    {
      "amount": {
        ...
      },
      "status": {
        "date": "2023-10-18T15:02:39-05:00",
        "status": "APPROVED",
        ...
      },
      "issuerName": "JPMORGAN CHASE BANK, N.A.",
      "paymentMethod": "visa",
      "processorFields": [
        {
          "value": "012988341",
          "keyword": "merchantCode",
          "displayOn": "none"
        },
        ...
      ],
      "paymentMethodName": "Visa",
      ...
    }
  ],
  "subscription": null
}

Settings your Webview

You can use this view CheckoutWebView to correctly display Checkout.

These settings help optimize and customize the browsing experience within the WebView. It's important that you can identify the return URL and cancellation URL to be able to close the WebView once the payment process is complete.

struct CheckoutWebView: UIViewRepresentable {
    let url: String
    
    func makeUIView(context: Context) -> WKWebView {
        let wkWebView = WKWebView()
        wkWebView.navigationDelegate = context.coordinator
        return wkWebView
    }
    
    func updateUIView(_ webView: WKWebView, context: Context) {
        let request = URLRequest(url: URL(string: url)!)
        webView.load(request)
    }
    
    func makeCoordinator() -> WebViewCoordinator {
        WebViewCoordinator(self)
    }
    
    class WebViewCoordinator: NSObject, WKNavigationDelegate {
        var parent: CheckoutWebView
        
        init(_ parent: CheckoutWebView) {
            self.parent = parent
        }
        
        func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
            if navigationAction.navigationType == .other {
                if let urlStr = navigationAction.request.url?.absoluteString, urlStr == returnUrl || urlStr == cancelUrl {
                    parent.showWebView = false
                    decisionHandler(.cancel)
                    return
                }
            }
            
            decisionHandler(.allow)
        }
    }
}
    

If cleaning cookies and webview data storage, see the clean cookies and data storage


Errors WebView

To clear the WebView cache, refer to How to remove cache in WKWebView?.

You may encounter one of the common errors in your integration with checkout:

  • Enabling JavaScript in the WebView (AppleWebKit has JavaScript enabled by default)

For iOS 14+ versions, WKWebpagePreferences should be used.

let webConfiguration = WKWebViewConfiguration()
        
if #available(iOS 14, *) {
    let preferences = WKWebpagePreferences()
    preferences.allowsContentJavaScript = true
    webConfiguration.defaultWebpagePreferences = preferences
} else {
    let oldPreferences = WKPreferences()
    oldPreferences.javaScriptEnabled = true
    oldPreferences.javaScriptCanOpenWindowsAutomatically = true
    webConfiguration.preferences = oldPreferences
}

...

let wkWebView = WKWebView(frame: .zero, configuration: webConfiguration)
  • Clear cache, cookies, session storage, etc.
let webConfiguration = WKWebViewConfiguration()

let websiteDataTypes: Set<String> = WKWebsiteDataStore.allWebsiteDataTypes()
//OR
let websiteDataTypes: Set<String> = [
            WKWebsiteDataTypeCookies, //(iOS 9.0)
            WKWebsiteDataTypeMemoryCache, //(iOS 9.0)
            WKWebsiteDataTypeLocalStorage, //(iOS 9.0)
            WKWebsiteDataTypeSessionStorage, //(iOS 9.0)
            //WKWebsiteDataTypeOfflineWebApplicationCache, //(iOS 9.0)
            //WKWebsiteDataTypeIndexedDBDatabases, //(iOS 9.0)
            //WKWebsiteDataTypeWebSQLDatabases, //(iOS 9.0)
            //WKWebsiteDataTypeFetchCache, //(iOS 11.3)
            //WKWebsiteDataTypeServiceWorkerRegistrations, //(iOS 11.3)
            //WKWebsiteDataTypeDiskCache, //(iOS 9.0)
            //WKWebsiteDataTypeFileSystem, //(iOS 16.0)
            //WKWebsiteDataTypeSearchFieldRecentSearches, //(iOS 17.0)
        ];
let date = NSDate(timeIntervalSince1970: 0)

let webStorage = WKWebsiteDataStore.default()
webStorage.removeData(ofTypes: websiteDataTypes, modifiedSince: date as Date, completionHandler: { print("RemoveData WebView - Done")})

webConfiguration.websiteDataStore = webStorage

let wkWebView = WKWebView(frame: .zero, configuration: webConfiguration)

To remove session cookies or HTTP header cookies

let wkWebView = WKWebView(...)
wkWebView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
  for cookie in cookies {
    if cookie.isSessionOnly || cookie.isHTTPOnly {
        wkWebView.configuration.websiteDataStore.httpCookieStore.delete(cookie)
    }
  }
}
  • Usage of user tracking, permission for Tracking must be requested from the user. You can use this example to validate permission and implement it where required.
import Foundation
...
import AppTrackingTransparency
import AdSupport

class .... : ... {

  func requestTrackingPermission(_ completion: @escaping (ATTrackingManager.AuthorizationStatus) -> Void) {
    ATTrackingManager.requestTrackingAuthorization(completionHandler: completion)
  }
  
  func isAuthorized() -> Bool {
    let status = ATTrackingManager.trackingAuthorizationStatus
    return status == ATTrackingManager.AuthorizationStatus.authorized
  }
  
  func updateTrakingStatus() {
    let status = ATTrackingManager.trackingAuthorizationStatus
      switch status {
      case .authorized:
        // Tracking authorization dialog was shown and we are authorized
        printDebug("Tracking - Authorized")
      case .denied:
        // Tracking authorization dialog was shown and permission is denied
        printDebug("Tracking - Denied")
      case .notDetermined:
        // Tracking authorization dialog has not been shown
        printDebug("Tracking - Not Determined")
      case .restricted:
        printDebug("Tracking - Restricted")
      @unknown default:
        printDebug("Tracking - Unknown")
      }
  } 
}

Remember to add the description or purpose of tracking in the Info.plist file.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    ...
    <key>NSUserTrackingUsageDescription</key>
    <string>App would like to access IDFA for tracking purpose</string>
    ...
  </dict>
</plist>

Remember to add the Nutrition Labels in the privacy file PrivacyInfo.xcprivacy. You can use this example, but remember to adjust it to your application's requirements.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>NSPrivacyCollectedDataTypes</key>
  <array>
    <dict>
      <key>NSPrivacyCollectedDataTypeTracking</key>
      <true/>
      <key>NSPrivacyCollectedDataTypeLinked</key>
      <true/>
      <key>NSPrivacyCollectedDataTypePurposes</key>
      <array>
          <string>App would like to access IDFA for tracking purpose</string>
      </array>
      <key>NSPrivacyCollectedDataType</key>
      <string>NSPrivacyCollectedDataType...</string>
    </dict>
  </array>
  <key>NSPrivacyTrackingDomains</key>
  <array>
    <string>dominio.com</string>
    ....
  </array>
  <key>NSPrivacyTracking</key>
  <true/>
</dict>
</plist>

⚠️ Remember that you can use this repository to test your credentials or visit P2p Client. If you encounter other errors in the integration, visit Common Errors.

Test your credentials

  • Clone this repository on your local machine.
git clone https://github.com/placetopay-org/checkout-ios.git
  • Open project in XCode.
  • Configure your login, secretKey and urlEnvironment credentials in

Redirection Home

  • Build and run.

Preview

Redirection Home

Redirection Home

Redirection Home

Redirection Home

Redirection Home

Redirection Home


Glossary

Name Description
login Site identifier, it can be considered public because it travels as plain data in API requests.
secretKey Secret key of the site, it must be private, from this data a new tranKey will be generated and sent in the request.
tranKey Generated on every request programmatically.
nonce Arbitrary value that identifies a request as unique.
seed This is the date the authentication was generated.
baseUrl Environment URL.

About

With this example you will be able to quickly connect with the PlacetoPay Checkout Services.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages