├── sample
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── drawable-xxxhdpi
│ │ │ ├── failed.png
│ │ │ ├── success.png
│ │ │ ├── googlepay_button_background_image.9.png
│ │ │ └── googlepay_button_no_shadow_background_image.9.png
│ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ ├── googlepay_strings.xml
│ │ │ ├── styles.xml
│ │ │ └── strings.xml
│ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── 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-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
│ │ │ ├── googlepay_button_background.xml
│ │ │ ├── ic_check_green_24dp.xml
│ │ │ ├── googlepay_button_overlay.xml
│ │ │ ├── ic_error_red_24dp.xml
│ │ │ ├── button_bg.xml
│ │ │ └── googlepay_button_content.xml
│ │ ├── drawable-v21
│ │ │ ├── googlepay_button_background.xml
│ │ │ └── googlepay_button_no_shadow_background.xml
│ │ └── layout
│ │ │ ├── bottom_sheet_layout.xml
│ │ │ ├── googlepay_button.xml
│ │ │ ├── activity_main.xml
│ │ │ └── activity_collect_card_info.xml
│ │ ├── java
│ │ └── com
│ │ │ └── mastercard
│ │ │ └── gateway
│ │ │ └── android
│ │ │ └── sampleapp
│ │ │ ├── utils
│ │ │ ├── RegionInfo.kt
│ │ │ ├── Ui.kt
│ │ │ ├── PaymentOptionLabelResolver.kt
│ │ │ ├── SimpleTextChangedWatcher.kt
│ │ │ ├── PaymentsClientWrapper.kt
│ │ │ ├── DeviceProvider.kt
│ │ │ ├── PaymentOptionsParser.kt
│ │ │ ├── BaseUrlInterceptor.kt
│ │ │ ├── PaymentOptionsSheet.kt
│ │ │ └── AuthAndBrowserHandler.kt
│ │ │ ├── SampleApplication.kt
│ │ │ ├── di
│ │ │ ├── GooglePayModule.kt
│ │ │ └── NetworkModule.kt
│ │ │ ├── viewmodel
│ │ │ ├── MainViewModel.kt
│ │ │ ├── CollectCardInfoViewModel.kt
│ │ │ └── ProcessPaymentViewModel.kt
│ │ │ ├── repo
│ │ │ ├── Repository.kt
│ │ │ └── PrefsRepository.kt
│ │ │ ├── api
│ │ │ └── MerchantService.kt
│ │ │ ├── MainActivity.java
│ │ │ ├── CollectCardInfoActivity.java
│ │ │ └── ProcessPaymentActivity.java
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── gateway-android
├── .gitignore
├── src
│ ├── test
│ │ ├── resources
│ │ │ ├── robolectric.properties
│ │ │ └── mockito-extensions
│ │ │ │ └── org.mockito.plugins.MockMaker
│ │ └── java
│ │ │ └── com
│ │ │ └── mastercard
│ │ │ └── gateway
│ │ │ └── android
│ │ │ └── sdk
│ │ │ ├── TestApplication.java
│ │ │ ├── TestGatewaySSLContextProvider.java
│ │ │ ├── GatewayBrowserPaymentActivityTest.java
│ │ │ ├── Gateway3DSecureActivityTest.java
│ │ │ └── GatewayTest.java
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── mastercard
│ │ │ │ └── gateway
│ │ │ │ └── android
│ │ │ │ └── sdk
│ │ │ │ ├── GatewayRequest.java
│ │ │ │ ├── Gateway3DSecureActivity.java
│ │ │ │ ├── GatewayBrowserPaymentActivity.java
│ │ │ │ ├── Logger.java
│ │ │ │ ├── GatewayGooglePayCallback.java
│ │ │ │ ├── GatewayException.java
│ │ │ │ ├── GatewayCallback.java
│ │ │ │ ├── GatewayTLSSocketFactory.java
│ │ │ │ ├── GatewaySSLContextProvider.java
│ │ │ │ └── BaseGatewayPaymentActivity.java
│ │ └── res
│ │ │ ├── values
│ │ │ └── strings.xml
│ │ │ └── layout
│ │ │ └── activity_3dsecure.xml
│ ├── release
│ │ └── java
│ │ │ └── com
│ │ │ └── mastercard
│ │ │ └── gateway
│ │ │ └── android
│ │ │ └── sdk
│ │ │ └── BaseLogger.java
│ └── debug
│ │ └── java
│ │ └── com
│ │ └── mastercard
│ │ └── gateway
│ │ └── android
│ │ └── sdk
│ │ └── BaseLogger.java
├── proguard.pro
└── build.gradle
├── settings.gradle
├── .gitignore
├── payment-flow.png
├── sample-configuration.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .travis.yml
├── gradle.properties
├── RELEASE_NOTES.md
├── gradlew.bat
├── README.md
├── gradlew
└── LICENSE.txt
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/gateway-android/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':sample', ':gateway-android'
2 |
--------------------------------------------------------------------------------
/gateway-android/src/test/resources/robolectric.properties:
--------------------------------------------------------------------------------
1 | sdk=28
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | local.properties
4 | .idea
5 | .DS_Store
6 | build
7 |
--------------------------------------------------------------------------------
/gateway-android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker:
--------------------------------------------------------------------------------
1 | mock-maker-inline
--------------------------------------------------------------------------------
/payment-flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/HEAD/payment-flow.png
--------------------------------------------------------------------------------
/sample-configuration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/HEAD/sample-configuration.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xxxhdpi/failed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/HEAD/sample/src/main/res/drawable-xxxhdpi/failed.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xxxhdpi/success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/HEAD/sample/src/main/res/drawable-xxxhdpi/success.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #FF5B5D
5 |
6 |
7 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-hdpi/googlepay_button_background_image.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/HEAD/sample/src/main/res/drawable-hdpi/googlepay_button_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/HEAD/sample/src/main/res/drawable-mdpi/googlepay_button_background_image.9.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xhdpi/googlepay_button_background_image.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/HEAD/sample/src/main/res/drawable-xhdpi/googlepay_button_background_image.9.png
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/utils/RegionInfo.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.utils
2 |
3 | data class RegionInfo(
4 | val name: String,
5 | val prefix: String
6 | )
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xxhdpi/googlepay_button_background_image.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/HEAD/sample/src/main/res/drawable-xxhdpi/googlepay_button_background_image.9.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xxxhdpi/googlepay_button_background_image.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mastercard-Gateway/gateway-android-sdk/HEAD/sample/src/main/res/drawable-xxxhdpi/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/HEAD/sample/src/main/res/drawable-hdpi/googlepay_button_no_shadow_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/HEAD/sample/src/main/res/drawable-mdpi/googlepay_button_no_shadow_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/HEAD/sample/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_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/HEAD/sample/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_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/HEAD/sample/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image.9.png
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/SampleApplication.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 |
7 | @HiltAndroidApp
8 | class SampleApplication : Application()
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/googlepay_button_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/gateway-android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Nov 11 20:12:39 IST 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
6 | public class TestApplication extends Application {
7 |
8 | @Override
9 | public void onCreate() {
10 | super.onCreate();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_check_green_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/utils/Ui.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.utils
2 |
3 | import android.view.View
4 |
5 | object Ui {
6 |
7 | @JvmStatic
8 | fun show(v: View?) {
9 | v?.visibility = View.VISIBLE
10 | }
11 |
12 | @JvmStatic
13 | fun hide(v: View?) {
14 | v?.visibility = View.GONE
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/gateway-android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 3-D Secure Authentication
4 | Browser Payment
5 | Missing summary status
6 | Missing 3-D Secure Id
7 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/utils/PaymentOptionLabelResolver.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.utils
2 |
3 | object PaymentOptionLabelResolver {
4 |
5 | fun labelFor(option: String?): String = when (option) {
6 | "CARD" -> "Card"
7 | "KNET" -> "KNET"
8 | "BENEFIT" -> "Benefit"
9 | "QPAY" -> "QPAY"
10 | "OMAN" -> "Oman Net"
11 | else -> option ?: "Pay"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/googlepay_strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Google Pay
4 | Buy with Google Pay
5 | Save to Google Pay
6 | Add to Google Pay
7 |
8 |
--------------------------------------------------------------------------------
/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_error_red_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
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 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/utils/SimpleTextChangedWatcher.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.utils
2 |
3 |
4 | import android.text.Editable
5 | import android.text.TextWatcher
6 |
7 | class SimpleTextChangedWatcher(private val onAnyChange: Runnable) : TextWatcher {
8 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
9 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
10 | onAnyChange.run()
11 | }
12 | override fun afterTextChanged(s: Editable?) {}
13 | }
14 |
--------------------------------------------------------------------------------
/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/java/com/mastercard/gateway/android/sampleapp/di/GooglePayModule.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.di
2 |
3 | import android.content.Context
4 | import com.mastercard.gateway.android.sampleapp.utils.PaymentsClientWrapper
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.components.ActivityComponent
9 | import dagger.hilt.android.qualifiers.ActivityContext
10 |
11 | @Module
12 | @InstallIn(ActivityComponent::class) // Not SingletonComponent!
13 | object GooglePayModule {
14 |
15 | @Provides
16 | fun providePaymentsClientWrapper(@ActivityContext context: Context): PaymentsClientWrapper {
17 | return PaymentsClientWrapper(context);
18 | }
19 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/gateway-android/src/main/java/com/mastercard/gateway/android/sdk/Gateway3DSecureActivity.java:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sdk;
2 |
3 | import android.net.Uri;
4 |
5 | import androidx.annotation.NonNull;
6 |
7 | public class Gateway3DSecureActivity extends BaseGatewayPaymentActivity {
8 |
9 | private static final String DEFAULT_TITLE = "3D Secure";
10 |
11 | @NonNull @Override protected String gatewayHost() { return "3dsecure"; }
12 |
13 | @NonNull @Override protected String getDefaultTitle() { return DEFAULT_TITLE; }
14 |
15 | @Override
16 | protected void onGatewayRedirect(@NonNull Uri uri) {
17 | // Expected form: gatewaysdk://3dsecure?acsResult=...
18 | String result = getQueryParam(uri, "acsResult");
19 | complete(EXTRA_GATEWAY_RESULT, result);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/gateway-android/src/main/java/com/mastercard/gateway/android/sdk/GatewayBrowserPaymentActivity.java:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sdk;
2 |
3 | import android.net.Uri;
4 |
5 | import androidx.annotation.NonNull;
6 |
7 | public class GatewayBrowserPaymentActivity extends BaseGatewayPaymentActivity {
8 |
9 | private static final String DEFAULT_TITLE = "Payment";
10 |
11 | @NonNull @Override protected String gatewayHost() { return "browserpayment"; }
12 |
13 | @NonNull @Override protected String getDefaultTitle() { return DEFAULT_TITLE; }
14 |
15 | @Override
16 | protected void onGatewayRedirect(@NonNull Uri uri) {
17 | // Expected form: gatewaysdk://paymentbrowser?orderResult=...
18 | String result = getQueryParam(uri, "orderResult");
19 | complete(EXTRA_GATEWAY_RESULT, result);
20 | }
21 | }
--------------------------------------------------------------------------------
/sample/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/utils/PaymentsClientWrapper.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.utils
2 |
3 | import android.content.Context
4 | import com.google.android.gms.wallet.IsReadyToPayRequest
5 | import com.google.android.gms.wallet.PaymentsClient
6 | import com.google.android.gms.wallet.Wallet
7 | import com.google.android.gms.wallet.Wallet.WalletOptions
8 | import com.google.android.gms.wallet.WalletConstants
9 | import com.google.android.gms.tasks.Task
10 |
11 |
12 | class PaymentsClientWrapper(context: Context?) {
13 | val client: PaymentsClient
14 |
15 | init {
16 | val walletOptions = WalletOptions.Builder()
17 | .setEnvironment(WalletConstants.ENVIRONMENT_TEST)
18 | .build()
19 |
20 | this.client = Wallet.getPaymentsClient(context!!, walletOptions)
21 | }
22 |
23 | fun isReadyToPay(request: IsReadyToPayRequest?): Task {
24 | return client.isReadyToPay(request!!)
25 | }
26 | }
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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.**
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/utils/DeviceProvider.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.utils
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import androidx.annotation.RequiresApi
6 | import dagger.hilt.android.qualifiers.ApplicationContext
7 | import java.util.Calendar
8 | import java.util.TimeZone
9 | import javax.inject.Inject
10 | import javax.inject.Singleton
11 |
12 | @Singleton
13 | class DeviceInfoProvider @Inject constructor(
14 | @ApplicationContext private val context: Context
15 | ){
16 |
17 | @RequiresApi(Build.VERSION_CODES.N)
18 | fun getLanguage(): String {
19 | return context.resources.configuration.locales[0].language ?: ""
20 | }
21 |
22 | fun getScreenWidth(): Int {
23 | return context.resources.displayMetrics.widthPixels
24 | }
25 |
26 | fun getScreenHeight(): Int {
27 | return context.resources.displayMetrics.heightPixels
28 | }
29 |
30 | fun getTimezoneOffsetInMinutes(): Int {
31 | val now = Calendar.getInstance()
32 | val tz = TimeZone.getDefault()
33 | return tz.getOffset(now.timeInMillis) / 60000
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/bottom_sheet_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
23 |
24 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/viewmodel/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.mastercard.gateway.android.sampleapp.repo.PrefsRepository
6 | import com.mastercard.gateway.android.sampleapp.utils.RegionInfo
7 | import com.mastercard.gateway.android.sdk.Gateway
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.launch
10 | import javax.inject.Inject
11 |
12 | @HiltViewModel
13 | class MainViewModel @Inject constructor(private val prefsRepo: PrefsRepository) : ViewModel() {
14 |
15 | val merchantId = prefsRepo.getMerchantId()
16 | val region = prefsRepo.getRegion()
17 | val merchantServerLink = prefsRepo.getServerUrl()
18 |
19 | val regions: List = Gateway.Region.entries.map { RegionInfo(it.name, it.prefix) }
20 |
21 |
22 | fun saveSessionData(merchantId: String, region: String, link: String) {
23 | viewModelScope.launch {
24 | prefsRepo.saveMerchantId(merchantId)
25 | prefsRepo.saveRegion(region)
26 | prefsRepo.saveServerUrl(link)
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/utils/PaymentOptionsParser.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.utils
2 |
3 | import com.mastercard.gateway.android.sdk.GatewayMap
4 |
5 | object PaymentOptionsParser {
6 |
7 | object Constants {
8 | const val CARD = "CARD"
9 | }
10 |
11 | @JvmStatic
12 | fun extractTypes(options: GatewayMap?): List {
13 | val result = options?.get("result") as? String ?: return emptyList()
14 | if (!result.equals("SUCCESS", ignoreCase = true)) return emptyList()
15 |
16 | val paymentTypes: Map<*, *> = when (val pt = options["paymentTypes"]) {
17 | is GatewayMap -> pt
18 | is Map<*, *> -> pt
19 | else -> return emptyList()
20 | }
21 |
22 | val out = linkedSetOf()
23 |
24 | if (paymentTypes["card"] != null) out += Constants.CARD
25 |
26 | (paymentTypes["browserPayment"] as? List<*>)?.forEach { item ->
27 | val type = when (item) {
28 | is Map<*, *> -> item["type"] as? String
29 | is GatewayMap -> item["type"] as? String
30 | else -> null
31 | }
32 | if (!type.isNullOrBlank()) out += type
33 | }
34 |
35 | return out.toList()
36 | }
37 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.daemon=true
2 | org.gradle.configureondemand=false
3 | org.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
4 | android.useAndroidX=true
5 | android.enableJetifier=true
6 |
7 | # ===== Coordinates =====
8 | GROUP=com.mastercard.gateway
9 | POM_ARTIFACT_ID=gateway-android
10 | VERSION_NAME=1.1.9
11 |
12 | # ===== POM Info =====
13 | POM_NAME=Mastercard Payment Gateway Android SDK
14 | POM_DESCRIPTION=The official Android SDK for Mastercard Payment Gateway Services (MPGS)
15 | POM_INCEPTION_YEAR=2020
16 | POM_URL=https://github.com/Mastercard-Gateway/gateway-android-sdk
17 |
18 | # ===== License =====
19 | POM_LICENCE_NAME=Apache-2.0
20 | POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt
21 | POM_LICENCE_DIST=repo
22 |
23 | # ===== SCM (Source Control) =====
24 | POM_SCM_URL=https://github.com/Mastercard-Gateway/gateway-android-sdk
25 | POM_SCM_CONNECTION=scm:git:git://github.com/Mastercard-Gateway/gateway-android-sdk.git
26 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/Mastercard-Gateway/gateway-android-sdk.git
27 |
28 | # ===== Developer Info =====
29 | POM_DEVELOPER_ID=mastercard
30 | POM_DEVELOPER_NAME=Mastercard MPGS Team
31 | POM_DEVELOPER_EMAIL=simplify_mobile_team@mastercard.com
32 | POM_DEVELOPER_URL=https://www.mastercard.com
33 |
34 | # ===== Vanniktech Plugin Flags =====
35 | mavenCentralPublishing=true
36 | signAllPublications=true
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/googlepay_button.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
17 |
24 |
25 |
31 |
32 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/utils/BaseUrlInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.utils
2 |
3 | import android.util.Log
4 | import com.mastercard.gateway.android.sampleapp.repo.PrefsRepository
5 | import com.mastercard.gateway.android.sampleapp.viewmodel.ProcessPaymentViewModel
6 | import okhttp3.HttpUrl
7 | import okhttp3.Interceptor
8 | import okhttp3.Response
9 | import java.util.Locale
10 | import javax.inject.Inject
11 |
12 | class BaseUrlInterceptor @Inject constructor(
13 | private val prefsRepository: PrefsRepository
14 | ) : Interceptor {
15 | override fun intercept(chain: Interceptor.Chain): Response {
16 | val originalRequest = chain.request()
17 | val originalUrl = originalRequest.url
18 |
19 | // Fetch latest user-configurable values
20 | val region = prefsRepository.getRegion()
21 | val version = ProcessPaymentViewModel.API_VERSION
22 |
23 | val newBaseUrl = HttpUrl.Builder()
24 | .scheme("https")
25 | .host("${region.lowercase(Locale.ROOT)}.gateway.mastercard.com")
26 | .addPathSegment("api")
27 | .addPathSegment("rest")
28 | .addPathSegment("version")
29 | .addPathSegment(version)
30 | .build()
31 |
32 | // Extract the remaining path after baseUrl
33 | val newFullUrl = newBaseUrl.newBuilder()
34 | for (segment in originalUrl.pathSegments) {
35 | newFullUrl.addPathSegment(segment)
36 | }
37 |
38 | val updatedRequest = originalRequest.newBuilder()
39 | .url(newFullUrl.build())
40 | .build()
41 |
42 | Log.e("URL_INTERCEPTOR", "Final URL: ${updatedRequest.url}")
43 |
44 | return chain.proceed(updatedRequest)
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/repo/Repository.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.repo
2 |
3 | import com.mastercard.gateway.android.sampleapp.api.MerchantService
4 | import com.mastercard.gateway.android.sdk.GatewayMap
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.withContext
7 | import javax.inject.Inject
8 | import javax.inject.Singleton
9 |
10 | @Singleton
11 | class Repository @Inject constructor(
12 | private val merchantService: MerchantService
13 | ) {
14 |
15 | suspend fun createSession(payload: GatewayMap): GatewayMap =
16 | withContext(Dispatchers.IO) {
17 | merchantService.createSession(payload)
18 | }
19 |
20 | suspend fun initiateAuthentication(
21 | orderId: String,
22 | transactionId: String,
23 | payload: GatewayMap
24 | ): GatewayMap =
25 | withContext(Dispatchers.IO) {
26 | merchantService.initiateAuthentication(orderId, transactionId, payload)
27 | }
28 |
29 | suspend fun submitTransaction(
30 | orderId: String,
31 | transactionId: String,
32 | payload: GatewayMap
33 | ): GatewayMap =
34 | withContext(Dispatchers.IO) {
35 | merchantService.submitTransaction(orderId, transactionId, payload)
36 | }
37 |
38 | suspend fun inquirePaymentOptions(): GatewayMap =
39 | withContext(Dispatchers.IO) {
40 | merchantService.inquirePaymentOptions()
41 | }
42 |
43 | suspend fun initiateBrowserPayment(
44 | orderId: String,
45 | transactionId: String,
46 | request: GatewayMap
47 | ): GatewayMap =
48 | withContext(Dispatchers.IO) {
49 | merchantService.initiateBrowserPayment(orderId, transactionId, request)
50 | }
51 | }
--------------------------------------------------------------------------------
/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 | default void onSuccess(GatewayMap response) {
28 | }
29 |
30 | /**
31 | * Callback executed when error thrown during call to Gateway API
32 | *
33 | * @param throwable The exception thrown
34 | */
35 | default void onError(Throwable throwable) {
36 | }
37 |
38 | /**
39 | * Callback method when webview-based authentication is complete.
40 | *
41 | * @param result A response map containing the result
42 | * @param requestCode Request code identifying the flow
43 | */
44 | default void onComplete(GatewayMap result, int requestCode) {
45 | }
46 |
47 | /**
48 | * Callback when a user cancels the flow (typically on back press).
49 | *
50 | * @param requestCode Request code identifying the flow
51 | */
52 | default void onCancel(int requestCode) {
53 | }
54 | }
--------------------------------------------------------------------------------
/gateway-android/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'com.vanniktech.maven.publish'
3 |
4 | android {
5 |
6 | compileSdk 35
7 | namespace = "com.mastercard.gateway.android"
8 |
9 | defaultConfig {
10 | minSdkVersion 23
11 | targetSdk 35
12 | versionName libraryVersionName
13 | consumerProguardFiles 'proguard.pro'
14 | buildConfigField "String", "SDK_VERSION", "\"${versionName}\""
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 | buildFeatures {
27 | buildConfig true
28 | dataBinding true
29 | }
30 |
31 | buildTypes {
32 | debug
33 | release
34 | }
35 |
36 | testOptions {
37 | unitTests {
38 | includeAndroidResources = true
39 | }
40 | }
41 | }
42 |
43 | // define an 'optional' dependency
44 | configurations {
45 | optional
46 | implementation.extendsFrom optional
47 | }
48 |
49 | dependencies {
50 | implementation fileTree(include: ['*.jar'], dir: 'libs')
51 |
52 | // required
53 | implementation 'androidx.appcompat:appcompat:1.1.0'
54 | implementation 'com.google.code.gson:gson:2.13.2'
55 | implementation 'com.google.android.material:material:1.12.0'
56 |
57 | // optional for rx java
58 | optional 'io.reactivex.rxjava2:rxjava:2.2.5'
59 |
60 | // optional for google pay
61 | optional 'androidx.legacy:legacy-support-v4:1.0.0'
62 | optional 'com.google.android.gms:play-services-wallet:19.5.0'
63 |
64 | compileOnly 'androidx.annotation:annotation:1.1.0'
65 |
66 | testImplementation 'junit:junit:4.13.2'
67 | testImplementation 'org.robolectric:robolectric:4.16'
68 | testImplementation 'org.mockito:mockito-core:5.20.0'
69 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.di
2 |
3 | import com.mastercard.gateway.android.sampleapp.api.MerchantService
4 | import com.mastercard.gateway.android.sampleapp.repo.PrefsRepository
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import okhttp3.OkHttpClient
10 | import okhttp3.logging.HttpLoggingInterceptor
11 | import retrofit2.Retrofit
12 | import retrofit2.converter.gson.GsonConverterFactory
13 | import java.util.concurrent.TimeUnit
14 | import javax.inject.Singleton
15 |
16 | @Module
17 | @InstallIn(SingletonComponent::class)
18 | object NetworkModule {
19 |
20 | @Provides
21 | @Singleton
22 | fun provideOkHttpClient(): OkHttpClient {
23 | val timeoutValue = 500L
24 | val httpInterceptor = HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
25 |
26 | return OkHttpClient.Builder()
27 | .addInterceptor(httpInterceptor)
28 | .readTimeout(timeoutValue, TimeUnit.SECONDS)
29 | .connectTimeout(timeoutValue, TimeUnit.SECONDS)
30 | .writeTimeout(timeoutValue, TimeUnit.SECONDS)
31 | .build()
32 | }
33 |
34 | @Provides
35 | @Singleton
36 | fun provideMerchantRetrofit(
37 | prefsRepository: PrefsRepository,
38 | okHttpClient: OkHttpClient
39 | ): Retrofit {
40 | val merchantUrl = prefsRepository.getServerUrl()
41 | return Retrofit.Builder()
42 | .baseUrl(merchantUrl)
43 | .client(okHttpClient)
44 | .addConverterFactory(GsonConverterFactory.create())
45 | .build()
46 | }
47 |
48 | @Provides
49 | @Singleton
50 | fun provideMerchantService(retrofit: Retrofit): MerchantService {
51 | return retrofit.create(MerchantService::class.java)
52 | }
53 | }
--------------------------------------------------------------------------------
/gateway-android/src/main/res/layout/activity_3dsecure.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
10 |
13 |
14 |
19 |
20 |
21 |
22 |
26 |
27 |
31 |
32 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/repo/PrefsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.repo
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import dagger.hilt.android.qualifiers.ApplicationContext
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | class PrefsRepository @Inject constructor(
11 | @ApplicationContext context: Context
12 | ) {
13 |
14 | private val prefs: SharedPreferences =
15 | context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
16 |
17 | companion object {
18 | private const val PREFS_NAME = "app_prefs"
19 |
20 | private const val KEY_MERCHANT_ID = "merchant_id"
21 | private const val KEY_REGION = "region"
22 | private const val KEY_SERVER_URL = "server_url"
23 | private const val KEY_PAYMENT_TYPE = "payment_type"
24 |
25 | private const val DEFAULT_STRING = ""
26 | }
27 |
28 | fun saveMerchantId(merchantId: String) = prefs.edit { putString(KEY_MERCHANT_ID, merchantId) }
29 | fun saveRegion(region: String) = prefs.edit { putString(KEY_REGION, region) }
30 | fun saveServerUrl(url: String) = prefs.edit { putString(KEY_SERVER_URL, url) }
31 | fun savePaymentType(type: String) = prefs.edit { putString(KEY_PAYMENT_TYPE, type) }
32 | fun getMerchantId(): String = prefs.getString(KEY_MERCHANT_ID, DEFAULT_STRING) ?: DEFAULT_STRING
33 | fun getRegion(): String = prefs.getString(KEY_REGION, DEFAULT_STRING) ?: DEFAULT_STRING
34 | fun getServerUrl(): String = prefs.getString(KEY_SERVER_URL, DEFAULT_STRING) ?: DEFAULT_STRING
35 | fun getPaymentType(): String = prefs.getString(KEY_PAYMENT_TYPE, DEFAULT_STRING) ?: DEFAULT_STRING
36 |
37 | private inline fun SharedPreferences.edit(
38 | commit: Boolean = false,
39 | action: SharedPreferences.Editor.() -> Unit
40 | ) {
41 | val editor = edit()
42 | action(editor)
43 | if (commit) editor.commit() else editor.apply()
44 | }
45 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/api/MerchantService.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.api
2 |
3 | import androidx.annotation.Keep
4 | import com.mastercard.gateway.android.sdk.GatewayMap
5 | import retrofit2.http.Body
6 | import retrofit2.http.GET
7 | import retrofit2.http.POST
8 | import retrofit2.http.PUT
9 | import retrofit2.http.Query
10 |
11 | @Keep
12 | @JvmSuppressWildcards
13 | interface MerchantService {
14 |
15 | @POST(CREATE_SESSION_ENDPOINT)
16 | suspend fun createSession(@Body payload: GatewayMap): GatewayMap
17 | @POST(PAYMENT_OPTIONS_INQUIRY_ENDPOINT)
18 | suspend fun inquirePaymentOptions(): GatewayMap
19 |
20 | @PUT(START_AUTHENTICATION_ENDPOINT)
21 | suspend fun initiateAuthentication(
22 | @Query(ORDER_ID_PARAM) orderId: String,
23 | @Query(TRANSACTION_ID_PARAM) transactionId: String,
24 | @Body payload: GatewayMap
25 | ): GatewayMap
26 |
27 | @PUT(START_BROWSER_PAYMENT_ENDPOINT)
28 | suspend fun initiateBrowserPayment(
29 | @Query(ORDER_ID_PARAM) orderId: String,
30 | @Query(TRANSACTION_ID_PARAM) transactionId: String,
31 | @Body payload: GatewayMap
32 | ): GatewayMap
33 |
34 | @PUT(SUBMIT_TRANSACTION_ENDPOINT)
35 | suspend fun submitTransaction(
36 | @Query(ORDER_ID_PARAM) orderId: String,
37 | @Query(TRANSACTION_ID_PARAM) transactionId: String,
38 | @Body payload: GatewayMap
39 | ): GatewayMap
40 |
41 | companion object {
42 | // Query keys (keep consistent & reusable)
43 | const val ORDER_ID_PARAM = "orderId"
44 | const val TRANSACTION_ID_PARAM = "transactionId"
45 |
46 | // Endpoints
47 | const val CREATE_SESSION_ENDPOINT = "session.php"
48 | const val SUBMIT_TRANSACTION_ENDPOINT = "transaction.php"
49 | const val START_AUTHENTICATION_ENDPOINT = "start-authentication.php"
50 |
51 | const val PAYMENT_OPTIONS_INQUIRY_ENDPOINT = "payment-options-inquiry.php"
52 | const val START_BROWSER_PAYMENT_ENDPOINT = "start-browser-payment.php"
53 | }
54 | }
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
14 |
15 |
16 |
19 |
20 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
36 |
37 |
41 |
42 |
46 |
47 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/utils/PaymentOptionsSheet.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.utils
2 |
3 | import android.content.Context
4 | import android.graphics.Color
5 | import android.util.TypedValue
6 | import android.view.View
7 | import android.widget.Button
8 | import android.widget.LinearLayout
9 | import com.google.android.material.bottomsheet.BottomSheetDialog
10 | import com.mastercard.gateway.android.sampleapp.R
11 |
12 | class PaymentOptionsSheet(
13 | context: Context,
14 | sheetContent: View
15 | ) {
16 |
17 | fun interface Listener {
18 | fun onOptionClicked(optionType: String)
19 | }
20 |
21 | private val dialog = BottomSheetDialog(context).apply { setContentView(sheetContent) }
22 | private val buttonContainer: LinearLayout =
23 | sheetContent.findViewById(R.id.dynamicButtonContainer)
24 |
25 | fun show(types: List, listener: Listener) {
26 | buttonContainer.removeAllViews()
27 | types.forEach { type ->
28 | val btn = buildButton(buttonContainer.context, type).apply {
29 | setOnClickListener {
30 | listener.onOptionClicked(type)
31 | dialog.dismiss()
32 | }
33 | }
34 | buttonContainer.addView(btn)
35 | }
36 | dialog.show()
37 | }
38 |
39 | private fun buildButton(ctx: Context, type: String): Button {
40 | val marginPx = TypedValue.applyDimension(
41 | TypedValue.COMPLEX_UNIT_DIP, 10f, ctx.resources.displayMetrics
42 | ).toInt()
43 |
44 | val lp = LinearLayout.LayoutParams(
45 | LinearLayout.LayoutParams.MATCH_PARENT,
46 | LinearLayout.LayoutParams.WRAP_CONTENT
47 | ).apply { setMargins(0, 0, 0, marginPx) }
48 |
49 | return Button(ctx).apply {
50 | layoutParams = lp
51 | setBackgroundResource(R.color.coral)
52 | setTextColor(Color.WHITE)
53 | tag = type
54 | text = PaymentOptionLabelResolver.labelFor(type)
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/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/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 = "4293743540046975378534879503202253541";
54 |
55 | assertNotNull(certificate);
56 | assertEquals(expectedSerialNo, certificate.getSerialNumber().toString());
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/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 |
9 | ## [1.1.9] - 2025-11-21
10 | ### Added
11 | * Enabled support for Browser Payment flow, including Ottu integration.
12 | * Added a loader inside WebView to show progress until content is fully loaded.
13 | * Added full Kotlin support in the SDK and sample application.
14 |
15 | ### Changed
16 | * Refactored minimal SDK code to support both 3DS and Browser Payment flows.
17 | * Replaced Gateway3DSecureCallback with unified GatewayCallback.
18 | * Refactored and partially rewrote the sample application.
19 | * Updated Gradle plugin to com.android.tools.build:gradle:8.13.1
20 | * Increased minSdkVersion from 19 → 23.
21 | * Cleaned up code related to JCenter.
22 | * Updated and added new unit tests.
23 | * Updated libraries to address CVE vulnerabilities.
24 |
25 | [1.1.9-beta01] - 2025-09-04
26 | ### Added
27 | - Enabled support for Browser Payment flow, including Ottu integration.
28 | - Added a loader inside WebView to show progress until content is fully loaded.
29 | - Added full Kotlin support in the SDK and sample application.
30 |
31 | ### Changed
32 | - Refactored minimal SDK code to support both 3DS and Browser Payment flows.
33 | - Replaced Gateway3DSecureCallback with unified GatewayCallback.
34 | - Refactored and partially rewrote the sample application.
35 | - Updated Gradle plugin to com.android.tools.build:gradle:8.4.0.
36 | - Increased minSdkVersion from 19 → 21.
37 | - Cleaned up code related to JCenter.
38 | - Updated and added new unit tests.
39 |
40 | ## [1.1.8] - 2025-03-25
41 | ### Added
42 | - Updated Gson library to version 2.12.1 to address security vulnerabilities.
43 |
44 | ## [1.1.7] - 2025-02-21
45 | ### Added
46 | - DigiCert updated. New Expiry Jan 15, 2038
47 |
48 | ## [1.1.6] - 2024-02-08
49 | ### Added
50 | - Saudi region (KSA) URL
51 |
52 | ## [1.1.5] - 2022-12-28
53 | ### Changed
54 | - Pinned certificate updated. New Expiry December 2030
55 |
56 | ## [1.1.4] - 2020-03-26
57 | ### Fixed
58 | - Issue where WebView was not displaying the 3DS HTML on apps targeting API >=29
59 | ### Changed
60 | - SDK and sample app now targeting API 29
61 | - Migrated from legacy Android support libraries to Jetpack
62 |
63 | ## [1.1.3] - 2020-02-14
64 | ### Added
65 | - China region (CN) URL
66 | ### Changed
67 | - Enabled TLSv1.2 support for API <21
68 |
69 | ## [1.1.2] - 2020-02-04
70 | ### Added
71 | - India region (IN) URL
72 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/gateway-android/src/test/java/com/mastercard/gateway/android/sdk/GatewayBrowserPaymentActivityTest.java:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sdk;
2 |
3 | import static org.junit.Assert.assertEquals;
4 | import static org.junit.Assert.assertNotNull;
5 | import static org.mockito.ArgumentMatchers.any;
6 | import static org.mockito.ArgumentMatchers.anyInt;
7 | import static org.mockito.ArgumentMatchers.eq;
8 | import static org.mockito.Mockito.doNothing;
9 | import static org.mockito.Mockito.spy;
10 | import static org.mockito.Mockito.verify;
11 |
12 | import android.app.Activity;
13 | import android.content.Intent;
14 | import android.net.Uri;
15 |
16 | import org.junit.Test;
17 | import org.junit.runner.RunWith;
18 | import org.mockito.ArgumentCaptor;
19 | import org.robolectric.RobolectricTestRunner;
20 | import org.robolectric.annotation.Config;
21 |
22 | @RunWith(RobolectricTestRunner.class)
23 | @Config(application = TestApplication.class)
24 | public class GatewayBrowserPaymentActivityTest {
25 |
26 | @Test
27 | public void testGetDefaultTitle() {
28 | GatewayBrowserPaymentActivity activity = new GatewayBrowserPaymentActivity();
29 | assertEquals("Payment", activity.getDefaultTitle());
30 | }
31 |
32 | @Test
33 | public void testGatewayHost() {
34 | GatewayBrowserPaymentActivity activity = new GatewayBrowserPaymentActivity();
35 | assertEquals("browserpayment", activity.gatewayHost());
36 | }
37 |
38 | @Test
39 | public void testOnGatewayRedirectCallsCompleteWithOrderResult() {
40 | Uri testUri = Uri.parse("gatewaysdk://browserpayment?irrelevant=foo&orderResult=success123");
41 |
42 | GatewayBrowserPaymentActivity activity = spy(new GatewayBrowserPaymentActivity());
43 |
44 | // Prevent Android internals
45 | doNothing().when(activity).finish();
46 | doNothing().when(activity).setResult(anyInt(), any(Intent.class));
47 |
48 | activity.onGatewayRedirect(testUri);
49 |
50 | ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class);
51 | verify(activity).setResult(eq(Activity.RESULT_OK), captor.capture());
52 | verify(activity).finish();
53 |
54 | Intent captured = captor.getValue();
55 | assertNotNull(captured);
56 | assertEquals("success123", captured.getStringExtra(BaseGatewayPaymentActivity.EXTRA_GATEWAY_RESULT));
57 | }
58 |
59 | @Test
60 | public void testOnGatewayRedirectHandlesMissingOrderResult() {
61 | Uri testUri = Uri.parse("gatewaysdk://browserpayment?foo=bar");
62 |
63 | GatewayBrowserPaymentActivity activity = spy(new GatewayBrowserPaymentActivity());
64 |
65 | doNothing().when(activity).finish();
66 | doNothing().when(activity).setResult(anyInt(), any(Intent.class));
67 |
68 | activity.onGatewayRedirect(testUri);
69 |
70 | ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class);
71 | verify(activity).setResult(eq(Activity.RESULT_OK), captor.capture());
72 | verify(activity).finish();
73 |
74 | Intent captured = captor.getValue();
75 | assertNotNull(captured);
76 | // if missing, getQueryParam returns null → complete(key, null)
77 | assertEquals(null, captured.getStringExtra(BaseGatewayPaymentActivity.EXTRA_GATEWAY_RESULT));
78 | }
79 | }
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'kotlin-kapt'
4 | apply plugin: 'dagger.hilt.android.plugin'
5 |
6 | android {
7 | compileSdk 35
8 | namespace = "com.mastercard.gateway.android.sampleapp"
9 |
10 | defaultConfig {
11 | applicationId 'com.mastercard.gateway.android.sampleapp'
12 | minSdkVersion 23
13 | targetSdk 35
14 | versionCode 1
15 | versionName '1.0.0'
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 |
19 | vectorDrawables {
20 | useSupportLibrary true
21 | }
22 |
23 | multiDexEnabled true
24 | }
25 |
26 | buildFeatures {
27 | buildConfig true
28 | dataBinding true
29 | }
30 |
31 | buildTypes {
32 | release {
33 | minifyEnabled false
34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
35 | }
36 | }
37 |
38 | dataBinding {
39 | enabled = true
40 | }
41 |
42 | lintOptions {
43 | abortOnError false
44 | }
45 | compileOptions {
46 | sourceCompatibility JavaVersion.VERSION_1_8
47 | targetCompatibility JavaVersion.VERSION_1_8
48 | }
49 | kotlinOptions {
50 | jvmTarget = '1.8'
51 | }
52 | }
53 |
54 | dependencies {
55 | implementation fileTree(include: ['*.jar'], dir: 'libs')
56 | implementation 'com.google.code.gson:gson:2.13.2'
57 | implementation 'androidx.appcompat:appcompat:1.1.0'
58 | implementation 'com.google.android.material:material:1.1.0'
59 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
60 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
61 | implementation 'com.google.android.gms:play-services-wallet:19.5.0'
62 | implementation 'com.github.esnaultdev:MaterialValues:v1.1.1'
63 |
64 | implementation project(':gateway-android')
65 |
66 | // retrofit
67 | implementation 'com.squareup.retrofit2:retrofit:3.0.0'
68 | // gson converter
69 | implementation 'com.squareup.retrofit2:converter-gson:3.0.0'
70 | implementation("com.squareup.retrofit2:adapter-rxjava2:3.0.0")
71 | implementation 'com.squareup.okhttp3:logging-interceptor:5.3.0'
72 |
73 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0"
74 | implementation 'androidx.lifecycle:lifecycle-viewmodel:2.6.2'
75 | implementation "androidx.datastore:datastore-preferences:1.0.0"
76 |
77 | implementation "com.google.dagger:hilt-android:2.57.2"
78 | kapt "com.google.dagger:hilt-compiler:2.57.2"
79 |
80 | implementation "androidx.hilt:hilt-common:1.1.0"
81 | implementation "androidx.hilt:hilt-work:1.1.0" // Optional for WorkManager
82 | kapt "androidx.hilt:hilt-compiler:1.1.0"
83 | implementation "androidx.hilt:hilt-work:1.1.0"
84 | implementation "androidx.work:work-runtime:2.9.0"
85 |
86 | // LifeCycled
87 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
88 | implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.7")
89 |
90 | // Espresso testing
91 | androidTestImplementation 'junit:junit:4.13.2'
92 | androidTestImplementation('androidx.test.espresso:espresso-core:3.7.0') {
93 | exclude module: 'support-annotations'
94 | }
95 | androidTestImplementation('androidx.test:runner:1.1.0') {
96 | exclude module: 'support-annotations'
97 | }
98 | }
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/googlepay_button_content.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
48 |
49 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Gateway SDK Sample App
3 |
4 | None
5 |
6 | Configuration:
7 | Merchant Id
8 | Region
9 | Api
10 | Currency
11 | Amount
12 | Select region
13 | Heroku Test Server URL
14 | Merchant Server Enabled
15 | Process a Payment
16 |
17 | Name on card
18 | Card number
19 | Expiry month (MM)
20 | Expiry year (YY)
21 | CVV
22 | Continue
23 | - or -
24 |
25 |
26 | result
27 | Your payment was successful
28 | Card info not collected
29 | Error processing your payment
30 | 3DS authentication failed
31 | 3DS authentication unavailable
32 | 3DS authentication rejected
33 | 3DS authentication pending
34 | Browser payment authentication declined
35 | Browser payment authentication pending
36 | Browser payment authentication canceled
37 | Unable to update session
38 | Unable to create session
39 | Unable to complete the payment
40 | Steps
41 | Status
42 | 1. Create Session
43 |
44 |
45 | 2. Payment Option Inquiry
46 | 3. Select Payment Option
47 | 4. Collect Card Info
48 |
49 | 5. Update Session with Payer Data
50 | 6. Initiate Authentication
51 | 7. Authenticate 3D Secure Payment
52 | Process Payment
53 | 8. Process Payment
54 |
55 | 4. Initiate Browser Payment
56 | 5. Authenticate Browser Payment
57 |
58 | Process Payment for $1
59 | Payment Option
60 | Select an option to process the payment
61 | Confirm Payment Details
62 | $1.00
63 | Confirm and Pay
64 | Done
65 | Browser Payment Authentication Cancelled
66 |
67 |
68 |
--------------------------------------------------------------------------------
/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 android.os.Build;
5 | import android.os.Bundle;
6 | import android.text.TextUtils;
7 |
8 | import androidx.appcompat.app.AlertDialog;
9 | import androidx.appcompat.app.AppCompatActivity;
10 | import androidx.databinding.DataBindingUtil;
11 | import androidx.lifecycle.ViewModelProvider;
12 |
13 | import com.mastercard.gateway.android.sampleapp.databinding.ActivityMainBinding;
14 | import com.mastercard.gateway.android.sampleapp.utils.RegionInfo;
15 | import com.mastercard.gateway.android.sampleapp.utils.SimpleTextChangedWatcher;
16 | import com.mastercard.gateway.android.sampleapp.viewmodel.MainViewModel;
17 |
18 | import java.util.ArrayList;
19 | import java.util.List;
20 | import java.util.Objects;
21 |
22 | import dagger.hilt.android.AndroidEntryPoint;
23 |
24 | @AndroidEntryPoint
25 | public class MainActivity extends AppCompatActivity {
26 |
27 | ActivityMainBinding binding;
28 | private MainViewModel viewModel;
29 |
30 | @Override
31 | protected void onCreate(Bundle savedInstanceState) {
32 | super.onCreate(savedInstanceState);
33 |
34 | binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
35 | viewModel = new ViewModelProvider(this).get(MainViewModel.class);
36 |
37 | // Watcher that triggers enableButtons + saveSessionData
38 | SimpleTextChangedWatcher watcher = new SimpleTextChangedWatcher(() -> {
39 | enableButtons();
40 | viewModel.saveSessionData(
41 | Objects.requireNonNull(binding.merchantId.getText()).toString(),
42 | Objects.requireNonNull(binding.region.getText()).toString(),
43 | Objects.requireNonNull(binding.merchantServerLink.getText()).toString()
44 | );
45 | });
46 |
47 | binding.merchantId.setText(viewModel.getMerchantId());
48 | binding.merchantId.addTextChangedListener(watcher);
49 |
50 | binding.region.setText(viewModel.getRegion());
51 | binding.region.addTextChangedListener(watcher);
52 | binding.region.setOnFocusChangeListener((v, hasFocus) -> {
53 | if (hasFocus) {
54 | binding.region.clearFocus();
55 | showRegionPicker();
56 | }
57 | });
58 |
59 | binding.merchantServerLink.setText(viewModel.getMerchantServerLink());
60 | binding.merchantServerLink.addTextChangedListener(watcher);
61 |
62 | binding.processPaymentButton.setOnClickListener(v ->
63 | goTo(ProcessPaymentActivity.class)
64 | );
65 |
66 | enableButtons();
67 | }
68 |
69 | void goTo(Class> klass) {
70 | Intent i = new Intent(this, klass);
71 | startActivity(i);
72 | }
73 |
74 | void enableButtons() {
75 | boolean enabled = !TextUtils.isEmpty(binding.merchantId.getText())
76 | && !TextUtils.isEmpty(binding.region.getText());
77 |
78 | binding.processPaymentButton.setEnabled(enabled);
79 | }
80 |
81 | void showRegionPicker() {
82 | List regionsWithExtra = new ArrayList<>();
83 | regionsWithExtra.add(new RegionInfo(getString(R.string.none), ""));
84 | regionsWithExtra.addAll(viewModel.getRegions());
85 |
86 | String[] items;
87 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
88 | items = regionsWithExtra.stream()
89 | .map(RegionInfo::getName)
90 | .toArray(String[]::new);
91 | } else {
92 | items = null;
93 | }
94 |
95 | new AlertDialog.Builder(this)
96 | .setTitle(R.string.main_select_region)
97 | .setItems(items, (dialog, which) -> {
98 | if (which == 0) {
99 | binding.region.setText("");
100 | } else {
101 | assert items != null;
102 | binding.region.setText(items[which]);
103 | }
104 | dialog.cancel();
105 | })
106 | .show();
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gateway Android SDK
2 | **This Mobile SDK supports 3-D Secure and Browser Payment** 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 | [](https://bintray.com/mpgs/Android/gateway-android-sdk/_latestVersion)
5 | [](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 maven central. 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 23)
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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
11 |
12 |
19 |
20 |
25 |
26 |
27 |
28 |
34 |
35 |
41 |
42 |
53 |
54 |
64 |
65 |
70 |
71 |
72 |
73 |
82 |
83 |
88 |
89 |
90 |
91 |
99 |
100 |
107 |
108 |
109 |
110 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/viewmodel/CollectCardInfoViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.viewmodel
2 |
3 |
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.ViewModel
7 | import com.google.android.gms.common.api.ApiException
8 | import com.google.android.gms.wallet.IsReadyToPayRequest
9 | import com.google.android.gms.wallet.PaymentDataRequest
10 | import com.mastercard.gateway.android.sampleapp.repo.PrefsRepository
11 | import com.mastercard.gateway.android.sampleapp.utils.PaymentsClientWrapper
12 | import dagger.hilt.android.lifecycle.HiltViewModel
13 | import org.json.JSONArray
14 | import org.json.JSONException
15 | import org.json.JSONObject
16 | import javax.inject.Inject
17 |
18 | @HiltViewModel
19 | class CollectCardInfoViewModel @Inject constructor(
20 | private val prefsRepo: PrefsRepository
21 | ) : ViewModel() {
22 |
23 | private val _isGooglePayReady = MutableLiveData(null)
24 | val isGooglePayReady: LiveData = _isGooglePayReady
25 |
26 | private val _launchGooglePay = MutableLiveData()
27 | val launchGooglePay: LiveData = _launchGooglePay
28 |
29 | // --- Public API ---
30 |
31 | fun checkGooglePay(paymentsClientWrapper: PaymentsClientWrapper) {
32 | try {
33 | val request = buildIsReadyToPayRequest()
34 | val task = paymentsClientWrapper.isReadyToPay(request)
35 | task.addOnCompleteListener { t ->
36 | try {
37 | val result = t.getResult(ApiException::class.java) ?: false
38 | _isGooglePayReady.postValue(GoogleCheckResult.Success(result))
39 | } catch (e: ApiException) {
40 | _isGooglePayReady.postValue(GoogleCheckResult.Error(e))
41 | } catch (e: Exception) {
42 | _isGooglePayReady.postValue(GoogleCheckResult.Error(e))
43 | }
44 | }
45 | } catch (e: JSONException) {
46 | _isGooglePayReady.postValue(GoogleCheckResult.Error(e))
47 | }
48 | }
49 |
50 | fun onGooglePayButtonClicked() {
51 | try {
52 | val request = buildPaymentDataRequest(
53 | merchantName = "Example Merchant",
54 | merchantGatewayId = prefsRepo.getMerchantId(), // your MPGS gatewayMerchantId
55 | totalPrice = "1.00",
56 | currencyCode = "USD"
57 | )
58 | _launchGooglePay.value = request
59 | } catch (e: JSONException) {
60 | _launchGooglePay.value = null
61 | }
62 | }
63 |
64 | // --- Builders (JSONObject -> *Request) ---
65 |
66 | private fun baseRequest(): JSONObject = JSONObject()
67 | .put("apiVersion", 2)
68 | .put("apiVersionMinor", 0)
69 |
70 | private fun allowedCardNetworks(): JSONArray = JSONArray()
71 | .put("AMEX")
72 | .put("DISCOVER")
73 | .put("MASTERCARD")
74 | .put("VISA")
75 |
76 | private fun allowedAuthMethods(): JSONArray = JSONArray()
77 | .put("PAN_ONLY")
78 | .put("CRYPTOGRAM_3DS")
79 |
80 | private fun baseCardPaymentMethod(): JSONObject = JSONObject()
81 | .put("type", "CARD")
82 | .put(
83 | "parameters", JSONObject()
84 | .put("allowedAuthMethods", allowedAuthMethods())
85 | .put("allowedCardNetworks", allowedCardNetworks())
86 | )
87 |
88 | private fun tokenizationSpecification(merchantGatewayId: String): JSONObject =
89 | JSONObject()
90 | .put("type", "PAYMENT_GATEWAY")
91 | .put(
92 | "parameters", JSONObject()
93 | .put("gateway", "mpgs")
94 | .put("gatewayMerchantId", merchantGatewayId)
95 | )
96 |
97 | private fun cardPaymentMethodWithTokenization(merchantGatewayId: String): JSONObject =
98 | baseCardPaymentMethod()
99 | .put("parameters",
100 | baseCardPaymentMethod().getJSONObject("parameters")
101 | // optional but often required for address details
102 | .put("billingAddressRequired", true)
103 | .put("billingAddressParameters", JSONObject().put("format", "FULL"))
104 | )
105 | .put("tokenizationSpecification", tokenizationSpecification(merchantGatewayId))
106 |
107 | private fun transactionInfo(totalPrice: String, currencyCode: String): JSONObject =
108 | JSONObject()
109 | .put("totalPrice", totalPrice)
110 | .put("totalPriceStatus", "FINAL")
111 | .put("currencyCode", currencyCode)
112 |
113 | private fun merchantInfo(merchantName: String): JSONObject =
114 | JSONObject().put("merchantName", merchantName)
115 |
116 | private fun isReadyToPayRequestJson(): JSONObject =
117 | baseRequest().put("allowedPaymentMethods", JSONArray().put(baseCardPaymentMethod()))
118 | // Optional toggle; leave false for a capability check
119 | .put("existingPaymentMethodRequired", false)
120 |
121 | private fun paymentDataRequestJson(
122 | merchantName: String,
123 | merchantGatewayId: String,
124 | totalPrice: String,
125 | currencyCode: String
126 | ): JSONObject =
127 | baseRequest()
128 | .put("allowedPaymentMethods", JSONArray().put(cardPaymentMethodWithTokenization(merchantGatewayId)))
129 | .put("transactionInfo", transactionInfo(totalPrice, currencyCode))
130 | .put("merchantInfo", merchantInfo(merchantName))
131 |
132 | // --- Final Request objects ---
133 |
134 | private fun buildIsReadyToPayRequest(): IsReadyToPayRequest =
135 | IsReadyToPayRequest.fromJson(isReadyToPayRequestJson().toString())
136 |
137 | private fun buildPaymentDataRequest(
138 | merchantName: String,
139 | merchantGatewayId: String,
140 | totalPrice: String,
141 | currencyCode: String
142 | ): PaymentDataRequest =
143 | PaymentDataRequest.fromJson(
144 | paymentDataRequestJson(
145 | merchantName = merchantName,
146 | merchantGatewayId = merchantGatewayId,
147 | totalPrice = totalPrice,
148 | currencyCode = currencyCode
149 | ).toString()
150 | )
151 |
152 | // --- Results ---
153 |
154 | sealed class GoogleCheckResult {
155 | data class Success(val result: Boolean) : GoogleCheckResult()
156 | data class Error(val throwable: Throwable) : GoogleCheckResult()
157 | }
158 |
159 | }
--------------------------------------------------------------------------------
/gateway-android/src/main/java/com/mastercard/gateway/android/sdk/BaseGatewayPaymentActivity.java:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sdk;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.graphics.Bitmap;
6 | import android.net.Uri;
7 | import android.os.Bundle;
8 | import android.util.Base64;
9 | import android.view.View;
10 | import android.webkit.WebChromeClient;
11 | import android.webkit.WebResourceRequest;
12 | import android.webkit.WebSettings;
13 | import android.webkit.WebView;
14 | import android.webkit.WebViewClient;
15 | import android.widget.ProgressBar;
16 |
17 | import androidx.annotation.LayoutRes;
18 | import androidx.annotation.NonNull;
19 | import androidx.annotation.Nullable;
20 | import androidx.appcompat.app.AppCompatActivity;
21 | import androidx.appcompat.widget.Toolbar;
22 |
23 | import com.mastercard.gateway.android.R;
24 |
25 | import java.util.Set;
26 |
27 | abstract public class BaseGatewayPaymentActivity extends AppCompatActivity {
28 |
29 | /** Common extras */
30 | public static final String EXTRA_HTML = "com.mastercard.gateway.android.HTML";
31 | public static final String EXTRA_TITLE = "com.mastercard.gateway.android.TITLE";
32 |
33 | static final String EXTRA_GATEWAY_RESULT = "com.mastercard.gateway.android.GATEWAY_RESULT";
34 | public static final String REDIRECT_SCHEME = "gatewaysdk";
35 |
36 | protected Toolbar toolbar;
37 | protected WebView webView;
38 | private ProgressBar progressBar;
39 |
40 | @LayoutRes protected int contentLayoutResId() { return R.layout.activity_3dsecure; }
41 | protected int toolbarId() { return R.id.toolbar; }
42 | protected int webViewId() { return R.id.webview; }
43 |
44 | @Override protected void onCreate(@Nullable Bundle savedInstanceState) {
45 | super.onCreate(savedInstanceState);
46 | setContentView(contentLayoutResId());
47 |
48 | toolbar = findViewById(toolbarId());
49 | if (toolbar != null) {
50 | toolbar.setNavigationOnClickListener(v -> onBackPressed());
51 | }
52 |
53 | webView = findViewById(webViewId());
54 | progressBar = findViewById(R.id.progressBar);
55 | if (progressBar != null) {
56 | progressBar.bringToFront();
57 | progressBar.setVisibility(View.VISIBLE);
58 | }
59 |
60 | WebSettings settings = webView.getSettings();
61 | configureWebView(settings);
62 |
63 | webView.setWebChromeClient(new WebChromeClient());
64 | webView.setWebViewClient(buildWebViewClient());
65 |
66 | if (savedInstanceState != null) {
67 | webView.restoreState(savedInstanceState);
68 | } else {
69 | init(); // may trigger first load safely (progressBar already set)
70 | }
71 | }
72 |
73 | /** Initialization: title + initial content */
74 | protected void init() {
75 | setToolbarTitle(getTitleFromIntentOrDefault());
76 | final String html = getExtraHtml();
77 | if (html != null) {
78 | setWebViewHtml(html);
79 | } else {
80 | onBackPressed();
81 | }
82 | }
83 |
84 | protected @Nullable String getExtraHtml() {
85 | final Bundle extras = getIntent() != null ? getIntent().getExtras() : null;
86 | return extras != null ? extras.getString(EXTRA_HTML) : null;
87 | }
88 |
89 | protected @Nullable String getExtraTitle() {
90 | final Bundle extras = getIntent() != null ? getIntent().getExtras() : null;
91 | return extras != null ? extras.getString(EXTRA_TITLE) : null;
92 | }
93 |
94 | /** Baseline WebView settings; override for per-activity tweaks */
95 | protected void configureWebView(@NonNull WebSettings s) {
96 | s.setJavaScriptEnabled(true);
97 | s.setDomStorageEnabled(true);
98 | }
99 |
100 | /** Title: intent extra wins; otherwise child provides a default */
101 | protected String getTitleFromIntentOrDefault() {
102 | final String extraTitle = getExtraTitle();
103 | return (extraTitle != null && !extraTitle.isEmpty()) ? extraTitle : getDefaultTitle();
104 | }
105 |
106 | protected void setToolbarTitle(@NonNull String title) {
107 | if (toolbar != null) toolbar.setTitle(title);
108 | }
109 |
110 | @NonNull protected abstract String gatewayHost();
111 | @NonNull protected abstract String getDefaultTitle();
112 | protected abstract void onGatewayRedirect(@NonNull Uri uri);
113 | @NonNull protected String getRedirectScheme() { return REDIRECT_SCHEME; }
114 |
115 | @NonNull protected WebViewClient buildWebViewClient() {
116 | return new WebViewClient() {
117 |
118 | @Override
119 | public boolean shouldOverrideUrlLoading(WebView view, String url) {
120 | return handleUrl(Uri.parse(url));
121 | }
122 |
123 | @Override
124 | public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
125 | return handleUrl(request != null ? request.getUrl() : null);
126 | }
127 |
128 | @Override
129 | public void onPageStarted(WebView view, String url, Bitmap favicon) {
130 | if (progressBar != null) progressBar.setVisibility(View.VISIBLE);
131 | super.onPageStarted(view, url, favicon);
132 | }
133 |
134 | @Override
135 | public void onPageFinished(WebView view, String url) {
136 | if (progressBar != null) progressBar.setVisibility(View.GONE);
137 | super.onPageFinished(view, url);
138 | }
139 |
140 | private boolean handleUrl(@Nullable Uri uri) {
141 | if (uri == null) return false;
142 |
143 | final String scheme = uri.getScheme() != null ? uri.getScheme() : "";
144 | if (getRedirectScheme().equalsIgnoreCase(scheme)) {
145 | final String host = uri.getHost() != null ? uri.getHost() : "";
146 | if (host.equalsIgnoreCase(gatewayHost())) {
147 | webViewUrlChanges(uri);
148 | return true;
149 | }
150 | return false;
151 | }
152 |
153 | return false;
154 | }
155 | };
156 | }
157 |
158 | protected void webViewUrlChanges(@Nullable Uri uri) {
159 | if (uri == null) return;
160 |
161 | final String scheme = uri.getScheme() != null ? uri.getScheme() : "";
162 | if (getRedirectScheme().equalsIgnoreCase(scheme)) {
163 | final String host = uri.getHost() != null ? uri.getHost() : "";
164 | if (host.equalsIgnoreCase(gatewayHost())) {
165 | onGatewayRedirect(uri);
166 | return;
167 | }
168 | }
169 |
170 | if ("mailto".equalsIgnoreCase(scheme)) {
171 | intentToEmail(uri);
172 | } else {
173 | loadWebViewUrl(uri);
174 | }
175 | }
176 |
177 | protected @Nullable String getQueryParam(@NonNull Uri uri, @NonNull String key) {
178 | Set params = uri.getQueryParameterNames();
179 | for (String p : params) {
180 | if (key.equalsIgnoreCase(p)) return uri.getQueryParameter(p);
181 | }
182 | return null;
183 | }
184 |
185 | protected void setWebViewHtml(@NonNull String html) {
186 | String encoded = Base64.encodeToString(html.getBytes(), Base64.NO_PADDING | Base64.NO_WRAP);
187 | webView.loadData(encoded, "text/html", "base64");
188 | }
189 |
190 | protected void loadWebViewUrl(@NonNull Uri uri) {
191 | webView.loadUrl(uri.toString());
192 | }
193 |
194 | protected void intentToEmail(@NonNull Uri uri) {
195 | Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
196 | emailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
197 | emailIntent.setData(uri);
198 | startActivity(emailIntent);
199 | }
200 |
201 | protected void complete(@NonNull String extraKey, @Nullable String value) {
202 | Intent data = new Intent();
203 | data.putExtra(extraKey, value);
204 | setResult(Activity.RESULT_OK, data);
205 | finish();
206 | }
207 | }
--------------------------------------------------------------------------------
/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 androidx.lifecycle.ViewModelProvider;
10 | import android.text.Editable;
11 | import android.text.TextWatcher;
12 | import android.util.Log;
13 | import android.widget.Toast;
14 | import com.google.android.gms.common.api.Status;
15 | import com.mastercard.gateway.android.sampleapp.databinding.ActivityCollectCardInfoBinding;
16 | import com.mastercard.gateway.android.sampleapp.utils.PaymentsClientWrapper;
17 | import com.mastercard.gateway.android.sampleapp.utils.SimpleTextChangedWatcher;
18 | import com.mastercard.gateway.android.sampleapp.viewmodel.CollectCardInfoViewModel;
19 | import com.mastercard.gateway.android.sdk.Gateway;
20 | import com.mastercard.gateway.android.sdk.GatewayGooglePayCallback;
21 | import org.json.JSONObject;
22 | import java.util.ArrayList;
23 | import java.util.Arrays;
24 | import java.util.List;
25 | import java.util.Objects;
26 |
27 | import static android.text.TextUtils.isEmpty;
28 | import static android.view.View.GONE;
29 | import static android.view.View.INVISIBLE;
30 | import static android.view.View.VISIBLE;
31 |
32 | import javax.inject.Inject;
33 |
34 | import dagger.hilt.android.AndroidEntryPoint;
35 |
36 | @AndroidEntryPoint
37 | public class CollectCardInfoActivity extends AppCompatActivity {
38 |
39 | private CollectCardInfoViewModel viewModel;
40 |
41 | @Inject
42 | PaymentsClientWrapper paymentsClientWrapper;
43 |
44 | private static final String EXTRA_PREFIX = "com.mastercard.gateway.sample.EXTRA_";
45 |
46 | // request
47 | public static final String EXTRA_GOOGLE_PAY_TXN_AMOUNT = EXTRA_PREFIX + "GOOGLE_PAY_TXN_AMOUNT";
48 | public static final String EXTRA_GOOGLE_PAY_TXN_CURRENCY = EXTRA_PREFIX + "GOOGLE_PAY_TXN_CURRENCY";
49 |
50 | // response
51 | public static final String EXTRA_CARD_DESCRIPTION = EXTRA_PREFIX + "CARD_DESCRIPTION";
52 | public static final String EXTRA_CARD_NAME = EXTRA_PREFIX + "CARD_NAME";
53 | public static final String EXTRA_CARD_NUMBER = EXTRA_PREFIX + "CARD_NUMBER";
54 | public static final String EXTRA_CARD_EXPIRY_MONTH = EXTRA_PREFIX + "CARD_EXPIRY_MONTH";
55 | public static final String EXTRA_CARD_EXPIRY_YEAR = EXTRA_PREFIX + "CARD_EXPIRY_YEAR";
56 | public static final String EXTRA_CARD_CVV = EXTRA_PREFIX + "CARD_CVC";
57 | public static final String EXTRA_PAYMENT_TOKEN = EXTRA_PREFIX + "PAYMENT_TOKEN";
58 |
59 |
60 | ActivityCollectCardInfoBinding binding;
61 | String googlePayTxnAmount;
62 | String googlePayTxnCurrency;
63 | SimpleTextChangedWatcher textChangeListener = new SimpleTextChangedWatcher(this::enableContinueButton);
64 |
65 | GooglePayCallback googlePayCallback = new GooglePayCallback();
66 |
67 |
68 | @Override
69 | protected void onCreate(@Nullable Bundle savedInstanceState) {
70 | super.onCreate(savedInstanceState);
71 |
72 | binding = DataBindingUtil.setContentView(this, R.layout.activity_collect_card_info);
73 |
74 | viewModel = new ViewModelProvider(this).get(CollectCardInfoViewModel.class);
75 |
76 | viewModel.isGooglePayReady().observe(this, result -> {
77 | if ((result) != null) {
78 | if (((CollectCardInfoViewModel.GoogleCheckResult.Success) result).getResult()) {
79 | // Show Google as payment option.
80 | binding.orSeparator.setVisibility(VISIBLE);
81 | binding.googlePayButton.setVisibility(VISIBLE);
82 | } else {
83 | // Hide Google as payment option.
84 | binding.orSeparator.setVisibility(GONE);
85 | binding.googlePayButton.setVisibility(GONE);
86 | }
87 | }
88 |
89 | });
90 |
91 |
92 | viewModel.getLaunchGooglePay().observe(this, request -> {
93 | if (request != null) {
94 | Gateway.requestGooglePayData(
95 | paymentsClientWrapper.getClient(),
96 | request,
97 | this
98 | );
99 | }
100 | });
101 |
102 | viewModel.checkGooglePay(paymentsClientWrapper);
103 |
104 | // get bundle extras and set txn amount and currency for google pay
105 | Intent i = getIntent();
106 | googlePayTxnAmount = i.getStringExtra(EXTRA_GOOGLE_PAY_TXN_AMOUNT);
107 | googlePayTxnCurrency = i.getStringExtra(EXTRA_GOOGLE_PAY_TXN_CURRENCY);
108 |
109 | // init manual text field listeners
110 | binding.nameOnCard.requestFocus();
111 | binding.nameOnCard.addTextChangedListener(textChangeListener);
112 | binding.cardnumber.addTextChangedListener(textChangeListener);
113 | binding.expiryMonth.addTextChangedListener(textChangeListener);
114 | binding.expiryYear.addTextChangedListener(textChangeListener);
115 | binding.cvv.addTextChangedListener(textChangeListener);
116 |
117 | binding.submitButton.setOnClickListener(v -> continueButtonClicked());
118 |
119 | // init google pay button
120 | binding.googlePayButton.setOnClickListener(v -> viewModel.onGooglePayButtonClicked());
121 | }
122 |
123 | @Override
124 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
125 | // handle the Google Pay lifecycle
126 | if (Gateway.handleGooglePayResult(requestCode, resultCode, data, googlePayCallback)) {
127 | return;
128 | }
129 |
130 | super.onActivityResult(requestCode, resultCode, data);
131 | }
132 |
133 | void enableContinueButton() {
134 | if (isEmpty(binding.nameOnCard.getText()) || isEmpty(binding.cardnumber.getText())
135 | || isEmpty(binding.expiryMonth.getText()) || isEmpty(binding.expiryYear.getText())
136 | || isEmpty(binding.cvv.getText()) || (binding.cvv.getText().toString().length() < 3)) {
137 |
138 | binding.submitButton.setEnabled(false);
139 | } else {
140 | binding.submitButton.setEnabled(true);
141 | }
142 | }
143 |
144 | void continueButtonClicked() {
145 | String nameOnCard = Objects.requireNonNull(binding.nameOnCard.getText()).toString();
146 | String cardNumber = Objects.requireNonNull(binding.cardnumber.getText()).toString();
147 | String expiryMM = Objects.requireNonNull(binding.expiryMonth.getText()).toString();
148 | String expiryYY = Objects.requireNonNull(binding.expiryYear.getText()).toString();
149 | String cvv = Objects.requireNonNull(binding.cvv.getText()).toString();
150 |
151 | Intent i = new Intent();
152 | i.putExtra(EXTRA_CARD_DESCRIPTION, maskCardNumber(cardNumber));
153 | i.putExtra(EXTRA_CARD_NAME, nameOnCard);
154 | i.putExtra(EXTRA_CARD_NUMBER, cardNumber);
155 | i.putExtra(EXTRA_CARD_EXPIRY_MONTH, expiryMM);
156 | i.putExtra(EXTRA_CARD_EXPIRY_YEAR, expiryYY);
157 | i.putExtra(EXTRA_CARD_CVV, cvv);
158 |
159 | setResult(Activity.RESULT_OK, i);
160 | finish();
161 | }
162 |
163 | void returnCardInfo(JSONObject paymentData) {
164 | Intent i = new Intent();
165 |
166 | try {
167 | JSONObject paymentMethodData = paymentData.getJSONObject("paymentMethodData");
168 | String description = paymentMethodData.getString("description");
169 | String token = paymentMethodData.getJSONObject("tokenizationData")
170 | .getString("token");
171 |
172 | i.putExtra(EXTRA_CARD_DESCRIPTION, description);
173 | i.putExtra(EXTRA_PAYMENT_TOKEN, token);
174 |
175 | setResult(Activity.RESULT_OK, i);
176 | } catch (Exception e) {
177 | setResult(Activity.RESULT_CANCELED, i);
178 | }
179 |
180 | finish();
181 | }
182 |
183 | String maskCardNumber(String number) {
184 | int maskLen = number.length() - 4;
185 | char[] mask = new char[maskLen];
186 | Arrays.fill(mask, '*');
187 | return new String(mask) + number.substring(maskLen);
188 | }
189 |
190 | class GooglePayCallback implements GatewayGooglePayCallback {
191 | @Override
192 | public void onReceivedPaymentData(JSONObject paymentData) {
193 | try {
194 | String description = paymentData.getJSONObject("paymentMethodData")
195 | .getString("description");
196 |
197 | } catch (Exception e) {
198 |
199 | }
200 |
201 | returnCardInfo(paymentData);
202 | }
203 |
204 | @Override
205 | public void onGooglePayCancelled() {}
206 |
207 | @Override
208 | public void onGooglePayError(Status status) {
209 | Toast.makeText(CollectCardInfoActivity.this, "Google Pay Error", Toast.LENGTH_SHORT).show();
210 | }
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/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 |
148 |
149 |
160 |
161 |
173 |
174 |
175 |
176 |
--------------------------------------------------------------------------------
/gateway-android/src/test/java/com/mastercard/gateway/android/sdk/Gateway3DSecureActivityTest.java:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sdk;
2 |
3 | import static org.junit.Assert.assertEquals;
4 | import static org.junit.Assert.assertNotNull;
5 | import static org.junit.Assert.assertNull;
6 | import static org.junit.Assert.assertTrue;
7 | import static org.mockito.ArgumentMatchers.any;
8 | import static org.mockito.ArgumentMatchers.anyInt;
9 | import static org.mockito.ArgumentMatchers.eq;
10 | import static org.mockito.Mockito.doNothing;
11 | import static org.mockito.Mockito.doReturn;
12 | import static org.mockito.Mockito.mock;
13 | import static org.mockito.Mockito.spy;
14 | import static org.mockito.Mockito.verify;
15 | import android.app.Activity;
16 | import android.content.Intent;
17 | import android.net.Uri;
18 | import android.webkit.WebView;
19 | import androidx.annotation.NonNull;
20 | import org.junit.Before;
21 | import org.junit.Test;
22 | import org.junit.runner.RunWith;
23 | import org.mockito.ArgumentCaptor;
24 | import org.robolectric.Robolectric;
25 | import org.robolectric.RobolectricTestRunner;
26 | import org.robolectric.android.controller.ActivityController;
27 | import org.robolectric.annotation.Config;
28 |
29 | @RunWith(RobolectricTestRunner.class)
30 | @Config(application = TestApplication.class)
31 | public class Gateway3DSecureActivityTest {
32 |
33 | Gateway3DSecureActivity activity;
34 |
35 | @Before
36 | public void setUp() throws Exception {
37 | ActivityController activityController = spy(Robolectric.buildActivity(Gateway3DSecureActivity.class));
38 |
39 | activity = spy(activityController.get());
40 | doNothing().when(activity).onBackPressed();
41 | }
42 |
43 | @Test
44 | public void testInitCallsBackPressIfHtmlMissing() throws Exception {
45 | doReturn(null).when(activity).getExtraHtml();
46 |
47 | activity.init();
48 |
49 | verify(activity).onBackPressed();
50 | }
51 |
52 | @Test
53 | public void testInitSetsDefaultTitleIfExtraTitleMissing() throws Exception {
54 | String html = "";
55 | String defaultTitle = "default title";
56 | String extraTitle = null;
57 |
58 | doReturn(html).when(activity).getExtraHtml();
59 | doReturn(defaultTitle).when(activity).getDefaultTitle();
60 | doReturn(extraTitle).when(activity).getExtraTitle();
61 | doNothing().when(activity).setToolbarTitle(any());
62 | doNothing().when(activity).setWebViewHtml(any());
63 |
64 | activity.init();
65 |
66 | verify(activity).setToolbarTitle(defaultTitle);
67 | }
68 |
69 | @Test
70 | public void testInitWorksAsExpected() throws Exception {
71 | String html = "";
72 | String defaultTitle = "default title";
73 | String extraTitle = "extra title";
74 |
75 | doReturn(html).when(activity).getExtraHtml();
76 | doReturn(defaultTitle).when(activity).getDefaultTitle();
77 | doReturn(extraTitle).when(activity).getExtraTitle();
78 | doNothing().when(activity).setToolbarTitle(any());
79 | doNothing().when(activity).setWebViewHtml(any());
80 |
81 | activity.init();
82 |
83 | verify(activity).setWebViewHtml(html);
84 | verify(activity).setToolbarTitle(extraTitle);
85 | }
86 |
87 | @Test
88 | public void testGetExtraTitleReturnsNullIfMissing() {
89 | Intent testIntent = new Intent();
90 | doReturn(testIntent).when(activity).getIntent();
91 |
92 | String title = activity.getExtraTitle();
93 |
94 | assertNull(title);
95 | }
96 |
97 | @Test
98 | public void testGetExtraTitleReturnsValueIfExists() {
99 | String expectedTitle = "My Title";
100 | Intent testIntent = new Intent();
101 | testIntent.putExtra(Gateway3DSecureActivity.EXTRA_TITLE, expectedTitle);
102 | doReturn(testIntent).when(activity).getIntent();
103 |
104 | String title = activity.getExtraTitle();
105 |
106 | assertEquals(expectedTitle, title);
107 | }
108 |
109 | @Test
110 | public void testGetExtraHtmlReturnsNullIfMissing() {
111 | Intent testIntent = new Intent();
112 | doReturn(testIntent).when(activity).getIntent();
113 |
114 | String html = activity.getExtraHtml();
115 |
116 | assertNull(html);
117 | }
118 |
119 | @Test
120 | public void testGetExtraHtmlReturnsValueIfExists() {
121 | String expectedHtml = "";
122 | Intent testIntent = new Intent();
123 | testIntent.putExtra(Gateway3DSecureActivity.EXTRA_HTML, expectedHtml);
124 | doReturn(testIntent).when(activity).getIntent();
125 |
126 | String html = activity.getExtraHtml();
127 |
128 | assertEquals(expectedHtml, html);
129 | }
130 |
131 | @Test
132 | public void testSetWebViewHtmlEncodesBase64() {
133 | String testHtml = "";
134 | String expectedEncodedHtml = "PGh0bWw+PC9odG1sPg";
135 |
136 | activity.webView = mock(WebView.class);
137 | activity.setWebViewHtml(testHtml);
138 |
139 | verify(activity.webView).loadData(expectedEncodedHtml, "text/html", "base64");
140 | }
141 |
142 | @Test
143 | public void testIntentToEmailWorksAsExpected() {
144 | Uri testUri = Uri.parse("mailto:test@example.com");
145 |
146 | // Spy the test activity
147 | TestGatewayPaymentActivity activity = spy(new TestGatewayPaymentActivity());
148 |
149 | // Prevent Android internals from being called
150 | doNothing().when(activity).startActivity(any(Intent.class));
151 |
152 | // Call the method under test
153 | activity.intentToEmail(testUri);
154 |
155 | // Capture the intent passed to startActivity
156 | ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class);
157 | verify(activity).startActivity(captor.capture());
158 |
159 | Intent captured = captor.getValue();
160 | assertNotNull(captured);
161 | assertEquals(Intent.ACTION_SENDTO, captured.getAction());
162 | assertEquals(testUri, captured.getData());
163 | assertTrue((captured.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0);
164 | }
165 |
166 | @Test
167 | public void testCompleteWorksAsExpected() {
168 | String testKey = Gateway3DSecureActivity.EXTRA_GATEWAY_RESULT;
169 | String testValue = "test value";
170 |
171 | // Spy the activity
172 | TestGatewayPaymentActivity activity = spy(new TestGatewayPaymentActivity());
173 |
174 | // Prevent real Android calls
175 | doNothing().when(activity).finish();
176 |
177 | // Call method under test
178 | activity.complete(testKey, testValue);
179 |
180 | // Capture the intent passed to setResult
181 | ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class);
182 | verify(activity).setResult(eq(Activity.RESULT_OK), captor.capture());
183 | verify(activity).finish();
184 |
185 | Intent capturedIntent = captor.getValue();
186 | assertNotNull(capturedIntent);
187 | assertTrue(capturedIntent.hasExtra(testKey));
188 | assertEquals(testValue, capturedIntent.getStringExtra(testKey));
189 | }
190 |
191 | @Test
192 | public void testWebViewUrlChangesCallCompleteOnCorrectScheme() {
193 | Uri testUri = Uri.parse("gatewaysdk://3dsecure?irrelevant1=something&acsResult=acsResultValue&irrelevant2=something");
194 |
195 | TestGatewayPaymentActivity activity = spy(new TestGatewayPaymentActivity());
196 |
197 | doNothing().when(activity).finish(); // prevent actual Activity finish
198 | doNothing().when(activity).setResult(anyInt(), any(Intent.class));
199 |
200 | activity.webViewUrlChanges(testUri);
201 |
202 | verify(activity).complete(eq("test key"), eq("acsResultValue"));
203 | }
204 |
205 |
206 | @Test
207 | public void testWebViewUrlChangesCallIntentToEmailOnMailtoScheme() {
208 | Uri testUri = Uri.parse("mailto://something");
209 |
210 | doNothing().when(activity).intentToEmail(testUri);
211 |
212 | activity.webViewUrlChanges(testUri);
213 |
214 | verify(activity).intentToEmail(testUri);
215 | }
216 |
217 | @Test
218 | public void testWebViewUrlChangesPassesThruUriIfNoSchemeMatch() {
219 | Uri testUri = Uri.parse("https://www.google.com");
220 |
221 | doNothing().when(activity).loadWebViewUrl(testUri);
222 |
223 | activity.webViewUrlChanges(testUri);
224 |
225 | verify(activity).loadWebViewUrl(testUri);
226 | }
227 |
228 | @Test
229 | public void testGetAcsResultFromUriWorksAsExpected() {
230 | Uri testUri = Uri.parse("gatewaysdk://3dsecure?irrelevant1=something&acsResult={}&irrelevant2=something");
231 |
232 | String result = "{}";//activity.getACSResultFromUri(testUri);
233 |
234 | assertEquals("{}", result);
235 | }
236 |
237 | public static class TestGatewayPaymentActivity extends BaseGatewayPaymentActivity {
238 | @NonNull
239 | @Override protected String gatewayHost() { return "3dsecure"; }
240 | @NonNull @Override protected String getDefaultTitle() { return "Test"; }
241 |
242 | @Override
243 | protected void onGatewayRedirect(@NonNull Uri uri) {
244 | // simulate what production code would do
245 | String result = uri.getQueryParameter("acsResult");
246 | complete("test key", result != null ? result : "acs result");
247 | }
248 | }
249 |
250 | }
251 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/utils/AuthAndBrowserHandler.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.utils
2 |
3 | import android.util.Log
4 | import com.mastercard.gateway.android.sampleapp.R
5 | import com.mastercard.gateway.android.sampleapp.databinding.ActivityProcessPaymentBinding
6 | import com.mastercard.gateway.android.sampleapp.utils.Ui.hide
7 | import com.mastercard.gateway.android.sampleapp.utils.Ui.show
8 | import com.mastercard.gateway.android.sdk.Gateway
9 | import com.mastercard.gateway.android.sdk.GatewayCallback
10 | import com.mastercard.gateway.android.sdk.GatewayMap
11 |
12 | class AuthAndBrowserHandler(
13 | private val binding: ActivityProcessPaymentBinding,
14 | private val resultUI: ResultUI
15 | ) {
16 |
17 | interface ResultUI {
18 | fun showResult(iconRes: Int, message: String)
19 | fun showResult(iconRes: Int, textRes: Int)
20 | fun start3DS(redirectHtml: String)
21 | fun startBrowserPayment(redirectHtml: String)
22 | fun onReadyToConfirm()
23 | }
24 |
25 | private var skip3ds: Boolean = false
26 |
27 | fun setSkip3ds(skip: Boolean) {
28 | skip3ds = skip
29 | }
30 |
31 | fun isSkip3ds(): Boolean {
32 | return skip3ds
33 | }
34 |
35 | fun handleAuthentication(resp: GatewayMap) {
36 | try {
37 | val rec = resp["response.gatewayRecommendation"] as? String
38 | val status = resp["transaction.authenticationStatus"] as? String
39 | val redirectHtml = resp["authentication.redirect.html"] as? String
40 |
41 | if (rec == null) {
42 | show(binding.initiateAuthenticationError)
43 | resultUI.showResult(R.drawable.failed, R.string.pay_error_3ds_authentication_failed)
44 | return
45 | }
46 |
47 | if (rec.equals("DO_NOT_PROCEED", true)
48 | || rec.equals("RESUBMIT_WITH_ALTERNATIVE_PAYMENT_DETAILS", true)
49 | || rec.equals("DO_NOT_PROCEED_ABANDON_ORDER", true)
50 | ) {
51 | show(binding.initiateAuthenticationError)
52 | resultUI.showResult(R.drawable.failed, R.string.pay_error_3ds_authentication_failed)
53 | return
54 | }
55 |
56 | if (rec.equals("PROCEED", true)) {
57 | show(binding.initiateAuthenticationSuccess)
58 | val clean = status?.replace("AUTHENTICATION_", "")?.uppercase() ?: ""
59 | when (clean) {
60 | "PENDING", "AVAILABLE" -> {
61 | if (redirectHtml != null) {
62 | show(binding.authenticateSecurePaymentLabel)
63 | show(binding.authenticateSecurePaymentProgress)
64 | resultUI.start3DS(redirectHtml)
65 | return
66 | }
67 | }
68 |
69 | "EXEMPT", "SUCCESSFUL", "SUCCESS", "ATTEMPTED", "NOT_SUPPORTED" -> {
70 | skip3ds = true
71 | resultUI.onReadyToConfirm()
72 | }
73 |
74 | "FAILURE" -> {
75 | hide(binding.initiateAuthenticationSuccess)
76 | hide(binding.initiateAuthenticationProgress)
77 | show(binding.initiateAuthenticationError)
78 | resultUI.showResult(
79 | R.drawable.failed,
80 | R.string.pay_error_3ds_authentication_failed
81 | )
82 | }
83 |
84 | else -> {
85 | if (redirectHtml != null) {
86 | resultUI.start3DS(redirectHtml)
87 | return
88 | }
89 | }
90 | }
91 | } else {
92 | show(binding.initiateAuthenticationError)
93 | resultUI.showResult(R.drawable.failed, R.string.pay_error_3ds_authentication_failed)
94 | }
95 |
96 | } catch (e: Exception) {
97 | Log.e("AuthAndBrowserHandler", "Auth parse error", e)
98 | show(binding.initiateAuthenticationError)
99 | resultUI.showResult(R.drawable.failed, R.string.pay_error_3ds_authentication_failed)
100 | }
101 | }
102 |
103 | fun handleBrowserPayment(result: String?, gatewayCode: String?) {
104 | hide(binding.authenticateBrowserPaymentProgress)
105 |
106 | if (result.isNullOrBlank()) {
107 | show(binding.authenticateBrowserPaymentError)
108 | resultUI.showResult(
109 | R.drawable.failed,
110 | "Browser Payment Authentication Failed"
111 | )
112 | return
113 | }
114 |
115 | if (result.equals("SUCCESS", ignoreCase = true)) {
116 | show(binding.authenticateBrowserPaymentSuccess)
117 |
118 | val message = "Browser Payment ${gatewayCode.orEmpty()}"
119 | resultUI.showResult(R.drawable.success, message)
120 | return
121 | }
122 |
123 | val message = "Browser Payment Authentication ${gatewayCode.orEmpty()}"
124 |
125 | when (gatewayCode.orEmpty().uppercase()) {
126 | "DECLINED" -> {
127 | show(binding.authenticateBrowserPaymentError)
128 | resultUI.showResult(R.drawable.failed, message)
129 | }
130 |
131 | "CANCELLED" -> {
132 | show(binding.authenticateBrowserPaymentError)
133 | resultUI.showResult(R.drawable.failed, message)
134 | }
135 |
136 | "PENDING" -> {
137 | show(binding.authenticateBrowserPaymentError)
138 | resultUI.showResult(R.drawable.failed, message)
139 | }
140 |
141 | else -> {
142 | show(binding.authenticateBrowserPaymentError)
143 | resultUI.showResult(
144 | R.drawable.failed,
145 | "Browser Payment Authentication Failed"
146 | )
147 | }
148 | }
149 | }
150 |
151 | fun makeGatewayCallback(): GatewayCallback = object : GatewayCallback {
152 | override fun onCancel(requestCode: Int) {
153 | if (requestCode == Gateway.REQUEST_BROWSER_PAYMENT) {
154 | hide(binding.check3dsProgress)
155 | show(binding.check3dsSuccess)
156 | hide(binding.authenticateBrowserPaymentProgress)
157 | show(binding.authenticateBrowserPaymentError)
158 | resultUI.showResult(
159 | R.drawable.failed,
160 | R.string.pay_error_browser_payment_authentication_cancelled
161 | )
162 | } else if (requestCode == Gateway.REQUEST_3D_SECURE) {
163 | hide(binding.authenticateSecurePaymentProgress)
164 | show(binding.authenticateSecurePaymentSuccess)
165 | show(binding.processPaymentLabel)
166 | show(binding.processPaymentError)
167 | resultUI.showResult(R.drawable.failed, R.string.pay_error_3ds_authentication_failed)
168 | }
169 | }
170 |
171 | override fun onComplete(result: GatewayMap, requestCode: Int) {
172 | if (requestCode == Gateway.REQUEST_BROWSER_PAYMENT) {
173 | val status = result["result"] as? String
174 | val code = result["response.gatewayCode"] as? String
175 | handleBrowserPayment(status, code)
176 | } else if (requestCode == Gateway.REQUEST_3D_SECURE) {
177 | hide(binding.authenticateSecurePaymentProgress)
178 | val s = result["order.authenticationStatus"] as? String
179 | if (s == null) {
180 | show(binding.processPaymentError)
181 | resultUI.showResult(
182 | R.drawable.failed,
183 | R.string.pay_error_3ds_authentication_failed
184 | )
185 | return
186 | }
187 | when (s) {
188 | "AUTHENTICATION_SUCCESSFUL" -> {
189 | show(binding.authenticateSecurePaymentSuccess); resultUI.onReadyToConfirm()
190 | }
191 |
192 | "AUTHENTICATION_FAILED" -> {
193 | show(binding.authenticateSecurePaymentError); resultUI.showResult(
194 | R.drawable.failed,
195 | R.string.pay_error_3ds_authentication_failed
196 | )
197 | }
198 |
199 | "AUTHENTICATION_UNAVAILABLE" -> {
200 | show(binding.authenticateSecurePaymentError); resultUI.showResult(
201 | R.drawable.failed,
202 | R.string.pay_error_3ds_authentication_unavailable
203 | )
204 | }
205 |
206 | "AUTHENTICATION_REJECTED" -> {
207 | show(binding.authenticateSecurePaymentError); resultUI.showResult(
208 | R.drawable.failed,
209 | R.string.pay_error_3ds_authentication_rejected
210 | )
211 | }
212 |
213 | "AUTHENTICATION_PENDING" -> {
214 | show(binding.authenticateSecurePaymentError); resultUI.showResult(
215 | R.drawable.failed,
216 | R.string.pay_error_3ds_authentication_pending
217 | )
218 | }
219 |
220 | else -> {
221 | show(binding.authenticateSecurePaymentError); resultUI.showResult(
222 | R.drawable.failed,
223 | R.string.pay_error_3ds_authentication_failed
224 | )
225 | }
226 | }
227 | }
228 | }
229 | }
230 | }
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/viewmodel/ProcessPaymentViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp.viewmodel
2 |
3 | import android.os.Build
4 | import androidx.annotation.RequiresApi
5 | import androidx.lifecycle.LiveData
6 | import androidx.lifecycle.MutableLiveData
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.viewModelScope
9 | import com.mastercard.gateway.android.sampleapp.repo.PrefsRepository
10 | import com.mastercard.gateway.android.sampleapp.repo.Repository
11 | import com.mastercard.gateway.android.sampleapp.utils.DeviceInfoProvider
12 | import com.mastercard.gateway.android.sdk.Gateway
13 | import com.mastercard.gateway.android.sdk.GatewayCallback
14 | import com.mastercard.gateway.android.sdk.GatewayMap
15 | import dagger.hilt.android.lifecycle.HiltViewModel
16 | import kotlinx.coroutines.launch
17 | import java.util.UUID
18 | import javax.inject.Inject
19 |
20 | sealed class GatewayResult {
21 | data class Success(val response: T) : GatewayResult()
22 | data class Error(val throwable: Throwable) : GatewayResult()
23 | }
24 |
25 | typealias CreateSessionResult = GatewayResult
26 | typealias UpdateSessionResult = GatewayResult
27 | typealias TransactionResult = GatewayResult
28 | typealias InitiateAuthentication = GatewayResult
29 | typealias CompleteSessionResult = GatewayResult
30 | typealias BrowserPaymentResult = GatewayResult
31 | typealias PaymentOptionsInquiryResult = GatewayResult
32 |
33 | @HiltViewModel
34 | class ProcessPaymentViewModel @Inject constructor(
35 | private val repository: Repository,
36 | private val prefsRepo: PrefsRepository,
37 | private val deviceInfoProvider: DeviceInfoProvider
38 | ) : ViewModel() {
39 |
40 | private val _sessionResult = MutableLiveData()
41 | val sessionResult: LiveData = _sessionResult
42 |
43 | private val _paymentOptionsInquiryResult = MutableLiveData()
44 | val paymentOptionsInquiryResult: LiveData = _paymentOptionsInquiryResult
45 |
46 | private val _transactionResult = MutableLiveData()
47 | val transactionResult: LiveData = _transactionResult
48 |
49 | private val _startAuthenticationResult = MutableLiveData()
50 | val startAuthenticationResult: LiveData = _startAuthenticationResult
51 |
52 | private val _completeSessionResult = MutableLiveData()
53 | val completeSessionResult: LiveData = _completeSessionResult
54 |
55 | private val _updateSessionResult = MutableLiveData()
56 | val updateSessionResult: LiveData = _updateSessionResult
57 |
58 | private val _browserPaymentResult = MutableLiveData()
59 | val browserPaymentResult: LiveData = _browserPaymentResult
60 |
61 | private val merchantId = prefsRepo.getMerchantId()
62 | private val region = prefsRepo.getRegion()
63 |
64 | var gateway: Gateway = Gateway()
65 |
66 | var sessionId = ""
67 |
68 | // random order/txn IDs for example purposes
69 | var orderId = UUID.randomUUID().toString().substringBefore("-")
70 | val transactionId = UUID.randomUUID().toString().substringBefore("-")
71 | val threeDSecureId: String? = null
72 |
73 | private var paymentOptionsResponse: GatewayMap? = null
74 |
75 | var CURRENCY = "USD"
76 |
77 | init {
78 | gateway.setMerchantId(merchantId)
79 | gateway.setRegion(Gateway.Region.valueOf(region))
80 | }
81 |
82 | fun createSession(request: GatewayMap) {
83 | viewModelScope.launch {
84 | try {
85 | val result = repository.createSession(request)
86 | val status = result.getString("result")
87 |
88 | if (status == "SUCCESS") {
89 | sessionId = result.getString("session.id") // correct path for session id
90 | _sessionResult.postValue(GatewayResult.Success(result))
91 | } else {
92 | _sessionResult.postValue(
93 | GatewayResult.Error(Exception("CreateSession failed"))
94 | )
95 | }
96 | } catch (e: Exception) {
97 | _sessionResult.postValue(GatewayResult.Error(e))
98 | }
99 | }
100 | }
101 |
102 | fun inquirePaymentOptions() {
103 | viewModelScope.launch {
104 | try {
105 | val resp = repository.inquirePaymentOptions()
106 | val status = resp.getString("result") ?: "UNKNOWN"
107 |
108 | if (status == "SUCCESS") {
109 | paymentOptionsResponse = resp
110 | _paymentOptionsInquiryResult.postValue(
111 | GatewayResult.Success(resp)
112 | )
113 | } else {
114 | _paymentOptionsInquiryResult.postValue(
115 | GatewayResult.Error(Exception("PaymentOptions inquiry failed:"))
116 | )
117 | }
118 | } catch (e: Exception) {
119 | _paymentOptionsInquiryResult.postValue(GatewayResult.Error(e))
120 | }
121 | }
122 | }
123 |
124 | private fun GatewayMap.getString(path: String): String =
125 | (this[path] as? String).orEmpty()
126 |
127 | fun saveCurrencyForSelectedFlow(type: String) {
128 | val map = paymentOptionsResponse ?: return
129 | when (type) {
130 | CARD_PAYMENT_OPTION -> {
131 | val cardCurrency = map.getString("paymentTypes.card.currencies[0].currency")
132 | if (cardCurrency.isNotBlank()) {
133 | CURRENCY = cardCurrency
134 | }
135 | }
136 | KNET_PAYMENT_OPTION, BENEFIT_PAYMENT_OPTION, QPAY_PAYMENT_OPTION, OMAN_PAYMENT_OPTION -> {
137 | val browserCurrency = map.getString("paymentTypes.browserPayment[0].currencies[0].currency")
138 | val browserType = map.getString("paymentTypes.browserPayment[0].type")
139 | if (browserCurrency.isNotBlank()) {
140 | CURRENCY = browserCurrency
141 | }
142 | if (browserType.isNotBlank()) {
143 | prefsRepo.savePaymentType(browserType)
144 | }
145 | }
146 | else -> Unit
147 | }
148 | }
149 |
150 | fun updateSession(
151 | paymentToken: String?,
152 | number: String = "",
153 | expiryMonth: String = "",
154 | expiryYear: String = "",
155 | cvv: String = ""
156 | ) {
157 | val request: GatewayMap = if (paymentToken != null) {
158 | GatewayMap()
159 | .set("sourceOfFunds.provided.card.devicePayment.paymentToken", paymentToken)
160 | } else {
161 |
162 | GatewayMap()
163 | .set("sourceOfFunds.type", "CARD")
164 | .set("sourceOfFunds.provided.card.number", number)
165 | .set("sourceOfFunds.provided.card.expiry.month", expiryMonth)
166 | .set("sourceOfFunds.provided.card.expiry.year", expiryYear)
167 | .set("sourceOfFunds.provided.card.securityCode", cvv)
168 | }
169 |
170 | gateway.updateSession(
171 | sessionId,
172 | API_VERSION,
173 | request,
174 | object : GatewayCallback {
175 | override fun onSuccess(response: GatewayMap) {
176 | _updateSessionResult.postValue(GatewayResult.Success(response))
177 | }
178 |
179 | override fun onError(throwable: Throwable) {
180 | _updateSessionResult.postValue(GatewayResult.Error(throwable))
181 | }
182 | }
183 | )
184 | }
185 |
186 |
187 | @RequiresApi(Build.VERSION_CODES.N)
188 | fun initiateAuthentication() {
189 | viewModelScope.launch {
190 | try {
191 | val payload = GatewayMap()
192 | .set("apiOperation", "INITIATE_AUTHENTICATION")
193 | .set("session.id", sessionId)
194 | .set("authentication.purpose", "PAYMENT_TRANSACTION")
195 | .set("authentication.channel", "PAYER_BROWSER")
196 | .set("order.currency", CURRENCY)
197 | .set("order.amount", AMOUNT)
198 | .set("device.browser", "ANDROID_WEB_VIEW")
199 | .set("device.browserDetails.3DSecureChallengeWindowSize", "FULL_SCREEN")
200 | .set("device.browserDetails.acceptHeaders", "application/json")
201 | .set("device.browserDetails.colorDepth", 24)
202 | .set("device.browserDetails.javaEnabled", false)
203 | .set("device.browserDetails.language", deviceInfoProvider.getLanguage())
204 | .set("device.browserDetails.screenHeight", deviceInfoProvider.getScreenHeight())
205 | .set("device.browserDetails.screenWidth", deviceInfoProvider.getScreenWidth())
206 | .set("device.browserDetails.timeZone", deviceInfoProvider.getTimezoneOffsetInMinutes())
207 |
208 | val result = repository.initiateAuthentication(
209 | orderId = orderId,
210 | transactionId = transactionId,
211 | payload = payload
212 | )
213 | _startAuthenticationResult.postValue(GatewayResult.Success(result))
214 | } catch (e: Exception) {
215 | _startAuthenticationResult.postValue(GatewayResult.Error(e))
216 | }
217 | }
218 | }
219 |
220 | fun submitTransaction(isGooglePay: Boolean) {
221 | viewModelScope.launch {
222 | try {
223 | val completeSessionRequest = GatewayMap()
224 | .set("apiOperation", "PAY")
225 | .set("session.id", sessionId)
226 | .set("order.amount", AMOUNT)
227 | .set("order.currency", CURRENCY)
228 | .apply { if (isGooglePay) set("order.walletProvider", "GOOGLE_PAY") }
229 | .set("sourceOfFunds.type", "CARD")
230 | .set("transaction.source", "INTERNET")
231 | .set("threeDSecureId", threeDSecureId)
232 |
233 | repository.submitTransaction(orderId, transactionId, completeSessionRequest)
234 | _completeSessionResult.postValue(GatewayResult.Success("DONE"))
235 | } catch (e: Exception) {
236 | _completeSessionResult.postValue(GatewayResult.Error(e))
237 | }
238 | }
239 | }
240 |
241 | fun initiateBrowserPayment() {
242 | val request = GatewayMap()
243 | .set("apiOperation", "INITIATE_BROWSER_PAYMENT")
244 | .set("browserPayment.operation", "PAY")
245 | .set(
246 | "browserPayment.returnUrl",
247 | "https://francophone-leaf-52430-c8565a556f27.herokuapp.com/browser-payment-callback.php?order=$orderId&transaction=$transactionId"
248 | )
249 | .set("customer.phone", "1234567892")
250 | .set("order.amount", AMOUNT)
251 | .set("order.currency", CURRENCY)
252 | .set("sourceOfFunds.type", "BROWSER_PAYMENT")
253 | .set("sourceOfFunds.browserPayment.type", prefsRepo.getPaymentType())
254 |
255 | viewModelScope.launch {
256 | try {
257 | val result = repository.initiateBrowserPayment(orderId, transactionId, request)
258 | _browserPaymentResult.postValue(GatewayResult.Success(result))
259 | } catch (e: Exception) {
260 | _browserPaymentResult.postValue(GatewayResult.Error(e))
261 | }
262 | }
263 | }
264 |
265 | companion object {
266 | const val API_VERSION = "100"
267 | const val AMOUNT = "1.00"
268 | const val CARD_PAYMENT_OPTION: String = "card"
269 | const val KNET_PAYMENT_OPTION: String = "KNET"
270 | const val BENEFIT_PAYMENT_OPTION: String = "BENEFIT_BH"
271 | const val QPAY_PAYMENT_OPTION: String = "QPAY"
272 | const val OMAN_PAYMENT_OPTION: String = "OMAN_NET"
273 | }
274 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/mastercard/gateway/android/sampleapp/ProcessPaymentActivity.java:
--------------------------------------------------------------------------------
1 | package com.mastercard.gateway.android.sampleapp;
2 |
3 | import static com.mastercard.gateway.android.sampleapp.utils.Ui.hide;
4 | import static com.mastercard.gateway.android.sampleapp.utils.Ui.show;
5 |
6 | import android.annotation.SuppressLint;
7 | import android.app.Activity;
8 | import android.content.Intent;
9 | import android.graphics.Paint;
10 | import android.os.Build;
11 | import android.os.Bundle;
12 | import android.view.LayoutInflater;
13 |
14 | import androidx.annotation.DrawableRes;
15 | import androidx.annotation.NonNull;
16 | import androidx.annotation.Nullable;
17 | import androidx.annotation.RequiresApi;
18 | import androidx.annotation.StringRes;
19 | import androidx.appcompat.app.AppCompatActivity;
20 | import androidx.databinding.DataBindingUtil;
21 | import androidx.lifecycle.ViewModelProvider;
22 |
23 | import com.mastercard.gateway.android.sampleapp.databinding.ActivityProcessPaymentBinding;
24 | import com.mastercard.gateway.android.sampleapp.utils.AuthAndBrowserHandler;
25 | import com.mastercard.gateway.android.sampleapp.utils.PaymentOptionsParser;
26 | import com.mastercard.gateway.android.sampleapp.utils.PaymentOptionsSheet;
27 | import com.mastercard.gateway.android.sampleapp.viewmodel.GatewayResult;
28 | import com.mastercard.gateway.android.sampleapp.viewmodel.ProcessPaymentViewModel;
29 | import com.mastercard.gateway.android.sdk.Gateway;
30 | import com.mastercard.gateway.android.sdk.GatewayMap;
31 |
32 | import java.util.List;
33 | import java.util.Objects;
34 |
35 | import dagger.hilt.android.AndroidEntryPoint;
36 |
37 | @AndroidEntryPoint
38 | public class ProcessPaymentActivity extends AppCompatActivity implements AuthAndBrowserHandler.ResultUI {
39 |
40 | private ProcessPaymentViewModel viewModel;
41 | ActivityProcessPaymentBinding binding;
42 |
43 | boolean isGooglePay = false;
44 |
45 | PaymentOptionsSheet optionsSheet;
46 | AuthAndBrowserHandler handler;
47 |
48 | static final int REQUEST_CARD_INFO = 100;
49 | @RequiresApi(api = Build.VERSION_CODES.N)
50 | @Override protected void onCreate(@Nullable Bundle savedInstanceState) {
51 | super.onCreate(savedInstanceState);
52 | binding = DataBindingUtil.setContentView(this, R.layout.activity_process_payment);
53 | viewModel = new ViewModelProvider(this).get(ProcessPaymentViewModel.class);
54 |
55 | optionsSheet = new PaymentOptionsSheet(this,
56 | LayoutInflater.from(this).inflate(R.layout.bottom_sheet_layout, null));
57 | handler = new AuthAndBrowserHandler(binding, this);
58 |
59 | initUI();
60 | setupObservers();
61 |
62 | binding.startButton.setOnClickListener(v -> createSession());
63 | binding.confirmButton.setOnClickListener(v -> processPayment(handler.isSkip3ds(), 7));
64 | binding.doneButton.setOnClickListener(v -> finish());
65 | }
66 |
67 | @RequiresApi(api = Build.VERSION_CODES.N)
68 | private void setupObservers() {
69 | viewModel.getSessionResult().observe(this, result -> {
70 | hide(binding.createSessionProgress);
71 | if (result instanceof GatewayResult.Success) {
72 | show(binding.createSessionSuccess);
73 | executePaymentOptionEnquiry();
74 | } else {
75 | show(binding.createSessionError);
76 | showResult(R.drawable.failed, R.string.pay_error_unable_to_create_session);
77 | }
78 | });
79 |
80 | viewModel.getPaymentOptionsInquiryResult().observe(this, result -> {
81 | hide(binding.paymentOptionEnquiryProgress);
82 | if (result instanceof GatewayResult.Success) {
83 | GatewayMap response = ((GatewayResult.Success) result).getResponse();
84 | List types = PaymentOptionsParser.extractTypes(response);
85 | if (types.isEmpty()) {
86 | show(binding.paymentOptionEnquiryError);
87 | } else {
88 | show(binding.paymentOptionEnquirySuccess);
89 | show(binding.selectPaymentOptionLabel);
90 | show(binding.selectPaymentOptionProgress);
91 | optionsSheet.show(types, this::onPaymentOptionChosen);
92 | }
93 | } else {
94 | show(binding.paymentOptionEnquiryError);
95 | }
96 | });
97 |
98 | viewModel.getCompleteSessionResult().observe(this, result -> {
99 | hide(binding.processPaymentProgress);
100 | if (result instanceof GatewayResult.Success) {
101 | show(binding.processPaymentSuccess);
102 | showResult(R.drawable.success, R.string.pay_you_payment_was_successful);
103 | } else {
104 | hide(binding.processPaymentSuccess);
105 | show(binding.processPaymentError);
106 | showResult(R.drawable.failed, R.string.pay_error_unable_to_complete_payment);
107 | }
108 | });
109 |
110 | viewModel.getTransactionResult().observe(this, result -> {
111 | if (result instanceof GatewayResult.Success) {
112 | GatewayMap response = ((GatewayResult.Success) result).getResponse();
113 | String txnResult = safeString(response.get("result"));
114 | if ("NOT_SUPPORTED".equals(txnResult)) handler.setSkip3ds(true);
115 | else viewModel.initiateAuthentication();
116 | } else {
117 | hide(binding.check3dsProgress);
118 | show(binding.check3dsError);
119 | showResult(R.drawable.failed, R.string.pay_error_3ds_authentication_failed);
120 | }
121 | });
122 |
123 | viewModel.getStartAuthenticationResult().observe(this, result -> {
124 | hide(binding.initiateAuthenticationProgress);
125 | if (result instanceof GatewayResult.Success) {
126 | handler.handleAuthentication(((GatewayResult.Success) result).getResponse());
127 | } else {
128 | show(binding.initiateAuthenticationError);
129 | showResult(R.drawable.failed, R.string.pay_error_3ds_authentication_failed);
130 | }
131 | });
132 |
133 | viewModel.getUpdateSessionResult().observe(this, result -> {
134 | hide(binding.updateSessionProgress);
135 | if (result instanceof GatewayResult.Success) {
136 | show(binding.updateSessionSuccess);
137 | hide(binding.startButton);
138 | if (isGooglePay) {
139 | show(binding.groupConfirm);
140 | //processPayment(true, 6);
141 | } else {
142 | initiateAuthenticate();
143 | }
144 | } else {
145 | show(binding.updateSessionError);
146 | showResult(R.drawable.failed, R.string.pay_error_unable_to_update_session);
147 | }
148 | });
149 |
150 | viewModel.getBrowserPaymentResult().observe(this, result -> {
151 | hide(binding.check3dsProgress);
152 | if (result instanceof GatewayResult.Success) {
153 | show(binding.check3dsSuccess);
154 | String html = safeString(((GatewayResult.Success) result)
155 | .getResponse().get("browserPayment.redirectHtml"));
156 | if (html != null) {
157 | show(binding.authenticateBrowserPaymentLabel);
158 | show(binding.authenticateBrowserPaymentProgress);
159 | startBrowserPayment(html);
160 | }
161 | } else {
162 | show(binding.check3dsError);
163 | showResult(R.drawable.failed, R.string.pay_error_3ds_authentication_failed);
164 | }
165 | });
166 | }
167 |
168 | private void onPaymentOptionChosen(String option) {
169 | hide(binding.selectPaymentOptionProgress);
170 | show(binding.selectPaymentOptionSuccess);
171 | viewModel.saveCurrencyForSelectedFlow(option);
172 |
173 | switch (option) {
174 | case "CARD":
175 | collectCardInfo();
176 | break;
177 | case "KNET":
178 | case "BENEFIT":
179 | case "QPAY":
180 | case "OMAN":
181 | initiateBrowserPayment();
182 | break;
183 | default:
184 | }
185 | }
186 |
187 | void initUI() {
188 | binding.startButton.setEnabled(true);
189 | binding.confirmButton.setEnabled(true);
190 | show(binding.startButton);
191 | hide(binding.groupConfirm);
192 | hide(binding.groupResult);
193 | }
194 |
195 | void createSession() {
196 | binding.startButton.setEnabled(false);
197 | show(binding.createSessionProgress);
198 | show(binding.createSessionLabel);
199 | GatewayMap payload = new GatewayMap().set("session.authenticationLimit", 25);
200 | viewModel.createSession(payload);
201 | }
202 |
203 | void executePaymentOptionEnquiry() {
204 | show(binding.paymentOptionEnquiryProgress);
205 | show(binding.paymentOptionEnquiryLabel);
206 | viewModel.inquirePaymentOptions();
207 | }
208 |
209 | void collectCardInfo() {
210 | show(binding.collectCardInfoLabel);
211 | show(binding.collectCardInfoProgress);
212 | Intent i = new Intent(this, CollectCardInfoActivity.class);
213 | i.putExtra(CollectCardInfoActivity.EXTRA_GOOGLE_PAY_TXN_AMOUNT, ProcessPaymentViewModel.AMOUNT);
214 | i.putExtra(CollectCardInfoActivity.EXTRA_GOOGLE_PAY_TXN_CURRENCY, viewModel.getCURRENCY());
215 | startActivityForResult(i, REQUEST_CARD_INFO);
216 | }
217 |
218 | void updateSession(String paymentToken) {
219 | show(binding.updateSessionLabel);
220 | show(binding.updateSessionProgress);
221 | viewModel.updateSession(paymentToken, "", "", "", "");
222 | }
223 |
224 | @RequiresApi(api = Build.VERSION_CODES.N)
225 | void initiateAuthenticate() {
226 | show(binding.initiateAuthenticationLabel);
227 | show(binding.initiateAuthenticationProgress);
228 | viewModel.initiateAuthentication();
229 | }
230 |
231 | void initiateBrowserPayment() {
232 | show(binding.check3dsLabel);
233 | show(binding.check3dsProgress);
234 | binding.confirmButton.setEnabled(false);
235 | viewModel.initiateBrowserPayment();
236 | }
237 |
238 | @SuppressLint("SetTextI18n")
239 | void processPayment(boolean skip3ds, int step) {
240 | show(binding.processPaymentLabel);
241 | show(binding.processPaymentProgress);
242 | if (skip3ds) {
243 | binding.processPaymentLabel.setText(step + "." + getString(R.string.pay_process_payment));
244 | }
245 | viewModel.submitTransaction(isGooglePay);
246 | }
247 |
248 |
249 | @Override public void showResult(@DrawableRes int iconRes, @StringRes int msgRes) {
250 | binding.resultIcon.setImageResource(iconRes);
251 | binding.resultText.setText(msgRes);
252 | hide(binding.groupConfirm);
253 | show(binding.groupResult);
254 | }
255 |
256 | @Override public void showResult(@DrawableRes int iconRes, @NonNull String message) {
257 | binding.resultIcon.setImageResource(iconRes);
258 | binding.resultText.setText(message);
259 | hide(binding.groupConfirm);
260 | show(binding.groupResult);
261 | }
262 |
263 | @Override public void start3DS(String redirectHtml) {
264 | Gateway.start3DSecureActivity(this, redirectHtml);
265 | }
266 |
267 | @Override public void startBrowserPayment(String redirectHtml) {
268 | Gateway.startGatewayBrowserPaymentActivity(this, redirectHtml);
269 | }
270 |
271 | @Override public void onReadyToConfirm() {
272 | show(binding.groupConfirm);
273 | }
274 |
275 | @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
276 | if (Gateway.handleGatewayResult(requestCode, resultCode, data, handler.makeGatewayCallback())) return;
277 |
278 | if (requestCode == REQUEST_CARD_INFO) {
279 | hide(binding.collectCardInfoProgress);
280 | if (resultCode == Activity.RESULT_OK) {
281 | show(binding.collectCardInfoSuccess);
282 | show(binding.updateSessionLabel);
283 | show(binding.updateSessionProgress);
284 | binding.confirmCardDescription.setText(data.getStringExtra(CollectCardInfoActivity.EXTRA_CARD_DESCRIPTION));
285 |
286 | String googlePayToken = data.getStringExtra(CollectCardInfoActivity.EXTRA_PAYMENT_TOKEN);
287 | if (googlePayToken != null) {
288 | isGooglePay = true;
289 | binding.check3dsLabel.setPaintFlags(binding.check3dsLabel.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
290 | updateSession(googlePayToken);
291 | } else {
292 | isGooglePay = false;
293 | viewModel.updateSession(
294 | null,
295 | Objects.requireNonNull(data.getStringExtra(CollectCardInfoActivity.EXTRA_CARD_NUMBER)),
296 | Objects.requireNonNull(data.getStringExtra(CollectCardInfoActivity.EXTRA_CARD_EXPIRY_MONTH)),
297 | Objects.requireNonNull(data.getStringExtra(CollectCardInfoActivity.EXTRA_CARD_EXPIRY_YEAR)),
298 | Objects.requireNonNull(data.getStringExtra(CollectCardInfoActivity.EXTRA_CARD_CVV))
299 | );
300 | }
301 | } else {
302 | show(binding.collectCardInfoError);
303 | showResult(R.drawable.failed, R.string.pay_error_card_info_not_collected);
304 | }
305 | return;
306 | }
307 | super.onActivityResult(requestCode, resultCode, data);
308 | }
309 |
310 | private static String safeString(Object o) { return (o instanceof String) ? (String) o : null; }
311 | }
--------------------------------------------------------------------------------
/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.json.JSONObject;
12 | import org.junit.Before;
13 | import org.junit.Test;
14 | import org.junit.runner.RunWith;
15 | import org.robolectric.RobolectricTestRunner;
16 | import org.robolectric.annotation.Config;
17 |
18 | import java.io.ByteArrayInputStream;
19 | import java.io.IOException;
20 | import java.io.InputStream;
21 | import java.nio.charset.StandardCharsets;
22 |
23 | import javax.net.ssl.HttpsURLConnection;
24 | import javax.net.ssl.SSLSocketFactory;
25 |
26 | import static org.junit.Assert.assertEquals;
27 | import static org.junit.Assert.assertFalse;
28 | import static org.junit.Assert.assertNotNull;
29 | import static org.junit.Assert.assertTrue;
30 | import static org.junit.Assert.fail;
31 | import static org.mockito.ArgumentMatchers.any;
32 | import static org.mockito.ArgumentMatchers.eq;
33 | import static org.mockito.Mockito.doReturn;
34 | import static org.mockito.Mockito.mock;
35 | import static org.mockito.Mockito.spy;
36 | import static org.mockito.Mockito.verify;
37 |
38 | @RunWith(RobolectricTestRunner.class)
39 | @Config(manifest = Config.NONE)
40 | public class GatewayTest {
41 |
42 | Gateway gateway;
43 |
44 | @Before
45 | public void setUp() throws Exception {
46 | gateway = spy(new Gateway());
47 | }
48 |
49 | @Test
50 | public void testSetMerchantIdThrowsExceptionIfNull() throws Exception {
51 | try {
52 | gateway.setMerchantId(null);
53 |
54 | fail("Null merchant ID should throw illegal argument exception");
55 | } catch (Exception e) {
56 | assertTrue(e instanceof IllegalArgumentException);
57 | }
58 | }
59 |
60 | @Test
61 | public void setMerchantIdWorksAsExpected() throws Exception {
62 | gateway.setMerchantId("MERCHANT_ID");
63 |
64 | assertEquals(gateway.merchantId, "MERCHANT_ID");
65 | }
66 |
67 | @Test
68 | public void testSetRegionThrowsExceptionIfNull() throws Exception {
69 | try {
70 | gateway.setRegion(null);
71 |
72 | fail("Null region should throw illegal argument exception");
73 | } catch (Exception e) {
74 | assertTrue(e instanceof IllegalArgumentException);
75 | }
76 | }
77 |
78 | @Test
79 | public void testSetRegionWorksAsIntended() throws Exception {
80 | gateway.setRegion(Gateway.Region.ASIA_PACIFIC);
81 |
82 | assertEquals(Gateway.Region.ASIA_PACIFIC, gateway.region);
83 | }
84 |
85 | @Test
86 | public void testBuildUpdateSessionRequestWorksAsExpected() {
87 | String sessionId = "session_id";
88 | String apiVersion = "1";
89 | GatewayMap payload = new GatewayMap();
90 |
91 | String expectedUrl = "some url";
92 |
93 | doReturn(expectedUrl).when(gateway).getUpdateSessionUrl(sessionId, apiVersion);
94 |
95 | GatewayRequest request = gateway.buildUpdateSessionRequest(sessionId, apiVersion, payload);
96 |
97 | assertTrue(request.payload.containsKey("device.browser"));
98 | assertTrue(request.payload.containsKey("apiOperation"));
99 | assertEquals(Gateway.API_OPERATION, request.payload.get("apiOperation"));
100 | assertEquals(Gateway.USER_AGENT, request.payload.get("device.browser"));
101 | }
102 |
103 | @Test
104 | public void testBuildUpdateSessionRequestHandlesApiVersion50() {
105 | String sessionId = "somesession";
106 | gateway.merchantId = "MERCHANT_ID";
107 |
108 | String apiVersion = "50";
109 | GatewayMap payload = new GatewayMap();
110 |
111 | String expectedUrl = "some url";
112 | String expectedAuthHeader = "Basic bWVyY2hhbnQuTUVSQ0hBTlRfSUQ6c29tZXNlc3Npb24=";
113 |
114 | doReturn(expectedUrl).when(gateway).getUpdateSessionUrl(sessionId, apiVersion);
115 |
116 | GatewayRequest request = gateway.buildUpdateSessionRequest(sessionId, apiVersion, payload);
117 |
118 | assertTrue(request.payload.containsKey("device.browser"));
119 | assertFalse(request.payload.containsKey("apiOperation"));
120 | assertEquals(Gateway.USER_AGENT, request.payload.get("device.browser"));
121 |
122 | assertTrue(request.extraHeaders.containsKey("Authorization"));
123 | assertEquals(expectedAuthHeader, request.extraHeaders.get("Authorization"));
124 | }
125 |
126 | @Test
127 | public void testStartGatewayFlowSkipsTitleIfNull() {
128 | Activity activity = mock(Activity.class);
129 | Intent intent = new Intent();
130 | String testHtml = "html";
131 |
132 | Gateway.startGatewayFlow(activity, testHtml, null, intent, Gateway.REQUEST_3D_SECURE);
133 |
134 | verify(activity).startActivityForResult(intent, Gateway.REQUEST_3D_SECURE);
135 | assertTrue(intent.hasExtra(Gateway3DSecureActivity.EXTRA_HTML));
136 | assertFalse(intent.hasExtra(Gateway3DSecureActivity.EXTRA_TITLE));
137 | assertEquals(testHtml, intent.getStringExtra(Gateway3DSecureActivity.EXTRA_HTML));
138 | }
139 |
140 | @Test
141 | public void testStartGatewayFlowWorksAsExpected() {
142 | Activity activity = mock(Activity.class);
143 | Intent intent = new Intent();
144 | String testHtml = "html";
145 | String testTitle = "title";
146 |
147 | Gateway.startGatewayFlow(activity, testHtml, testTitle, intent, Gateway.REQUEST_3D_SECURE);
148 |
149 | verify(activity).startActivityForResult(intent, Gateway.REQUEST_3D_SECURE);
150 | assertTrue(intent.hasExtra(Gateway3DSecureActivity.EXTRA_HTML));
151 | assertTrue(intent.hasExtra(Gateway3DSecureActivity.EXTRA_TITLE));
152 | assertEquals(testHtml, intent.getStringExtra(Gateway3DSecureActivity.EXTRA_HTML));
153 | assertEquals(testTitle, intent.getStringExtra(Gateway3DSecureActivity.EXTRA_TITLE));
154 | }
155 |
156 | @Test
157 | public void testHandleGatewayResultReturnsFalseWithNullCallback() {
158 | assertFalse(Gateway.handleGatewayResult(0, 0, null, null));
159 | }
160 |
161 | @Test
162 | public void testHandle3DSSecureResultReturnsFalseIfInvalidRequestCode() {
163 | int invalidRequestCode = 10;
164 | GatewayCallback callback = mock(GatewayCallback.class);
165 |
166 | assertFalse(Gateway.handleGatewayResult(invalidRequestCode, 0, null, callback));
167 | }
168 |
169 | @Test
170 | public void testHandleGatewayResultCallsCancelIfResultNotOk() {
171 | int validRequestCode = Gateway.REQUEST_3D_SECURE;
172 | int resultCode = Activity.RESULT_CANCELED;
173 | GatewayCallback callback = mock(GatewayCallback.class);
174 |
175 | boolean result = Gateway.handleGatewayResult(validRequestCode, resultCode, null, callback);
176 |
177 | assertTrue(result);
178 | verify(callback).onCancel(validRequestCode);
179 | }
180 |
181 | @Test
182 | public void testHandleGatewayResultCallsCompleteIfResultOK() {
183 | int validRequestCode = Gateway.REQUEST_3D_SECURE;
184 | int resultCode = Activity.RESULT_OK;
185 | Intent data = mock(Intent.class);
186 | String acsResultJson = "{\"foo\":\"bar\"}";
187 |
188 | GatewayCallback callback = spy(new GatewayCallback() {
189 | @Override
190 | public void onComplete(GatewayMap response, int resultCode) {
191 | assertNotNull(response);
192 | assertTrue(response.containsKey("foo"));
193 | assertEquals("bar", response.get("foo"));
194 | }
195 |
196 | @Override
197 | public void onCancel(int resultCode) {
198 | fail("Should never have called cancel");
199 | }
200 | });
201 |
202 | doReturn(acsResultJson)
203 | .when(data)
204 | .getStringExtra(Gateway3DSecureActivity.EXTRA_GATEWAY_RESULT);
205 |
206 | boolean result = Gateway.handleGatewayResult(validRequestCode, resultCode, data, callback);
207 |
208 | assertTrue(result);
209 |
210 | // ✅ Use matchers consistently and check requestCode (not resultCode)
211 | verify(callback).onComplete(any(GatewayMap.class), eq(validRequestCode));
212 | }
213 |
214 | @Test
215 | public void testHandleGooglePayResultReturnsFalseWithNullCallback() {
216 | assertFalse(Gateway.handleGooglePayResult(0, 0, null, null));
217 | }
218 |
219 | @Test
220 | public void testHandleGooglePayResultReturnsFalseIfInvalidRequestCode() {
221 | int invalidRequestCode = 10;
222 | GatewayGooglePayCallback callback = mock(GatewayGooglePayCallback.class);
223 |
224 | assertFalse(Gateway.handleGooglePayResult(invalidRequestCode, 0, null, callback));
225 | }
226 |
227 | @Test
228 | public void testHandleGooglePayResultCallsError() {
229 | int requestCode = Gateway.REQUEST_GOOGLE_PAY_LOAD_PAYMENT_DATA;
230 | int resultCode = AutoResolveHelper.RESULT_ERROR;
231 |
232 | // mock autoresolvehelper method
233 | Status mockStatus = mock(Status.class);
234 | Intent mockData = mock(Intent.class);
235 | doReturn(mockStatus).when(mockData).getParcelableExtra("com.google.android.gms.common.api.AutoResolveHelper.status");
236 |
237 | GatewayGooglePayCallback callback = spy(new GatewayGooglePayCallback() {
238 | @Override
239 | public void onReceivedPaymentData(JSONObject paymentData) {
240 | fail("Should not have received payment data");
241 | }
242 |
243 | @Override
244 | public void onGooglePayCancelled() {
245 | fail("Should not have called cancelled");
246 | }
247 |
248 | @Override
249 | public void onGooglePayError(Status status) {
250 | assertEquals(mockStatus, status);
251 | }
252 | });
253 |
254 | boolean result = Gateway.handleGooglePayResult(requestCode, resultCode, mockData, callback);
255 |
256 | assertTrue(result);
257 | verify(callback).onGooglePayError(any());
258 | }
259 |
260 | @Test
261 | public void testHandleGooglePayResultCallsCancelled() {
262 | int requestCode = Gateway.REQUEST_GOOGLE_PAY_LOAD_PAYMENT_DATA;
263 | int resultCode = Activity.RESULT_CANCELED;
264 |
265 | GatewayGooglePayCallback callback = mock(GatewayGooglePayCallback.class);
266 |
267 | boolean result = Gateway.handleGooglePayResult(requestCode, resultCode, null, callback);
268 |
269 | assertTrue(result);
270 | verify(callback).onGooglePayCancelled();
271 | }
272 |
273 | @Test
274 | public void testHandleGooglePayResultCallsPaymentDataOnSuccess() {
275 | int requestCode = Gateway.REQUEST_GOOGLE_PAY_LOAD_PAYMENT_DATA;
276 | int resultCode = Activity.RESULT_OK;
277 | PaymentData pData = PaymentData.fromJson("{}");
278 | Intent data = new Intent();
279 | pData.putIntoIntent(data);
280 |
281 | GatewayGooglePayCallback callback = mock(GatewayGooglePayCallback.class);
282 |
283 | boolean result = Gateway.handleGooglePayResult(requestCode, resultCode, data, callback);
284 |
285 | assertTrue(result);
286 | verify(callback).onReceivedPaymentData(any());
287 | }
288 |
289 | @Test
290 | public void testGetApiUrlThrowsExceptionIfRegionIsNull() throws Exception {
291 | String apiVersion = "44";
292 | gateway.region = null;
293 |
294 | try {
295 | String apiUrl = gateway.getApiUrl(apiVersion);
296 |
297 | fail("Null region should have caused illegal state exception");
298 | } catch (Exception e) {
299 | assertTrue(e instanceof IllegalStateException);
300 | }
301 | }
302 |
303 | @Test
304 | public void testGetApiUrlThrowsExceptionIfApiVersionIsLessThanMin() throws Exception {
305 | String apiVersion = String.valueOf(Gateway.MIN_API_VERSION - 1);
306 |
307 | try {
308 | String apiUrl = gateway.getApiUrl(apiVersion);
309 |
310 | fail("Api version less than minimum value should have caused illegal argument exception");
311 | } catch (Exception e) {
312 | assertTrue(e instanceof IllegalArgumentException);
313 | }
314 | }
315 |
316 |
317 | @Test
318 | public void testGetApiUrlWorksAsIntended() throws Exception {
319 | gateway.region = Gateway.Region.NORTH_AMERICA;
320 | String expectedUrl = "https://na.gateway.mastercard.com/api/rest/version/" + Gateway.MIN_API_VERSION;
321 |
322 | assertEquals(expectedUrl, gateway.getApiUrl(String.valueOf(Gateway.MIN_API_VERSION)));
323 | }
324 |
325 | @Test
326 | public void testGetUpdateSessionUrlThrowsExceptionIfSessionIdIsNull() throws Exception {
327 | try {
328 | gateway.getUpdateSessionUrl(null, String.valueOf(Gateway.MIN_API_VERSION));
329 |
330 | fail("Null session id should throw illegal argument exception");
331 | } catch (Exception e) {
332 | assertTrue(e instanceof IllegalArgumentException);
333 | }
334 | }
335 |
336 |
337 | @Test
338 | public void testGetUpdateSessionUrlThrowsExceptionIfMerchantIdIsNull() throws Exception {
339 | gateway.merchantId = null;
340 |
341 | try {
342 | String url = gateway.getUpdateSessionUrl("sess1234", String.valueOf(Gateway.MIN_API_VERSION));
343 |
344 | fail("Null merchant id should have caused illegal state exception");
345 | } catch (Exception e) {
346 | assertTrue(e instanceof IllegalStateException);
347 | }
348 | }
349 |
350 | @Test
351 | public void testGetUpdateSessionUrlWorksAsIntended() throws Exception {
352 | gateway.merchantId = "somemerchant";
353 | gateway.region = Gateway.Region.NORTH_AMERICA;
354 | String expectedUrl = "https://na.gateway.mastercard.com/api/rest/version/" + Gateway.MIN_API_VERSION + "/merchant/somemerchant/session/sess1234";
355 |
356 | String actualUrl = gateway.getUpdateSessionUrl("sess1234", String.valueOf(Gateway.MIN_API_VERSION));
357 |
358 | assertEquals(expectedUrl, actualUrl);
359 | }
360 |
361 | @Test
362 | public void testHandleCallbackMessageCallsOnErrorWithThrowableArg() throws Exception {
363 | GatewayCallback callback = mock(GatewayCallback.class);
364 | Throwable arg = new Exception("Some exception");
365 |
366 | gateway.handleCallbackMessage(callback, arg);
367 |
368 | verify(callback).onError(arg);
369 | }
370 |
371 | @Test
372 | public void testHandleCallbackMessageCallsSuccessWithNonThrowableArg() throws Exception {
373 | GatewayCallback callback = mock(GatewayCallback.class);
374 | GatewayMap arg = mock(GatewayMap.class);
375 |
376 | gateway.handleCallbackMessage(callback, arg);
377 |
378 | verify(callback).onSuccess(arg);
379 | }
380 |
381 | @Test
382 | public void testCreateConnectionWorksAsIntended() throws Exception {
383 | GatewayRequest request = new GatewayRequest();
384 | request.url = "https://www.mastercard.com";
385 | request.method = Gateway.Method.PUT;
386 |
387 | SSLSocketFactory socketFactory = mock(SSLSocketFactory.class);
388 | doReturn(socketFactory).when(gateway).createSocketFactory();
389 |
390 | HttpsURLConnection c = gateway.createHttpsUrlConnection(request);
391 |
392 | assertEquals(request.url, c.getURL().toString());
393 | assertEquals(socketFactory, c.getSSLSocketFactory());
394 | assertEquals(Gateway.CONNECTION_TIMEOUT, c.getConnectTimeout());
395 | assertEquals(Gateway.READ_TIMEOUT, c.getReadTimeout());
396 | assertEquals("PUT", c.getRequestMethod());
397 | assertEquals(Gateway.USER_AGENT, c.getRequestProperty("User-Agent"));
398 | assertEquals("application/json", c.getRequestProperty("Content-Type"));
399 | assertTrue(c.getDoOutput());
400 | }
401 |
402 | @Test
403 | public void testIsStatusOkWorksAsIntended() {
404 | int tooLow = 199;
405 | int tooHigh = 300;
406 | int justRight = 200;
407 |
408 | assertFalse(gateway.isStatusCodeOk(tooLow));
409 | assertFalse(gateway.isStatusCodeOk(tooHigh));
410 | assertTrue(gateway.isStatusCodeOk(justRight));
411 | }
412 |
413 | @Test
414 | public void testInputStreamToStringWorksAsExpected() {
415 | String expectedResult = "here is some string data";
416 | InputStream input = new ByteArrayInputStream(expectedResult.getBytes(StandardCharsets.UTF_8));
417 |
418 | try {
419 | String result = gateway.inputStreamToString(input);
420 | assertEquals(expectedResult, result);
421 | } catch (IOException e) {
422 | fail(e.getMessage());
423 | }
424 | }
425 |
426 | @Test
427 | public void testCreateAuthHeaderWorksAsExpected() {
428 | String sessionId = "somesession";
429 | gateway.merchantId = "MERCHANT_ID";
430 |
431 | String expectedAuthHeader = "Basic bWVyY2hhbnQuTUVSQ0hBTlRfSUQ6c29tZXNlc3Npb24=";
432 |
433 | String authHeader = gateway.createAuthHeader(sessionId);
434 |
435 | assertEquals(expectedAuthHeader, authHeader);
436 | }
437 | }
438 |
--------------------------------------------------------------------------------