├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── RELEASE_NOTES.md ├── build.gradle ├── gateway-android ├── .gitignore ├── build.gradle ├── proguard.pro └── src │ ├── debug │ └── java │ │ └── com │ │ └── mastercard │ │ └── gateway │ │ └── android │ │ └── sdk │ │ └── BaseLogger.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── mastercard │ │ │ └── gateway │ │ │ └── android │ │ │ └── sdk │ │ │ ├── Gateway.java │ │ │ ├── Gateway3DSecureActivity.java │ │ │ ├── Gateway3DSecureCallback.java │ │ │ ├── GatewayCallback.java │ │ │ ├── GatewayException.java │ │ │ ├── GatewayGooglePayCallback.java │ │ │ ├── GatewayMap.java │ │ │ ├── GatewayRequest.java │ │ │ ├── GatewaySSLContextProvider.java │ │ │ ├── GatewayTLSSocketFactory.java │ │ │ └── Logger.java │ └── res │ │ ├── layout │ │ └── activity_3dsecure.xml │ │ └── values │ │ └── strings.xml │ ├── release │ └── java │ │ └── com │ │ └── mastercard │ │ └── gateway │ │ └── android │ │ └── sdk │ │ └── BaseLogger.java │ └── test │ ├── java │ └── com │ │ └── mastercard │ │ └── gateway │ │ └── android │ │ └── sdk │ │ ├── Gateway3DSecureActivityTest.java │ │ ├── GatewayMapTest.java │ │ ├── GatewayTest.java │ │ ├── TestApplication.java │ │ └── TestGatewaySSLContextProvider.java │ └── resources │ ├── mockito-extensions │ └── org.mockito.plugins.MockMaker │ └── robolectric.properties ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── payment-flow.png ├── sample-configuration.png ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── mastercard │ │ └── gateway │ │ └── android │ │ └── sampleapp │ │ ├── ApiController.java │ │ ├── CollectCardInfoActivity.java │ │ ├── Config.java │ │ ├── MainActivity.java │ │ ├── ProcessPaymentActivity.java │ │ └── TLSSocketFactory.java │ └── res │ ├── drawable-hdpi │ ├── googlepay_button_background_image.9.png │ └── googlepay_button_no_shadow_background_image.9.png │ ├── drawable-mdpi │ ├── googlepay_button_background_image.9.png │ └── googlepay_button_no_shadow_background_image.9.png │ ├── drawable-v21 │ ├── googlepay_button_background.xml │ └── googlepay_button_no_shadow_background.xml │ ├── drawable-xhdpi │ ├── googlepay_button_background_image.9.png │ └── googlepay_button_no_shadow_background_image.9.png │ ├── drawable-xxhdpi │ ├── googlepay_button_background_image.9.png │ └── googlepay_button_no_shadow_background_image.9.png │ ├── drawable-xxxhdpi │ ├── failed.png │ ├── googlepay_button_background_image.9.png │ ├── googlepay_button_no_shadow_background_image.9.png │ └── success.png │ ├── drawable │ ├── button_bg.xml │ ├── googlepay_button_background.xml │ ├── googlepay_button_content.xml │ ├── googlepay_button_overlay.xml │ ├── ic_check_green_24dp.xml │ └── ic_error_red_24dp.xml │ ├── layout │ ├── activity_collect_card_info.xml │ ├── activity_main.xml │ ├── activity_process_payment.xml │ └── googlepay_button.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ └── values │ ├── colors.xml │ ├── googlepay_strings.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | local.properties 4 | .idea 5 | .DS_Store 6 | build 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | android: 4 | components: 5 | - tools 6 | - platform-tools 7 | - tools 8 | - build-tools-28.0.3 9 | - android-29 10 | - extra-google-m2repository 11 | - extra-android-m2repository 12 | 13 | # workaround for license accepting issue 14 | before_install: 15 | - yes | sdkmanager "platforms;android-29" 16 | 17 | # dependency caching 18 | before_cache: 19 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 20 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 21 | cache: 22 | directories: 23 | - $HOME/.m2 24 | - $HOME/.gradle/caches/ 25 | - $HOME/.gradle/wrapper/ 26 | 27 | script: travis_retry ./gradlew clean --refresh-dependencies gateway-android:lintRelease gateway-android:testReleaseUnitTest gateway-android:assembleRelease gateway-android:androidSourcesJar gateway-android:androidJavadocsJar gateway-android:generatePomFileForAarPublication 28 | 29 | deploy: 30 | provider: script 31 | skip_cleanup: true 32 | script: ./gradlew gateway-android:bintrayUpload 33 | on: 34 | tags: true 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gateway Android SDK 2 | **This Mobile SDK supports 3-D Secure 1 only.** If you require EMV 3DS support, please obtain the version 2 Mobile SDK by following these instructions: https://na.gateway.mastercard.com/api/documentation/integrationGuidelines/mobileSDK/emv3DSsdk.html 3 | 4 | [![Download](https://api.bintray.com/packages/mpgs/Android/gateway-android-sdk/images/download.svg)](https://bintray.com/mpgs/Android/gateway-android-sdk/_latestVersion) 5 | [![Build Status](https://travis-ci.org/Mastercard-Gateway/gateway-android-sdk.svg?branch=master)](https://travis-ci.org/Mastercard-Gateway/gateway-android-sdk) 6 | 7 | Our Android SDK allows you to easily integrate payments into your Android app. By updating a session directly with the Gateway, you avoid the risk of handling sensitive card details on your server. This sample app demonstrates the basics of installing and configuring the SDK to complete a simple payment. 8 | 9 | For more information, visit the [**Gateway Android SDK Wiki**](https://github.com/Mastercard-Gateway/gateway-android-sdk/wiki) to find details about the basic transaction lifecycle and 3-D Secure support. 10 | 11 | 12 | ## Scope 13 | 14 | The primary responsibility of this SDK is to eliminate the need for card details to pass thru your merchant service while collecting card information from a mobile device. The Gateway provides this ability by exposing an API call to update a session with card information. This is an "unathenticated" call in the sense that you are not required to provide your private API credentials. It is important to retain your private API password in a secure location and NOT distribute it within your mobile app. 15 | 16 | Once you have updated a session with card information from the app, you may then perform a variety of operations using this session from your secure server. Some of these operations include creating an authorization or payment, creating a card token to save card information for a customer, etc. Refer to your gateway integration guide for more details on how a Session can be used in your application. 17 | 18 | 19 | ## Installation 20 | 21 | This library is hosted in the jCenter repository. To import the Android SDK, include it as a dependency in your build.gradle file. Be sure to replace `X.X.X` with the version number in the shield above. (Minimum supported Android SDK version 19) 22 | 23 | ```groovy 24 | implementation 'com.mastercard.gateway:gateway-android:X.X.X' 25 | ``` 26 | 27 | [**Release Notes**](https://github.com/Mastercard-Gateway/gateway-android-sdk/wiki/Release-Notes) 28 | 29 | 30 | ## Configuration 31 | 32 | In order to use the SDK, you must initialize the Gateway object with your merchant ID and your gateway's region. If you are unsure about which region to select, please direct your inquiry to your gateway support team. 33 | 34 | ```java 35 | Gateway gateway = new Gateway(); 36 | gateway.setMerchantId("YOUR_MERCHANT_ID"); 37 | gateway.setRegion(Gateway.Region.YOUR_REGION); 38 | ``` 39 | 40 | 41 | ## Basic Implementation 42 | 43 | Using an existing Session Id, you may pass card information directly to the `Gateway` object: 44 | 45 | ```java 46 | // The GatewayMap object provides support for building a nested map structure using key-based dot(.) notation. 47 | // Each parameter is similarly defined in your online integration guide. 48 | GatewayMap request = new GatewayMap() 49 | .set("sourceOfFunds.provided.card.nameOnCard", nameOnCard) 50 | .set("sourceOfFunds.provided.card.number", cardNumber) 51 | .set("sourceOfFunds.provided.card.securityCode", cardCvv) 52 | .set("sourceOfFunds.provided.card.expiry.month", cardExpiryMM) 53 | .set("sourceOfFunds.provided.card.expiry.year", cardExpiryYY); 54 | 55 | gateway.updateSession(sessionId, apiVersion, request, callback); 56 | ``` 57 | 58 | 59 | ## Rx-Enabled 60 | 61 | You may optionally include the **[RxJava2]** library in your project and utilize the appropriate methods provided in the `Gateway` class. 62 | 63 | ```java 64 | Single single = gateway.updateSession(sessionId, apiVersion, request); 65 | ``` 66 | 67 | 68 | --- 69 | 70 | # Sample App 71 | 72 | Included in this project is a sample app that demonstrates how to take a payment using the SDK. This sample app requires a running instance of our **[Gateway Test Merchant Server]**. Follow the instructions for that project and copy the resulting URL of the instance you create. 73 | 74 | 75 | ## Configuration 76 | 77 | To configure the sample app, compile and run the app on your device. There are three fields which must be completed in order for the sample app to operate correctly: 78 | 79 | ![Sample app configuration](./sample-configuration.png) 80 | 81 | 1. The merchant id should have the prefix 'TEST' 82 | 1. The region options include ASIA_PACIFIC, EUROPE, NORTH_AMERICA, INDIA, CHINA, or MTF 83 | 1. To find the Heroku test server URL, consult the **[Gateway Test Merchant Server]** (ex: https://{your-app-name}.herokuapp.com) 84 | 85 | 86 | 87 | [RxJava2]: https://github.com/ReactiveX/RxJava 88 | [Gateway Test Merchant Server]: https://github.com/Mastercard-Gateway/gateway-test-merchant-server 89 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | ## [1.1.8] - 2025-03-25 9 | ### Added 10 | - Updated Gson library to version 2.12.1 to address security vulnerabilities. 11 | 12 | ## [1.1.7] - 2025-02-21 13 | ### Added 14 | - DigiCert updated. New Expiry Jan 15, 2038 15 | 16 | ## [1.1.6] - 2024-02-08 17 | ### Added 18 | - Saudi region (KSA) URL 19 | 20 | ## [1.1.5] - 2022-12-28 21 | ### Changed 22 | - Pinned certificate updated. New Expiry December 2030 23 | 24 | ## [1.1.4] - 2020-03-26 25 | ### Fixed 26 | - Issue where WebView was not displaying the 3DS HTML on apps targeting API >=29 27 | ### Changed 28 | - SDK and sample app now targeting API 29 29 | - Migrated from legacy Android support libraries to Jetpack 30 | 31 | ## [1.1.3] - 2020-02-14 32 | ### Added 33 | - China region (CN) URL 34 | ### Changed 35 | - Enabled TLSv1.2 support for API <21 36 | 37 | ## [1.1.2] - 2020-02-04 38 | ### Added 39 | - India region (IN) URL -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | google() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:3.5.3' 8 | classpath 'org.jfrog.buildinfo:build-info-extractor-gradle:4.15.1' 9 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | jcenter() 16 | google() 17 | } 18 | } 19 | 20 | task clean(type: Delete) { 21 | delete rootProject.buildDir 22 | } 23 | 24 | ext { 25 | libraryVersionName = '1.1.8' 26 | } -------------------------------------------------------------------------------- /gateway-android/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /gateway-android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.jfrog.bintray' 3 | apply plugin: 'maven-publish' 4 | 5 | android { 6 | 7 | compileSdkVersion 29 8 | 9 | defaultConfig { 10 | minSdkVersion 19 11 | targetSdkVersion 29 12 | versionName libraryVersionName 13 | 14 | consumerProguardFiles 'proguard.pro' 15 | } 16 | 17 | compileOptions { 18 | sourceCompatibility JavaVersion.VERSION_1_8 19 | targetCompatibility JavaVersion.VERSION_1_8 20 | } 21 | 22 | lintOptions { 23 | abortOnError false 24 | } 25 | 26 | buildTypes { 27 | debug 28 | release 29 | } 30 | 31 | testOptions { 32 | unitTests { 33 | includeAndroidResources = true 34 | } 35 | } 36 | } 37 | 38 | // define an 'optional' dependency 39 | configurations { 40 | optional 41 | implementation.extendsFrom optional 42 | } 43 | 44 | dependencies { 45 | implementation fileTree(include: ['*.jar'], dir: 'libs') 46 | 47 | // required 48 | implementation 'androidx.appcompat:appcompat:1.1.0' 49 | implementation 'com.google.code.gson:gson:2.12.1' 50 | 51 | // optional for rx java 52 | optional 'io.reactivex.rxjava2:rxjava:2.2.5' 53 | 54 | // optional for google pay 55 | optional 'androidx.legacy:legacy-support-v4:1.0.0' 56 | optional 'com.google.android.gms:play-services-wallet:18.0.0' 57 | 58 | compileOnly 'androidx.annotation:annotation:1.1.0' 59 | 60 | testImplementation 'junit:junit:4.12' 61 | testImplementation 'org.robolectric:robolectric:4.3' 62 | testImplementation 'org.mockito:mockito-core:2.23.4' 63 | } 64 | 65 | task androidJavadocs(type: Javadoc) { 66 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 67 | android.libraryVariants.all { variant -> 68 | if (variant.name == 'release') { 69 | owner.classpath += variant.javaCompile.classpath 70 | } 71 | } 72 | source = android.sourceSets.main.java.srcDirs 73 | exclude '**/R.html', '**/R.*.html', '**/index.html' 74 | } 75 | 76 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { 77 | classifier = 'javadoc' 78 | from androidJavadocs.destinationDir 79 | } 80 | 81 | task androidSourcesJar(type: Jar) { 82 | classifier = 'sources' 83 | from android.sourceSets.main.java.srcDirs 84 | } 85 | 86 | publishing { 87 | publications { 88 | aar(MavenPublication) { 89 | groupId = 'com.mastercard.gateway' 90 | artifactId project.name 91 | version = libraryVersionName 92 | 93 | afterEvaluate { 94 | artifact bundleReleaseAar 95 | artifact androidJavadocsJar 96 | artifact androidSourcesJar 97 | } 98 | 99 | pom.withXml { 100 | def root = asNode() 101 | 102 | // adds basic info 103 | root.appendNode('name', 'Mastercard Payment Gateway Android SDK') 104 | root.appendNode('description', 'The Android SDK for Mastercard Payment Gateway') 105 | root.appendNode('url', 'https://www.mastercard.com/gateway') 106 | 107 | // adds license info 108 | def license = root.appendNode('licenses').appendNode('license') 109 | license.appendNode('name', 'Apache License, Version 2.0') 110 | license.appendNode('url', 'https://www.apache.org/licenses/LICENSE-2.0.txt') 111 | license.appendNode('distribution', 'repo') 112 | 113 | // adds source control info 114 | def scm = root.appendNode('scm') 115 | scm.appendNode('url', 'https://github.com/Mastercard/gateway-android-sdk') 116 | scm.appendNode('connection', 'https://github.com/Mastercard/gateway-android-sdk.git') 117 | 118 | // adds dependencies 119 | def dependenciesNode = root.appendNode('dependencies') 120 | configurations.implementation.allDependencies.each { dp -> 121 | if (dp.group) { 122 | def dependencyNode = dependenciesNode.appendNode('dependency') 123 | dependencyNode.appendNode('groupId', dp.group) 124 | dependencyNode.appendNode('artifactId', dp.name) 125 | dependencyNode.appendNode('version', dp.version) 126 | 127 | if (configurations.optional.allDependencies.contains(dp)) { 128 | dependencyNode.appendNode('optional', true) 129 | } 130 | } 131 | } 132 | 133 | // adds developer info 134 | def developers = root.appendNode('developers') 135 | def dev = developers.appendNode('developer') 136 | dev.appendNode('id', 'simplify-mobile-team') 137 | dev.appendNode('name', 'Mastercard Payment Gateway Services') 138 | dev.appendNode('email', 'simplify_mobile_team@mastercard.com') 139 | } 140 | } 141 | } 142 | } 143 | 144 | 145 | bintray { 146 | user = System.getenv('BINTRAY_USER') 147 | key = System.getenv('BINTRAY_API_KEY') 148 | publications = ['aar'] 149 | publish = true 150 | pkg { 151 | repo = 'Android' 152 | name = 'gateway-android-sdk' 153 | userOrg = 'mpgs' 154 | licenses = ['Apache-2.0'] 155 | vcsUrl = 'https://github.com/Mastercard/gateway-android-sdk.git' 156 | version { 157 | name = libraryVersionName 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /gateway-android/proguard.pro: -------------------------------------------------------------------------------- 1 | ## proguard 2 | -dontwarn java.lang.invoke.* 3 | -dontwarn **$$Lambda$* 4 | 5 | ## GSON ## 6 | # Gson uses generic type information stored in a class file when working with fields. Proguard 7 | # removes such information by default, so configure it to keep all of it. 8 | -keepattributes Signature 9 | 10 | # For using GSON @Expose annotation 11 | -keepattributes *Annotation* 12 | 13 | # Gson specific classes 14 | -dontwarn sun.misc.** 15 | #-keep class com.google.gson.stream.** { *; } 16 | 17 | # Prevent proguard from stripping interface information from TypeAdapterFactory, 18 | # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) 19 | -keep class * implements com.google.gson.TypeAdapterFactory 20 | -keep class * implements com.google.gson.JsonSerializer 21 | -keep class * implements com.google.gson.JsonDeserializer 22 | 23 | 24 | # keep api contract and enums 25 | -keep class com.mastercard.gateway.android.sdk.api.** { *; } 26 | -keep enum com.mastercard.gateway.android.sdk.** { *; } 27 | 28 | # Optional libraries will warn on missing classes 29 | -dontwarn io.reactivex.** -------------------------------------------------------------------------------- /gateway-android/src/debug/java/com/mastercard/gateway/android/sdk/BaseLogger.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sdk; 2 | 3 | 4 | import android.util.Log; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Set; 9 | 10 | import javax.net.ssl.HttpsURLConnection; 11 | 12 | class BaseLogger implements Logger { 13 | 14 | @Override 15 | public void logRequest(HttpsURLConnection c, String data) { 16 | String log = "REQUEST: " + c.getRequestMethod() + " " + c.getURL().toString(); 17 | 18 | if (data != null) { 19 | log += "\n-- Data: " + data; 20 | } 21 | 22 | // log request headers 23 | Map> properties = c.getRequestProperties(); 24 | Set keys = properties.keySet(); 25 | for (String key : keys) { 26 | List values = properties.get(key); 27 | for (String value : values) { 28 | log += "\n-- " + key + ": " + value; 29 | } 30 | } 31 | 32 | String[] parts = log.split("\n"); 33 | for (String part : parts) { 34 | logDebug(part); 35 | } 36 | } 37 | 38 | @Override 39 | public void logResponse(HttpsURLConnection c, String data) { 40 | String log = "RESPONSE: "; 41 | 42 | // log response headers 43 | Map> headers = c.getHeaderFields(); 44 | Set keys = headers.keySet(); 45 | 46 | int i = 0; 47 | for (String key : keys) { 48 | List values = headers.get(key); 49 | for (String value : values) { 50 | if (i == 0 && key == null) { 51 | log += value; 52 | 53 | if (data != null && data.length() > 0) { 54 | log += "\n-- Data: " + data; 55 | } 56 | } else { 57 | log += "\n-- " + (key == null ? "" : key + ": ") + value; 58 | } 59 | i++; 60 | } 61 | } 62 | 63 | log += "\n-- Cipher Suite: " + c.getCipherSuite(); 64 | 65 | String[] parts = log.split("\n"); 66 | for (String part : parts) { 67 | logDebug(part); 68 | } 69 | } 70 | 71 | @Override 72 | public void logDebug(String message) { 73 | Log.d(Gateway.class.getSimpleName(), message); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /gateway-android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /gateway-android/src/main/java/com/mastercard/gateway/android/sdk/Gateway.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Mastercard 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.mastercard.gateway.android.sdk; 18 | 19 | 20 | import android.app.Activity; 21 | import android.content.Intent; 22 | import android.os.Handler; 23 | import android.os.Message; 24 | import android.util.Base64; 25 | 26 | import com.google.android.gms.common.api.Status; 27 | import com.google.android.gms.wallet.AutoResolveHelper; 28 | 29 | import com.google.android.gms.wallet.PaymentData; 30 | import com.google.android.gms.wallet.PaymentDataRequest; 31 | import com.google.android.gms.wallet.PaymentsClient; 32 | import com.google.gson.Gson; 33 | 34 | import org.json.JSONObject; 35 | 36 | import java.io.BufferedReader; 37 | import java.io.IOException; 38 | import java.io.InputStream; 39 | import java.io.InputStreamReader; 40 | import java.io.OutputStream; 41 | import java.net.URL; 42 | 43 | import javax.net.ssl.HttpsURLConnection; 44 | import javax.net.ssl.SSLContext; 45 | import javax.net.ssl.SSLSocketFactory; 46 | 47 | import io.reactivex.Single; 48 | 49 | /** 50 | * The public interface to the Gateway SDK. 51 | *

