Last active
January 4, 2019 09:41
-
-
Save IvanovDeveloper/f7b6caf097fdf73104e56e7a00109d2e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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