├── .gitignore
├── 3DSView
├── .gitignore
├── bintray-publish.gradle
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── eu
│ │ │ └── livotov
│ │ │ └── labs
│ │ │ └── android
│ │ │ └── d3s
│ │ │ ├── D3SRegexUtils.java
│ │ │ ├── D3SSViewAuthorizationListener.java
│ │ │ └── D3SView.java
│ └── res
│ │ └── layout
│ │ └── dialog_3ds.xml
│ └── test
│ └── java
│ └── eu
│ └── livotov
│ └── labs
│ └── android
│ └── d3s
│ └── D3SRegexUtilsTest.java
├── 3DSViewSample
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── eu
│ │ └── livotov
│ │ └── labs
│ │ └── android
│ │ └── d3s
│ │ └── sample
│ │ └── MainActivity.java
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ └── ic_launcher_background.xml
│ ├── layout
│ └── activity_main.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── values-night
│ └── themes.xml
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
├── LICENSE
├── README.md
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── maven.properties
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | .idea
4 | *.iml
5 | *.jpr
6 | .DS_Store
7 | /build
8 | maven.secret.properties
--------------------------------------------------------------------------------
/3DSView/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/3DSView/bintray-publish.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.github.dcendents.android-maven'
2 | apply plugin: 'com.jfrog.bintray'
3 |
4 | Properties properties = new Properties()
5 | Properties propertiesSecret = new Properties()
6 | propertiesSecret.load(project.rootProject.file('maven.secret.properties').newDataInputStream())
7 | properties.load(project.rootProject.file('maven.properties').newDataInputStream())
8 |
9 | group properties.getProperty("maven.group")
10 | version properties.getProperty("maven.version")
11 |
12 |
13 | install {
14 | repositories.mavenInstaller {
15 | // This generates POM.xml with proper parameters
16 | pom {
17 | project {
18 | packaging 'aar'
19 | groupId properties.getProperty("maven.group")
20 | artifactId properties.getProperty("maven.artifact")
21 | version properties.getProperty("maven.version")
22 | name properties.getProperty("maven.info")
23 | url properties.getProperty("maven.url.home")
24 |
25 | // Set your license
26 | licenses {
27 | license {
28 | name properties.getProperty("maven.license.name")
29 | url properties.getProperty("maven.license.url")
30 | }
31 | }
32 | developers {
33 | developer {
34 | id properties.getProperty("maven.developer.id")
35 | name properties.getProperty("maven.developer.name")
36 | email properties.getProperty("maven.developer.email")
37 | }
38 | }
39 | scm {
40 | connection properties.getProperty("maven.url.vcs")
41 | developerConnection properties.getProperty("maven.url.vcs")
42 | url properties.getProperty("maven.url.home")
43 |
44 | }
45 | }
46 | }
47 | }
48 | }
49 |
50 | bintray {
51 | user = propertiesSecret.getProperty("maven.bintray.user")
52 | key = propertiesSecret.getProperty("bintray.apikey")
53 |
54 | configurations = ['archives']
55 | pkg {
56 | repo = propertiesSecret.getProperty("maven.bintray.repo")
57 | name = properties.getProperty("maven.name")
58 | desc = properties.getProperty("maven.info")
59 | userOrg = propertiesSecret.getProperty("maven.bintray.org")
60 | websiteUrl = properties.getProperty("maven.url.home")
61 | vcsUrl = properties.getProperty("maven.url.vcs")
62 | issueTrackerUrl = properties.getProperty("maven.url.issues")
63 | licenses = ["Apache-2.0"]
64 | labels = ['orm', 'sqlite', 'android', 'aar']
65 | publish = true
66 | version {
67 | name = properties.getProperty("maven.version")
68 | desc = properties.getProperty("maven.info")
69 | released = new Date();
70 | vcsTag = properties.getProperty("maven.version.tag")
71 | gpg {
72 | sign = true
73 | passphrase = propertiesSecret.getProperty("gpg.secret.password")
74 | }
75 | }
76 | }
77 | }
78 |
79 | task sourcesJar(type: Jar) {
80 | from android.sourceSets.main.java.srcDirs
81 | classifier = 'sources'
82 | }
83 |
84 | task javadoc(type: Javadoc) {
85 | source = android.sourceSets.main.java.srcDirs
86 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
87 | failOnError false
88 | }
89 |
90 | task javadocJar(type: Jar, dependsOn: javadoc) {
91 | classifier = 'javadoc'
92 | from javadoc.destinationDir
93 | }
94 |
95 | artifacts {
96 | archives javadocJar
97 | archives sourcesJar
98 | }
--------------------------------------------------------------------------------
/3DSView/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 30
5 |
6 | defaultConfig {
7 | minSdkVersion 10
8 | targetSdkVersion 30
9 | versionCode 1
10 | versionName "3DSView"
11 | }
12 | buildTypes {
13 | release {
14 | minifyEnabled false
15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
16 | }
17 | }
18 | compileOptions {
19 | sourceCompatibility JavaVersion.VERSION_1_8
20 | targetCompatibility JavaVersion.VERSION_1_8
21 | }
22 | }
23 |
24 | dependencies {
25 | implementation 'androidx.annotation:annotation:1.1.0'
26 | testImplementation 'junit:junit:4.13.1'
27 | testImplementation 'com.google.truth:truth:1.1'
28 | }
--------------------------------------------------------------------------------
/3DSView/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/dlivotov/Developer/Android/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/3DSView/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
41 | * Note: If more than one MD is found in a page only the first will be returned. 42 | * 43 | * @param html String representation of the html page to search within. 44 | * @return MD or null if not found 45 | */ 46 | @Nullable 47 | public static String findMd(@NonNull String html) { 48 | if (html.trim().isEmpty()) return null; 49 | 50 | String md = null; 51 | Matcher paresMatcher = mdFinder.matcher(html); 52 | if (paresMatcher.find()) { 53 | md = paresMatcher.group(1); 54 | } 55 | 56 | return md; 57 | } 58 | 59 | /** 60 | * Finds the PaRes in an html page. 61 | *
62 | * Note: If more than one PaRes is found in a page only the first will be returned. 63 | * 64 | * @param html String representation of the html page to search within. 65 | * @return PaRes or null if not found 66 | */ 67 | @Nullable 68 | static String findPaRes(@NonNull String html) { 69 | if (html.trim().isEmpty()) return null; 70 | 71 | String paRes = null; 72 | Matcher paresMatcher = paresFinder.matcher(html); 73 | if (paresMatcher.find()) { 74 | paRes = paresMatcher.group(1); 75 | } 76 | 77 | return paRes; 78 | } 79 | 80 | /** 81 | * Finds the CRes in an html page. 82 | *
83 | * Note: If more than one CRes is found in a page only the first will be returned. 84 | * 85 | * @param html String representation of the html page to search within. 86 | * @return CRes or null if not found 87 | */ 88 | @Nullable 89 | public static String findCRes(@NonNull String html) { 90 | if (html.trim().isEmpty()) return null; 91 | 92 | String cRes = null; 93 | Matcher cresMatcher = cresFinder.matcher(html); 94 | if (cresMatcher.find()) { 95 | cRes = cresMatcher.group(1); 96 | } 97 | 98 | return cRes; 99 | } 100 | 101 | /** 102 | * Finds the threeDSSessionData in an html page. 103 | *
104 | * Note: If more than one threeDSSessionData is found in a page only the first will be returned. 105 | * 106 | * @param html String representation of the html page to search within. 107 | * @return threeDSSessionData or null if not found 108 | */ 109 | @Nullable 110 | public static String findThreeDSSessionData(@NonNull String html) { 111 | if (html.trim().isEmpty()) return null; 112 | 113 | String cRes = null; 114 | Matcher threeDSSessionDataMatcher = threeDSSessionDataFinder.matcher(html); 115 | if (threeDSSessionDataMatcher.find()) { 116 | cRes = threeDSSessionDataMatcher.group(1); 117 | } 118 | 119 | return cRes; 120 | } 121 | } -------------------------------------------------------------------------------- /3DSView/src/main/java/eu/livotov/labs/android/d3s/D3SSViewAuthorizationListener.java: -------------------------------------------------------------------------------- 1 | package eu.livotov.labs.android.d3s; 2 | 3 | /** 4 | * (c) Livotov Labs Ltd. 2013 5 | * Alex Askerov, Dmitri Livotov 6 | *
7 | * Date: 20/09/2013 8 | * 9 | * Callback interface to receive authorization events 10 | */ 11 | public interface D3SSViewAuthorizationListener 12 | { 13 | 14 | /** 15 | * Called when remote banking ACS server finishes 3DS authorization. Now you may pass the returned 16 | * MD and PaRes parameters to your credit card processing gateway for finalizing the transaction. 17 | * 18 | * This is called when 3-D Secure v1 completes 19 | * 20 | * @param md MD parameter, sent by ACS server 21 | * @param paRes paRes parameter, sent by ACS server 22 | */ 23 | @Deprecated // 3-D Secure v1 ... this will disappear by the end of 2020 24 | void onAuthorizationCompleted(final String md, final String paRes); 25 | 26 | /** 27 | * Called when remote banking ACS server finishes 3DS authorization. You must 28 | * pass the CRes value to the payment processing gateway to complete the transaction. 29 | * 30 | * This is called when 3-D Secure v2 completes 31 | * 32 | * @param cres 33 | * @param threeDSSessionData - session data from request that's been reflected back in the callback 34 | */ 35 | void onAuthorizationCompleted3dsV2(final String cres, final String threeDSSessionData); 36 | 37 | /** 38 | * Called when authorization process is started and web page from ACS server is being loaded. 39 | * For isntace, you may display progress now, etc... 40 | * 41 | * @param view reference for the DDDSView instance 42 | */ 43 | void onAuthorizationStarted(D3SView view); 44 | 45 | /** 46 | * Called to update the ACS web page loading progress. 47 | * 48 | * @param progress current loading progress from 0 to 100. 49 | */ 50 | void onAuthorizationWebPageLoadingProgressChanged(int progress); 51 | 52 | /** 53 | * Called if a loading error occurs 54 | * 55 | * @param errorCode 56 | * @param description 57 | * @param failingUrl 58 | */ 59 | void onAuthorizationWebPageLoadingError(int errorCode, String description, String failingUrl); 60 | 61 | } 62 | -------------------------------------------------------------------------------- /3DSView/src/main/java/eu/livotov/labs/android/d3s/D3SView.java: -------------------------------------------------------------------------------- 1 | package eu.livotov.labs.android.d3s; 2 | 3 | import android.content.Context; 4 | import android.text.TextUtils; 5 | import android.util.AttributeSet; 6 | import android.webkit.WebChromeClient; 7 | import android.webkit.WebResourceResponse; 8 | import android.webkit.WebView; 9 | import android.webkit.WebViewClient; 10 | 11 | import java.io.UnsupportedEncodingException; 12 | import java.net.URLEncoder; 13 | import java.util.Locale; 14 | import java.util.concurrent.atomic.AtomicBoolean; 15 | 16 | 17 | /** 18 | * (c) Livotov Labs Ltd. 2013 19 | * Alex Askerov, Dmitri Livotov 20 | * 21 | * Date: 20/09/2013 22 | * 23 | *This is the 3DSecure WebView component. It can be used to perform 3D-Secure authorizations when processing internet 26 | * payments in apps. The technology is also named Verified By Visa and MasterCard Secure Code.
27 | * 28 | *The main idea is to route cardholder to the card issuer financial institution where cardholder will be required to 29 | * answer extra security question or enter one-time sms or token code in order to confirm the card transaction.
30 | * 31 | *Add DDDSView to your layout, set custom (if required) postback url and authorization results listener via the 34 | * corresponding setters. Then invoke the authorize(...) method to start 3D-Secure authorization. You will need to 35 | * provide payment data, which will be issued by your card processor in attempt to make a transaction with the 3DS-capable 36 | * credit card.
37 | * 38 | *Once user completes the authorization, the authorization listener's onAuthorizationCompleted() method will be called 39 | * with the parameters, came from banking ACS server. Now you can use those parameters in your bckend processing server 40 | * to finalize the payment.
41 | */ 42 | public class D3SView extends WebView { 43 | 44 | /** 45 | * Namespace for JS bridge 46 | */ 47 | private static String JavaScriptNS = "D3SJS"; 48 | 49 | /** 50 | * Url that will be used by ACS server for posting result data on authorization completion. We will be monitoring 51 | * this URL in WebView handler to intercept its loading and grabbing the resulting data from POST message instead. 52 | */ 53 | private String postbackUrl = "https://www.google.com"; 54 | 55 | private AtomicBoolean postbackHandled = new AtomicBoolean(false); 56 | 57 | /** 58 | * 3-D Secure v2 (AKA Strong Customer Authentication). 59 | * This is an indicator as to whether the payment is 3-D Secure v1 or v2 is being performed, so the library has a 60 | * hint to know what field(s) to search for (and avoid unnecessary regular expressions) 61 | */ 62 | private boolean is3dsV2; 63 | 64 | /** 65 | * Callback to send authorization events to 66 | */ 67 | private D3SSViewAuthorizationListener authorizationListener = null; 68 | 69 | 70 | public D3SView(final Context context) { 71 | super(context); 72 | initUI(); 73 | } 74 | 75 | private void initUI() { 76 | getSettings().setJavaScriptEnabled(true); 77 | getSettings().setBuiltInZoomControls(true); 78 | addJavascriptInterface(new D3SJSInterface(), JavaScriptNS); 79 | 80 | setWebViewClient(new WebViewClient() { 81 | 82 | @Override 83 | public WebResourceResponse shouldInterceptRequest(WebView view, String url) { 84 | if (isPostbackUrl(url)) { 85 | // Wait for the form data to be processed in the other thread. 86 | // 1.5s should be more than enough 87 | // 88 | // If for whatever reason the form data isn't captured successfully, this carries on and posts to 89 | // the callback URL (AKA postback URL) 90 | try { 91 | Thread.sleep(1500); 92 | } catch (InterruptedException e) { 93 | // Ignore 94 | } 95 | } 96 | return null; 97 | } 98 | 99 | /* 100 | * In this lifecycle hook the HTML is available, although all resources (CSS, Images etc) may not be 101 | * We are merely processing the HTML though, so hooking in here is fine 102 | */ 103 | @Override 104 | public void onPageCommitVisible(WebView view, String url) { 105 | 106 | if (!isPostbackUrl(url)) { 107 | view.loadUrl(String.format("javascript:window.%s.processHTML(document.getElementsByTagName('html')[0].innerHTML);", JavaScriptNS)); 108 | } 109 | 110 | super.onPageCommitVisible(view, url); 111 | } 112 | 113 | // 114 | public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { 115 | if (!isPostbackUrl(failingUrl)) { 116 | authorizationListener.onAuthorizationWebPageLoadingError(errorCode, description, failingUrl); 117 | } 118 | } 119 | 120 | private boolean isPostbackUrl(String url) { 121 | return url.toLowerCase().startsWith(postbackUrl.toLowerCase()); 122 | } 123 | 124 | }); 125 | 126 | setWebChromeClient(new WebChromeClient() { 127 | 128 | public void onProgressChanged(WebView view, int newProgress) { 129 | if (authorizationListener != null) { 130 | authorizationListener.onAuthorizationWebPageLoadingProgressChanged(newProgress); 131 | } 132 | } 133 | }); 134 | } 135 | 136 | public D3SView(final Context context, final AttributeSet attrs) { 137 | super(context, attrs); 138 | initUI(); 139 | } 140 | 141 | public D3SView(final Context context, final AttributeSet attrs, final int defStyle) { 142 | super(context, attrs, defStyle); 143 | initUI(); 144 | } 145 | 146 | public D3SView(final Context context, final AttributeSet attrs, final int defStyle, final boolean privateBrowsing) { 147 | super(context, attrs, defStyle); 148 | initUI(); 149 | } 150 | 151 | private void completeAuthorizationIfPossible(final String html) { 152 | 153 | // Process HTML in a thread to improve performance 154 | Runnable runnable = () -> { 155 | // If the postback has already been handled, stop now 156 | if (postbackHandled.get()) { 157 | return; 158 | } 159 | 160 | if (is3dsV2) { 161 | match3DSV2Parameters(html); 162 | } else { 163 | match3DSV1Parameters(html); 164 | } 165 | 166 | }; 167 | Thread thread = new Thread(runnable); 168 | thread.start(); 169 | 170 | } 171 | 172 | private void match3DSV2Parameters(String html) { 173 | // Try and find the CRes and threeDSSessionData form elements in the supplied html 174 | final String cRes = D3SRegexUtils.findCRes(html); 175 | if (cRes == null) return; 176 | 177 | final String threeDSSessionData = D3SRegexUtils.findThreeDSSessionData(html); 178 | if (threeDSSessionData == null) return; 179 | 180 | // If we get to this point, we've definitely got values for both the CRes and threeDSSessionData 181 | 182 | // The postbackHandled check is just to ensure we've not already called back. 183 | // We don't want onAuthorizationCompleted to be called twice. 184 | if (postbackHandled.compareAndSet(false, true) && authorizationListener != null) { 185 | authorizationListener.onAuthorizationCompleted3dsV2(cRes, threeDSSessionData); 186 | } 187 | } 188 | 189 | private void match3DSV1Parameters(String html) { 190 | // Try and find the MD and PaRes form elements in the supplied html 191 | final String md = D3SRegexUtils.findMd(html); 192 | if (md == null) return; 193 | 194 | final String paRes = D3SRegexUtils.findPaRes(html); 195 | if (paRes == null) return; 196 | 197 | // If we get to this point, we've definitely got values for both the MD and PaRes 198 | 199 | // The postbackHandled check is just to ensure we've not already called back. 200 | // We don't want onAuthorizationCompleted to be called twice. 201 | if (postbackHandled.compareAndSet(false, true) && authorizationListener != null) { 202 | authorizationListener.onAuthorizationCompleted(md, paRes); 203 | } 204 | } 205 | 206 | /** 207 | * Sets the callback to receive authorization events 208 | * 209 | * @param authorizationListener 210 | */ 211 | public void setAuthorizationListener(final D3SSViewAuthorizationListener authorizationListener) { 212 | this.authorizationListener = authorizationListener; 213 | } 214 | 215 | /** 216 | * Starts 3DS v1 authorization 217 | * 218 | * @param acsUrl ACS server url, returned by the credit card processing gateway 219 | * @param md MD parameter, returned by the credit card processing gateway 220 | * @param paReq PaReq parameter, returned by the credit card processing gateway 221 | */ 222 | public void authorize(final String acsUrl, final String md, final String paReq) { 223 | authorize(acsUrl, null, md, paReq, null, null); 224 | } 225 | 226 | /** 227 | * Starts 3-D Secure v2 authentication 228 | * 229 | * @param acsUrl ACS server url - supplied by the payment gateway 230 | * @param creq - CReq to post to the ACS 231 | * @param threeDSSessionData - Session data to pass to the ACS. This will be reflected back in the callback 232 | * @param postbackUrl - the URL to wait for, so the CRes can be extracted 233 | */ 234 | public void authorize(final String acsUrl, final String creq, final String threeDSSessionData, final String postbackUrl) { 235 | authorize(acsUrl, creq, null, null, threeDSSessionData, postbackUrl); 236 | } 237 | 238 | /** 239 | * Starts 3DS authorization 240 | * 241 | * @param acsUrl ACS server url, returned by the credit card processing gateway 242 | * @param creq CReq parameter (replaces MD and PaReq). 243 | * @param md MD parameter, returned by the credit card processing gateway 244 | * @param paReq PaReq parameter, returned by the credit card processing gateway 245 | * @param postbackUrl custom postback url for intercepting ACS server result posting. You may use any url you like 246 | * here, if you need, even non existing ones. 247 | */ 248 | public void authorize(final String acsUrl, final String creq, final String md, final String paReq, final String threeDSSessionData, final String postbackUrl) { 249 | postbackHandled.set(false); 250 | 251 | if (authorizationListener != null) { 252 | authorizationListener.onAuthorizationStarted(this); 253 | } 254 | 255 | if (!TextUtils.isEmpty(postbackUrl)) { 256 | this.postbackUrl = postbackUrl; 257 | } 258 | 259 | String postParams; 260 | try { 261 | if (creq != null) { 262 | // 3-D Secure v2 263 | is3dsV2 = true; 264 | postParams = String.format(Locale.US, "creq=%1$s&threeDSSessionData=%2$s", URLEncoder.encode(creq, "UTF-8"), URLEncoder.encode(threeDSSessionData, "UTF-8")); 265 | } else { 266 | // 3-D Secure v1 267 | postParams = String.format(Locale.US, "MD=%1$s&TermUrl=%2$s&PaReq=%3$s", URLEncoder.encode(md, "UTF-8"), URLEncoder.encode(this.postbackUrl, "UTF-8"), URLEncoder.encode(paReq, "UTF-8")); 268 | } 269 | } catch (UnsupportedEncodingException e) { 270 | throw new RuntimeException(e); 271 | } 272 | 273 | postUrl(acsUrl, postParams.getBytes()); 274 | } 275 | 276 | class D3SJSInterface { 277 | 278 | D3SJSInterface() { 279 | } 280 | 281 | @android.webkit.JavascriptInterface 282 | public void processHTML(final String html) { 283 | completeAuthorizationIfPossible(html); 284 | } 285 | } 286 | } -------------------------------------------------------------------------------- /3DSView/src/main/res/layout/dialog_3ds.xml: -------------------------------------------------------------------------------- 1 |