Skip to content

Instantly share code, notes, and snippets.

@IvanovDeveloper
Last active January 4, 2019 09:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save IvanovDeveloper/f7b6caf097fdf73104e56e7a00109d2e to your computer and use it in GitHub Desktop.
Save IvanovDeveloper/f7b6caf097fdf73104e56e7a00109d2e to your computer and use it in GitHub Desktop.
import Foundation
import UIKit
import Swifter
import WebKit
public enum BLTPaymentViewControllerError: Error, LocalizedError {
case orderTokenNotFound
case resourceNotFound(_ resourcesName: String)
case javaScriptError(_ errorMessage: String)
case httpResponseError(statusCode: Int)
public var errorDescription: String? {
switch self {
case .orderTokenNotFound:
return NSLocalizedString("Order token not found. You should configure BLTPaymentViewController with order token.", comment: "")
case .resourceNotFound(let resourcesName):
return NSLocalizedString("Resource '\(resourcesName)' not found", comment: "")
case .javaScriptError(let errorMessage):
return NSLocalizedString("Java script error: '\(errorMessage)'", comment: "")
case .httpResponseError(let statusCode):
return NSLocalizedString("Checkout loading fail with status code: '\(statusCode)'", comment: "")
}
}
}
enum BLTJSMessageHandler: String {
case success = "handleSuccess"
case close = "handleClose"
case error = "handleError"
}
var BLTResourcesBundle: Bundle {
let bundle = Bundle(for: BLTPaymentViewController.self)
let bundleURL = bundle.resourceURL!.appendingPathComponent("Bolt.bundle")
let resourceBundle = Bundle.init(url: bundleURL)
return resourceBundle!
}
/**
The methods in BLTPaymentViewControllerDelegate aid in integration of the payment view into third party applications. Using these methods a merchant application can determine when various events in the payment view's lifecycle have occurred.
*/
public protocol BLTPaymentViewControllerDelegate: class {
/**
Called when a payment has succeeded.
- Parameter paymentViewController: The payment view invoking the delegate method.
- Parameter transaction: The payment transaction responce object.
- Parameter transaction: The payment transaction responce json object.
*/
func paymentViewControllerPaymentDidSucceed(_ paymentViewController: BLTPaymentViewController, with transaction: BLTTransactionResponse?, transactionJsonBlob: String)
/**
Called when the payer dismisses the payment view from the UI without completing a successful payment.
- Parameter paymentViewController: The payment view invoking the delegate method.
*/
func paymentViewControllerDidClose(_ paymentViewController: BLTPaymentViewController)
/**
Called when the payment view encounters either an HTTP error or a JavaScript exception.
- Parameter paymentViewController: The payment view invoking the delegate method.
- Parameter error: The error encountered. It can either be an HTTP code or BLTErrorDomainJavascriptErrorCode in the case of a JavaScript exception.
*/
func paymentViewController(_ paymentViewController: BLTPaymentViewController, didEncounter error: Error)
}
/// Handles the presentation and lifecycle of a Bolt payment form.
public class BLTPaymentViewController: UIViewController {
/// The delegate that should receive notifications related to the payment view's lifecycle (payment success, error, view dismissed by user).
public weak var delegate: BLTPaymentViewControllerDelegate?
/// Specifies merchant-specific configuration options for the payment view.
let paymentConfiguration: BLTPaymentViewConfiguration
/// The token from order creation.
var orderToken: String?
/// Optional information about the payer used to pre-fill the payment form.
var payerInfo: BLTPayerInfo?
/// Configured Local web server instance.
let webServer: BLTServer!
let webView: WKWebView = {
let webViewConfiguration = WKWebViewConfiguration()
let contentController = WKUserContentController()
webViewConfiguration.userContentController = contentController
let webView = WKWebView(frame: CGRect.zero, configuration: webViewConfiguration)
webView.translatesAutoresizingMaskIntoConstraints = false
return webView
}()
let activityIndicatorView: UIActivityIndicatorView = {
let view = UIActivityIndicatorView(style: .gray)
view.hidesWhenStopped = true
return view
}()
// MARK: Initialization
/**
The payment view controller's designated initializer. Will use the default configuration as specified in the Info.plist (see documentation for the configuration property).
- Parameter paymentConfiguration: Optional. If not specified, the default configuration will be used (see documentation for the configuration property).
- Throws: `BLTServerError` will throw if failed to start the server.
- Returns: A new payment view controller instance.
*/
public init(paymentConfiguration: BLTPaymentViewConfiguration = BLTPaymentViewConfiguration()) throws {
self.paymentConfiguration = paymentConfiguration
do {
self.webServer = try BLTServer(publishableKey: self.paymentConfiguration.publishableKey, cdnURL: self.paymentConfiguration.cdnURL)
} catch {
throw error
}
super.init(nibName: nil, bundle: nil)
self.configureWebView()
}
/**
The payment view controller's designated initializer. Will use the default configuration as specified in the Info.plist (see documentation for the configuration property).
- Parameter paymentConfiguration: Optional. If not specified, the default configuration will be used (see documentation for the configuration property).
- Parameter orderToken: You need to use the token from order creation.
- Parameter payerInfo: Optional information about the payer used to pre-fill the payment form.
- Throws: `BLTServerError` will throw if failed to start the server.
- Returns: A new payment view controller instance.
*/
public convenience init(paymentConfiguration: BLTPaymentViewConfiguration = BLTPaymentViewConfiguration(), orderToken: String, payerInfo: BLTPayerInfo? = nil) throws {
do {
try self.init(paymentConfiguration: paymentConfiguration)
self.orderToken = orderToken
self.payerInfo = payerInfo
configureCheckoutInfo(orderToken: orderToken, payerInfo: payerInfo)
} catch {
throw error
}
}
public required init?(coder aDecoder: NSCoder) {
self.paymentConfiguration = BLTPaymentViewConfiguration()
do {
self.webServer = try BLTServer(publishableKey: self.paymentConfiguration.publishableKey, cdnURL: self.paymentConfiguration.cdnURL)
} catch {
return nil
}
super.init(coder: aDecoder)
self.configureWebView()
}
// MARK: Life Cycle
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
self.configureActivityView()
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
webView.configuration.userContentController.add(self, name: BLTJSMessageHandler.success.rawValue)
webView.configuration.userContentController.add(self, name: BLTJSMessageHandler.close.rawValue)
webView.configuration.userContentController.add(self, name: BLTJSMessageHandler.error.rawValue)
loadCheckoutForm()
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if orderToken == nil {
self.delegate?.paymentViewController(self, didEncounter: BLTPaymentViewControllerError.orderTokenNotFound)
}
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
webView.configuration.userContentController.removeScriptMessageHandler(forName: BLTJSMessageHandler.success.rawValue)
webView.configuration.userContentController.removeScriptMessageHandler(forName: BLTJSMessageHandler.close.rawValue)
webView.configuration.userContentController.removeScriptMessageHandler(forName: BLTJSMessageHandler.error.rawValue)
}
deinit {
print("")
}
// MARK: WebView Configuration
func configureWebView() {
webView.navigationDelegate = self
webView.uiDelegate = self
webView.scrollView.delegate = self
self.view.addSubview(webView)
let topConstraint = NSLayoutConstraint(item: webView, attribute: NSLayoutConstraint.Attribute.top,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.top,
multiplier: 1,
constant: 0)
let bottomConstraint = NSLayoutConstraint(item: webView,
attribute: NSLayoutConstraint.Attribute.bottom,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.bottom,
multiplier: 1,
constant: 0)
let leadingConstraint = NSLayoutConstraint(item: webView,
attribute: NSLayoutConstraint.Attribute.leading,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.leading,
multiplier: 1,
constant: 0)
let trailingConstraint = NSLayoutConstraint(item: webView,
attribute: NSLayoutConstraint.Attribute.trailing,
relatedBy: NSLayoutConstraint.Relation.equal,
toItem: view,
attribute: NSLayoutConstraint.Attribute.trailing,
multiplier: 1,
constant: 0)
view.addConstraints([topConstraint, bottomConstraint, leadingConstraint, trailingConstraint])
}
func configureActivityView() {
view.addSubview(activityIndicatorView)
activityIndicatorView.center = view.center
}
/**
Configure checkout info.
- Parameter orderToken: You need to use the token from order creation.
- Parameter payerInfo: Optional information about the payer used to pre-fill the payment form.
*/
public func configureCheckoutInfo(orderToken: String, payerInfo: BLTPayerInfo? = nil) {
self.orderToken = orderToken
self.payerInfo = payerInfo
configureCheckoutScript(orderToken: orderToken, payerInfo: payerInfo)
}
/**
Add BoltCheckoutConfiguration script with configuration data.
- Parameter orderToken: You need to use the token from order creation.
- Parameter payerInfo: Optional information about the payer used to pre-fill the payment form.
*/
func configureCheckoutScript(orderToken: String, payerInfo: BLTPayerInfo?) {
webView.configuration.userContentController.removeAllUserScripts()
guard let jsFile = BLTResourcesBundle.path(forResource: "BoltCheckoutConfiguration", ofType: "js"),
let scriptString = try? String(contentsOfFile: jsFile, encoding: String.Encoding.utf8) else {
self.delegate?.paymentViewController(self, didEncounter: BLTPaymentViewControllerError.resourceNotFound("BoltCheckoutConfiguration.js"))
return
}
var prefill = ""
if
let encodedObject = try? JSONEncoder().encode(payerInfo),
let encodedObjectJsonString = String(data: encodedObject, encoding: .utf8) {
prefill = "\"prefill\":\(encodedObjectJsonString)"
}
let formatedScriptString = String(format: scriptString,
orderToken,
prefill,
BLTJSMessageHandler.success.rawValue,
BLTJSMessageHandler.close.rawValue,
BLTJSMessageHandler.error.rawValue)
let script = WKUserScript(source: formatedScriptString, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
webView.configuration.userContentController.addUserScript(script)
}
func loadCheckoutForm() {
guard let url = URL(string: webServer.serverURL) else { return }
let urlRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 30.0)
webView.load(urlRequest)
}
}
// MARK: - WKScriptMessageHandler
extension BLTPaymentViewController: WKScriptMessageHandler {
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
#if DEBUG
print("Did recive script message. Name:\(message.name). \n Body: \(message.body).")
#endif
guard let name = BLTJSMessageHandler.init(rawValue: message.name) else { return }
switch name {
case .success:
var transactionResponce: BLTTransactionResponse? = nil
if let jsonString = message.body as? String, let jsonData = jsonString.data(using: .utf8) {
transactionResponce = try? JSONDecoder().decode(BLTTransactionResponse.self, from: jsonData)
}
self.delegate?.paymentViewControllerPaymentDidSucceed(self, with: transactionResponce, transactionJsonBlob: message.body as! String)
case .close:
self.delegate?.paymentViewControllerDidClose(self)
case .error:
self.delegate?.paymentViewController(self, didEncounter: BLTPaymentViewControllerError.javaScriptError("\(message.body)"))
}
}
}
// MARK: - WKUIDelegate
extension BLTPaymentViewController: WKUIDelegate {
public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
let alertController = UIAlertController(title: frame.request.url?.absoluteString, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction.init(title: "OK", style: .default, handler: { (action) in
completionHandler()
}))
self.present(alertController, animated: true, completion: nil)
}
public func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
let alertController = UIAlertController(title: frame.request.url?.absoluteString, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction.init(title: "Cancel", style: .default, handler: { (action) in
completionHandler(false)
}))
alertController.addAction(UIAlertAction.init(title: "OK", style: .default, handler: { (action) in
completionHandler(true)
}))
self.present(alertController, animated: true, completion: nil)
}
}
// MARK: - WKNavigationDelegate
extension BLTPaymentViewController: WKNavigationDelegate {
public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
view.bringSubviewToFront(activityIndicatorView)
activityIndicatorView.startAnimating()
}
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
activityIndicatorView.stopAnimating()
}
public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
self.delegate?.paymentViewController(self, didEncounter: error)
activityIndicatorView.stopAnimating()
}
public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
self.delegate?.paymentViewController(self, didEncounter: error)
}
public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
if let response = navigationResponse.response as? HTTPURLResponse {
if response.statusCode >= 400 {
self.delegate?.paymentViewController(self, didEncounter: BLTPaymentViewControllerError.httpResponseError(statusCode: response.statusCode))
}
}
decisionHandler(.allow)
}
}
// MARK: - UIScrollViewDelegate
extension BLTPaymentViewController: UIScrollViewDelegate {
public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return nil
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment