Skip to content

Commit

Permalink
README, Podspec and code
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisleversuch committed Apr 12, 2018
1 parent 60035aa commit bb3d3fa
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 0 deletions.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# ThreeDSecureView

ThreeDSecureView is primarily a WKWebView that handles the 3DSecure payment process by sending a POST request to the provided card issuer URL with the MD and PaReq parameters set. The WKWebView then intercepts the POST response from the card issuer, extracts the MD and PaRes values and passes them back to your app.

## Requirements

- iOS 9.0+
- Swift 4.0

## Installation

### CocoaPods

pod 'ThreeDSecureView', '~> 1.0.0'

## Usage

### Easy

The easiest way to use ThreeDSecureView is to instantiate ThreeDSecureViewController and present it in a UINavigationController.

let config = ThreeDSecureConfig(md: "YOUR MD", paReq: "YOUR PAREQ", cardUrl: "YOUR CARD URL")
let viewController = ThreeDSecureViewController(config: config)
viewController.delegate = self // Implement ThreeDSecureViewDelegate

let navController = UINavigationController(rootViewController: viewController)
present(navController, animated: true, completion: nil)

Handle the callbacks:

extension YourViewController: ThreeDSecureViewDelegate {

func threeDSecure(view: ThreeDSecureView, md: String, paRes: String) {
// Handle success here
}

func threeDSecure(view: ThreeDSecureView, error: Error) {
// Handle errors here
}

}

### Advanced

If you don't want to use the provided UIViewController, you can just instantiate ThreeDSecureView directly, add it to your view hierarchy and call start3DSecure() yourself.
27 changes: 27 additions & 0 deletions Source/ThreeDSecureConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2018 Brightec Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

public struct ThreeDSecureConfig {
let md: String
let paReq: String
let cardUrl: URL

public init(md: String, paReq: String, cardUrl: URL) {
self.md = md
self.paReq = paReq
self.cardUrl = cardUrl
}
}
176 changes: 176 additions & 0 deletions Source/ThreeDSecureView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright 2018 Brightec Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation
import WebKit

public protocol ThreeDSecureViewDelegate: class {
func threeDSecure(view: ThreeDSecureView, md: String, paRes: String)
func threeDSecure(view: ThreeDSecureView, error: Error)
}

public class ThreeDSecureView: UIView {

/// The webView that loads the 3DSecure URL
private var webView: WKWebView!

/// Delegate for passing back the 3DSecure result
public weak var delegate: ThreeDSecureViewDelegate?

public override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}

required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}

/// Initialises the webView and adds it to the view hierarchy
private func commonInit() {
let webView = WKWebView(frame: .zero)
webView.navigationDelegate = self

addSubview(webView)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
webView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
webView.topAnchor.constraint(equalTo: topAnchor).isActive = true
webView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true

self.webView = webView
}

/// Starts the 3DSecure process by loading the provided URL with the md/paRes parameters
///
/// - Parameter config: The config required to start the 3DSecure process
public func start3DSecure(config: ThreeDSecureConfig) {
var charSet = CharacterSet.urlHostAllowed
charSet.remove("+")
charSet.remove("&")

guard let mdEncoded = config.md.addingPercentEncoding(withAllowedCharacters: charSet),
let urlEncoded = "https://www.google.com".addingPercentEncoding(withAllowedCharacters: charSet),
let paReqEncoded = config.paReq.addingPercentEncoding(withAllowedCharacters: charSet) else {
return
}

var request = URLRequest(url: config.cardUrl)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpBody = "MD=\(mdEncoded)&TermUrl=\(urlEncoded)&PaReq=\(paReqEncoded)".data(using: .utf8)
webView.load(request)
}

}

// MARK: - WKNavigationDelegate
extension ThreeDSecureView: WKNavigationDelegate {

public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse,
decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
if let host = webView.url?.host, host == "www.google.com" {
let javaScript = "function getHTML() { return document.getElementsByTagName('html')[0].innerHTML; } getHTML();"
webView.evaluateJavaScript(javaScript) { (result, error) in
if let error = error {
self.delegate?.threeDSecure(view: self, error: error)
} else if let result = result as? String {
guard let md = self.getMd(html: result), let mdValue = self.getValue(input: md),
let paRes = self.getPaRes(html: result), let paResValue = self.getValue(input: paRes) else {
return
}
self.delegate?.threeDSecure(view: self, md: mdValue, paRes: paResValue)
}
}
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
}

public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
// Ignore errors relating to us cancelling the request to the callback URL
if let errorUrl = (error as NSError).userInfo["NSErrorFailingURLKey"] as? URL,
errorUrl.host == "www.google.com" {
return
}
delegate?.threeDSecure(view: self, error: error)
}

