From 3c3be5c9c75a35352b827bb4dd5c3f4baac8ec4a Mon Sep 17 00:00:00 2001 From: Anas Naouchi <113893333+AnasNaouchi@users.noreply.github.com> Date: Fri, 14 Jun 2024 04:46:28 +0300 Subject: [PATCH] Load intallment terms from capabilities (#348) * Fetch capability on SDK open and remove hard coded values * Update example app * Format * change var to val * remove non null assertion * Add error message translation --- .../android/example/CheckoutActivity.java | 23 -- .../omise/android/example/CheckoutActivity.kt | 23 -- sdk/build.gradle | 5 +- .../android/ui/PaymentCreatorActivityTest.kt | 97 ++++++++- .../android/extensions/IntentExtensions.kt | 10 + .../co/omise/android/models/Capability.kt | 2 +- .../co/omise/android/models/PaymentMethod.kt | 7 +- .../co/omise/android/models/SourceType.kt | 23 -- .../android/ui/PaymentChooserFragment.kt | 2 +- .../android/ui/PaymentCreatorActivity.kt | 199 +++++++++++++----- .../res/layout/activity_payment_creator.xml | 23 +- sdk/src/main/res/values-ja/strings.xml | 1 + sdk/src/main/res/values-th/strings.xml | 1 + sdk/src/main/res/values/strings.xml | 1 + 14 files changed, 285 insertions(+), 132 deletions(-) diff --git a/app/src/java/java/co/omise/android/example/CheckoutActivity.java b/app/src/java/java/co/omise/android/example/CheckoutActivity.java index 2a8b4937..f2d99e71 100644 --- a/app/src/java/java/co/omise/android/example/CheckoutActivity.java +++ b/app/src/java/java/co/omise/android/example/CheckoutActivity.java @@ -33,7 +33,6 @@ import co.omise.android.config.UiCustomization; import co.omise.android.config.UiCustomizationBuilder; import co.omise.android.models.Amount; -import co.omise.android.models.Capability; import co.omise.android.models.Source; import co.omise.android.models.Token; import co.omise.android.ui.AuthorizingPaymentActivity; @@ -64,8 +63,6 @@ public class CheckoutActivity extends AppCompatActivity { private EditText currencyEdit; private Snackbar snackbar; - private Capability capability; - private ActivityResultLauncher authorizingPaymentLauncher; private ActivityResultLauncher paymentCreatorLauncher; private ActivityResultLauncher creditCardLauncher; @@ -97,29 +94,11 @@ protected void onCreate(Bundle savedInstanceState) { creditCardButton.setOnClickListener(view -> payByCreditCard()); authorizeUrlButton.setOnClickListener(view -> AuthorizingPaymentDialog.showAuthorizingPaymentDialog(this, this::startAuthoringPaymentActivity)); - Client client = new Client(PUBLIC_KEY); - Request request = new Capability.GetCapabilitiesRequestBuilder().build(); - client.send(request, new RequestListener() { - @Override - public void onRequestSucceed(@NotNull Capability model) { - capability = model; - } - - @Override - public void onRequestFailed(@NotNull Throwable throwable) { - snackbar.setText(Objects.requireNonNull(capitalize(throwable.getMessage()))).show(); - } - }); } private void choosePaymentMethod() { boolean isUsedSpecificsPaymentMethods = PaymentSetting.isUsedSpecificsPaymentMethods(this); - if (!isUsedSpecificsPaymentMethods && capability == null) { - snackbar.setText(R.string.error_capability_have_not_set_yet); - return; - } - double localAmount = Double.parseDouble(amountEdit.getText().toString().trim()); String currency = currencyEdit.getText().toString().trim().toLowerCase(); Amount amount = Amount.fromLocalAmount(localAmount, currency); @@ -137,8 +116,6 @@ private void choosePaymentMethod() { if (isUsedSpecificsPaymentMethods) { intent.putExtra(OmiseActivity.EXTRA_CAPABILITY, PaymentSetting.createCapabilityFromPreferences(this)); - } else { - intent.putExtra(OmiseActivity.EXTRA_CAPABILITY, capability); } paymentCreatorLauncher.launch(intent); diff --git a/app/src/kotlin/java/co/omise/android/example/CheckoutActivity.kt b/app/src/kotlin/java/co/omise/android/example/CheckoutActivity.kt index 89cd4385..48f8e482 100644 --- a/app/src/kotlin/java/co/omise/android/example/CheckoutActivity.kt +++ b/app/src/kotlin/java/co/omise/android/example/CheckoutActivity.kt @@ -25,9 +25,7 @@ import co.omise.android.config.TextBoxCustomizationBuilder import co.omise.android.config.ThemeConfig import co.omise.android.config.ToolbarCustomizationBuilder import co.omise.android.config.UiCustomizationBuilder -import co.omise.android.extensions.capitalizeFirstChar import co.omise.android.models.Amount -import co.omise.android.models.Capability import co.omise.android.models.Source import co.omise.android.models.Token import co.omise.android.ui.AuthorizingPaymentActivity @@ -75,8 +73,6 @@ class CheckoutActivity : AppCompatActivity() { Snackbar.make(findViewById(R.id.content), "", Snackbar.LENGTH_SHORT) } - private var capability: Capability? = null - private lateinit var authorizingPaymentLauncher: ActivityResultLauncher private lateinit var paymentCreatorLauncher: ActivityResultLauncher private lateinit var creditCardLauncher: ActivityResultLauncher @@ -125,28 +121,11 @@ class CheckoutActivity : AppCompatActivity() { } } - - val client = Client(PUBLIC_KEY) - val request = Capability.GetCapabilitiesRequestBuilder().build() - client.send(request, object : RequestListener { - override fun onRequestSucceed(model: Capability) { - capability = model - } - - override fun onRequestFailed(throwable: Throwable) { - snackbar.setText(throwable.message?.capitalizeFirstChar().orEmpty()).show() - } - }) } private fun choosePaymentMethod() { val isUsedSpecificsPaymentMethods = PaymentSetting.isUsedSpecificsPaymentMethods(this) - if (!isUsedSpecificsPaymentMethods && capability == null) { - snackbar.setText(getString(R.string.error_capability_have_not_set_yet)) - return - } - val localAmount = amountEdit.text.toString().trim().toDouble() val currency = currencyEdit.text.toString().trim().lowercase() val amount = Amount.fromLocalAmount(localAmount, currency) @@ -161,8 +140,6 @@ class CheckoutActivity : AppCompatActivity() { if (isUsedSpecificsPaymentMethods) { putExtra(OmiseActivity.EXTRA_CAPABILITY, PaymentSetting.createCapabilityFromPreferences(this@CheckoutActivity)) - } else { - putExtra(OmiseActivity.EXTRA_CAPABILITY, capability) } paymentCreatorLauncher.launch(this) diff --git a/sdk/build.gradle b/sdk/build.gradle index 5d6ddeee..5710939a 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -219,8 +219,7 @@ repositories { mavenCentral() maven { url 'https://jitpack.io' } } -// Use locally to generate full report -// TODO: Fix flaky tests so that this can be used in CI to reflect full coverage +// Unit and instrumented test tasks.create(name: 'jacocoTestReport', type: JacocoReport, dependsOn: ['testProductionDebugUnitTest', 'lint', 'createProductionDebugCoverageReport']) { reports { xml.required = true @@ -249,7 +248,7 @@ tasks.create(name: 'jacocoTestReport', type: JacocoReport, dependsOn: ['testProd '**/*.ec' ]) } -// Use in CI to coverage generate report without instrumentation testing +// Unit tests only tasks.create(name: 'jacocoUnitTestReport', type: JacocoReport, dependsOn: ['compileProductionDebugAndroidTestJavaWithJavac','generateProductionDebugAndroidTestBuildConfig','checkProductionDebugAndroidTestAarMetadata','mergeProductionDebugAndroidTestResources','processProductionDebugAndroidTestManifest','testProductionDebugUnitTest', 'lint']) { reports { xml.required = true diff --git a/sdk/src/androidTest/java/co/omise/android/ui/PaymentCreatorActivityTest.kt b/sdk/src/androidTest/java/co/omise/android/ui/PaymentCreatorActivityTest.kt index 622ad2be..085aec5d 100644 --- a/sdk/src/androidTest/java/co/omise/android/ui/PaymentCreatorActivityTest.kt +++ b/sdk/src/androidTest/java/co/omise/android/ui/PaymentCreatorActivityTest.kt @@ -1,35 +1,62 @@ package co.omise.android.ui +import android.app.Activity import android.app.Activity.RESULT_OK +import android.app.Application import android.content.Intent +import android.os.Bundle import android.view.WindowManager import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.matcher.ComponentNameMatchers.hasClassName import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent import androidx.test.espresso.intent.rule.IntentsRule +import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry import co.omise.android.R +import co.omise.android.api.Client +import co.omise.android.api.Request +import co.omise.android.api.RequestListener import co.omise.android.models.Capability +import co.omise.android.models.SourceType import co.omise.android.models.Token +import co.omise.android.models.TokenizationMethod import co.omise.android.ui.OmiseActivity.Companion.EXTRA_TOKEN +import co.omise.android.utils.itemCount +import co.omise.android.utils.withListId +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.reset +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class PaymentCreatorActivityTest { @get:Rule val intentRule = IntentsRule() - private val capability = Capability() + // capabilities requested by the merchant + private val capability = + Capability.create( + allowCreditCard = true, + sourceTypes = listOf(SourceType.Fpx(), SourceType.TrueMoney), + tokenizationMethods = listOf(TokenizationMethod.GooglePay), + ) + private val mockClient: Client = mock() private val intent = Intent( ApplicationProvider.getApplicationContext(), @@ -40,6 +67,53 @@ class PaymentCreatorActivityTest { putExtra(OmiseActivity.EXTRA_CURRENCY, "thb") putExtra(OmiseActivity.EXTRA_CAPABILITY, capability) } + private val application = (InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application) + private val activityLifecycleCallbacks = + object : Application.ActivityLifecycleCallbacks { + override fun onActivityCreated( + activity: Activity, + savedInstanceState: Bundle?, + ) { + (activity as? PaymentCreatorActivity)?.setClient(mockClient) + } + + override fun onActivityStarted(activity: Activity) {} + + override fun onActivityResumed(activity: Activity) {} + + override fun onActivityPaused(activity: Activity) {} + + override fun onActivityStopped(activity: Activity) {} + + override fun onActivitySaveInstanceState( + activity: Activity, + outState: Bundle, + ) {} + + override fun onActivityDestroyed(activity: Activity) {} + } + + @Before + fun setUp() { + application.registerActivityLifecycleCallbacks(activityLifecycleCallbacks) + whenever(mockClient.send(any>(), any())).doAnswer { invocation -> + val callback = invocation.getArgument>(1) + // Capabilities retrieved from api + callback.onRequestSucceed( + Capability.create( + allowCreditCard = true, + sourceTypes = listOf(SourceType.TrueMoney, SourceType.PromptPay), + tokenizationMethods = listOf(TokenizationMethod.GooglePay, TokenizationMethod.Card), + ), + ) + } + } + + @After + fun tearDown() { + reset(mockClient) + application.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks) + } @Test fun initialActivity_collectExtrasIntent() { @@ -60,6 +134,27 @@ class PaymentCreatorActivityTest { intended(hasComponent(hasClassName(CreditCardActivity::class.java.name))) } + @Test + fun shouldShowOnlyPaymentMethodsRequestedByMerchantAndAvailableInCapability() { + ActivityScenario.launchActivityForResult(intent) + + onView( + withListId(R.id.recycler_view).atPosition(0), + ).check(matches(ViewMatchers.isEnabled())) + .check(matches(ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.payment_method_credit_card_title)))) + onView( + withListId(R.id.recycler_view).atPosition(1), + ).check(matches(ViewMatchers.isEnabled())) + .check(matches(ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.googlepay)))) + onView( + withListId(R.id.recycler_view).atPosition(2), + ).check(matches(ViewMatchers.isEnabled())) + .check(matches(ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.payment_truemoney_title)))) + onView(ViewMatchers.withText(R.string.payment_method_fpx_title)).check(doesNotExist()) + onView(withId(R.id.recycler_view)) + .check(matches(itemCount(3))) + } + @Test fun creditCardResult_resultOk() { val creditCardIntent = diff --git a/sdk/src/main/java/co/omise/android/extensions/IntentExtensions.kt b/sdk/src/main/java/co/omise/android/extensions/IntentExtensions.kt index a33be9a0..36e5d587 100644 --- a/sdk/src/main/java/co/omise/android/extensions/IntentExtensions.kt +++ b/sdk/src/main/java/co/omise/android/extensions/IntentExtensions.kt @@ -14,3 +14,13 @@ internal inline fun Intent.parcelable(key: String?): T? getParcelableExtra(key) as? T } + +internal inline fun Intent.parcelableNullable(key: String?): T? = + when { + // https://stackoverflow.com/questions/72571804/getserializableextra-and-getparcelableextra-are-deprecated-what-is-the-alternat/73543350#73543350 + SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getParcelableExtra(key, T::class.java) + else -> + @Suppress("DEPRECATION") + getParcelableExtra(key) + as? T + } diff --git a/sdk/src/main/java/co/omise/android/models/Capability.kt b/sdk/src/main/java/co/omise/android/models/Capability.kt index b63f2fcf..24f8067d 100644 --- a/sdk/src/main/java/co/omise/android/models/Capability.kt +++ b/sdk/src/main/java/co/omise/android/models/Capability.kt @@ -22,7 +22,7 @@ data class Capability( @field:JsonProperty("tokenization_methods") var tokenizationMethods: List? = null, @field:JsonProperty("zero_interest_installments") - val zeroInterestInstallments: Boolean = false, + var zeroInterestInstallments: Boolean = false, @field:JsonProperty("limits") var limits: Limits? = null, @field:JsonProperty diff --git a/sdk/src/main/java/co/omise/android/models/PaymentMethod.kt b/sdk/src/main/java/co/omise/android/models/PaymentMethod.kt index 4fcac9ef..9ed8ebde 100644 --- a/sdk/src/main/java/co/omise/android/models/PaymentMethod.kt +++ b/sdk/src/main/java/co/omise/android/models/PaymentMethod.kt @@ -35,11 +35,8 @@ data class PaymentMethod( fun createSourceTypeMethod(sourceType: SourceType): PaymentMethod = PaymentMethod( name = sourceType.name, - installmentTerms = - when (sourceType) { - is SourceType.Installment -> SourceType.Installment.availableTerms(sourceType) - else -> null - }, + // empty list as it will be replaced by the actual terms from capability + installmentTerms = listOf(), banks = when (sourceType) { is SourceType.Fpx -> sourceType.banks diff --git a/sdk/src/main/java/co/omise/android/models/SourceType.kt b/sdk/src/main/java/co/omise/android/models/SourceType.kt index 891f4f12..44d66954 100644 --- a/sdk/src/main/java/co/omise/android/models/SourceType.kt +++ b/sdk/src/main/java/co/omise/android/models/SourceType.kt @@ -140,29 +140,6 @@ sealed class SourceType( data class Unknown( @JsonValue override val name: String?, ) : Installment(name) - - companion object { - fun availableTerms(installment: Installment): List = - when (installment) { - Bay -> listOf(3, 4, 6, 9, 10) - BayWlb -> listOf(3, 4, 6, 9, 10) - FirstChoice -> listOf(3, 4, 6, 9, 10, 12, 18, 24, 36) - FirstChoiceWlb -> listOf(3, 4, 6, 9, 10, 12, 18, 24, 36) - Bbl -> listOf(4, 6, 8, 9, 10) - BblWlb -> listOf(4, 6, 8, 9, 10) - Mbb -> listOf(6, 12, 18, 24) - Ktc -> listOf(3, 4, 5, 6, 7, 8, 9, 10) - KtcWlb -> listOf(3, 4, 5, 6, 7, 8, 9, 10) - KBank -> listOf(3, 4, 6, 10) - KBankWlb -> listOf(3, 4, 6, 10) - Scb -> listOf(3, 4, 6, 9, 10) - ScbWlb -> listOf(3, 4, 6, 9, 10) - Ttb -> listOf(3, 4, 6, 10) - TtbWlb -> listOf(3, 4, 6, 10) - Uob -> listOf(3, 4, 6, 10) - is Unknown -> emptyList() - } - } } companion object { diff --git a/sdk/src/main/java/co/omise/android/ui/PaymentChooserFragment.kt b/sdk/src/main/java/co/omise/android/ui/PaymentChooserFragment.kt index 83d53c1d..32b5394a 100644 --- a/sdk/src/main/java/co/omise/android/ui/PaymentChooserFragment.kt +++ b/sdk/src/main/java/co/omise/android/ui/PaymentChooserFragment.kt @@ -73,7 +73,7 @@ internal class PaymentChooserFragment : OmiseListFragment -> item.sourceType?.let(::sendRequest) PaymentMethodResource.Fpx -> navigation.navigateToFpxEmailForm() PaymentMethodResource.GooglePay -> navigation.navigateToGooglePayForm() - PaymentMethodResource.DuitNowOBW -> navigation.navigateToDuitNowOBWBankChooser() + PaymentMethodResource.DuitNowOBW -> navigation.navigateToDuitNowOBWBankChooser(capability) PaymentMethodResource.Atome -> navigation.navigateToAtomeForm() } } diff --git a/sdk/src/main/java/co/omise/android/ui/PaymentCreatorActivity.kt b/sdk/src/main/java/co/omise/android/ui/PaymentCreatorActivity.kt index 0070cc67..26dce74c 100644 --- a/sdk/src/main/java/co/omise/android/ui/PaymentCreatorActivity.kt +++ b/sdk/src/main/java/co/omise/android/ui/PaymentCreatorActivity.kt @@ -3,7 +3,11 @@ package co.omise.android.ui import android.app.Activity import android.content.Intent import android.os.Bundle +import android.view.Menu +import android.view.MenuItem import android.view.WindowManager +import android.widget.ProgressBar +import android.widget.TextView import androidx.activity.OnBackPressedCallback import androidx.annotation.VisibleForTesting import androidx.fragment.app.Fragment @@ -13,12 +17,14 @@ import co.omise.android.api.Request import co.omise.android.api.RequestListener import co.omise.android.extensions.getMessageFromResources import co.omise.android.extensions.parcelable +import co.omise.android.extensions.parcelableNullable import co.omise.android.models.APIError import co.omise.android.models.Bank import co.omise.android.models.Capability import co.omise.android.models.Model import co.omise.android.models.PaymentMethod import co.omise.android.models.Source +import co.omise.android.models.SourceType import co.omise.android.models.SupportedEcontext import co.omise.android.models.Token import co.omise.android.ui.OmiseActivity.Companion.EXTRA_AMOUNT @@ -32,6 +38,7 @@ import co.omise.android.ui.OmiseActivity.Companion.EXTRA_PKEY import co.omise.android.ui.OmiseActivity.Companion.EXTRA_SOURCE_OBJECT import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.activity_payment_creator.payment_creator_container +import org.jetbrains.annotations.TestOnly import java.io.IOError /** @@ -49,28 +56,20 @@ class PaymentCreatorActivity : OmiseActivity() { private var googlepayRequestPhoneNumber: Boolean = false private val snackbar: Snackbar by lazy { Snackbar.make(payment_creator_container, "", Snackbar.LENGTH_SHORT) } - private val client: Client by lazy { Client(pkey) } + private lateinit var client: Client - private val requester: PaymentCreatorRequester by lazy { - PaymentCreatorRequesterImpl(client, amount, currency, capability) + @TestOnly + fun setClient(client: Client) { + this.client = client } + private lateinit var requester: PaymentCreatorRequester + @VisibleForTesting - val navigation: PaymentCreatorNavigation by lazy { - PaymentCreatorNavigationImpl( - this, - pkey, - amount, - currency, - cardBrands, - googlepayMerchantId, - googlepayRequestBillingAddress, - googlepayRequestPhoneNumber, - REQUEST_CREDIT_CARD, - requester, - capability, - ) - } + lateinit var navigation: PaymentCreatorNavigation + + private lateinit var progressBar: ProgressBar + private lateinit var errorMessage: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -80,6 +79,13 @@ class PaymentCreatorActivity : OmiseActivity() { setContentView(R.layout.activity_payment_creator) + progressBar = findViewById(R.id.progressBar) + errorMessage = findViewById(R.id.errorMessage) + // Initially hide the ProgressBar and error message + progressBar.visibility = ProgressBar.GONE + errorMessage.visibility = TextView.GONE + + title = getString(R.string.payment_chooser_title) val onBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { @@ -93,7 +99,87 @@ class PaymentCreatorActivity : OmiseActivity() { onBackPressedDispatcher.addCallback(this, onBackPressedCallback) initialize() - navigation.navigateToPaymentChooser(capability) + loadCapability() + } + + // Set the menu button to close the view by the user + override fun onCreateOptionsMenu(menu: Menu): Boolean { + if (supportFragmentManager.findFragmentById(R.id.payment_creator_container) !is PaymentChooserFragment) { + menuInflater.inflate(R.menu.menu_toolbar, menu) + return true + } + return false + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.close_menu -> { + setResult(Activity.RESULT_CANCELED) + finish() + } + } + return super.onOptionsItemSelected(item) + } + + private fun loadCapability() { + // Start loading + progressBar.visibility = ProgressBar.VISIBLE + // Hide error message + errorMessage.visibility = TextView.GONE + // Get capability + val capabilityRequest = Capability.GetCapabilitiesRequestBuilder().build() + client.send( + capabilityRequest, + object : RequestListener { + override fun onRequestSucceed(model: Capability) { + updateActivityWithCapability(model) + // Invalidate the options menu to trigger a refresh and hide the menu button + // as new button will come from the next view + invalidateOptionsMenu() + // Hide loading + progressBar.visibility = ProgressBar.GONE + } + + override fun onRequestFailed(throwable: Throwable) { + progressBar.visibility = ProgressBar.GONE + // Show the error message + errorMessage.text = getString(R.string.error_loading_payment_methods) + errorMessage.visibility = TextView.VISIBLE + } + }, + ) + } + + // Detect if the current activity is still active + private fun isActivityActive(): Boolean { + return !isFinishing && !isDestroyed + } + + private fun updateActivityWithCapability(newCapability: Capability) { + capability = newCapability + requester = PaymentCreatorRequesterImpl(client, amount, currency, newCapability) + navigation = + PaymentCreatorNavigationImpl( + this, + pkey, + amount, + currency, + cardBrands, + googlepayMerchantId, + googlepayRequestBillingAddress, + googlepayRequestPhoneNumber, + REQUEST_CREDIT_CARD, + requester, + newCapability, + ) + capability = filterCapabilities(newCapability) + + // Replace the capability passed from merchant by the new capability + intent.putExtra(EXTRA_CAPABILITY, capability) + // Open the payment method chooser if the activity is still active + if (isActivityActive()) { + navigation.navigateToPaymentChooser(capability) + } requester.listener = object : PaymentCreatorRequestListener { @@ -116,7 +202,6 @@ class PaymentCreatorActivity : OmiseActivity() { } } } - supportFragmentManager.addOnBackStackChangedListener { supportActionBar?.let { if (supportFragmentManager.findFragmentById(R.id.payment_creator_container) is PaymentChooserFragment) { @@ -130,6 +215,42 @@ class PaymentCreatorActivity : OmiseActivity() { } } + // Filter the capabilities based on the merchant request and what is available in the capabilities of the merchant account + private fun filterCapabilities(capability: Capability): Capability { + val merchantPassedCapabilities = intent.parcelableNullable(EXTRA_CAPABILITY) + var filteredPaymentMethods: List? = null + var filteredTokenizationMethods: List? = null + + if (merchantPassedCapabilities != null) { + val selectedPaymentMethods = merchantPassedCapabilities.paymentMethods + val selectedTokenizationMethods = merchantPassedCapabilities.tokenizationMethods + if (selectedPaymentMethods != null) { + filteredPaymentMethods = + capability.paymentMethods?.filter { capMethod -> + selectedPaymentMethods.map { it.name }.contains(capMethod.name) + } + capability.paymentMethods = filteredPaymentMethods?.toMutableList() + } + if (selectedTokenizationMethods != null) { + filteredTokenizationMethods = + capability.tokenizationMethods?.filter { + selectedTokenizationMethods.contains(it) + } + capability.tokenizationMethods = filteredTokenizationMethods + } + capability.zeroInterestInstallments = merchantPassedCapabilities.zeroInterestInstallments + // add the tokenization methods into payment methods since the SDK only shows paymentMethods + val combinedMethods = capability.paymentMethods?.toMutableList() + capability.tokenizationMethods?.forEach { method -> + run { + combinedMethods?.add(PaymentMethod(method)) + } + } + capability.paymentMethods = combinedMethods + } + return capability + } + // TODO: find a way to unit test ActivityResult launcher in order to be able to move from deprecated onActivityResult override fun onActivityResult( requestCode: Int, @@ -165,15 +286,15 @@ class PaymentCreatorActivity : OmiseActivity() { } private fun initialize() { - listOf(EXTRA_PKEY, EXTRA_AMOUNT, EXTRA_CURRENCY, EXTRA_CAPABILITY).forEach { + listOf(EXTRA_PKEY, EXTRA_AMOUNT, EXTRA_CURRENCY).forEach { require(intent.hasExtra(it)) { "Could not found $it." } } pkey = requireNotNull(intent.getStringExtra(EXTRA_PKEY)) { "${::EXTRA_PKEY.name} must not be null." } + if (!this::client.isInitialized) { + client = Client(pkey) + } amount = intent.getLongExtra(EXTRA_AMOUNT, 0) currency = requireNotNull(intent.getStringExtra(EXTRA_CURRENCY)) { "${::EXTRA_CURRENCY.name} must not be null." } - capability = requireNotNull(intent.parcelable(EXTRA_CAPABILITY)) { "${::EXTRA_CAPABILITY.name} must not be null." } - val fetchBrands: List? = capability.paymentMethods?.find { it.name == "card" }?.cardBrands - cardBrands = if (fetchBrands != null) fetchBrands as ArrayList else arrayListOf() googlepayMerchantId = intent.getStringExtra(EXTRA_GOOGLEPAY_MERCHANT_ID) ?: "[GOOGLEPAY_MERCHANT_ID]" googlepayRequestBillingAddress = intent.getBooleanExtra(EXTRA_GOOGLEPAY_REQUEST_BILLING_ADDRESS, false) googlepayRequestPhoneNumber = intent.getBooleanExtra(EXTRA_GOOGLEPAY_REQUEST_PHONE_NUMBER, false) @@ -218,7 +339,7 @@ interface PaymentCreatorNavigation { fun navigateToGooglePayForm() - fun navigateToDuitNowOBWBankChooser() + fun navigateToDuitNowOBWBankChooser(capability: Capability) } private class PaymentCreatorNavigationImpl( @@ -366,32 +487,8 @@ private class PaymentCreatorNavigationImpl( activity.startActivityForResult(intent, requestCode) } - override fun navigateToDuitNowOBWBankChooser() { - /** - * DuitNow OBW didn't support capability api for banks list - * so need to define local banks list - */ - val banks = - listOf( - Bank(name = "Affin Bank", code = "affin", active = true), - Bank(name = "Alliance Bank (Personal)", code = "alliance", active = true), - Bank(name = "AGRONet", code = "agro", active = true), - Bank(name = "AmBank", code = "ambank", active = true), - Bank(name = "Bank Islam", code = "islam", active = true), - Bank(name = "Bank Muamalat", code = "muamalat", active = true), - Bank(name = "Bank Rakyat", code = "rakyat", active = true), - Bank(name = "BSN", code = "bsn", active = true), - Bank(name = "CIMB Clicks", code = "cimb", active = true), - Bank(name = "Hong Leong Bank", code = "hongleong", active = true), - Bank(name = "HSBC Bank", code = "hsbc", active = true), - Bank(name = "KFH", code = "kfh", active = true), - Bank(name = "Maybank2U", code = "maybank2u", active = true), - Bank(name = "OCBC Bank", code = "ocbc", active = true), - Bank(name = "Public Bank", code = "public", active = true), - Bank(name = "RHB Bank", code = "rhb", active = true), - Bank(name = "Standard Chartered", code = "sc", active = true), - Bank(name = "UOB Bank", code = "uob", active = true), - ) + override fun navigateToDuitNowOBWBankChooser(capability: Capability) { + val banks = capability.paymentMethods?.find { it.name == SourceType.DuitNowOBW.name }?.banks ?: emptyList() val fragment = DuitNowOBWBankChooserFragment.newInstance(banks).apply { diff --git a/sdk/src/main/res/layout/activity_payment_creator.xml b/sdk/src/main/res/layout/activity_payment_creator.xml index d813d5b3..d5719ab4 100644 --- a/sdk/src/main/res/layout/activity_payment_creator.xml +++ b/sdk/src/main/res/layout/activity_payment_creator.xml @@ -4,5 +4,26 @@ android:id="@+id/payment_creator_container" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.PaymentCreatorActivity" /> + tools:context=".ui.PaymentCreatorActivity"> + + + + + + + diff --git a/sdk/src/main/res/values-ja/strings.xml b/sdk/src/main/res/values-ja/strings.xml index 9e23c53f..ac078f21 100644 --- a/sdk/src/main/res/values-ja/strings.xml +++ b/sdk/src/main/res/values-ja/strings.xml @@ -5,6 +5,7 @@ クレジットカード カードブランドロゴ ネットワーク通信エラー, 安定したネット環境で再度試してください. (%1$s) + 決済方法を読み込むことができません クレジットカード番号が無効です クレジットカード所有者名が無効です クレジットカードの有効期限が無効です diff --git a/sdk/src/main/res/values-th/strings.xml b/sdk/src/main/res/values-th/strings.xml index c73a4b98..185d6d22 100644 --- a/sdk/src/main/res/values-th/strings.xml +++ b/sdk/src/main/res/values-th/strings.xml @@ -5,6 +5,7 @@ บัตรเครดิต โลโก้บัตรเครดิต ไม่สามารถเชื่อมต่ออินเตอร์เน็ทได้ กรุณาตรวจสอบและลองใหม่อีกครั้ง (%1$s) + โหลดวิธีการชำระเงินไม่สำเร็จ หมายเลขบัตรไม่ถูกต้อง ชื่อผู้ถือบัตรไม่ถูกต้อง diff --git a/sdk/src/main/res/values/strings.xml b/sdk/src/main/res/values/strings.xml index 30843eee..bdbe6698 100644 --- a/sdk/src/main/res/values/strings.xml +++ b/sdk/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ Credit Card Card brand logo Network communication error, please make sure you have a stable internet connection. (%1$s) + Unable to load payment methods Credit card number is invalid Card holder name is invalid Card expiration date is invalid