Skip to content

Commit bb3d3fa

Browse files
README, Podspec and code
1 parent 60035aa commit bb3d3fa

File tree

5 files changed

+314
-0
lines changed

5 files changed

+314
-0
lines changed

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# ThreeDSecureView
2+
3+
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.
4+
5+
## Requirements
6+
7+
- iOS 9.0+
8+
- Swift 4.0
9+
10+
## Installation
11+
12+
### CocoaPods
13+
14+
pod 'ThreeDSecureView', '~> 1.0.0'
15+
16+
## Usage
17+
18+
### Easy
19+
20+
The easiest way to use ThreeDSecureView is to instantiate ThreeDSecureViewController and present it in a UINavigationController.
21+
22+
let config = ThreeDSecureConfig(md: "YOUR MD", paReq: "YOUR PAREQ", cardUrl: "YOUR CARD URL")
23+
let viewController = ThreeDSecureViewController(config: config)
24+
viewController.delegate = self // Implement ThreeDSecureViewDelegate
25+
26+
let navController = UINavigationController(rootViewController: viewController)
27+
present(navController, animated: true, completion: nil)
28+
29+
Handle the callbacks:
30+
31+
extension YourViewController: ThreeDSecureViewDelegate {
32+
33+
func threeDSecure(view: ThreeDSecureView, md: String, paRes: String) {
34+
// Handle success here
35+
}
36+
37+
func threeDSecure(view: ThreeDSecureView, error: Error) {
38+
// Handle errors here
39+
}
40+
41+
}
42+
43+
### Advanced
44+
45+
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.

Source/ThreeDSecureConfig.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2018 Brightec Ltd
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
public struct ThreeDSecureConfig {
18+
let md: String
19+
let paReq: String
20+
let cardUrl: URL
21+
22+
public init(md: String, paReq: String, cardUrl: URL) {
23+
self.md = md
24+
self.paReq = paReq
25+
self.cardUrl = cardUrl
26+
}
27+
}