public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
delegate?.threeDSecure(view: self, error: error)
}

}

// MARK: - Helpers
extension ThreeDSecureView {

/// Gets the HTML input tag with a name of MD from the provided HTML
///
/// - Parameter html: The HTML to search
/// - Returns: The input tag, or nil if not found
private func getMd(html: String) -> String? {
guard let regEx = try? NSRegularExpression(pattern: ".*?(<input[^<>]* name=\"MD\"[^<>]*>).*?") else {
return nil
}

return getFirstSubGroup(string: html, regEx: regEx)
}

/// Gets the HTML input tag with a name of PaRes from the provided HTML
///
/// - Parameter html: The HTML to search
/// - Returns: The input tag, or nil if not found
private func getPaRes(html: String) -> String? {
guard let regEx = try? NSRegularExpression(pattern: ".*?(<input[^<>]* name=\"PaRes\"[^<>]*>).*?") else {
return nil
}

return getFirstSubGroup(string: html, regEx: regEx)
}

/// Gets the value attribute from the provided input tag
///
/// - Parameter input: The input tag to search
/// - Returns: The found value, or nil
private func getValue(input: String) -> String? {
guard let regEx = try? NSRegularExpression(pattern: ".*? value=\"(.*?)\"") else {
return nil
}

return getFirstSubGroup(string: input, regEx: regEx)
}

/// Gets the first sub group from the provided string using the provided regular expression
///
/// - Parameters:
/// - string: The string to search
/// - regEx: The regular expression to evaluate
/// - Returns: The first sub group, or nil
private func getFirstSubGroup(string: String, regEx: NSRegularExpression) -> String? {
guard let match = regEx.firstMatch(in: string, options: [], range: NSRange(string.startIndex..., in: string)) else {
return nil
}

let matchSubGroup = match.range(at: 1)
guard let range = Range(matchSubGroup, in: string) else {
return nil
}

return String(string[range])
}

}
45 changes: 45 additions & 0 deletions Source/ThreeDSecureViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2018 Brightec Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import UIKit

public class ThreeDSecureViewController: UIViewController {

private var config: ThreeDSecureConfig?
public var delegate: ThreeDSecureViewDelegate?

public convenience init(config: ThreeDSecureConfig) {
self.init(nibName: nil, bundle: nil)
self.config = config
}

override public func viewDidLoad() {
super.viewDidLoad()

let threeDSecureView = ThreeDSecureView(frame: .zero)
threeDSecureView.delegate = delegate

view.addSubview(threeDSecureView)
threeDSecureView.translatesAutoresizingMaskIntoConstraints = false
threeDSecureView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
threeDSecureView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
threeDSecureView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
threeDSecureView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

if let config = config {
threeDSecureView.start3DSecure(config: config)
}
}

}
21 changes: 21 additions & 0 deletions ThreeDSecureView.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Pod::Spec.new do |s|

s.name = "ThreeDSecureView"
s.version = "1.0.0"
s.summary = "ThreeDSecureView allows you to handle the 3DSecure payment process in your iOS app."
s.description = <<-DESC
ThreeDSecureView is primarily a WKWebView that handles the 3DSecure payment process by sending a POST request to the provided
card issuer URL with the MD and PaReq parameters set. The WKWebView then intercepts the POST response from the card issuer,
extracts the MD and PaRes values and passes them back to your app.
DESC

s.homepage = "https://github.com/brightec/3DSecureView"
s.license = "Apache License, Version 2.0"
s.author = { "Chris Leversuch" => "[email protected]" }
s.platform = :ios, "9.0"
s.source = { :git => "https://github.com/brightec/3DSecureView.git", :tag => "#{s.version}" }
s.source_files = "Source"
s.requires_arc = true
s.swift_version = "4.0"

end

0 comments on commit bb3d3fa

Please sign in to comment.