52 | * Example set up: 53 | *

54 | * 55 | * Gateway gateway = new Gateway(); 56 | * gateway.setMerchantId("your-merchant-id"); 57 | * gateway.setRegion(Gateway.Region.NORTH_AMERICA); 58 | * 59 | */ 60 | @SuppressWarnings("unused,WeakerAccess") 61 | public class Gateway { 62 | 63 | /** 64 | * The available gateway regions 65 | */ 66 | public enum Region { 67 | ASIA_PACIFIC("ap."), 68 | EUROPE("eu."), 69 | NORTH_AMERICA("na."), 70 | INDIA("in."), 71 | CHINA("cn."), 72 | MTF("mtf."), 73 | SAUDI_ARABIA("ksa."); 74 | 75 | String prefix; 76 | 77 | Region(String prefix) { 78 | this.prefix = prefix; 79 | } 80 | 81 | String getPrefix() { 82 | return prefix; 83 | } 84 | } 85 | 86 | // internally supported request methods 87 | enum Method { 88 | PUT 89 | } 90 | 91 | 92 | static final int MIN_API_VERSION = 39; 93 | static final int CONNECTION_TIMEOUT = 15000; 94 | static final int READ_TIMEOUT = 60000; 95 | static final int REQUEST_3D_SECURE = 10000; 96 | static final int REQUEST_GOOGLE_PAY_LOAD_PAYMENT_DATA = 10001; 97 | static final String API_OPERATION = "UPDATE_PAYER_DATA"; 98 | static final String USER_AGENT = "Gateway-Android-SDK/" + BuildConfig.VERSION_NAME; 99 | 100 | 101 | Logger logger = new BaseLogger(); 102 | Gson gson = new Gson(); 103 | String merchantId; 104 | Region region; 105 | 106 | 107 | /** 108 | * Constructs a new instance. 109 | */ 110 | public Gateway() { 111 | } 112 | 113 | /** 114 | * Gets the current Merchant ID 115 | * 116 | * @return The current Merchant ID 117 | */ 118 | public String getMerchantId() { 119 | return merchantId; 120 | } 121 | 122 | /** 123 | * Sets the current Merchant ID 124 | * 125 | * @param merchantId A valid Merchant ID 126 | * @return The Gateway instance 127 | * @throws IllegalArgumentException If the provided Merchant ID is null 128 | */ 129 | public Gateway setMerchantId(String merchantId) { 130 | if (merchantId == null) { 131 | throw new IllegalArgumentException("Merchant ID may not be null"); 132 | } 133 | 134 | this.merchantId = merchantId; 135 | 136 | return this; 137 | } 138 | 139 | /** 140 | * Gets the current {@link Region} 141 | * 142 | * @return The region 143 | */ 144 | public Region getRegion() { 145 | return region; 146 | } 147 | 148 | /** 149 | * Sets the current {@link Region} to target 150 | * 151 | * @param region The region 152 | * @return The Gateway instance 153 | * @throws IllegalArgumentException If the provided Merchant ID is null 154 | */ 155 | public Gateway setRegion(Region region) { 156 | if (region == null) { 157 | throw new IllegalArgumentException("Region may not be null"); 158 | } 159 | 160 | this.region = region; 161 | 162 | return this; 163 | } 164 | 165 | /** 166 | * Updates a Mastercard Gateway session with the provided information.
167 | * The API version number provided MUST match the version used when the session was created. 168 | *

169 | * This will execute the necessary network request on a background thread 170 | * and return the response (or error) to the provided callback. 171 | * 172 | * @param sessionId A session ID from the Mastercard Gateway 173 | * @param apiVersion The API version number used when the session was created 174 | * @param payload A map of the request data 175 | * @param callback A callback to handle success and error messages 176 | * @throws IllegalArgumentException If the provided session id is null 177 | */ 178 | public void updateSession(String sessionId, String apiVersion, GatewayMap payload, GatewayCallback callback) { 179 | GatewayRequest request = buildUpdateSessionRequest(sessionId, apiVersion, payload); 180 | 181 | runGatewayRequest(request, callback); 182 | } 183 | 184 | /** 185 | * Updates a Mastercard Gateway session with the provided information. 186 | * The API version number provided MUST match the version used when the session was created. 187 | *

188 | * Does not adhere to any particular scheduler 189 | * 190 | * @param sessionId A session ID from the Mastercard Gateway 191 | * @param apiVersion The API version number used when the session was created 192 | * @param payload A map of the request data 193 | * @return A Single of the response map 194 | * @throws IllegalArgumentException If the provided session id is null 195 | * @see RxJava: Single 196 | */ 197 | public Single updateSession(String sessionId, String apiVersion, GatewayMap payload) { 198 | GatewayRequest request = buildUpdateSessionRequest(sessionId, apiVersion, payload); 199 | 200 | return runGatewayRequest(request); 201 | } 202 | 203 | GatewayRequest buildUpdateSessionRequest(String sessionId, String apiVersion, GatewayMap payload) { 204 | GatewayRequest request = new GatewayRequest(); 205 | request.url = getUpdateSessionUrl(sessionId, apiVersion); 206 | request.method = Method.PUT; 207 | request.payload = payload; 208 | request.payload.put("device.browser", USER_AGENT); 209 | 210 | // version 50 of the API dropped the requirement for the apiOperation parameter 211 | // 50+ uses the standard Update Session API 212 | if (Integer.parseInt(apiVersion) < 50) { 213 | request.payload.put("apiOperation", API_OPERATION); 214 | } else { 215 | // Auth header required for v50+ 216 | request.extraHeaders.put("Authorization", createAuthHeader(sessionId)); 217 | } 218 | 219 | return request; 220 | } 221 | 222 | /** 223 | * Starts the {@link Gateway3DSecureActivity} for result, initializing it with the provided html 224 | * 225 | * @param activity The calling activity context 226 | * @param html The initial HTML to render in the web view 227 | */ 228 | public static void start3DSecureActivity(Activity activity, String html) { 229 | start3DSecureActivity(activity, html, null); 230 | } 231 | 232 | /** 233 | * Starts the {@link Gateway3DSecureActivity} for result, initializing it with the provided html 234 | * 235 | * @param activity The calling activity context 236 | * @param html The initial HTML to render in the web view 237 | * @param title An optional title to render in the toolbar 238 | */ 239 | public static void start3DSecureActivity(Activity activity, String html, String title) { 240 | Intent intent = new Intent(activity, Gateway3DSecureActivity.class); 241 | start3DSecureActivity(activity, html, title, intent); 242 | } 243 | 244 | // separated for testability 245 | static void start3DSecureActivity(Activity activity, String html, String title, Intent intent) { 246 | intent.putExtra(Gateway3DSecureActivity.EXTRA_HTML, html); // required 247 | 248 | if (title != null) { 249 | intent.putExtra(Gateway3DSecureActivity.EXTRA_TITLE, title); 250 | } 251 | 252 | activity.startActivityForResult(intent, REQUEST_3D_SECURE); 253 | } 254 | 255 | /** 256 | * A convenience method for handling activity result messages returned from {@link Gateway3DSecureActivity}. 257 | * This method should be called within the calling Activity's onActivityResult() lifecycle method. 258 | * This helper only works if the 3-D Secure Activity was launched using the 259 | * {@link Gateway#start3DSecureActivity(Activity, String, String)} method. 260 | * 261 | * @param requestCode The request code returning from the activity result 262 | * @param resultCode The result code returning from the activity result 263 | * @param data The intent data returning from the activity result 264 | * @param callback An implementation of {@link Gateway3DSecureCallback} 265 | * @return True if handled, False otherwise 266 | * @see Gateway#start3DSecureActivity(Activity, String) 267 | * @see Gateway#start3DSecureActivity(Activity, String, String) 268 | */ 269 | public static boolean handle3DSecureResult(int requestCode, int resultCode, Intent data, Gateway3DSecureCallback callback) { 270 | if (callback == null) { 271 | return false; 272 | } 273 | 274 | if (requestCode == REQUEST_3D_SECURE) { 275 | if (resultCode == Activity.RESULT_OK) { 276 | String acsResultJson = data.getStringExtra(Gateway3DSecureActivity.EXTRA_ACS_RESULT); 277 | GatewayMap acsResult = new GatewayMap(acsResultJson); 278 | 279 | callback.on3DSecureComplete(acsResult); 280 | } else { 281 | callback.on3DSecureCancel(); 282 | } 283 | 284 | return true; 285 | } 286 | 287 | return false; 288 | } 289 | 290 | 291 | /** 292 | * A convenience method for initializing the request to get Google Pay card info 293 | * 294 | * @param paymentsClient An instance of the PaymentClient 295 | * @param request A properly formatted PaymentDataRequest 296 | * @param activity The calling activity 297 | * @see Payments Client 298 | * @see Payment Data Request 299 | */ 300 | public static void requestGooglePayData(PaymentsClient paymentsClient, PaymentDataRequest request, Activity activity) { 301 | AutoResolveHelper.resolveTask(paymentsClient.loadPaymentData(request), activity, REQUEST_GOOGLE_PAY_LOAD_PAYMENT_DATA); 302 | } 303 | 304 | /** 305 | * A convenience method for handling activity result messages returned from Google Pay. 306 | * This method should be called withing the calling Activity's onActivityResult() lifecycle method. 307 | * This helper only works if the Google Pay dialog was launched using the 308 | * {@link Gateway#requestGooglePayData(PaymentsClient, PaymentDataRequest, Activity)} method. 309 | * 310 | * @param requestCode The request code returning from the activity result 311 | * @param resultCode The result code returning from the activity result 312 | * @param data The intent data returning from the activity result 313 | * @param callback An implementation of {@link GatewayGooglePayCallback} 314 | * @return True if handled, False otherwise 315 | * @see Gateway#requestGooglePayData(PaymentsClient, PaymentDataRequest, Activity) 316 | */ 317 | public static boolean handleGooglePayResult(int requestCode, int resultCode, Intent data, GatewayGooglePayCallback callback) { 318 | if (callback == null) { 319 | return false; 320 | } 321 | 322 | if (requestCode == REQUEST_GOOGLE_PAY_LOAD_PAYMENT_DATA) { 323 | if (resultCode == Activity.RESULT_OK) { 324 | try { 325 | PaymentData paymentData = PaymentData.getFromIntent(data); 326 | JSONObject json = new JSONObject(paymentData.toJson()); 327 | callback.onReceivedPaymentData(json); 328 | } catch (Exception e) { 329 | callback.onGooglePayError(Status.RESULT_INTERNAL_ERROR); 330 | } 331 | } else if (resultCode == Activity.RESULT_CANCELED) { 332 | callback.onGooglePayCancelled(); 333 | } else if (resultCode == AutoResolveHelper.RESULT_ERROR) { 334 | Status status = AutoResolveHelper.getStatusFromIntent(data); 335 | callback.onGooglePayError(status); 336 | } 337 | 338 | return true; 339 | } 340 | 341 | return false; 342 | } 343 | 344 | 345 | String getApiUrl(String apiVersion) { 346 | if (Integer.valueOf(apiVersion) < MIN_API_VERSION) { 347 | throw new IllegalArgumentException("API version must be >= " + MIN_API_VERSION); 348 | } 349 | 350 | if (region == null) { 351 | throw new IllegalStateException("You must initialize the the Gateway instance with a Region before use"); 352 | } 353 | 354 | return "https://" + region.getPrefix() + "gateway.mastercard.com/api/rest/version/" + apiVersion; 355 | } 356 | 357 | String getUpdateSessionUrl(String sessionId, String apiVersion) { 358 | if (sessionId == null) { 359 | throw new IllegalArgumentException("Session Id may not be null"); 360 | } 361 | 362 | if (merchantId == null) { 363 | throw new IllegalStateException("You must initialize the the Gateway instance with a Merchant Id before use"); 364 | } 365 | 366 | return getApiUrl(apiVersion) + "/merchant/" + merchantId + "/session/" + sessionId; 367 | } 368 | 369 | void runGatewayRequest(GatewayRequest request, GatewayCallback callback) { 370 | // create handler on current thread 371 | Handler handler = new Handler(msg -> handleCallbackMessage(callback, msg.obj)); 372 | 373 | new Thread(() -> { 374 | Message m = handler.obtainMessage(); 375 | try { 376 | m.obj = executeGatewayRequest(request); 377 | } catch (Exception e) { 378 | m.obj = e; 379 | } 380 | 381 | handler.sendMessage(m); 382 | }).start(); 383 | } 384 | 385 | Single runGatewayRequest(GatewayRequest request) { 386 | return Single.fromCallable(() -> executeGatewayRequest(request)); 387 | } 388 | 389 | // handler callback method when executing a request on a new thread 390 | @SuppressWarnings("unchecked") 391 | boolean handleCallbackMessage(GatewayCallback callback, Object arg) { 392 | if (callback != null) { 393 | if (arg instanceof Throwable) { 394 | callback.onError((Throwable) arg); 395 | } else { 396 | callback.onSuccess((GatewayMap) arg); 397 | } 398 | } 399 | return true; 400 | } 401 | 402 | GatewayMap executeGatewayRequest(GatewayRequest request) throws Exception { 403 | // init connection 404 | HttpsURLConnection c = createHttpsUrlConnection(request); 405 | 406 | // encode request data to json 407 | String requestData = gson.toJson(request.payload); 408 | 409 | // log request data 410 | logger.logRequest(c, requestData); 411 | 412 | // write request data 413 | if (requestData != null) { 414 | OutputStream os = c.getOutputStream(); 415 | os.write(requestData.getBytes("UTF-8")); 416 | os.close(); 417 | } 418 | 419 | // initiate the connection 420 | c.connect(); 421 | 422 | String responseData = null; 423 | int statusCode = c.getResponseCode(); 424 | boolean isStatusOk = (statusCode >= 200 && statusCode < 300); 425 | 426 | // if connection has output stream, get the data 427 | // socket time-out exceptions will be thrown here 428 | if (c.getDoInput()) { 429 | InputStream is = isStatusOk ? c.getInputStream() : c.getErrorStream(); 430 | responseData = inputStreamToString(is); 431 | is.close(); 432 | } 433 | 434 | c.disconnect(); 435 | 436 | // log response 437 | logger.logResponse(c, responseData); 438 | 439 | // parse the response body 440 | GatewayMap response = new GatewayMap(responseData); 441 | 442 | // if response static is good, return response 443 | if (isStatusOk) { 444 | return response; 445 | } 446 | 447 | // otherwise, create a gateway exception and throw it 448 | String message = (String) response.get("error.explanation"); 449 | if (message == null) { 450 | message = "An error occurred"; 451 | } 452 | 453 | GatewayException exception = new GatewayException(message); 454 | exception.setStatusCode(statusCode); 455 | exception.setErrorResponse(response); 456 | 457 | throw exception; 458 | } 459 | 460 | HttpsURLConnection createHttpsUrlConnection(GatewayRequest request) throws Exception { 461 | // parse url 462 | URL url = new URL(request.url); 463 | 464 | HttpsURLConnection c = (HttpsURLConnection) url.openConnection(); 465 | c.setSSLSocketFactory(createSocketFactory()); 466 | c.setConnectTimeout(CONNECTION_TIMEOUT); 467 | c.setReadTimeout(READ_TIMEOUT); 468 | c.setRequestMethod(request.method.name()); 469 | c.setDoOutput(true); 470 | 471 | c.setRequestProperty("User-Agent", USER_AGENT); 472 | c.setRequestProperty("Content-Type", "application/json"); 473 | 474 | // add extra headers 475 | if (request.extraHeaders != null) { 476 | for (String key : request.extraHeaders.keySet()) { 477 | c.setRequestProperty(key, request.extraHeaders.get(key)); 478 | } 479 | } 480 | 481 | return c; 482 | } 483 | 484 | SSLSocketFactory createSocketFactory() throws Exception { 485 | // custom ssl context to enforce certificate pinning 486 | SSLContext sslContext = new GatewaySSLContextProvider().createSSLContext(); 487 | 488 | // custom socket factory to enable TLSv1.2 on all supported devices 489 | return new GatewayTLSSocketFactory(sslContext); 490 | } 491 | 492 | String createAuthHeader(String sessionId) { 493 | String value = "merchant." + merchantId + ":" + sessionId; 494 | return "Basic " + Base64.encodeToString(value.getBytes(), Base64.NO_WRAP); 495 | } 496 | 497 | boolean isStatusCodeOk(int statusCode) { 498 | return (statusCode >= 200 && statusCode < 300); 499 | } 500 | 501 | String inputStreamToString(InputStream is) throws IOException { 502 | // get buffered reader from stream 503 | BufferedReader rd = new BufferedReader(new InputStreamReader(is)); 504 | 505 | // read stream into string builder 506 | StringBuilder total = new StringBuilder(); 507 | 508 | String line; 509 | while ((line = rd.readLine()) != null) { 510 | total.append(line); 511 | } 512 | 513 | return total.toString(); 514 | } 515 | } 516 | -------------------------------------------------------------------------------- /gateway-android/src/main/java/com/mastercard/gateway/android/sdk/Gateway3DSecureActivity.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sdk; 2 | 3 | 4 | import android.annotation.SuppressLint; 5 | import android.app.Activity; 6 | import android.content.Intent; 7 | import android.net.Uri; 8 | import android.os.Bundle; 9 | import androidx.annotation.Nullable; 10 | import androidx.appcompat.app.AppCompatActivity; 11 | import androidx.appcompat.widget.Toolbar; 12 | 13 | import android.util.Base64; 14 | import android.webkit.WebChromeClient; 15 | import android.webkit.WebView; 16 | import android.webkit.WebViewClient; 17 | 18 | import java.util.Set; 19 | 20 | 21 | public class Gateway3DSecureActivity extends AppCompatActivity { 22 | 23 | /** 24 | * The HTML used to initialize the WebView. Should be the HTML content returned from the Gateway 25 | * during the Check 3DS Enrollment call 26 | */ 27 | public static final String EXTRA_HTML = "com.mastercard.gateway.android.HTML"; 28 | 29 | /** 30 | * An OPTIONAL title to display in the toolbar for this activity 31 | */ 32 | public static final String EXTRA_TITLE = "com.mastercard.gateway.android.TITLE"; 33 | 34 | /** 35 | * The ACS Result data after performing 3DS 36 | */ 37 | public static final String EXTRA_ACS_RESULT = "com.mastercard.gateway.android.ACS_RESULT"; 38 | 39 | 40 | static final String REDIRECT_SCHEME = "gatewaysdk"; 41 | 42 | Toolbar toolbar; 43 | WebView webView; 44 | 45 | 46 | @Override 47 | @SuppressLint("SetJavaScriptEnabled") 48 | protected void onCreate(@Nullable Bundle savedInstanceState) { 49 | super.onCreate(savedInstanceState); 50 | setContentView(R.layout.activity_3dsecure); 51 | 52 | // init toolbar 53 | toolbar = findViewById(R.id.toolbar); 54 | toolbar.setNavigationOnClickListener(view -> onBackPressed()); 55 | 56 | // init web view 57 | webView = findViewById(R.id.webview); 58 | webView.setWebChromeClient(new WebChromeClient()); 59 | webView.getSettings().setDomStorageEnabled(true); 60 | webView.getSettings().setJavaScriptEnabled(true); 61 | webView.setWebViewClient(buildWebViewClient()); 62 | 63 | init(); 64 | } 65 | 66 | void init() { 67 | // init html 68 | String extraHtml = getExtraHtml(); 69 | if (extraHtml == null) { 70 | onBackPressed(); 71 | return; 72 | } else { 73 | setWebViewHtml(extraHtml); 74 | } 75 | 76 | // init title 77 | String defaultTitle = getDefaultTitle(); 78 | String extraTitle = getExtraTitle(); 79 | setToolbarTitle(extraTitle != null ? extraTitle : defaultTitle); 80 | } 81 | 82 | String getDefaultTitle() { 83 | return getString(R.string.gateway_3d_secure_authentication); 84 | } 85 | 86 | String getExtraTitle() { 87 | Bundle extras = getIntent().getExtras(); 88 | if (extras != null) { 89 | return extras.getString(EXTRA_TITLE); 90 | } 91 | 92 | return null; 93 | } 94 | 95 | String getExtraHtml() { 96 | Bundle extras = getIntent().getExtras(); 97 | if (extras != null) { 98 | return extras.getString(EXTRA_HTML); 99 | } 100 | 101 | return null; 102 | } 103 | 104 | void setToolbarTitle(String title) { 105 | toolbar.setTitle(title); 106 | } 107 | 108 | void setWebViewHtml(String html) { 109 | String encoded = Base64.encodeToString(html.getBytes(), Base64.NO_PADDING | Base64.NO_WRAP); 110 | webView.loadData(encoded, "text/html", "base64"); 111 | } 112 | 113 | void webViewUrlChanges(Uri uri) { 114 | String scheme = uri.getScheme(); 115 | if (REDIRECT_SCHEME.equalsIgnoreCase(scheme)) { 116 | complete(getACSResultFromUri(uri)); 117 | } else if ("mailto".equalsIgnoreCase(scheme)) { 118 | intentToEmail(uri); 119 | } else { 120 | loadWebViewUrl(uri); 121 | } 122 | } 123 | 124 | void complete(String acsResult) { 125 | Intent intent = new Intent(); 126 | complete(acsResult, intent); 127 | } 128 | 129 | // separate for testability 130 | void complete(String acsResult, Intent intent) { 131 | intent.putExtra(EXTRA_ACS_RESULT, acsResult); 132 | setResult(Activity.RESULT_OK, intent); 133 | finish(); 134 | } 135 | 136 | void loadWebViewUrl(Uri uri) { 137 | webView.loadUrl(uri.toString()); 138 | } 139 | 140 | void intentToEmail(Uri uri) { 141 | Intent emailIntent = new Intent(Intent.ACTION_SENDTO); 142 | intentToEmail(uri, emailIntent); 143 | } 144 | 145 | // separate for testability 146 | void intentToEmail(Uri uri, Intent intent) { 147 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 148 | intent.setData(uri); 149 | 150 | startActivity(intent); 151 | } 152 | 153 | String getACSResultFromUri(Uri uri) { 154 | String result = null; 155 | 156 | Set params = uri.getQueryParameterNames(); 157 | for (String param : params) { 158 | if ("acsResult".equalsIgnoreCase(param)) { 159 | result = uri.getQueryParameter(param); 160 | } 161 | } 162 | 163 | return result; 164 | } 165 | 166 | WebViewClient buildWebViewClient() { 167 | return new WebViewClient() { 168 | @Override 169 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 170 | webViewUrlChanges(Uri.parse(url)); 171 | return true; 172 | } 173 | }; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /gateway-android/src/main/java/com/mastercard/gateway/android/sdk/Gateway3DSecureCallback.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sdk; 2 | 3 | public interface Gateway3DSecureCallback { 4 | 5 | /** 6 | * Callback method when webview-based 3DS authentication is complete 7 | * 8 | * @param acsResult A response map containing the ACS result 9 | */ 10 | void on3DSecureComplete(GatewayMap acsResult); 11 | 12 | /** 13 | * Callback when a user cancels the 3DS authentication flow. (typically on back press) 14 | */ 15 | void on3DSecureCancel(); 16 | } 17 | -------------------------------------------------------------------------------- /gateway-android/src/main/java/com/mastercard/gateway/android/sdk/GatewayCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Mastercard 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.mastercard.gateway.android.sdk; 18 | 19 | 20 | public interface GatewayCallback { 21 | 22 | /** 23 | * Callback on a successful call to the Gateway API 24 | * 25 | * @param response A response map 26 | */ 27 | void onSuccess(GatewayMap response); 28 | 29 | /** 30 | * Callback executed when error thrown during call to Gateway API 31 | * 32 | * @param throwable The exception thrown 33 | */ 34 | void onError(Throwable throwable); 35 | } 36 | -------------------------------------------------------------------------------- /gateway-android/src/main/java/com/mastercard/gateway/android/sdk/GatewayException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Mastercard 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.mastercard.gateway.android.sdk; 18 | 19 | public class GatewayException extends Exception { 20 | 21 | int statusCode; 22 | GatewayMap error; 23 | 24 | public GatewayException() { 25 | } 26 | 27 | public GatewayException(String message) { 28 | super(message); 29 | } 30 | 31 | 32 | public int getStatusCode() { 33 | return statusCode; 34 | } 35 | 36 | public void setStatusCode(int statusCode) { 37 | this.statusCode = statusCode; 38 | } 39 | 40 | public GatewayMap getErrorResponse() { 41 | return error; 42 | } 43 | 44 | public void setErrorResponse(GatewayMap error) { 45 | this.error = error; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /gateway-android/src/main/java/com/mastercard/gateway/android/sdk/GatewayGooglePayCallback.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sdk; 2 | 3 | 4 | import com.google.android.gms.common.api.Status; 5 | import com.google.android.gms.wallet.PaymentData; 6 | 7 | import org.json.JSONObject; 8 | 9 | public interface GatewayGooglePayCallback { 10 | 11 | /** 12 | * Called when payment data is returned from GooglePay 13 | * 14 | * @param paymentData A json object containing details about the payment 15 | * @see PaymentData 16 | */ 17 | void onReceivedPaymentData(JSONObject paymentData); 18 | 19 | /** 20 | * Called when a user cancels a GooglePay transaction 21 | */ 22 | void onGooglePayCancelled(); 23 | 24 | /** 25 | * Called when an error occurs during a GooglePay transaction 26 | * 27 | * @param status The corresponding status object of the request 28 | */ 29 | void onGooglePayError(Status status); 30 | } 31 | -------------------------------------------------------------------------------- /gateway-android/src/main/java/com/mastercard/gateway/android/sdk/GatewayMap.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sdk; 2 | 3 | 4 | import com.google.gson.Gson; 5 | import com.google.gson.reflect.TypeToken; 6 | 7 | import java.util.ArrayList; 8 | import java.util.LinkedHashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.regex.Matcher; 12 | import java.util.regex.Pattern; 13 | 14 | 15 | /** 16 | * Map object that extends the LinkedHashMap map with support for insertion and retrieval of keys using special 17 | * key path values. The key path support nested maps and array values. 18 | *

19 | * A key path consists of a sequence of key values separated by '.' characters. Each part of the key path 20 | * consists of a separate map. For example a key path of 'k1.k2.k3' is a map containing a key 'k1' whose 21 | * value is a map containing a key 'k2' whose values is a map containing a key 'k3'. A key path can also 22 | * contain an array notation '[<number>]' in which case the value of 'a' in the map is a list containing 23 | * a map. For example 'a[1].k2' refers to the key value 'k2' in the 2nd element of the list referred to by 24 | * the value of key 'a' in the map. If no index value is given (i.e., '[]') then a put() method appends 25 | * to the list while a get() method returns the last value in the list. 26 | *

27 | * When using the array index notation the value inserted must be a map; inserting values is not permitted. 28 | * For example using put("a[3].k1", 1) is permitted while put("a[3]", 1) results 29 | * in an IllegalArgumentException. 30 | *

31 | * Examples: 32 | *

 33 |  * GatewayMap map  = new GatewayMap();
 34 |  * map.put("sourceOfFunds.provided.card.nameOnCard", "Joseph Cardholder");
 35 |  * map.put("sourceOfFunds.provided.card.number", "5111111111111118");
 36 |  * map.put("sourceOfFunds.provided.card.securityCode", "100");
 37 |  * map.put("sourceOfFunds.provided.card.expiry.month", "05");
 38 |  * map.put("sourceOfFunds.provided.card.expiry.year", "21")
 39 |  * map.put("customer.email", "test@example.com");
 40 |  * map.put("customer.firstName", "Joe");
 41 |  * map.put("customer.lastName", "Cardholder");
 42 |  * 
43 | * There is also an set() method which is similar to put() but returns the map providing a fluent map builder. 44 | *
 45 |  * GatewayMap map = new GatewayMap()
 46 |  *      .set("sourceOfFunds.provided.card.nameOnCard", "Joe Cardholder")
 47 |  *      .set("sourceOfFunds.provided.card.number", "5111111111111118")
 48 |  *      .set("sourceOfFunds.provided.card.securityCode", "100")
 49 |  *      .set("sourceOfFunds.provided.card.expiry.month", "05")
 50 |  *      .set("sourceOfFunds.provided.card.expiry.year", "21")
 51 |  *      .set("customer.email", "test@example.com")
 52 |  *      .set("customer.firstName", "Joe")
 53 |  *      .set("customer.lastName", "Cardholder");
 54 |  * 
55 | * Both of these examples construct a GatewayMap containing the keys 'sourceOfFunds' and 'customer'. The 56 | * value for the 'customer' key is a map containing the keys 'email', 'firstName', and 'lastName'. 57 | */ 58 | public class GatewayMap extends LinkedHashMap { 59 | private static final Pattern arrayIndexPattern = Pattern.compile("(.*)\\[(.*)\\]"); 60 | 61 | /** 62 | * Constructs an empty map with the specified capacity and load factor. 63 | * 64 | * @param initialCapacity the initial capacity 65 | * @param loadFactor the load factor 66 | */ 67 | public GatewayMap(int initialCapacity, float loadFactor) { 68 | super(initialCapacity, loadFactor); 69 | } 70 | 71 | /** 72 | * Constructs an empty map with the specified capacity and default load factor. 73 | * 74 | * @param initialCapacity the initial capacity 75 | */ 76 | public GatewayMap(int initialCapacity) { 77 | super(initialCapacity); 78 | } 79 | 80 | /** 81 | * Constructs an empty map with the default capacity and load factor. 82 | */ 83 | public GatewayMap() { 84 | super(); 85 | } 86 | 87 | /** 88 | * Constructs a map with the same mappings as in the specifed map. 89 | * 90 | * @param map the map whose mappings are to be placed in this map 91 | */ 92 | public GatewayMap(Map map) { 93 | super(map); 94 | } 95 | 96 | /** 97 | * Constructs a map based of the speficied JSON string. 98 | * 99 | * @param jsonMapString the JSON string used to construct the map 100 | */ 101 | public GatewayMap(String jsonMapString) { 102 | super(); 103 | 104 | Map map = new Gson().fromJson(jsonMapString, new TypeToken>() { 105 | }.getType()); 106 | 107 | putAll(map); 108 | } 109 | 110 | 111 | /** 112 | * Constructs a map with an initial mapping of keyPath to value. 113 | * 114 | * @param keyPath key path with which the specified value is to be associated. 115 | * @param value value to be associated with the specified key path. 116 | */ 117 | public GatewayMap(String keyPath, Object value) { 118 | put(keyPath, value); 119 | } 120 | 121 | /** 122 | * Associates the specified value to the specified key path. 123 | * 124 | * @param keyPath key path to which the specified value is to be associated. 125 | * @param value the value which is to be associated with the specified key path. 126 | * @throws IllegalArgumentException if part of the key path does not match the expected type. 127 | * @throws IndexOutOfBoundsException if using an array index in the key path is out of bounds. 128 | */ 129 | @Override 130 | public Object put(String keyPath, Object value) { 131 | String[] properties = keyPath.split("\\."); 132 | Map destinationObject = this; 133 | 134 | if (properties.length > 1) { 135 | for (int i = 0; i < (properties.length - 1); i++) { 136 | String property = properties[i]; 137 | if (property.contains("[")) { 138 | destinationObject = getDestinationMap(property, destinationObject, i == properties.length - 1); 139 | } else { 140 | destinationObject = getPropertyMapFrom(property, destinationObject); 141 | } 142 | } 143 | } else if (keyPath.contains("[")) { 144 | destinationObject = getDestinationMap(keyPath, this, true); 145 | } 146 | 147 | // TODO: need to take care of the case where we are inserting a value into an array rather than 148 | // map ( eg map.put("a[2]", 123); 149 | 150 | if (destinationObject == this) { 151 | return super.put(keyPath, value); 152 | } else if (value instanceof Map) { // if putting a map, call put all 153 | destinationObject.clear(); 154 | GatewayMap m = new GatewayMap(); 155 | m.putAll((Map) value); 156 | destinationObject.put(properties[properties.length - 1], m); 157 | return destinationObject; 158 | } else { 159 | return destinationObject.put(properties[properties.length - 1], value); 160 | } 161 | } 162 | 163 | 164 | /** 165 | * Associates the specified value to the specified key path and returns a reference to 166 | * this map. 167 | * 168 | * @param keyPath key path to which the specified value is to be associated. 169 | * @param value the value which is to be associated with the specified key path. 170 | * @return this map 171 | * @throws IllegalArgumentException if part of the key path does not match the expected type. 172 | * @throws IndexOutOfBoundsException if using an array index in the key path is out of bounds. 173 | */ 174 | public GatewayMap set(String keyPath, Object value) { 175 | put(keyPath, value); 176 | return this; 177 | } 178 | 179 | 180 | /** 181 | * Returns the value associated with the specified key path or null if there is no associated value. 182 | * 183 | * @param keyPath key path whose associated value is to be returned 184 | * @return the value to which the specified key is mapped 185 | * @throws IllegalArgumentException if part of the key path does not match the expected type. 186 | * @throws IndexOutOfBoundsException if using an array index in the key path is out of bounds. 187 | */ 188 | @Override 189 | public Object get(Object keyPath) { 190 | String[] keys = ((String) keyPath).split("\\."); 191 | 192 | if (keys.length <= 1) { 193 | Matcher m = arrayIndexPattern.matcher(keys[0]); 194 | if (!m.matches()) { // handles keyPath: "x" 195 | return super.get(keys[0]); 196 | } else { // handle the keyPath: "x[]" 197 | String key = m.group(1); // gets the key to retrieve from the matcher 198 | Object o = super.get(key); // get the list from the map 199 | if (!(o instanceof List)) { 200 | throw new IllegalArgumentException("Property '" + key + "' is not an array"); 201 | } 202 | List> l = (List>) o; // get the list from the map 203 | 204 | Integer index = l.size() - 1; //get last item if none specified 205 | if (!"".equals(m.group(2))) { 206 | index = Integer.parseInt(m.group(2)); 207 | } 208 | return l.get(index); // retrieve the map from the list 209 | } 210 | } 211 | 212 | Map map = findLastMapInKeyPath((String) keyPath); // handles keyPaths beyond 'root' keyPath. i.e. "x.y OR x.y[].z, etc." 213 | 214 | // retrieve the value at the end of the object path i.e. x.y.z, this retrieves whatever is in 'z' 215 | return map.get(keys[keys.length - 1]); 216 | } 217 | 218 | /** 219 | * Returns true if there is a value associated with the specified key path. 220 | * 221 | * @param keyPath key path whose associated value is to be tested 222 | * @return true if this map contains an value associated with the specified key path 223 | * @throws IllegalArgumentException if part of the key path does not match the expected type. 224 | * @throws IndexOutOfBoundsException if using an array index in the key path is out of bounds. 225 | */ 226 | @Override 227 | public boolean containsKey(Object keyPath) { 228 | String[] keys = ((String) keyPath).split("\\."); 229 | 230 | if (keys.length <= 1) { 231 | Matcher m = arrayIndexPattern.matcher(keys[0]); 232 | if (!m.matches()) { // handles keyPath: "x" 233 | return super.containsKey(keys[0]); 234 | } else { // handle the keyPath: "x[]" 235 | String key = m.group(1); 236 | Object o = super.get(key); // get the list from the map 237 | if (!(o instanceof List)) { 238 | throw new IllegalArgumentException("Property '" + key + "' is not an array"); 239 | } 240 | List> l = (List>) o; // get the list from the map 241 | 242 | Integer index = l.size() - 1; 243 | if (!"".equals(m.group(2))) { 244 | index = Integer.parseInt(m.group(2)); 245 | } 246 | return index >= 0 && index < l.size(); 247 | } 248 | } 249 | 250 | Map map = findLastMapInKeyPath((String) keyPath); 251 | if (map == null) { 252 | return false; 253 | } 254 | return map.containsKey(keys[keys.length - 1]); 255 | } 256 | 257 | 258 | /** 259 | * Removes the value associated with the specified key path from the map. 260 | * 261 | * @param keyPath key path whose associated value is to be removed 262 | * @throws IllegalArgumentException if part of the key path does not match the expected type. 263 | * @throws IndexOutOfBoundsException if using an array index in the key path is out of bounds. 264 | */ 265 | @Override 266 | public Object remove(Object keyPath) { 267 | 268 | String[] keys = ((String) keyPath).split("\\."); 269 | 270 | if (keys.length <= 1) { 271 | Matcher m = arrayIndexPattern.matcher(keys[0]); 272 | if (!m.matches()) { 273 | return super.remove(keys[0]); 274 | } else { // handle the keyPath: "x[]" 275 | String key = m.group(1); // gets the key to retrieve from the matcher 276 | Object o = super.get(key); // get the list from the map 277 | if (!(o instanceof List)) { 278 | throw new IllegalArgumentException("Property '" + key + "' is not an array"); 279 | } 280 | List> l = (List>) o; // get the list from the map 281 | 282 | Integer index = l.size() - 1; //get last item if none specified 283 | if (!"".equals(m.group(2))) { 284 | index = Integer.parseInt(m.group(2)); 285 | } 286 | return l.remove(index.intValue()); 287 | } 288 | } 289 | 290 | Map map = findLastMapInKeyPath((String) keyPath); 291 | 292 | return map.remove(keys[keys.length - 1]); 293 | } 294 | 295 | 296 | private Map findLastMapInKeyPath(String keyPath) { 297 | String[] keys = ((String) keyPath).split("\\."); 298 | 299 | Map map = null; 300 | for (int i = 0; i <= (keys.length - 2); i++) { 301 | Matcher m = arrayIndexPattern.matcher(keys[i]); 302 | String thisKey = keys[i]; 303 | if (m.matches()) { 304 | thisKey = m.group(1); 305 | 306 | Object o = null; 307 | if (null == map) { // if we are at the "root" of the object path 308 | o = super.get(thisKey); 309 | } else { 310 | o = map.get(thisKey); 311 | } 312 | 313 | if (!(o instanceof List)) { 314 | throw new IllegalArgumentException("Property '" + thisKey + "' is not an array"); 315 | } 316 | List> l = (List>) o; 317 | 318 | Integer index = l.size() - 1; //get last item if none specified 319 | 320 | if (!"".equals(m.group(2))) { 321 | index = Integer.parseInt(m.group(2)); 322 | } 323 | 324 | map = (Map) l.get(index); 325 | 326 | } else { 327 | if (null == map) { 328 | map = (Map) super.get(thisKey); 329 | } else { 330 | map = (Map) map.get(thisKey); 331 | } 332 | 333 | } 334 | 335 | } 336 | 337 | return map; 338 | } 339 | 340 | 341 | private Map getDestinationMap(String property, Map destinationObject, boolean createMap) { 342 | 343 | Matcher m = arrayIndexPattern.matcher(property); 344 | if (m.matches()) { 345 | String propName = m.group(1); 346 | Integer index = null; 347 | if (!"".equals(m.group(2))) { 348 | index = Integer.parseInt(m.group(2)); 349 | } 350 | return findOrAddToList(destinationObject, propName, index, createMap); 351 | } 352 | 353 | return destinationObject; 354 | 355 | } 356 | 357 | private Map findOrAddToList(Map destinationObject, String propName, Integer index, boolean createMap) { 358 | // 359 | 360 | List> list = new ArrayList>(); 361 | // find existing list or put the new list 362 | if (destinationObject.containsKey(propName)) { 363 | Object o = destinationObject.get(propName); 364 | if (!(o instanceof List)) { 365 | throw new IllegalArgumentException("Property '" + propName + "' is not an array"); 366 | } 367 | list = (List>) o; 368 | } else { 369 | destinationObject.put(propName, list); 370 | } 371 | 372 | // get the existing object in the list at the index 373 | Map propertyValue = null; 374 | if (index != null && list.size() > index) { 375 | propertyValue = list.get(index); 376 | } 377 | 378 | // no object at the index, create a new map and add it 379 | if (null == propertyValue) { 380 | propertyValue = new LinkedHashMap(); 381 | if (null == index) { 382 | list.add(propertyValue); 383 | } else { 384 | list.add(index, propertyValue); 385 | } 386 | } 387 | 388 | // return the map retrieved from or added to the list 389 | destinationObject = propertyValue; 390 | 391 | return destinationObject; 392 | } 393 | 394 | private Map getPropertyMapFrom(String property, Map object) { 395 | // create a new map at the key specified if it doesnt already exist 396 | if (!object.containsKey(property)) { 397 | Map val = new LinkedHashMap(); 398 | object.put(property, val); 399 | } 400 | 401 | Object o = object.get(property); 402 | if (o instanceof Map) { 403 | return (Map) o; 404 | } else { 405 | throw new IllegalArgumentException("cannot change nested property to map"); 406 | } 407 | } 408 | 409 | /** 410 | * Returns an identical copy of the map 411 | * 412 | * @param m The map to copy 413 | * @return A copy of the original map 414 | */ 415 | public static Map normalize(Map m) { 416 | 417 | GatewayMap pm = new GatewayMap(); 418 | 419 | for (String k : m.keySet()) { 420 | Object v = m.get(k); 421 | 422 | if (v == null) { 423 | pm.set(k, v); 424 | 425 | } else if (v instanceof List) { 426 | pm.set(k, normalize((List) v)); 427 | 428 | } else if (v instanceof Map) { 429 | pm.set(k, normalize((Map) v)); 430 | 431 | } else if (v instanceof String || 432 | v instanceof Double || 433 | v instanceof Float || 434 | v instanceof Number || 435 | v instanceof Boolean) { 436 | pm.set(k, v); 437 | 438 | } else { 439 | pm.set(k, v.toString()); 440 | } 441 | } 442 | 443 | return pm; 444 | } 445 | 446 | private static List normalize(List l) { 447 | List pl = new ArrayList(); 448 | 449 | for (Object v : l) { 450 | if (v == null) { 451 | pl.add(v); 452 | 453 | } else if (v instanceof List) { 454 | pl.add(normalize((List) v)); 455 | 456 | } else if (v instanceof Map) { 457 | pl.add(normalize((Map) v)); 458 | 459 | } else if (v instanceof String || 460 | v instanceof Double || 461 | v instanceof Float || 462 | v instanceof Number || 463 | v instanceof Boolean) { 464 | pl.add(v); 465 | 466 | } else { 467 | pl.add(v.toString()); 468 | } 469 | } 470 | return pl; 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /gateway-android/src/main/java/com/mastercard/gateway/android/sdk/GatewayRequest.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sdk; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | class GatewayRequest { 7 | 8 | String url; 9 | Gateway.Method method; 10 | 11 | Map extraHeaders = new HashMap<>(); 12 | 13 | GatewayMap payload; 14 | } 15 | -------------------------------------------------------------------------------- /gateway-android/src/main/java/com/mastercard/gateway/android/sdk/GatewaySSLContextProvider.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sdk; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.InputStream; 5 | import java.security.KeyStore; 6 | import java.security.cert.CertificateException; 7 | import java.security.cert.CertificateFactory; 8 | import java.security.cert.X509Certificate; 9 | 10 | import javax.net.ssl.SSLContext; 11 | import javax.net.ssl.TrustManagerFactory; 12 | 13 | class GatewaySSLContextProvider { 14 | 15 | //Root certificate: Entrust G2 expires 18th Dec 2030 https://www.entrust.com/resources/certificate-solutions/tools/root-certificate-downloads 16 | static final String ROOT_CERTIFICATE_ENTRUST = 17 | "MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC\n" + 18 | "VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50\n" + 19 | "cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs\n" + 20 | "IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz\n" + 21 | "dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy\n" + 22 | "NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu\n" + 23 | "dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt\n" + 24 | "dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0\n" + 25 | "aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj\n" + 26 | "YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\n" + 27 | "AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T\n" + 28 | "RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN\n" + 29 | "cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW\n" + 30 | "wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1\n" + 31 | "U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0\n" + 32 | "jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP\n" + 33 | "BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN\n" + 34 | "BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/\n" + 35 | "jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ\n" + 36 | "Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v\n" + 37 | "1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R\n" + 38 | "nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH\n" + 39 | "VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g==\n"; 40 | 41 | // Root certificate: DigiCert Global Root CA expires in 15th Jan 2038. 42 | static final String ROOT_CERTIFICATE_DIGICERT = 43 | "MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\n" + 44 | "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" + 45 | "d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n" + 46 | "MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\n" + 47 | "MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n" + 48 | "b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n" + 49 | "9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n" + 50 | "2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n" + 51 | "1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\n" + 52 | "q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\n" + 53 | "tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\n" + 54 | "vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\n" + 55 | "BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n" + 56 | "5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n" + 57 | "1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\n" + 58 | "NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\n" + 59 | "Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n" + 60 | "8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\n" + 61 | "pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\n" + 62 | "MrY=\n"; 63 | 64 | public GatewaySSLContextProvider() { 65 | } 66 | 67 | SSLContext createSSLContext() throws Exception { 68 | // create and initialize a KeyStore 69 | KeyStore keyStore = createKeyStore(); 70 | 71 | // create a TrustManager that trusts the INTERMEDIATE_CA in our KeyStore 72 | TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 73 | tmf.init(keyStore); 74 | 75 | SSLContext context = SSLContext.getInstance("TLSv1.2"); 76 | context.init(null, tmf.getTrustManagers(), null); 77 | 78 | return context; 79 | } 80 | 81 | KeyStore createKeyStore() throws Exception { 82 | KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 83 | keyStore.load(null, null); 84 | 85 | // add our trusted cert to the keystore 86 | keyStore.setCertificateEntry("gateway.mastercard.com.ca_entrust", readCertificate(ROOT_CERTIFICATE_ENTRUST)); 87 | keyStore.setCertificateEntry("gateway.mastercard.com.ca_digicert", readCertificate(ROOT_CERTIFICATE_DIGICERT)); 88 | 89 | return keyStore; 90 | } 91 | 92 | X509Certificate readCertificate(String cert) throws CertificateException { 93 | String updatedCert = "-----BEGIN CERTIFICATE-----\n" + cert + "-----END CERTIFICATE-----\n"; 94 | byte[] bytes = updatedCert.getBytes(); 95 | InputStream is = new ByteArrayInputStream(bytes); 96 | 97 | return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /gateway-android/src/main/java/com/mastercard/gateway/android/sdk/GatewayTLSSocketFactory.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sdk; 2 | 3 | import java.io.IOException; 4 | import java.net.InetAddress; 5 | import java.net.Socket; 6 | import java.net.UnknownHostException; 7 | 8 | import javax.net.ssl.SSLContext; 9 | import javax.net.ssl.SSLSocket; 10 | import javax.net.ssl.SSLSocketFactory; 11 | 12 | /** 13 | * Custom SSL socket factory required to enable TLSv1.2 on KitKat devices and below 14 | */ 15 | class GatewayTLSSocketFactory extends SSLSocketFactory { 16 | 17 | private SSLSocketFactory internalSSLSocketFactory; 18 | 19 | public GatewayTLSSocketFactory(SSLContext context) { 20 | internalSSLSocketFactory = context.getSocketFactory(); 21 | } 22 | 23 | @Override 24 | public String[] getDefaultCipherSuites() { 25 | return internalSSLSocketFactory.getDefaultCipherSuites(); 26 | } 27 | 28 | @Override 29 | public String[] getSupportedCipherSuites() { 30 | return internalSSLSocketFactory.getSupportedCipherSuites(); 31 | } 32 | 33 | @Override 34 | public Socket createSocket() throws IOException { 35 | return enableTLSOnSocket(internalSSLSocketFactory.createSocket()); 36 | } 37 | 38 | @Override 39 | public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { 40 | return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)); 41 | } 42 | 43 | @Override 44 | public Socket createSocket(String host, int port) throws IOException, UnknownHostException { 45 | return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); 46 | } 47 | 48 | @Override 49 | public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { 50 | return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)); 51 | } 52 | 53 | @Override 54 | public Socket createSocket(InetAddress host, int port) throws IOException { 55 | return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); 56 | } 57 | 58 | @Override 59 | public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { 60 | return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); 61 | } 62 | 63 | private Socket enableTLSOnSocket(Socket socket) { 64 | if(socket != null && (socket instanceof SSLSocket)) { 65 | ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.1", "TLSv1.2"}); 66 | } 67 | return socket; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /gateway-android/src/main/java/com/mastercard/gateway/android/sdk/Logger.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Mastercard 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.mastercard.gateway.android.sdk; 18 | 19 | 20 | import javax.net.ssl.HttpsURLConnection; 21 | 22 | interface Logger { 23 | void logRequest(HttpsURLConnection c, String data); 24 | void logResponse(HttpsURLConnection c, String data); 25 | void logDebug(String message); 26 | } 27 | -------------------------------------------------------------------------------- /gateway-android/src/main/res/layout/activity_3dsecure.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 13 | 14 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /gateway-android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 3-D Secure Authentication 4 | Missing summary status 5 | Missing 3-D Secure Id 6 | -------------------------------------------------------------------------------- /gateway-android/src/release/java/com/mastercard/gateway/android/sdk/BaseLogger.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sdk; 2 | 3 | 4 | import javax.net.ssl.HttpsURLConnection; 5 | 6 | 7 | class BaseLogger implements Logger { 8 | 9 | @Override 10 | public void logRequest(HttpsURLConnection c, String data) { 11 | // no-op 12 | } 13 | 14 | @Override 15 | public void logResponse(HttpsURLConnection c, String data) { 16 | // no-op 17 | } 18 | 19 | @Override 20 | public void logDebug(String message) { 21 | // no-op 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gateway-android/src/test/java/com/mastercard/gateway/android/sdk/Gateway3DSecureActivityTest.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sdk; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | import android.webkit.WebView; 7 | 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.robolectric.Robolectric; 12 | import org.robolectric.RobolectricTestRunner; 13 | import org.robolectric.android.controller.ActivityController; 14 | import org.robolectric.annotation.Config; 15 | 16 | import static org.junit.Assert.assertEquals; 17 | import static org.junit.Assert.assertNotEquals; 18 | import static org.junit.Assert.assertNull; 19 | import static org.junit.Assert.assertTrue; 20 | import static org.mockito.ArgumentMatchers.any; 21 | import static org.mockito.Mockito.doNothing; 22 | import static org.mockito.Mockito.doReturn; 23 | import static org.mockito.Mockito.mock; 24 | import static org.mockito.Mockito.spy; 25 | import static org.mockito.Mockito.verify; 26 | 27 | 28 | @RunWith(RobolectricTestRunner.class) 29 | @Config(application = TestApplication.class) 30 | public class Gateway3DSecureActivityTest { 31 | 32 | Gateway3DSecureActivity activity; 33 | 34 | @Before 35 | public void setUp() throws Exception { 36 | ActivityController activityController = spy(Robolectric.buildActivity(Gateway3DSecureActivity.class)); 37 | 38 | activity = spy(activityController.get()); 39 | doNothing().when(activity).onBackPressed(); 40 | } 41 | 42 | @Test 43 | public void testInitCallsBackPressIfHtmlMissing() throws Exception { 44 | doReturn(null).when(activity).getExtraHtml(); 45 | 46 | activity.init(); 47 | 48 | verify(activity).onBackPressed(); 49 | } 50 | 51 | @Test 52 | public void testInitSetsDefaultTitleIfExtraTitleMissing() throws Exception { 53 | String html = ""; 54 | String defaultTitle = "default title"; 55 | String extraTitle = null; 56 | 57 | doReturn(html).when(activity).getExtraHtml(); 58 | doReturn(defaultTitle).when(activity).getDefaultTitle(); 59 | doReturn(extraTitle).when(activity).getExtraTitle(); 60 | doNothing().when(activity).setToolbarTitle(any()); 61 | doNothing().when(activity).setWebViewHtml(any()); 62 | 63 | activity.init(); 64 | 65 | verify(activity).setToolbarTitle(defaultTitle); 66 | } 67 | 68 | @Test 69 | public void testInitWorksAsExpected() throws Exception { 70 | String html = ""; 71 | String defaultTitle = "default title"; 72 | String extraTitle = "extra title"; 73 | 74 | doReturn(html).when(activity).getExtraHtml(); 75 | doReturn(defaultTitle).when(activity).getDefaultTitle(); 76 | doReturn(extraTitle).when(activity).getExtraTitle(); 77 | doNothing().when(activity).setToolbarTitle(any()); 78 | doNothing().when(activity).setWebViewHtml(any()); 79 | 80 | activity.init(); 81 | 82 | verify(activity).setWebViewHtml(html); 83 | verify(activity).setToolbarTitle(extraTitle); 84 | } 85 | 86 | @Test 87 | public void testGetExtraTitleReturnsNullIfMissing() { 88 | Intent testIntent = new Intent(); 89 | doReturn(testIntent).when(activity).getIntent(); 90 | 91 | String title = activity.getExtraTitle(); 92 | 93 | assertNull(title); 94 | } 95 | 96 | @Test 97 | public void testGetExtraTitleReturnsValueIfExists() { 98 | String expectedTitle = "My Title"; 99 | Intent testIntent = new Intent(); 100 | testIntent.putExtra(Gateway3DSecureActivity.EXTRA_TITLE, expectedTitle); 101 | doReturn(testIntent).when(activity).getIntent(); 102 | 103 | String title = activity.getExtraTitle(); 104 | 105 | assertEquals(expectedTitle, title); 106 | } 107 | 108 | @Test 109 | public void testGetExtraHtmlReturnsNullIfMissing() { 110 | Intent testIntent = new Intent(); 111 | doReturn(testIntent).when(activity).getIntent(); 112 | 113 | String html = activity.getExtraHtml(); 114 | 115 | assertNull(html); 116 | } 117 | 118 | @Test 119 | public void testGetExtraHtmlReturnsValueIfExists() { 120 | String expectedHtml = ""; 121 | Intent testIntent = new Intent(); 122 | testIntent.putExtra(Gateway3DSecureActivity.EXTRA_HTML, expectedHtml); 123 | doReturn(testIntent).when(activity).getIntent(); 124 | 125 | String html = activity.getExtraHtml(); 126 | 127 | assertEquals(expectedHtml, html); 128 | } 129 | 130 | @Test 131 | public void testSetWebViewHtmlEncodesBase64() { 132 | String testHtml = ""; 133 | String expectedEncodedHtml = "PGh0bWw+PC9odG1sPg"; 134 | 135 | activity.webView = mock(WebView.class); 136 | activity.setWebViewHtml(testHtml); 137 | 138 | verify(activity.webView).loadData(expectedEncodedHtml, "text/html", "base64"); 139 | } 140 | 141 | @Test 142 | public void testIntentToEmailWorksAsExpected() { 143 | Uri testUri = mock(Uri.class); 144 | Intent testIntent = new Intent(); 145 | 146 | activity.intentToEmail(testUri, testIntent); 147 | 148 | int flags = testIntent.getFlags(); 149 | 150 | assertNotEquals(0, flags & Intent.FLAG_ACTIVITY_NEW_TASK); 151 | assertEquals(testUri, testIntent.getData()); 152 | verify(activity).startActivity(testIntent); 153 | } 154 | 155 | @Test 156 | public void testCompleteWorksAsExpected() { 157 | String testAcsResult = "test result"; 158 | Intent testIntent = new Intent(); 159 | 160 | activity.complete(testAcsResult, testIntent); 161 | 162 | assertTrue(testIntent.hasExtra(Gateway3DSecureActivity.EXTRA_ACS_RESULT)); 163 | assertEquals(testAcsResult, testIntent.getStringExtra(Gateway3DSecureActivity.EXTRA_ACS_RESULT)); 164 | verify(activity).setResult(Activity.RESULT_OK, testIntent); 165 | verify(activity).finish(); 166 | } 167 | 168 | @Test 169 | public void testWebViewUrlChangesCallCompleteOnCorrectScheme() { 170 | Uri testUri = Uri.parse("gatewaysdk://3dsecure?irrelevant1=something&acsResult={}&irrelevant2=something"); 171 | String testResult = "acs result"; 172 | 173 | doReturn(testResult).when(activity).getACSResultFromUri(testUri); 174 | 175 | activity.webViewUrlChanges(testUri); 176 | 177 | verify(activity).complete(testResult); 178 | } 179 | 180 | @Test 181 | public void testWebViewUrlChangesCallIntentToEmailOnMailtoScheme() { 182 | Uri testUri = Uri.parse("mailto://something"); 183 | 184 | doNothing().when(activity).intentToEmail(testUri); 185 | 186 | activity.webViewUrlChanges(testUri); 187 | 188 | verify(activity).intentToEmail(testUri); 189 | } 190 | 191 | @Test 192 | public void testWebViewUrlChangesPassesThruUriIfNoSchemeMatch() { 193 | Uri testUri = Uri.parse("https://www.google.com"); 194 | 195 | doNothing().when(activity).loadWebViewUrl(testUri); 196 | 197 | activity.webViewUrlChanges(testUri); 198 | 199 | verify(activity).loadWebViewUrl(testUri); 200 | } 201 | 202 | @Test 203 | public void testGetAcsResultFromUriWorksAsExpected() { 204 | Uri testUri = Uri.parse("gatewaysdk://3dsecure?irrelevant1=something&acsResult={}&irrelevant2=something"); 205 | 206 | String result = activity.getACSResultFromUri(testUri); 207 | 208 | assertEquals("{}", result); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /gateway-android/src/test/java/com/mastercard/gateway/android/sdk/GatewayTest.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sdk; 2 | 3 | 4 | import android.app.Activity; 5 | import android.content.Intent; 6 | 7 | import com.google.android.gms.common.api.Status; 8 | import com.google.android.gms.wallet.AutoResolveHelper; 9 | import com.google.android.gms.wallet.PaymentData; 10 | 11 | import org.apache.tools.ant.filters.StringInputStream; 12 | import org.json.JSONObject; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.robolectric.RobolectricTestRunner; 17 | import org.robolectric.annotation.Config; 18 | 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.net.URL; 22 | import java.security.KeyStore; 23 | import java.security.cert.X509Certificate; 24 | import java.util.List; 25 | import java.util.Map; 26 | 27 | import javax.net.ssl.HttpsURLConnection; 28 | import javax.net.ssl.SSLContext; 29 | import javax.net.ssl.SSLSocketFactory; 30 | 31 | import io.reactivex.Single; 32 | 33 | import static org.junit.Assert.assertEquals; 34 | import static org.junit.Assert.assertFalse; 35 | import static org.junit.Assert.assertNotNull; 36 | import static org.junit.Assert.assertTrue; 37 | import static org.junit.Assert.fail; 38 | import static org.mockito.Matchers.any; 39 | import static org.mockito.Mockito.doNothing; 40 | import static org.mockito.Mockito.doReturn; 41 | import static org.mockito.Mockito.mock; 42 | import static org.mockito.Mockito.spy; 43 | import static org.mockito.Mockito.verify; 44 | 45 | @RunWith(RobolectricTestRunner.class) 46 | @Config(manifest = Config.NONE) 47 | public class GatewayTest { 48 | 49 | Gateway gateway; 50 | 51 | @Before 52 | public void setUp() throws Exception { 53 | gateway = spy(new Gateway()); 54 | } 55 | 56 | @Test 57 | public void testSetMerchantIdThrowsExceptionIfNull() throws Exception { 58 | try { 59 | gateway.setMerchantId(null); 60 | 61 | fail("Null merchant ID should throw illegal argument exception"); 62 | } catch (Exception e) { 63 | assertTrue(e instanceof IllegalArgumentException); 64 | } 65 | } 66 | 67 | @Test 68 | public void setMerchantIdWorksAsExpected() throws Exception { 69 | gateway.setMerchantId("MERCHANT_ID"); 70 | 71 | assertEquals(gateway.merchantId, "MERCHANT_ID"); 72 | } 73 | 74 | @Test 75 | public void testSetRegionThrowsExceptionIfNull() throws Exception { 76 | try { 77 | gateway.setRegion(null); 78 | 79 | fail("Null region should throw illegal argument exception"); 80 | } catch (Exception e) { 81 | assertTrue(e instanceof IllegalArgumentException); 82 | } 83 | } 84 | 85 | @Test 86 | public void testSetRegionWorksAsIntended() throws Exception { 87 | gateway.setRegion(Gateway.Region.ASIA_PACIFIC); 88 | 89 | assertEquals(Gateway.Region.ASIA_PACIFIC, gateway.region); 90 | } 91 | 92 | @Test 93 | public void testBuildUpdateSessionRequestWorksAsExpected() { 94 | String sessionId = "session_id"; 95 | String apiVersion = "1"; 96 | GatewayMap payload = new GatewayMap(); 97 | 98 | String expectedUrl = "some url"; 99 | 100 | doReturn(expectedUrl).when(gateway).getUpdateSessionUrl(sessionId, apiVersion); 101 | 102 | GatewayRequest request = gateway.buildUpdateSessionRequest(sessionId, apiVersion, payload); 103 | 104 | assertTrue(request.payload.containsKey("device.browser")); 105 | assertTrue(request.payload.containsKey("apiOperation")); 106 | assertEquals(Gateway.API_OPERATION, request.payload.get("apiOperation")); 107 | assertEquals(Gateway.USER_AGENT, request.payload.get("device.browser")); 108 | } 109 | 110 | @Test 111 | public void testBuildUpdateSessionRequestHandlesApiVersion50() { 112 | String sessionId = "somesession"; 113 | gateway.merchantId = "MERCHANT_ID"; 114 | 115 | String apiVersion = "50"; 116 | GatewayMap payload = new GatewayMap(); 117 | 118 | String expectedUrl = "some url"; 119 | String expectedAuthHeader = "Basic bWVyY2hhbnQuTUVSQ0hBTlRfSUQ6c29tZXNlc3Npb24="; 120 | 121 | doReturn(expectedUrl).when(gateway).getUpdateSessionUrl(sessionId, apiVersion); 122 | 123 | GatewayRequest request = gateway.buildUpdateSessionRequest(sessionId, apiVersion, payload); 124 | 125 | assertTrue(request.payload.containsKey("device.browser")); 126 | assertFalse(request.payload.containsKey("apiOperation")); 127 | assertEquals(Gateway.USER_AGENT, request.payload.get("device.browser")); 128 | 129 | assertTrue(request.extraHeaders.containsKey("Authorization")); 130 | assertEquals(expectedAuthHeader, request.extraHeaders.get("Authorization")); 131 | } 132 | 133 | @Test 134 | public void testStart3DSecureActivitySkipsTitleIfNull() { 135 | Activity activity = mock(Activity.class); 136 | Intent intent = new Intent(); 137 | String testHtml = "html"; 138 | 139 | Gateway.start3DSecureActivity(activity, testHtml, null, intent); 140 | 141 | verify(activity).startActivityForResult(intent, Gateway.REQUEST_3D_SECURE); 142 | assertTrue(intent.hasExtra(Gateway3DSecureActivity.EXTRA_HTML)); 143 | assertFalse(intent.hasExtra(Gateway3DSecureActivity.EXTRA_TITLE)); 144 | assertEquals(testHtml, intent.getStringExtra(Gateway3DSecureActivity.EXTRA_HTML)); 145 | } 146 | 147 | @Test 148 | public void testStart3DSecureActivityWorksAsExpected() { 149 | Activity activity = mock(Activity.class); 150 | Intent intent = new Intent(); 151 | String testHtml = "html"; 152 | String testTitle = "title"; 153 | 154 | Gateway.start3DSecureActivity(activity, testHtml, testTitle, intent); 155 | 156 | verify(activity).startActivityForResult(intent, Gateway.REQUEST_3D_SECURE); 157 | assertTrue(intent.hasExtra(Gateway3DSecureActivity.EXTRA_HTML)); 158 | assertTrue(intent.hasExtra(Gateway3DSecureActivity.EXTRA_TITLE)); 159 | assertEquals(testHtml, intent.getStringExtra(Gateway3DSecureActivity.EXTRA_HTML)); 160 | assertEquals(testTitle, intent.getStringExtra(Gateway3DSecureActivity.EXTRA_TITLE)); 161 | } 162 | 163 | @Test 164 | public void testHandle3DSecureResultReturnsFalseWithNullCallback() { 165 | assertFalse(Gateway.handle3DSecureResult(0, 0, null, null)); 166 | } 167 | 168 | @Test 169 | public void testHandle3DSSecureResultReturnsFalseIfInvalidRequestCode() { 170 | int invalidRequestCode = 10; 171 | Gateway3DSecureCallback callback = mock(Gateway3DSecureCallback.class); 172 | 173 | assertFalse(Gateway.handle3DSecureResult(invalidRequestCode, 0, null, callback)); 174 | } 175 | 176 | @Test 177 | public void testHandle3DSecureResultCallsCancelIfResultNotOk() { 178 | int validRequestCode = Gateway.REQUEST_3D_SECURE; 179 | int resultCode = Activity.RESULT_CANCELED; 180 | Gateway3DSecureCallback callback = mock(Gateway3DSecureCallback.class); 181 | 182 | boolean result = Gateway.handle3DSecureResult(validRequestCode, resultCode, null, callback); 183 | 184 | assertTrue(result); 185 | verify(callback).on3DSecureCancel(); 186 | } 187 | 188 | @Test 189 | public void testHandle3DSecureResultCallsCompleteIfResultOK() { 190 | int validRequestCode = Gateway.REQUEST_3D_SECURE; 191 | int resultCode = Activity.RESULT_OK; 192 | Intent data = mock(Intent.class); 193 | String acsResultJson = "{\"foo\":\"bar\"}"; 194 | 195 | Gateway3DSecureCallback callback = spy(new Gateway3DSecureCallback() { 196 | @Override 197 | public void on3DSecureComplete(GatewayMap response) { 198 | assertNotNull(response); 199 | assertTrue(response.containsKey("foo")); 200 | assertEquals("bar", response.get("foo")); 201 | } 202 | 203 | @Override 204 | public void on3DSecureCancel() { 205 | fail("Should never have called cancel"); 206 | } 207 | }); 208 | 209 | doReturn(acsResultJson).when(data).getStringExtra(Gateway3DSecureActivity.EXTRA_ACS_RESULT); 210 | 211 | 212 | boolean result = Gateway.handle3DSecureResult(validRequestCode, resultCode, data, callback); 213 | 214 | assertTrue(result); 215 | verify(callback).on3DSecureComplete(any()); 216 | } 217 | 218 | @Test 219 | public void testHandleGooglePayResultReturnsFalseWithNullCallback() { 220 | assertFalse(Gateway.handleGooglePayResult(0, 0, null, null)); 221 | } 222 | 223 | @Test 224 | public void testHandleGooglePayResultReturnsFalseIfInvalidRequestCode() { 225 | int invalidRequestCode = 10; 226 | GatewayGooglePayCallback callback = mock(GatewayGooglePayCallback.class); 227 | 228 | assertFalse(Gateway.handleGooglePayResult(invalidRequestCode, 0, null, callback)); 229 | } 230 | 231 | @Test 232 | public void testHandleGooglePayResultCallsError() { 233 | int requestCode = Gateway.REQUEST_GOOGLE_PAY_LOAD_PAYMENT_DATA; 234 | int resultCode = AutoResolveHelper.RESULT_ERROR; 235 | 236 | // mock autoresolvehelper method 237 | Status mockStatus = mock(Status.class); 238 | Intent mockData = mock(Intent.class); 239 | doReturn(mockStatus).when(mockData).getParcelableExtra("com.google.android.gms.common.api.AutoResolveHelper.status"); 240 | 241 | GatewayGooglePayCallback callback = spy(new GatewayGooglePayCallback() { 242 | @Override 243 | public void onReceivedPaymentData(JSONObject paymentData) { 244 | fail("Should not have received payment data"); 245 | } 246 | 247 | @Override 248 | public void onGooglePayCancelled() { 249 | fail("Should not have called cancelled"); 250 | } 251 | 252 | @Override 253 | public void onGooglePayError(Status status) { 254 | assertEquals(mockStatus, status); 255 | } 256 | }); 257 | 258 | boolean result = Gateway.handleGooglePayResult(requestCode, resultCode, mockData, callback); 259 | 260 | assertTrue(result); 261 | verify(callback).onGooglePayError(any()); 262 | } 263 | 264 | @Test 265 | public void testHandleGooglePayResultCallsCancelled() { 266 | int requestCode = Gateway.REQUEST_GOOGLE_PAY_LOAD_PAYMENT_DATA; 267 | int resultCode = Activity.RESULT_CANCELED; 268 | 269 | GatewayGooglePayCallback callback = mock(GatewayGooglePayCallback.class); 270 | 271 | boolean result = Gateway.handleGooglePayResult(requestCode, resultCode, null, callback); 272 | 273 | assertTrue(result); 274 | verify(callback).onGooglePayCancelled(); 275 | } 276 | 277 | @Test 278 | public void testHandleGooglePayResultCallsPaymentDataOnSuccess() { 279 | int requestCode = Gateway.REQUEST_GOOGLE_PAY_LOAD_PAYMENT_DATA; 280 | int resultCode = Activity.RESULT_OK; 281 | PaymentData pData = PaymentData.fromJson("{}"); 282 | Intent data = new Intent(); 283 | pData.putIntoIntent(data); 284 | 285 | GatewayGooglePayCallback callback = mock(GatewayGooglePayCallback.class); 286 | 287 | boolean result = Gateway.handleGooglePayResult(requestCode, resultCode, data, callback); 288 | 289 | assertTrue(result); 290 | verify(callback).onReceivedPaymentData(any()); 291 | } 292 | 293 | @Test 294 | public void testGetApiUrlThrowsExceptionIfRegionIsNull() throws Exception { 295 | String apiVersion = "44"; 296 | gateway.region = null; 297 | 298 | try { 299 | String apiUrl = gateway.getApiUrl(apiVersion); 300 | 301 | fail("Null region should have caused illegal state exception"); 302 | } catch (Exception e) { 303 | assertTrue(e instanceof IllegalStateException); 304 | } 305 | } 306 | 307 | @Test 308 | public void testGetApiUrlThrowsExceptionIfApiVersionIsLessThanMin() throws Exception { 309 | String apiVersion = String.valueOf(Gateway.MIN_API_VERSION - 1); 310 | 311 | try { 312 | String apiUrl = gateway.getApiUrl(apiVersion); 313 | 314 | fail("Api version less than minimum value should have caused illegal argument exception"); 315 | } catch (Exception e) { 316 | assertTrue(e instanceof IllegalArgumentException); 317 | } 318 | } 319 | 320 | 321 | @Test 322 | public void testGetApiUrlWorksAsIntended() throws Exception { 323 | gateway.region = Gateway.Region.NORTH_AMERICA; 324 | String expectedUrl = "https://na.gateway.mastercard.com/api/rest/version/" + Gateway.MIN_API_VERSION; 325 | 326 | assertEquals(expectedUrl, gateway.getApiUrl(String.valueOf(Gateway.MIN_API_VERSION))); 327 | } 328 | 329 | @Test 330 | public void testGetUpdateSessionUrlThrowsExceptionIfSessionIdIsNull() throws Exception { 331 | try { 332 | gateway.getUpdateSessionUrl(null, String.valueOf(Gateway.MIN_API_VERSION)); 333 | 334 | fail("Null session id should throw illegal argument exception"); 335 | } catch (Exception e) { 336 | assertTrue(e instanceof IllegalArgumentException); 337 | } 338 | } 339 | 340 | 341 | @Test 342 | public void testGetUpdateSessionUrlThrowsExceptionIfMerchantIdIsNull() throws Exception { 343 | gateway.merchantId = null; 344 | 345 | try { 346 | String url = gateway.getUpdateSessionUrl("sess1234", String.valueOf(Gateway.MIN_API_VERSION)); 347 | 348 | fail("Null merchant id should have caused illegal state exception"); 349 | } catch (Exception e) { 350 | assertTrue(e instanceof IllegalStateException); 351 | } 352 | } 353 | 354 | @Test 355 | public void testGetUpdateSessionUrlWorksAsIntended() throws Exception { 356 | gateway.merchantId = "somemerchant"; 357 | gateway.region = Gateway.Region.NORTH_AMERICA; 358 | String expectedUrl = "https://na.gateway.mastercard.com/api/rest/version/" + Gateway.MIN_API_VERSION + "/merchant/somemerchant/session/sess1234"; 359 | 360 | String actualUrl = gateway.getUpdateSessionUrl("sess1234", String.valueOf(Gateway.MIN_API_VERSION)); 361 | 362 | assertEquals(expectedUrl, actualUrl); 363 | } 364 | 365 | @Test 366 | public void testHandleCallbackMessageCallsOnErrorWithThrowableArg() throws Exception { 367 | GatewayCallback callback = mock(GatewayCallback.class); 368 | Throwable arg = new Exception("Some exception"); 369 | 370 | gateway.handleCallbackMessage(callback, arg); 371 | 372 | verify(callback).onError(arg); 373 | } 374 | 375 | @Test 376 | public void testHandleCallbackMessageCallsSuccessWithNonThrowableArg() throws Exception { 377 | GatewayCallback callback = mock(GatewayCallback.class); 378 | GatewayMap arg = mock(GatewayMap.class); 379 | 380 | gateway.handleCallbackMessage(callback, arg); 381 | 382 | verify(callback).onSuccess(arg); 383 | } 384 | 385 | @Test 386 | public void testCreateConnectionWorksAsIntended() throws Exception { 387 | GatewayRequest request = new GatewayRequest(); 388 | request.url = "https://www.mastercard.com"; 389 | request.method = Gateway.Method.PUT; 390 | 391 | SSLSocketFactory socketFactory = mock(SSLSocketFactory.class); 392 | doReturn(socketFactory).when(gateway).createSocketFactory(); 393 | 394 | HttpsURLConnection c = gateway.createHttpsUrlConnection(request); 395 | 396 | assertEquals(request.url, c.getURL().toString()); 397 | assertEquals(socketFactory, c.getSSLSocketFactory()); 398 | assertEquals(Gateway.CONNECTION_TIMEOUT, c.getConnectTimeout()); 399 | assertEquals(Gateway.READ_TIMEOUT, c.getReadTimeout()); 400 | assertEquals("PUT", c.getRequestMethod()); 401 | assertEquals(Gateway.USER_AGENT, c.getRequestProperty("User-Agent")); 402 | assertEquals("application/json", c.getRequestProperty("Content-Type")); 403 | assertTrue(c.getDoOutput()); 404 | } 405 | 406 | @Test 407 | public void testIsStatusOkWorksAsIntended() { 408 | int tooLow = 199; 409 | int tooHigh = 300; 410 | int justRight = 200; 411 | 412 | assertFalse(gateway.isStatusCodeOk(tooLow)); 413 | assertFalse(gateway.isStatusCodeOk(tooHigh)); 414 | assertTrue(gateway.isStatusCodeOk(justRight)); 415 | } 416 | 417 | @Test 418 | public void testInputStreamToStringWorksAsExpected() { 419 | String expectedResult = "here is some string data"; 420 | InputStream testInputStream = new StringInputStream(expectedResult); 421 | 422 | try { 423 | String result = gateway.inputStreamToString(testInputStream); 424 | assertEquals(expectedResult, result); 425 | } catch (IOException e) { 426 | fail(e.getMessage()); 427 | } 428 | } 429 | 430 | @Test 431 | public void testCreateAuthHeaderWorksAsExpected() { 432 | String sessionId = "somesession"; 433 | gateway.merchantId = "MERCHANT_ID"; 434 | 435 | String expectedAuthHeader = "Basic bWVyY2hhbnQuTUVSQ0hBTlRfSUQ6c29tZXNlc3Npb24="; 436 | 437 | String authHeader = gateway.createAuthHeader(sessionId); 438 | 439 | assertEquals(expectedAuthHeader, authHeader); 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /gateway-android/src/test/java/com/mastercard/gateway/android/sdk/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sdk; 2 | 3 | import android.app.Application; 4 | 5 | public class TestApplication extends Application { 6 | 7 | @Override 8 | public void onCreate() { 9 | super.onCreate(); 10 | setTheme(R.style.Theme_AppCompat); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /gateway-android/src/test/java/com/mastercard/gateway/android/sdk/TestGatewaySSLContextProvider.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sdk; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.robolectric.RobolectricTestRunner; 7 | import org.robolectric.annotation.Config; 8 | 9 | import java.security.KeyStore; 10 | import java.security.cert.X509Certificate; 11 | 12 | import static org.junit.Assert.assertEquals; 13 | import static org.junit.Assert.assertNotNull; 14 | import static org.junit.Assert.assertTrue; 15 | import static org.mockito.ArgumentMatchers.any; 16 | import static org.mockito.Mockito.doReturn; 17 | import static org.mockito.Mockito.mock; 18 | import static org.mockito.Mockito.spy; 19 | 20 | @RunWith(RobolectricTestRunner.class) 21 | @Config(manifest = Config.NONE) 22 | public class TestGatewaySSLContextProvider { 23 | 24 | GatewaySSLContextProvider trustProvider; 25 | 26 | @Before 27 | public void setUp() throws Exception { 28 | trustProvider = spy(new GatewaySSLContextProvider()); 29 | } 30 | 31 | @Test 32 | public void testCreateSslKeystoreContainsInternalCertificate() throws Exception { 33 | doReturn(mock(X509Certificate.class)).when(trustProvider).readCertificate(any()); 34 | 35 | KeyStore keyStore = trustProvider.createKeyStore(); 36 | 37 | assertTrue(keyStore.containsAlias("gateway.mastercard.com.ca_entrust")); 38 | assertTrue(keyStore.containsAlias("gateway.mastercard.com.ca_digicert")); 39 | } 40 | 41 | @Test 42 | public void testReadingRootEntrustCertificateWorksAsExpected() throws Exception { 43 | X509Certificate certificate = trustProvider.readCertificate(GatewaySSLContextProvider.ROOT_CERTIFICATE_ENTRUST); 44 | String expectedSerialNo = "1246989352"; 45 | 46 | assertNotNull(certificate); 47 | assertEquals(expectedSerialNo, certificate.getSerialNumber().toString()); 48 | } 49 | 50 | @Test 51 | public void testReadingRootDigiCertificateWorksAsExpected() throws Exception { 52 | X509Certificate certificate = trustProvider.readCertificate(GatewaySSLContextProvider.ROOT_CERTIFICATE_DIGICERT); 53 | String expectedSerialNo = "10944719598952040374951832963794454346"; 54 | 55 | assertNotNull(certificate); 56 | assertEquals(expectedSerialNo, certificate.getSerialNumber().toString()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /gateway-android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /gateway-android/src/test/resources/robolectric.properties: -------------------------------------------------------------------------------- 1 | sdk=28 -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.daemon=true 2 | org.gradle.configureondemand=false 3 | org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 4 | android.useAndroidX=true 5 | android.enableJetifier=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/5fa0049b3e530ff2bdee6d1feba5a08258422e19/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jan 18 11:11:31 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /payment-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/5fa0049b3e530ff2bdee6d1feba5a08258422e19/payment-flow.png -------------------------------------------------------------------------------- /sample-configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/5fa0049b3e530ff2bdee6d1feba5a08258422e19/sample-configuration.png -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 29 5 | 6 | defaultConfig { 7 | applicationId 'com.mastercard.gateway.android.sampleapp' 8 | minSdkVersion 19 9 | targetSdkVersion 29 10 | versionCode 1 11 | versionName '1.0.0' 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | 15 | vectorDrawables { 16 | useSupportLibrary true 17 | } 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | 27 | dataBinding { 28 | enabled = true 29 | } 30 | 31 | lintOptions { 32 | abortOnError false 33 | } 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_1_8 36 | targetCompatibility JavaVersion.VERSION_1_8 37 | } 38 | } 39 | 40 | dependencies { 41 | implementation fileTree(include: ['*.jar'], dir: 'libs') 42 | implementation 'com.google.code.gson:gson:2.8.2' 43 | implementation 'androidx.appcompat:appcompat:1.1.0' 44 | implementation 'com.google.android.material:material:1.1.0' 45 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 46 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 47 | implementation 'com.google.android.gms:play-services-wallet:18.0.0' 48 | implementation 'blue.aodev:material-values:1.1.1' 49 | 50 | implementation project(':gateway-android') 51 | 52 | // Espresso testing 53 | androidTestImplementation 'junit:junit:4.12' 54 | androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0') { 55 | exclude module: 'support-annotations' 56 | } 57 | androidTestImplementation('androidx.test:runner:1.1.0') { 58 | exclude module: 'support-annotations' 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /sample/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 C:\Users\e036307\AppData\Local\Android\Sdk/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 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mastercard/gateway/android/sampleapp/ApiController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Mastercard 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.mastercard.gateway.android.sampleapp; 18 | 19 | import android.os.Handler; 20 | import android.os.Message; 21 | import androidx.annotation.BoolRes; 22 | import android.util.Base64; 23 | import android.util.Log; 24 | import android.util.Pair; 25 | 26 | import com.google.gson.Gson; 27 | import com.google.gson.GsonBuilder; 28 | import com.mastercard.gateway.android.sdk.GatewayMap; 29 | 30 | import java.io.BufferedReader; 31 | import java.io.IOException; 32 | import java.io.InputStream; 33 | import java.io.InputStreamReader; 34 | import java.io.PrintWriter; 35 | import java.net.HttpURLConnection; 36 | import java.net.MalformedURLException; 37 | import java.net.SocketTimeoutException; 38 | import java.net.URL; 39 | import java.security.KeyManagementException; 40 | import java.security.NoSuchAlgorithmException; 41 | 42 | import javax.net.ssl.HttpsURLConnection; 43 | 44 | import static android.text.TextUtils.isEmpty; 45 | 46 | /** 47 | * ApiController object used to send create and update session requests. Conforms to the singleton 48 | * pattern. 49 | * 50 | * NOTE: This code is sample code only and is not intended to be used for production applications. Any use in production applications is at your own risk. 51 | */ 52 | public class ApiController { 53 | 54 | private static final ApiController INSTANCE = new ApiController(); 55 | 56 | static final Gson GSON = new GsonBuilder().create(); 57 | 58 | String merchantServerUrl; 59 | 60 | 61 | interface CreateSessionCallback { 62 | void onSuccess(String sessionId, String apiVersion); 63 | 64 | void onError(Throwable throwable); 65 | } 66 | 67 | interface Check3DSecureEnrollmentCallback { 68 | void onSuccess(GatewayMap response); 69 | 70 | void onError(Throwable throwable); 71 | } 72 | 73 | interface CompleteSessionCallback { 74 | void onSuccess(String result); 75 | 76 | void onError(Throwable throwable); 77 | } 78 | 79 | private ApiController() { 80 | } 81 | 82 | public static ApiController getInstance() { 83 | return INSTANCE; 84 | } 85 | 86 | public void setMerchantServerUrl(String url) { 87 | merchantServerUrl = url; 88 | } 89 | 90 | public void createSession(final CreateSessionCallback callback) { 91 | final Handler handler = new Handler(message -> { 92 | if (callback != null) { 93 | if (message.obj instanceof Throwable) { 94 | callback.onError((Throwable) message.obj); 95 | } else { 96 | Pair pair = (Pair) message.obj; 97 | callback.onSuccess(pair.first, pair.second); 98 | } 99 | } 100 | return true; 101 | }); 102 | 103 | new Thread(() -> { 104 | Message m = handler.obtainMessage(); 105 | try { 106 | m.obj = executeCreateSession(); 107 | } catch (Exception e) { 108 | m.obj = e; 109 | } 110 | handler.sendMessage(m); 111 | }).start(); 112 | } 113 | 114 | public void check3DSecureEnrollment(final String sessionId, final String amount, final String currency, final String threeDSecureId, final Check3DSecureEnrollmentCallback callback) { 115 | final Handler handler = new Handler(message -> { 116 | if (callback != null) { 117 | if (message.obj instanceof Throwable) { 118 | callback.onError((Throwable) message.obj); 119 | } else { 120 | callback.onSuccess((GatewayMap) message.obj); 121 | } 122 | } 123 | return true; 124 | }); 125 | 126 | new Thread(() -> { 127 | Message m = handler.obtainMessage(); 128 | try { 129 | m.obj = executeCheck3DSEnrollment(sessionId, amount, currency, threeDSecureId); 130 | } catch (Exception e) { 131 | m.obj = e; 132 | } 133 | handler.sendMessage(m); 134 | }).start(); 135 | } 136 | 137 | public void completeSession(final String sessionId, final String orderId, final String transactionId, final String amount, final String currency, final String threeDSecureId, final Boolean isGooglePay, final CompleteSessionCallback callback) { 138 | final Handler handler = new Handler(message -> { 139 | if (callback != null) { 140 | if (message.obj instanceof Throwable) { 141 | callback.onError((Throwable) message.obj); 142 | } else { 143 | callback.onSuccess((String) message.obj); 144 | } 145 | } 146 | return true; 147 | }); 148 | 149 | new Thread(() -> { 150 | Message m = handler.obtainMessage(); 151 | try { 152 | m.obj = executeCompleteSession(sessionId, orderId, transactionId, amount, currency, threeDSecureId, isGooglePay); 153 | } catch (Exception e) { 154 | m.obj = e; 155 | } 156 | handler.sendMessage(m); 157 | }).start(); 158 | } 159 | 160 | Pair executeCreateSession() throws Exception { 161 | String jsonResponse = doJsonRequest(new URL(merchantServerUrl + "/session.php"), "", "POST", null, null, HttpsURLConnection.HTTP_OK); 162 | 163 | GatewayMap response = new GatewayMap(jsonResponse); 164 | 165 | if (!response.containsKey("gatewayResponse")) { 166 | throw new RuntimeException("Could not read gateway response"); 167 | } 168 | 169 | if (!response.containsKey("gatewayResponse.result") || !"SUCCESS".equalsIgnoreCase((String) response.get("gatewayResponse.result"))) { 170 | throw new RuntimeException("Create session result: " + response.get("gatewayResponse.result")); 171 | } 172 | 173 | String apiVersion = (String) response.get("apiVersion"); 174 | String sessionId = (String) response.get("gatewayResponse.session.id"); 175 | Log.i("createSession", "Created session with ID " + sessionId + " with API version " + apiVersion); 176 | 177 | return new Pair<>(sessionId, apiVersion); 178 | } 179 | 180 | GatewayMap executeCheck3DSEnrollment(String sessionId, String amount, String currency, String threeDSecureId) throws Exception { 181 | GatewayMap request = new GatewayMap() 182 | .set("apiOperation", "CHECK_3DS_ENROLLMENT") 183 | .set("session.id", sessionId) 184 | .set("order.amount", amount) 185 | .set("order.currency", currency) 186 | .set("3DSecure.authenticationRedirect.responseUrl", merchantServerUrl + "/3DSecureResult.php?3DSecureId=" + threeDSecureId); 187 | 188 | String jsonRequest = GSON.toJson(request); 189 | 190 | String jsonResponse = doJsonRequest(new URL(merchantServerUrl + "/3DSecure.php?3DSecureId=" + threeDSecureId), jsonRequest, "PUT", null, null, HttpsURLConnection.HTTP_OK); 191 | 192 | GatewayMap response = new GatewayMap(jsonResponse); 193 | 194 | if (!response.containsKey("gatewayResponse")) { 195 | throw new RuntimeException("Could not read gateway response"); 196 | } 197 | 198 | // if there is an error result, throw it 199 | if (response.containsKey("gatewayResponse.result") && "ERROR".equalsIgnoreCase((String) response.get("gatewayResponse.result"))) { 200 | throw new RuntimeException("Check 3DS Enrollment Error: " + response.get("gatewayResponse.error.explanation")); 201 | } 202 | 203 | return response; 204 | } 205 | 206 | String executeCompleteSession(String sessionId, String orderId, String transactionId, String amount, String currency, String threeDSecureId, Boolean isGooglePay) throws Exception { 207 | GatewayMap request = new GatewayMap() 208 | .set("apiOperation", "PAY") 209 | .set("session.id", sessionId) 210 | .set("order.amount", amount) 211 | .set("order.currency", currency) 212 | .set("sourceOfFunds.type", "CARD") 213 | // .set("transaction.frequency", "SINGLE") // NOTE: 'transaction.frequency` is only applicable to API versions <=53 214 | .set("transaction.source", "INTERNET"); 215 | 216 | if (threeDSecureId != null) { 217 | request.put("3DSecureId", threeDSecureId); 218 | } 219 | 220 | if (isGooglePay) { 221 | request.put("order.walletProvider", "GOOGLE_PAY"); 222 | } 223 | 224 | String jsonRequest = GSON.toJson(request); 225 | 226 | String jsonResponse = doJsonRequest(new URL(merchantServerUrl + "/transaction.php?order=" + orderId + "&transaction=" + transactionId), jsonRequest, "PUT", null, null, HttpsURLConnection.HTTP_OK); 227 | 228 | GatewayMap response = new GatewayMap(jsonResponse); 229 | 230 | if (!response.containsKey("gatewayResponse")) { 231 | throw new RuntimeException("Could not read gateway response"); 232 | } 233 | 234 | if (!response.containsKey("gatewayResponse.result") || !"SUCCESS".equalsIgnoreCase((String) response.get("gatewayResponse.result"))) { 235 | throw new RuntimeException("Error processing payment"); 236 | } 237 | 238 | return (String) response.get("gatewayResponse.result"); 239 | } 240 | 241 | 242 | /** 243 | * Initialise a new SSL context using the algorithm, key manager(s), trust manager(s) and 244 | * source of randomness. 245 | * 246 | * @throws NoSuchAlgorithmException if the algorithm is not supported by the android platform 247 | * @throws KeyManagementException if initialization of the context fails 248 | */ 249 | void initialiseSslContext() throws NoSuchAlgorithmException, KeyManagementException { 250 | HttpsURLConnection.setDefaultSSLSocketFactory(new TLSSocketFactory()); 251 | } 252 | 253 | /** 254 | * Open an HTTP or HTTPS connection to a particular URL 255 | * 256 | * @param address a valid HTTP[S] URL to connect to 257 | * @return an HTTP or HTTPS connection as appropriate 258 | * @throws KeyManagementException if initialization of the SSL context fails 259 | * @throws NoSuchAlgorithmException if the SSL algorithm is not supported by the android platform 260 | * @throws MalformedURLException if the address was not in the HTTP or HTTPS scheme 261 | * @throws IOException if the connection could not be opened 262 | */ 263 | HttpURLConnection openConnection(URL address) 264 | throws KeyManagementException, NoSuchAlgorithmException, IOException { 265 | 266 | switch (address.getProtocol().toUpperCase()) { 267 | case "HTTPS": 268 | initialiseSslContext(); 269 | break; 270 | case "HTTP": 271 | break; 272 | default: 273 | throw new MalformedURLException("Not an HTTP[S] address"); 274 | } 275 | 276 | HttpURLConnection connection = (HttpURLConnection) address.openConnection(); 277 | connection.setConnectTimeout(30000); 278 | connection.setReadTimeout(60000); 279 | return connection; 280 | } 281 | 282 | /** 283 | * Send a JSON object to an open HTTP[S] connection 284 | * 285 | * @param connection an open HTTP[S] connection, as returned by {@link #openConnection(URL)} 286 | * @param method an HTTP method, e.g. PUT, POST or GET 287 | * @param json a valid JSON-formatted object 288 | * @param username user name for basic authorization (can be null for no auth) 289 | * @param password password for basic authorization (can be null for no auth) 290 | * @return an HTTP response code 291 | * @throws IOException if the connection could not be written to 292 | */ 293 | int makeJsonRequest(HttpURLConnection connection, String method, String json, 294 | String username, String password) throws IOException { 295 | 296 | connection.setDoOutput(true); 297 | connection.setRequestMethod(method); 298 | connection.setFixedLengthStreamingMode(json.getBytes().length); 299 | connection.setRequestProperty("Content-Type", "application/json"); 300 | 301 | if (!isEmpty(username) && !isEmpty(password)) { 302 | String basicAuth = username + ':' + password; 303 | basicAuth = Base64.encodeToString(basicAuth.getBytes(), Base64.DEFAULT); 304 | connection.setRequestProperty("Authorization", "Basic " + basicAuth); 305 | } 306 | 307 | PrintWriter out = new PrintWriter(connection.getOutputStream()); 308 | out.print(json); 309 | out.close(); 310 | 311 | return connection.getResponseCode(); 312 | } 313 | 314 | /** 315 | * Retrieve a JSON response from an open HTTP[S] connection. This would typically be called 316 | * after {@link #makeJsonRequest(HttpURLConnection, String, String, String, String)} 317 | * 318 | * @param connection an open HTTP[S] connection 319 | * @return a json object in string form 320 | * @throws IOException if the connection could not be read from 321 | */ 322 | String getJsonResponse(HttpURLConnection connection) throws IOException { 323 | StringBuilder responseOutput = new StringBuilder(); 324 | String line; 325 | BufferedReader br = null; 326 | 327 | try { 328 | // If the HTTP response code is 4xx or 5xx, we need error rather than input stream 329 | InputStream stream = (connection.getResponseCode() < 400) 330 | ? connection.getInputStream() 331 | : connection.getErrorStream(); 332 | 333 | br = new BufferedReader(new InputStreamReader(stream)); 334 | 335 | while ((line = br.readLine()) != null) { 336 | responseOutput.append(line); 337 | } 338 | } finally { 339 | if (br != null) { 340 | try { 341 | br.close(); 342 | } catch (Exception e) { 343 | /* Ignore close exception */ 344 | } 345 | } 346 | } 347 | 348 | return responseOutput.toString(); 349 | } 350 | 351 | /** 352 | * End-to-end method to send some json to an url and retrieve a response 353 | * 354 | * @param address url to send the request to 355 | * @param jsonRequest a valid JSON-formatted object 356 | * @param httpMethod an HTTP method, e.g. PUT, POST or GET 357 | * @param username user name for basic authorization (can be null for no auth) 358 | * @param password password for basic authorization (can be null for no auth) 359 | * @param expectResponseCodes permitted HTTP response codes, e.g. HTTP_OK (200) 360 | * @return a json response object in string form 361 | */ 362 | String doJsonRequest(URL address, String jsonRequest, String httpMethod, String username, String password, int... expectResponseCodes) { 363 | 364 | HttpURLConnection connection; 365 | int responseCode; 366 | 367 | try { 368 | connection = openConnection(address); 369 | } catch (NoSuchAlgorithmException | KeyManagementException e) { 370 | throw new RuntimeException("Couldn't initialise SSL context", e); 371 | } catch (IOException e) { 372 | throw new RuntimeException("Couldn't open an HTTP[S] connection", e); 373 | } 374 | 375 | try { 376 | responseCode = 377 | makeJsonRequest(connection, httpMethod, jsonRequest, username, password); 378 | 379 | if (!contains(expectResponseCodes, responseCode)) { 380 | throw new RuntimeException("Unexpected response code " + responseCode); 381 | } 382 | } catch (SocketTimeoutException e) { 383 | throw new RuntimeException("Timeout whilst sending JSON data"); 384 | } catch (IOException e) { 385 | throw new RuntimeException("Error sending JSON data", e); 386 | } 387 | 388 | try { 389 | String responseBody = getJsonResponse(connection); 390 | 391 | if (responseBody == null) { 392 | throw new RuntimeException("No data in response"); 393 | } 394 | 395 | return responseBody; 396 | } catch (SocketTimeoutException e) { 397 | throw new RuntimeException("Timeout whilst retrieving JSON response"); 398 | } catch (IOException e) { 399 | throw new RuntimeException("Error retrieving JSON response", e); 400 | } 401 | } 402 | 403 | 404 | static boolean contains(int[] haystack, int needle) { 405 | for (int candidate : haystack) { 406 | if (candidate == needle) { 407 | return true; 408 | } 409 | } 410 | 411 | return false; 412 | } 413 | } 414 | 415 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mastercard/gateway/android/sampleapp/CollectCardInfoActivity.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sampleapp; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import androidx.databinding.DataBindingUtil; 6 | import android.os.Bundle; 7 | import androidx.annotation.Nullable; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | import android.text.Editable; 10 | import android.text.TextWatcher; 11 | import android.util.Log; 12 | import android.view.View; 13 | import android.widget.Toast; 14 | 15 | import com.google.android.gms.common.api.ApiException; 16 | import com.google.android.gms.common.api.Status; 17 | import com.google.android.gms.tasks.Task; 18 | import com.google.android.gms.wallet.IsReadyToPayRequest; 19 | import com.google.android.gms.wallet.PaymentDataRequest; 20 | import com.google.android.gms.wallet.PaymentsClient; 21 | import com.google.android.gms.wallet.Wallet; 22 | import com.google.android.gms.wallet.WalletConstants; 23 | import com.mastercard.gateway.android.sampleapp.databinding.ActivityCollectCardInfoBinding; 24 | import com.mastercard.gateway.android.sdk.Gateway; 25 | import com.mastercard.gateway.android.sdk.GatewayGooglePayCallback; 26 | 27 | import org.json.JSONArray; 28 | import org.json.JSONException; 29 | import org.json.JSONObject; 30 | 31 | import java.util.Arrays; 32 | 33 | import static android.text.TextUtils.isEmpty; 34 | 35 | public class CollectCardInfoActivity extends AppCompatActivity { 36 | 37 | private static final String EXTRA_PREFIX = "com.mastercard.gateway.sample.EXTRA_"; 38 | 39 | // request 40 | public static final String EXTRA_GOOGLE_PAY_TXN_AMOUNT = EXTRA_PREFIX + "GOOGLE_PAY_TXN_AMOUNT"; 41 | public static final String EXTRA_GOOGLE_PAY_TXN_CURRENCY = EXTRA_PREFIX + "GOOGLE_PAY_TXN_CURRENCY"; 42 | 43 | // response 44 | public static final String EXTRA_CARD_DESCRIPTION = EXTRA_PREFIX + "CARD_DESCRIPTION"; 45 | public static final String EXTRA_CARD_NAME = EXTRA_PREFIX + "CARD_NAME"; 46 | public static final String EXTRA_CARD_NUMBER = EXTRA_PREFIX + "CARD_NUMBER"; 47 | public static final String EXTRA_CARD_EXPIRY_MONTH = EXTRA_PREFIX + "CARD_EXPIRY_MONTH"; 48 | public static final String EXTRA_CARD_EXPIRY_YEAR = EXTRA_PREFIX + "CARD_EXPIRY_YEAR"; 49 | public static final String EXTRA_CARD_CVV = EXTRA_PREFIX + "CARD_CVC"; 50 | public static final String EXTRA_PAYMENT_TOKEN = EXTRA_PREFIX + "PAYMENT_TOKEN"; 51 | 52 | 53 | ActivityCollectCardInfoBinding binding; 54 | String googlePayTxnAmount; 55 | String googlePayTxnCurrency; 56 | PaymentsClient paymentsClient; 57 | TextChangeListener textChangeListener = new TextChangeListener(); 58 | GooglePayCallback googlePayCallback = new GooglePayCallback(); 59 | 60 | 61 | @Override 62 | protected void onCreate(@Nullable Bundle savedInstanceState) { 63 | super.onCreate(savedInstanceState); 64 | 65 | binding = DataBindingUtil.setContentView(this, R.layout.activity_collect_card_info); 66 | 67 | // get bundle extras and set txn amount and currency for google pay 68 | Intent i = getIntent(); 69 | googlePayTxnAmount = i.getStringExtra(EXTRA_GOOGLE_PAY_TXN_AMOUNT); 70 | googlePayTxnCurrency = i.getStringExtra(EXTRA_GOOGLE_PAY_TXN_CURRENCY); 71 | 72 | // init manual text field listeners 73 | binding.nameOnCard.requestFocus(); 74 | binding.nameOnCard.addTextChangedListener(textChangeListener); 75 | binding.cardnumber.addTextChangedListener(textChangeListener); 76 | binding.expiryMonth.addTextChangedListener(textChangeListener); 77 | binding.expiryYear.addTextChangedListener(textChangeListener); 78 | binding.cvv.addTextChangedListener(textChangeListener); 79 | 80 | binding.submitButton.setEnabled(false); 81 | binding.submitButton.setOnClickListener(v -> continueButtonClicked()); 82 | 83 | 84 | // init Google Pay client 85 | paymentsClient = Wallet.getPaymentsClient(this, new Wallet.WalletOptions.Builder() 86 | .setEnvironment(WalletConstants.ENVIRONMENT_TEST) 87 | .build()); 88 | 89 | // init google pay button 90 | binding.googlePayButton.setOnClickListener(v -> googlePayButtonClicked()); 91 | 92 | // check if Google Pay is available 93 | isReadyToPay(); 94 | } 95 | 96 | @Override 97 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 98 | // handle the Google Pay lifecycle 99 | if (Gateway.handleGooglePayResult(requestCode, resultCode, data, googlePayCallback)) { 100 | return; 101 | } 102 | 103 | super.onActivityResult(requestCode, resultCode, data); 104 | } 105 | 106 | void enableContinueButton() { 107 | if (isEmpty(binding.nameOnCard.getText()) || isEmpty(binding.cardnumber.getText()) 108 | || isEmpty(binding.expiryMonth.getText()) || isEmpty(binding.expiryYear.getText()) 109 | || isEmpty(binding.cvv.getText()) || (binding.cvv.getText().toString().length() < 3)) { 110 | 111 | binding.submitButton.setEnabled(false); 112 | } else { 113 | binding.submitButton.setEnabled(true); 114 | } 115 | } 116 | 117 | void continueButtonClicked() { 118 | String nameOnCard = binding.nameOnCard.getText().toString(); 119 | String cardNumber = binding.cardnumber.getText().toString(); 120 | String expiryMM = binding.expiryMonth.getText().toString(); 121 | String expiryYY = binding.expiryYear.getText().toString(); 122 | String cvv = binding.cvv.getText().toString(); 123 | 124 | Intent i = new Intent(); 125 | i.putExtra(EXTRA_CARD_DESCRIPTION, maskCardNumber(cardNumber)); 126 | i.putExtra(EXTRA_CARD_NAME, nameOnCard); 127 | i.putExtra(EXTRA_CARD_NUMBER, cardNumber); 128 | i.putExtra(EXTRA_CARD_EXPIRY_MONTH, expiryMM); 129 | i.putExtra(EXTRA_CARD_EXPIRY_YEAR, expiryYY); 130 | i.putExtra(EXTRA_CARD_CVV, cvv); 131 | 132 | setResult(Activity.RESULT_OK, i); 133 | finish(); 134 | } 135 | 136 | void googlePayButtonClicked() { 137 | try { 138 | PaymentDataRequest request = PaymentDataRequest.fromJson(getPaymentDataRequest().toString()); 139 | if (request != null) { 140 | // use the Gateway convenience handler for launching the Google Pay flow 141 | Gateway.requestGooglePayData(paymentsClient, request, CollectCardInfoActivity.this); 142 | } 143 | } catch (JSONException e) { 144 | Toast.makeText(this, "Could not request payment data", Toast.LENGTH_SHORT).show(); 145 | } 146 | } 147 | 148 | void returnCardInfo(JSONObject paymentData) { 149 | Intent i = new Intent(); 150 | 151 | try { 152 | JSONObject paymentMethodData = paymentData.getJSONObject("paymentMethodData"); 153 | String description = paymentMethodData.getString("description"); 154 | String token = paymentMethodData.getJSONObject("tokenizationData") 155 | .getString("token"); 156 | 157 | i.putExtra(EXTRA_CARD_DESCRIPTION, description); 158 | i.putExtra(EXTRA_PAYMENT_TOKEN, token); 159 | 160 | setResult(Activity.RESULT_OK, i); 161 | } catch (Exception e) { 162 | setResult(Activity.RESULT_CANCELED, i); 163 | } 164 | 165 | finish(); 166 | } 167 | 168 | String maskCardNumber(String number) { 169 | int maskLen = number.length() - 4; 170 | char[] mask = new char[maskLen]; 171 | Arrays.fill(mask, '*'); 172 | return new String(mask) + number.substring(maskLen); 173 | } 174 | 175 | void isReadyToPay() { 176 | try { 177 | IsReadyToPayRequest request = IsReadyToPayRequest.fromJson(getIsReadyToPayRequest().toString()); 178 | 179 | Task task = paymentsClient.isReadyToPay(request); 180 | task.addOnCompleteListener(task12 -> { 181 | try { 182 | boolean result = task12.getResult(ApiException.class); 183 | if (result) { 184 | // Show Google as payment option. 185 | binding.orSeparator.setVisibility(View.VISIBLE); 186 | binding.googlePayButton.setVisibility(View.VISIBLE); 187 | } else { 188 | // Hide Google as payment option. 189 | binding.orSeparator.setVisibility(View.GONE); 190 | binding.googlePayButton.setVisibility(View.GONE); 191 | } 192 | } catch (ApiException e) { 193 | } 194 | }); 195 | } catch (JSONException e) { 196 | // do nothing 197 | } 198 | } 199 | 200 | JSONObject getIsReadyToPayRequest() throws JSONException { 201 | return getBaseRequest() 202 | .put("allowedPaymentMethods", new JSONArray() 203 | .put(getBaseCardPaymentMethod())); 204 | } 205 | 206 | JSONObject getCardPaymentMethod() throws JSONException { 207 | return getBaseCardPaymentMethod() 208 | .put("tokenizationSpecification", getTokenizationSpecification()); 209 | } 210 | 211 | JSONObject getBaseRequest() throws JSONException { 212 | return new JSONObject() 213 | .put("apiVersion", 2) 214 | .put("apiVersionMinor", 0); 215 | } 216 | 217 | JSONObject getBaseCardPaymentMethod() throws JSONException { 218 | return new JSONObject() 219 | .put("type", "CARD") 220 | .put("parameters", new JSONObject() 221 | .put("allowedAuthMethods", getAllowedCardAuthMethods()) 222 | .put("allowedCardNetworks", getAllowedCardNetworks())); 223 | } 224 | 225 | JSONArray getAllowedCardNetworks() { 226 | return new JSONArray() 227 | .put("AMEX") 228 | .put("DISCOVER") 229 | .put("MASTERCARD") 230 | .put("VISA"); 231 | } 232 | 233 | JSONArray getAllowedCardAuthMethods() { 234 | return new JSONArray() 235 | .put("PAN_ONLY") 236 | .put("CRYPTOGRAM_3DS"); 237 | } 238 | 239 | JSONObject getTokenizationSpecification() throws JSONException { 240 | return new JSONObject() 241 | .put("type", "PAYMENT_GATEWAY") 242 | .put("parameters", new JSONObject() 243 | .put("gateway", "mpgs") 244 | .put("gatewayMerchantId", Config.MERCHANT_ID.getValue(this))); 245 | } 246 | 247 | JSONObject getTransactionInfo() throws JSONException { 248 | return new JSONObject() 249 | .put("totalPrice", googlePayTxnAmount) 250 | .put("totalPriceStatus", "FINAL") 251 | .put("currencyCode", googlePayTxnCurrency); 252 | } 253 | 254 | JSONObject getMerchantInfo() throws JSONException { 255 | return new JSONObject() 256 | .put("merchantName", "Example Merchant"); 257 | } 258 | 259 | JSONObject getPaymentDataRequest() throws JSONException { 260 | return getBaseRequest() 261 | .put("allowedPaymentMethods", new JSONArray() 262 | .put(getCardPaymentMethod())) 263 | .put("transactionInfo", getTransactionInfo()) 264 | .put("merchantInfo", getMerchantInfo()); 265 | } 266 | 267 | class TextChangeListener implements TextWatcher { 268 | @Override 269 | public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { 270 | 271 | } 272 | 273 | @Override 274 | public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { 275 | enableContinueButton(); 276 | } 277 | 278 | @Override 279 | public void afterTextChanged(Editable editable) { 280 | 281 | } 282 | } 283 | 284 | class GooglePayCallback implements GatewayGooglePayCallback { 285 | @Override 286 | public void onReceivedPaymentData(JSONObject paymentData) { 287 | try { 288 | String description = paymentData.getJSONObject("paymentMethodData") 289 | .getString("description"); 290 | 291 | Log.d(GooglePayCallback.class.getSimpleName(), "ReceivedPaymentData: " + description); 292 | } catch (Exception e) { 293 | 294 | } 295 | 296 | returnCardInfo(paymentData); 297 | } 298 | 299 | @Override 300 | public void onGooglePayCancelled() { 301 | Log.d(GooglePayCallback.class.getSimpleName(), "Cancelled"); 302 | } 303 | 304 | @Override 305 | public void onGooglePayError(Status status) { 306 | Log.d(GooglePayCallback.class.getSimpleName(), "Error"); 307 | Toast.makeText(CollectCardInfoActivity.this, "Google Pay Error", Toast.LENGTH_SHORT).show(); 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mastercard/gateway/android/sampleapp/Config.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sampleapp; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.preference.PreferenceManager; 6 | 7 | import com.mastercard.gateway.android.sdk.Gateway; 8 | 9 | public enum Config { 10 | 11 | MERCHANT_ID(""), 12 | REGION(Gateway.Region.NORTH_AMERICA.name()), 13 | MERCHANT_URL(""); 14 | 15 | String defValue; 16 | 17 | Config(String defValue) { 18 | this.defValue = defValue; 19 | } 20 | 21 | public String getValue(Context context) { 22 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 23 | return prefs.getString(this.name(), defValue); 24 | } 25 | 26 | public void setValue(Context context, String value) { 27 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 28 | SharedPreferences.Editor editor = prefs.edit(); 29 | editor.putString(this.name(), value); 30 | editor.apply(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mastercard/gateway/android/sampleapp/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sampleapp; 2 | 3 | import android.content.Intent; 4 | import androidx.databinding.DataBindingUtil; 5 | import android.os.Bundle; 6 | import androidx.appcompat.app.AlertDialog; 7 | import androidx.appcompat.app.AppCompatActivity; 8 | import android.text.Editable; 9 | import android.text.TextUtils; 10 | import android.text.TextWatcher; 11 | 12 | import com.mastercard.gateway.android.sampleapp.databinding.ActivityMainBinding; 13 | import com.mastercard.gateway.android.sdk.Gateway; 14 | 15 | public class MainActivity extends AppCompatActivity { 16 | 17 | ActivityMainBinding binding; 18 | TextChangeListener textChangeListener = new TextChangeListener(); 19 | 20 | @Override 21 | protected void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | 24 | binding = DataBindingUtil.setContentView(this, R.layout.activity_main); 25 | 26 | binding.merchantId.setText(Config.MERCHANT_ID.getValue(this)); 27 | binding.merchantId.addTextChangedListener(textChangeListener); 28 | 29 | binding.region.setText(Config.REGION.getValue(this)); 30 | binding.region.addTextChangedListener(textChangeListener); 31 | binding.region.setOnFocusChangeListener((v, hasFocus) -> { 32 | if (hasFocus) { 33 | binding.region.clearFocus(); 34 | showRegionPicker(); 35 | } 36 | }); 37 | 38 | binding.merchantUrl.setText(Config.MERCHANT_URL.getValue(this)); 39 | binding.merchantUrl.addTextChangedListener(textChangeListener); 40 | 41 | binding.processPaymentButton.setOnClickListener(v -> goTo(ProcessPaymentActivity.class)); 42 | 43 | enableButtons(); 44 | } 45 | 46 | void goTo(Class klass) { 47 | Intent i = new Intent(this, klass); 48 | startActivity(i); 49 | } 50 | 51 | void persistConfig() { 52 | Config.MERCHANT_ID.setValue(this, binding.merchantId.getText().toString()); 53 | Config.REGION.setValue(this, binding.region.getText().toString()); 54 | Config.MERCHANT_URL.setValue(this, binding.merchantUrl.getText().toString()); 55 | 56 | // update api controller url 57 | ApiController.getInstance().setMerchantServerUrl(Config.MERCHANT_URL.getValue(this)); 58 | } 59 | 60 | void enableButtons() { 61 | boolean enabled = !TextUtils.isEmpty(binding.merchantId.getText()) 62 | && !TextUtils.isEmpty(binding.region.getText()) 63 | && !TextUtils.isEmpty(binding.merchantUrl.getText()); 64 | 65 | binding.processPaymentButton.setEnabled(enabled); 66 | } 67 | 68 | void showRegionPicker() { 69 | Gateway.Region[] regions = Gateway.Region.values(); 70 | final String[] items = new String[regions.length + 1]; 71 | items[0] = getString(R.string.none); 72 | for (int i = 0; i < regions.length; i++) { 73 | items[i + 1] = regions[i].name(); 74 | } 75 | 76 | new AlertDialog.Builder(this) 77 | .setTitle(R.string.main_select_region) 78 | .setItems(items, (dialog, which) -> { 79 | if (which == 0) { 80 | binding.region.setText(""); 81 | } else { 82 | binding.region.setText(items[which]); 83 | } 84 | dialog.cancel(); 85 | }) 86 | .show(); 87 | } 88 | 89 | class TextChangeListener implements TextWatcher { 90 | @Override 91 | public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { 92 | 93 | } 94 | 95 | @Override 96 | public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { 97 | enableButtons(); 98 | persistConfig(); 99 | } 100 | 101 | @Override 102 | public void afterTextChanged(Editable editable) { 103 | 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mastercard/gateway/android/sampleapp/ProcessPaymentActivity.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sampleapp; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import androidx.databinding.DataBindingUtil; 6 | import android.graphics.Paint; 7 | import android.os.Bundle; 8 | import androidx.annotation.DrawableRes; 9 | import androidx.annotation.Nullable; 10 | import androidx.annotation.StringRes; 11 | import androidx.appcompat.app.AppCompatActivity; 12 | import android.util.Log; 13 | import android.view.View; 14 | 15 | import com.mastercard.gateway.android.sampleapp.databinding.ActivityProcessPaymentBinding; 16 | import com.mastercard.gateway.android.sdk.Gateway; 17 | import com.mastercard.gateway.android.sdk.Gateway3DSecureCallback; 18 | import com.mastercard.gateway.android.sdk.GatewayCallback; 19 | import com.mastercard.gateway.android.sdk.GatewayMap; 20 | 21 | import java.util.UUID; 22 | 23 | public class ProcessPaymentActivity extends AppCompatActivity { 24 | 25 | static final int REQUEST_CARD_INFO = 100; 26 | 27 | // static for demo 28 | static final String AMOUNT = "1.00"; 29 | static final String CURRENCY = "USD"; 30 | 31 | ActivityProcessPaymentBinding binding; 32 | Gateway gateway; 33 | String sessionId, apiVersion, threeDSecureId, orderId, transactionId; 34 | boolean isGooglePay = false; 35 | ApiController apiController = ApiController.getInstance(); 36 | 37 | @Override 38 | protected void onCreate(@Nullable Bundle savedInstanceState) { 39 | super.onCreate(savedInstanceState); 40 | binding = DataBindingUtil.setContentView(this, R.layout.activity_process_payment); 41 | 42 | // init api controller 43 | apiController.setMerchantServerUrl(Config.MERCHANT_URL.getValue(this)); 44 | 45 | // init gateway 46 | gateway = new Gateway(); 47 | gateway.setMerchantId(Config.MERCHANT_ID.getValue(this)); 48 | try { 49 | Gateway.Region region = Gateway.Region.valueOf(Config.REGION.getValue(this)); 50 | gateway.setRegion(region); 51 | } catch (Exception e) { 52 | Log.e(ProcessPaymentActivity.class.getSimpleName(), "Invalid Gateway region value provided", e); 53 | } 54 | 55 | // random order/txn IDs for example purposes 56 | orderId = UUID.randomUUID().toString(); 57 | orderId = orderId.substring(0, orderId.indexOf('-')); 58 | transactionId = UUID.randomUUID().toString(); 59 | transactionId = transactionId.substring(0, transactionId.indexOf('-')); 60 | 61 | // bind buttons 62 | binding.startButton.setOnClickListener(v -> createSession()); 63 | binding.confirmButton.setOnClickListener(v -> { 64 | // 3DS is not applicable to Google Pay transactions 65 | if (isGooglePay) { 66 | processPayment(); 67 | } else { 68 | check3dsEnrollment(); 69 | } 70 | }); 71 | binding.doneButton.setOnClickListener(v -> finish()); 72 | 73 | initUI(); 74 | } 75 | 76 | @Override 77 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 78 | // handle the 3DSecure lifecycle 79 | if (Gateway.handle3DSecureResult(requestCode, resultCode, data, new ThreeDSecureCallback())) { 80 | return; 81 | } 82 | 83 | if (requestCode == REQUEST_CARD_INFO) { 84 | binding.collectCardInfoProgress.setVisibility(View.GONE); 85 | 86 | if (resultCode == Activity.RESULT_OK) { 87 | binding.collectCardInfoSuccess.setVisibility(View.VISIBLE); 88 | 89 | String googlePayToken = data.getStringExtra(CollectCardInfoActivity.EXTRA_PAYMENT_TOKEN); 90 | 91 | String cardDescription = data.getStringExtra(CollectCardInfoActivity.EXTRA_CARD_DESCRIPTION); 92 | binding.confirmCardDescription.setText(cardDescription); 93 | 94 | if (googlePayToken != null) { 95 | isGooglePay = true; 96 | 97 | binding.check3dsLabel.setPaintFlags(binding.check3dsLabel.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); 98 | 99 | String paymentToken = data.getStringExtra(CollectCardInfoActivity.EXTRA_PAYMENT_TOKEN); 100 | 101 | updateSession(paymentToken); 102 | } else { 103 | isGooglePay = false; 104 | 105 | String cardName = data.getStringExtra(CollectCardInfoActivity.EXTRA_CARD_NAME); 106 | String cardNumber = data.getStringExtra(CollectCardInfoActivity.EXTRA_CARD_NUMBER); 107 | String cardExpiryMonth = data.getStringExtra(CollectCardInfoActivity.EXTRA_CARD_EXPIRY_MONTH); 108 | String cardExpiryYear = data.getStringExtra(CollectCardInfoActivity.EXTRA_CARD_EXPIRY_YEAR); 109 | String cardCvv = data.getStringExtra(CollectCardInfoActivity.EXTRA_CARD_CVV); 110 | 111 | updateSession(cardName, cardNumber, cardExpiryMonth, cardExpiryYear, cardCvv); 112 | } 113 | 114 | } else { 115 | binding.collectCardInfoError.setVisibility(View.VISIBLE); 116 | 117 | showResult(R.drawable.failed, R.string.pay_error_card_info_not_collected); 118 | } 119 | 120 | return; 121 | } 122 | 123 | super.onActivityResult(requestCode, resultCode, data); 124 | } 125 | 126 | void initUI() { 127 | binding.createSessionProgress.setVisibility(View.GONE); 128 | binding.createSessionSuccess.setVisibility(View.GONE); 129 | binding.createSessionError.setVisibility(View.GONE); 130 | 131 | binding.collectCardInfoProgress.setVisibility(View.GONE); 132 | binding.collectCardInfoSuccess.setVisibility(View.GONE); 133 | binding.collectCardInfoError.setVisibility(View.GONE); 134 | 135 | binding.updateSessionProgress.setVisibility(View.GONE); 136 | binding.updateSessionSuccess.setVisibility(View.GONE); 137 | binding.updateSessionError.setVisibility(View.GONE); 138 | 139 | binding.check3dsProgress.setVisibility(View.GONE); 140 | binding.check3dsSuccess.setVisibility(View.GONE); 141 | binding.check3dsError.setVisibility(View.GONE); 142 | 143 | binding.processPaymentProgress.setVisibility(View.GONE); 144 | binding.processPaymentSuccess.setVisibility(View.GONE); 145 | binding.processPaymentError.setVisibility(View.GONE); 146 | 147 | binding.startButton.setEnabled(true); 148 | binding.confirmButton.setEnabled(true); 149 | 150 | binding.startButton.setVisibility(View.VISIBLE); 151 | binding.groupConfirm.setVisibility(View.GONE); 152 | binding.groupResult.setVisibility(View.GONE); 153 | } 154 | 155 | void createSession() { 156 | binding.startButton.setEnabled(false); 157 | binding.createSessionProgress.setVisibility(View.VISIBLE); 158 | 159 | apiController.createSession(new CreateSessionCallback()); 160 | } 161 | 162 | void collectCardInfo() { 163 | binding.collectCardInfoProgress.setVisibility(View.VISIBLE); 164 | 165 | Intent i = new Intent(this, CollectCardInfoActivity.class); 166 | i.putExtra(CollectCardInfoActivity.EXTRA_GOOGLE_PAY_TXN_AMOUNT, AMOUNT); 167 | i.putExtra(CollectCardInfoActivity.EXTRA_GOOGLE_PAY_TXN_CURRENCY, CURRENCY); 168 | 169 | startActivityForResult(i, REQUEST_CARD_INFO); 170 | } 171 | 172 | void updateSession(String paymentToken) { 173 | binding.updateSessionProgress.setVisibility(View.VISIBLE); 174 | 175 | GatewayMap request = new GatewayMap() 176 | .set("sourceOfFunds.provided.card.devicePayment.paymentToken", paymentToken); 177 | 178 | gateway.updateSession(sessionId, apiVersion, request, new UpdateSessionCallback()); 179 | } 180 | 181 | void updateSession(String name, String number, String expiryMonth, String expiryYear, String cvv) { 182 | binding.updateSessionProgress.setVisibility(View.VISIBLE); 183 | 184 | // build the gateway request 185 | GatewayMap request = new GatewayMap() 186 | .set("sourceOfFunds.provided.card.nameOnCard", name) 187 | .set("sourceOfFunds.provided.card.number", number) 188 | .set("sourceOfFunds.provided.card.securityCode", cvv) 189 | .set("sourceOfFunds.provided.card.expiry.month", expiryMonth) 190 | .set("sourceOfFunds.provided.card.expiry.year", expiryYear); 191 | 192 | gateway.updateSession(sessionId, apiVersion, request, new UpdateSessionCallback()); 193 | } 194 | 195 | void check3dsEnrollment() { 196 | binding.check3dsProgress.setVisibility(View.VISIBLE); 197 | binding.confirmButton.setEnabled(false); 198 | 199 | // generate a random 3DSecureId for testing 200 | String threeDSId = UUID.randomUUID().toString(); 201 | threeDSId = threeDSId.substring(0, threeDSId.indexOf('-')); 202 | 203 | apiController.check3DSecureEnrollment(sessionId, AMOUNT, CURRENCY, threeDSId, new Check3DSecureEnrollmentCallback()); 204 | } 205 | 206 | void processPayment() { 207 | binding.processPaymentProgress.setVisibility(View.VISIBLE); 208 | 209 | apiController.completeSession(sessionId, orderId, transactionId, AMOUNT, CURRENCY, threeDSecureId, isGooglePay, new CompleteSessionCallback()); 210 | } 211 | 212 | void showResult(@DrawableRes int iconId, @StringRes int messageId) { 213 | binding.resultIcon.setImageResource(iconId); 214 | binding.resultText.setText(messageId); 215 | 216 | binding.groupConfirm.setVisibility(View.GONE); 217 | binding.groupResult.setVisibility(View.VISIBLE); 218 | } 219 | 220 | 221 | class CreateSessionCallback implements ApiController.CreateSessionCallback { 222 | @Override 223 | public void onSuccess(String sessionId, String apiVersion) { 224 | Log.i("CreateSessionTask", "Session established"); 225 | binding.createSessionProgress.setVisibility(View.GONE); 226 | binding.createSessionSuccess.setVisibility(View.VISIBLE); 227 | 228 | ProcessPaymentActivity.this.sessionId = sessionId; 229 | ProcessPaymentActivity.this.apiVersion = apiVersion; 230 | 231 | collectCardInfo(); 232 | } 233 | 234 | @Override 235 | public void onError(Throwable throwable) { 236 | Log.e(ProcessPaymentActivity.class.getSimpleName(), throwable.getMessage(), throwable); 237 | 238 | binding.createSessionProgress.setVisibility(View.GONE); 239 | binding.createSessionError.setVisibility(View.VISIBLE); 240 | 241 | showResult(R.drawable.failed, R.string.pay_error_unable_to_create_session); 242 | } 243 | } 244 | 245 | class UpdateSessionCallback implements GatewayCallback { 246 | @Override 247 | public void onSuccess(GatewayMap response) { 248 | Log.i(ProcessPaymentActivity.class.getSimpleName(), "Successfully updated session"); 249 | binding.updateSessionProgress.setVisibility(View.GONE); 250 | binding.updateSessionSuccess.setVisibility(View.VISIBLE); 251 | 252 | binding.startButton.setVisibility(View.GONE); 253 | binding.groupConfirm.setVisibility(View.VISIBLE); 254 | } 255 | 256 | @Override 257 | public void onError(Throwable throwable) { 258 | Log.e(ProcessPaymentActivity.class.getSimpleName(), throwable.getMessage(), throwable); 259 | 260 | binding.updateSessionProgress.setVisibility(View.GONE); 261 | binding.updateSessionError.setVisibility(View.VISIBLE); 262 | 263 | showResult(R.drawable.failed, R.string.pay_error_unable_to_update_session); 264 | } 265 | } 266 | 267 | class Check3DSecureEnrollmentCallback implements ApiController.Check3DSecureEnrollmentCallback { 268 | @Override 269 | public void onSuccess(GatewayMap response) { 270 | int apiVersionInt = Integer.valueOf(apiVersion); 271 | String threeDSecureId = (String) response.get("gatewayResponse.3DSecureID"); 272 | 273 | String html = null; 274 | if (response.containsKey("gatewayResponse.3DSecure.authenticationRedirect.simple.htmlBodyContent")) { 275 | html = (String) response.get("gatewayResponse.3DSecure.authenticationRedirect.simple.htmlBodyContent"); 276 | } 277 | 278 | // for API versions <= 46, you must use the summary status field to determine next steps for 3DS 279 | if (apiVersionInt <= 46) { 280 | String summaryStatus = (String) response.get("gatewayResponse.3DSecure.summaryStatus"); 281 | 282 | if ("CARD_ENROLLED".equalsIgnoreCase(summaryStatus)) { 283 | Gateway.start3DSecureActivity(ProcessPaymentActivity.this, html); 284 | return; 285 | } 286 | 287 | binding.check3dsProgress.setVisibility(View.GONE); 288 | binding.check3dsSuccess.setVisibility(View.VISIBLE); 289 | ProcessPaymentActivity.this.threeDSecureId = null; 290 | 291 | // for these 2 cases, you still provide the 3DSecureId with the pay operation 292 | if ("CARD_NOT_ENROLLED".equalsIgnoreCase(summaryStatus) || "AUTHENTICATION_NOT_AVAILABLE".equalsIgnoreCase(summaryStatus)) { 293 | ProcessPaymentActivity.this.threeDSecureId = threeDSecureId; 294 | } 295 | 296 | processPayment(); 297 | } 298 | 299 | // for API versions >= 47, you must look to the gateway recommendation and the presence of 3DS info in the payload 300 | else { 301 | String gatewayRecommendation = (String) response.get("gatewayResponse.response.gatewayRecommendation"); 302 | 303 | // if DO_NOT_PROCEED returned in recommendation, should stop transaction 304 | if ("DO_NOT_PROCEED".equalsIgnoreCase(gatewayRecommendation)) { 305 | binding.check3dsProgress.setVisibility(View.GONE); 306 | binding.check3dsError.setVisibility(View.VISIBLE); 307 | 308 | showResult(R.drawable.failed, R.string.pay_error_3ds_authentication_failed); 309 | return; 310 | } 311 | 312 | // if PROCEED in recommendation, and we have HTML for 3ds, perform 3DS 313 | if (html != null) { 314 | Gateway.start3DSecureActivity(ProcessPaymentActivity.this, html); 315 | return; 316 | } 317 | 318 | ProcessPaymentActivity.this.threeDSecureId = threeDSecureId; 319 | 320 | processPayment(); 321 | } 322 | } 323 | 324 | @Override 325 | public void onError(Throwable throwable) { 326 | Log.e(ProcessPaymentActivity.class.getSimpleName(), throwable.getMessage(), throwable); 327 | 328 | binding.check3dsProgress.setVisibility(View.GONE); 329 | binding.check3dsError.setVisibility(View.VISIBLE); 330 | 331 | showResult(R.drawable.failed, R.string.pay_error_3ds_authentication_failed); 332 | } 333 | } 334 | 335 | class ThreeDSecureCallback implements Gateway3DSecureCallback { 336 | @Override 337 | public void on3DSecureCancel() { 338 | showError(); 339 | } 340 | 341 | @Override 342 | public void on3DSecureComplete(GatewayMap result) { 343 | int apiVersionInt = Integer.valueOf(apiVersion); 344 | 345 | if (apiVersionInt <= 46) { 346 | if ("AUTHENTICATION_FAILED".equalsIgnoreCase((String) result.get("3DSecure.summaryStatus"))) { 347 | showError(); 348 | return; 349 | } 350 | } else { // version >= 47 351 | if ("DO_NOT_PROCEED".equalsIgnoreCase((String) result.get("response.gatewayRecommendation"))) { 352 | showError(); 353 | return; 354 | } 355 | } 356 | 357 | binding.check3dsProgress.setVisibility(View.GONE); 358 | binding.check3dsSuccess.setVisibility(View.VISIBLE); 359 | 360 | ProcessPaymentActivity.this.threeDSecureId = threeDSecureId; 361 | 362 | processPayment(); 363 | } 364 | 365 | void showError() { 366 | binding.check3dsProgress.setVisibility(View.GONE); 367 | binding.check3dsError.setVisibility(View.VISIBLE); 368 | 369 | showResult(R.drawable.failed, R.string.pay_error_3ds_authentication_failed); 370 | } 371 | } 372 | 373 | class CompleteSessionCallback implements ApiController.CompleteSessionCallback { 374 | @Override 375 | public void onSuccess(String result) { 376 | binding.processPaymentProgress.setVisibility(View.GONE); 377 | binding.processPaymentSuccess.setVisibility(View.VISIBLE); 378 | 379 | showResult(R.drawable.success, R.string.pay_you_payment_was_successful); 380 | } 381 | 382 | @Override 383 | public void onError(Throwable throwable) { 384 | Log.e(ProcessPaymentActivity.class.getSimpleName(), throwable.getMessage(), throwable); 385 | 386 | binding.processPaymentProgress.setVisibility(View.GONE); 387 | binding.processPaymentError.setVisibility(View.VISIBLE); 388 | 389 | showResult(R.drawable.failed, R.string.pay_error_processing_your_payment); 390 | } 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /sample/src/main/java/com/mastercard/gateway/android/sampleapp/TLSSocketFactory.java: -------------------------------------------------------------------------------- 1 | package com.mastercard.gateway.android.sampleapp; 2 | 3 | import java.io.IOException; 4 | import java.net.InetAddress; 5 | import java.net.Socket; 6 | import java.net.UnknownHostException; 7 | import java.security.KeyManagementException; 8 | import java.security.NoSuchAlgorithmException; 9 | 10 | import javax.net.ssl.SSLContext; 11 | import javax.net.ssl.SSLSocket; 12 | import javax.net.ssl.SSLSocketFactory; 13 | 14 | 15 | public class TLSSocketFactory extends SSLSocketFactory { 16 | 17 | private SSLSocketFactory internalSSLSocketFactory; 18 | 19 | public TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException { 20 | SSLContext context = SSLContext.getInstance("TLS"); 21 | context.init(null, null, null); 22 | internalSSLSocketFactory = context.getSocketFactory(); 23 | } 24 | 25 | @Override 26 | public String[] getDefaultCipherSuites() { 27 | return internalSSLSocketFactory.getDefaultCipherSuites(); 28 | } 29 | 30 | @Override 31 | public String[] getSupportedCipherSuites() { 32 | return internalSSLSocketFactory.getSupportedCipherSuites(); 33 | } 34 | 35 | @Override 36 | public Socket createSocket() throws IOException { 37 | return enableTLSOnSocket(internalSSLSocketFactory.createSocket()); 38 | } 39 | 40 | @Override 41 | public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { 42 | return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)); 43 | } 44 | 45 | @Override 46 | public Socket createSocket(String host, int port) throws IOException, UnknownHostException { 47 | return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); 48 | } 49 | 50 | @Override 51 | public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { 52 | return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)); 53 | } 54 | 55 | @Override 56 | public Socket createSocket(InetAddress host, int port) throws IOException { 57 | return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); 58 | } 59 | 60 | @Override 61 | public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { 62 | return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); 63 | } 64 | 65 | private Socket enableTLSOnSocket(Socket socket) { 66 | if(socket != null && (socket instanceof SSLSocket)) { 67 | ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.1", "TLSv1.2"}); 68 | } 69 | return socket; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/googlepay_button_background_image.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/5fa0049b3e530ff2bdee6d1feba5a08258422e19/sample/src/main/res/drawable-hdpi/googlepay_button_background_image.9.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/googlepay_button_no_shadow_background_image.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/5fa0049b3e530ff2bdee6d1feba5a08258422e19/sample/src/main/res/drawable-hdpi/googlepay_button_no_shadow_background_image.9.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/googlepay_button_background_image.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/5fa0049b3e530ff2bdee6d1feba5a08258422e19/sample/src/main/res/drawable-mdpi/googlepay_button_background_image.9.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/googlepay_button_no_shadow_background_image.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/5fa0049b3e530ff2bdee6d1feba5a08258422e19/sample/src/main/res/drawable-mdpi/googlepay_button_no_shadow_background_image.9.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-v21/googlepay_button_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-v21/googlepay_button_no_shadow_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/googlepay_button_background_image.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/5fa0049b3e530ff2bdee6d1feba5a08258422e19/sample/src/main/res/drawable-xhdpi/googlepay_button_background_image.9.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/5fa0049b3e530ff2bdee6d1feba5a08258422e19/sample/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image.9.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/googlepay_button_background_image.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/5fa0049b3e530ff2bdee6d1feba5a08258422e19/sample/src/main/res/drawable-xxhdpi/googlepay_button_background_image.9.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/5fa0049b3e530ff2bdee6d1feba5a08258422e19/sample/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image.9.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/5fa0049b3e530ff2bdee6d1feba5a08258422e19/sample/src/main/res/drawable-xxxhdpi/failed.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/googlepay_button_background_image.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/5fa0049b3e530ff2bdee6d1feba5a08258422e19/sample/src/main/res/drawable-xxxhdpi/googlepay_button_background_image.9.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/5fa0049b3e530ff2bdee6d1feba5a08258422e19/sample/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image.9.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/5fa0049b3e530ff2bdee6d1feba5a08258422e19/sample/src/main/res/drawable-xxxhdpi/success.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/button_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/googlepay_button_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/googlepay_button_content.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 18 | 24 | 30 | 36 | 42 | 48 | 49 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/googlepay_button_overlay.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_check_green_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_error_red_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_collect_card_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 19 | 20 | 25 | 26 | 27 | 28 | 34 | 35 | 41 | 42 | 52 | 53 | 58 | 59 | 60 | 61 | 70 | 71 | 77 | 78 | 79 | 80 | 89 | 90 | 96 | 97 | 98 | 99 | 108 | 109 | 115 | 116 | 117 | 118 | 127 | 128 | 134 | 135 | 136 | 137 |