Source/ThreeDSecureView.swift

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright 2018 Brightec Ltd
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
import WebKit
17+
18+
public protocol ThreeDSecureViewDelegate: class {
19+
func threeDSecure(view: ThreeDSecureView, md: String, paRes: String)
20+
func threeDSecure(view: ThreeDSecureView, error: Error)
21+
}
22+
23+
public class ThreeDSecureView: UIView {
24+
25+
/// The webView that loads the 3DSecure URL
26+
private var webView: WKWebView!
27+
28+
/// Delegate for passing back the 3DSecure result
29+
public weak var delegate: ThreeDSecureViewDelegate?
30+
31+
public override init(frame: CGRect) {
32+
super.init(frame: frame)
33+
commonInit()
34+
}
35+
36+
required public init?(coder aDecoder: NSCoder) {
37+
super.init(coder: aDecoder)
38+
commonInit()
39+
}
40+
41+
/// Initialises the webView and adds it to the view hierarchy
42+
private func commonInit() {
43+
let webView = WKWebView(frame: .zero)
44+
webView.navigationDelegate = self
45+
46+
addSubview(webView)
47+
webView.translatesAutoresizingMaskIntoConstraints = false
48+
webView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
49+
webView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
50+
webView.topAnchor.constraint(equalTo: topAnchor).isActive = true
51+
webView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
52+
53+
self.webView = webView
54+
}
55+
56+
/// Starts the 3DSecure process by loading the provided URL with the md/paRes parameters
57+
///
58+
/// - Parameter config: The config required to start the 3DSecure process
59+
public func start3DSecure(config: ThreeDSecureConfig) {
60+
var charSet = CharacterSet.urlHostAllowed
61+
charSet.remove("+")
62+
charSet.remove("&")
63+
64+
guard let mdEncoded = config.md.addingPercentEncoding(withAllowedCharacters: charSet),
65+
let urlEncoded = "https://www.google.com".addingPercentEncoding(withAllowedCharacters: charSet),
66+
let paReqEncoded = config.paReq.addingPercentEncoding(withAllowedCharacters: charSet) else {
67+
return
68+
}
69+
70+
var request = URLRequest(url: config.cardUrl)
71+
request.httpMethod = "POST"
72+
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
73+
request.httpBody = "MD=\(mdEncoded)&TermUrl=\(urlEncoded)&PaReq=\(paReqEncoded)".data(using: .utf8)
74+
webView.load(request)
75+
}
76+
77+
}
78+
79+
// MARK: - WKNavigationDelegate
80+
extension ThreeDSecureView: WKNavigationDelegate {
81+
82+
public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse,
83+
decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
84+
if let host = webView.url?.host, host == "www.google.com" {
85+
let javaScript = "function getHTML() { return document.getElementsByTagName('html')[0].innerHTML; } getHTML();"
86+
webView.evaluateJavaScript(javaScript) { (result, error) in
87+
if let error = error {
88+
self.delegate?.threeDSecure(view: self, error: error)
89+
} else if let result = result as? String {
90+
guard let md = self.getMd(html: result), let mdValue = self.getValue(input: md),
91+
let paRes = self.getPaRes(html: result), let paResValue = self.getValue(input: paRes) else {
92+
return
93+
}
94+
self.delegate?.threeDSecure(view: self, md: mdValue, paRes: paResValue)
95+
}
96+
}
97+
decisionHandler(.cancel)
98+
} else {
99+
decisionHandler(.allow)
100+
}
101+
}
102+
103+
public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
104+
// Ignore errors relating to us cancelling the request to the callback URL
105+
if let errorUrl = (error as NSError).userInfo["NSErrorFailingURLKey"] as? URL,
106+
errorUrl.host == "www.google.com" {
107+
return
108+
}
109+
delegate?.threeDSecure(view: self, error: error)
110+
}
111+
112+
public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
113+
delegate?.threeDSecure(view: self, error: error)
114+
}
115+
116+
}
117+
118+
// MARK: - Helpers
119+
extension ThreeDSecureView {
120+
121+
/// Gets the HTML input tag with a name of MD from the provided HTML
122+
///
123+
/// - Parameter html: The HTML to search
124+
/// - Returns: The input tag, or nil if not found
125+
private func getMd(html: String) -> String? {
126+
guard let regEx = try? NSRegularExpression(pattern: ".*?(<input[^<>]* name=\"MD\"[^<>]*>).*?") else {
127+
return nil
128+
}
129+
130+
return getFirstSubGroup(string: html, regEx: regEx)
131+
}
132+
133+
/// Gets the HTML input tag with a name of PaRes from the provided HTML
134+
///
135+
/// - Parameter html: The HTML to search
136+
/// - Returns: The input tag, or nil if not found
137+
private func getPaRes(html: String) -> String? {
138+
guard let regEx = try? NSRegularExpression(pattern: ".*?(<input[^<>]* name=\"PaRes\"[^<>]*>).*?") else {
139+
return nil
140+
}
141+
142+
return getFirstSubGroup(string: html, regEx: regEx)
143+
}
144+
145+
/// Gets the value attribute from the provided input tag
146+
///
147+
/// - Parameter input: The input tag to search
148+
/// - Returns: The found value, or nil
149+
private func getValue(input: String) -> String? {
150+
guard let regEx = try? NSRegularExpression(pattern: ".*? value=\"(.*?)\"") else {
151+
return nil
152+
}
153+
154+
return getFirstSubGroup(string: input, regEx: regEx)
155+
}
156+
157+
/// Gets the first sub group from the provided string using the provided regular expression
158+
///
159+
/// - Parameters:
160+
/// - string: The string to search
161+
/// - regEx: The regular expression to evaluate
162+
/// - Returns: The first sub group, or nil
163+
private func getFirstSubGroup(string: String, regEx: NSRegularExpression) -> String? {
164+
guard let match = regEx.firstMatch(in: string, options: [], range: NSRange(string.startIndex..., in: string)) else {
165+
return nil
166+
}
167+
168+
let matchSubGroup = match.range(at: 1)
169+
guard let range = Range(matchSubGroup, in: string) else {
170+
return nil
171+
}
172+
173+
return String(string[range])
174+
}
175+
176+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2018 Brightec Ltd
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import UIKit
16+
17+
public class ThreeDSecureViewController: UIViewController {
18+
19+
private var config: ThreeDSecureConfig?
20+
public var delegate: ThreeDSecureViewDelegate?
21+
22+
public convenience init(config: ThreeDSecureConfig) {
23+
self.init(nibName: nil, bundle: nil)
24+
self.config = config
25+
}
26+
27+
override public func viewDidLoad() {
28+
super.viewDidLoad()
29+
30+
let threeDSecureView = ThreeDSecureView(frame: .zero)
31+
threeDSecureView.delegate = delegate
32+
33+
view.addSubview(threeDSecureView)
34+
threeDSecureView.translatesAutoresizingMaskIntoConstraints = false
35+
threeDSecureView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
36+
threeDSecureView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
37+
threeDSecureView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
38+
threeDSecureView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
39+
40+
if let config = config {
41+
threeDSecureView.start3DSecure(config: config)
42+
}
43+
}
44+
45+
}

ThreeDSecureView.podspec

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Pod::Spec.new do |s|
2+
3+
s.name = "ThreeDSecureView"
4+
s.version = "1.0.0"
5+
s.summary = "ThreeDSecureView allows you to handle the 3DSecure payment process in your iOS app."
6+
s.description = <<-DESC
7+
ThreeDSecureView is primarily a WKWebView that handles the 3DSecure payment process by sending a POST request to the provided
8+
card issuer URL with the MD and PaReq parameters set. The WKWebView then intercepts the POST response from the card issuer,
9+
extracts the MD and PaRes values and passes them back to your app.
10+
DESC
11+
12+
s.homepage = "https://github.com/brightec/3DSecureView"
13+
s.license = "Apache License, Version 2.0"
14+
s.author = { "Chris Leversuch" => "chris@brightec.co.uk" }
15+
s.platform = :ios, "9.0"
16+
s.source = { :git => "https://github.com/brightec/3DSecureView.git", :tag => "#{s.version}" }
17+
s.source_files = "Source"
18+
s.requires_arc = true
19+
s.swift_version = "4.0"
20+
21+
end

0 commit comments

Comments
 (0)