Skip to content

Commit

Permalink
Load intallment terms from capabilities (#348)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
AnasNaouchi authored Jun 14, 2024
1 parent 957cfeb commit 3c3be5c
Show file tree
Hide file tree
Showing 14 changed files with 285 additions and 132 deletions.
23 changes: 0 additions & 23 deletions app/src/java/java/co/omise/android/example/CheckoutActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -64,8 +63,6 @@ public class CheckoutActivity extends AppCompatActivity {
private EditText currencyEdit;
private Snackbar snackbar;

private Capability capability;

private ActivityResultLauncher<Intent> authorizingPaymentLauncher;
private ActivityResultLauncher<Intent> paymentCreatorLauncher;
private ActivityResultLauncher<Intent> creditCardLauncher;
Expand Down Expand Up @@ -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<Capability> request = new Capability.GetCapabilitiesRequestBuilder().build();
client.send(request, new RequestListener<Capability>() {
@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);
Expand All @@ -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);
Expand Down
23 changes: 0 additions & 23 deletions app/src/kotlin/java/co/omise/android/example/CheckoutActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Intent>
private lateinit var paymentCreatorLauncher: ActivityResultLauncher<Intent>
private lateinit var creditCardLauncher: ActivityResultLauncher<Intent>
Expand Down Expand Up @@ -125,28 +121,11 @@ class CheckoutActivity : AppCompatActivity() {
}
}


val client = Client(PUBLIC_KEY)
val request = Capability.GetCapabilitiesRequestBuilder().build()
client.send(request, object : RequestListener<Capability> {
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)
Expand All @@ -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)
Expand Down
5 changes: 2 additions & 3 deletions sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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(),
Expand All @@ -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<Request<Capability>>(), any())).doAnswer { invocation ->
val callback = invocation.getArgument<RequestListener<Capability>>(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() {
Expand All @@ -60,6 +134,27 @@ class PaymentCreatorActivityTest {
intended(hasComponent(hasClassName(CreditCardActivity::class.java.name)))
}

@Test
fun shouldShowOnlyPaymentMethodsRequestedByMerchantAndAvailableInCapability() {
ActivityScenario.launchActivityForResult<PaymentCreatorActivity>(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 =
Expand Down
10 changes: 10 additions & 0 deletions sdk/src/main/java/co/omise/android/extensions/IntentExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,13 @@ internal inline fun <reified T : Parcelable> Intent.parcelable(key: String?): T?
getParcelableExtra(key)
as? T
}

internal inline fun <reified T : Parcelable?> 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
}
2 changes: 1 addition & 1 deletion sdk/src/main/java/co/omise/android/models/Capability.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ data class Capability(
@field:JsonProperty("tokenization_methods")
var tokenizationMethods: List<String>? = null,
@field:JsonProperty("zero_interest_installments")
val zeroInterestInstallments: Boolean = false,
var zeroInterestInstallments: Boolean = false,
@field:JsonProperty("limits")
var limits: Limits? = null,
@field:JsonProperty
Expand Down
7 changes: 2 additions & 5 deletions sdk/src/main/java/co/omise/android/models/PaymentMethod.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 0 additions & 23 deletions sdk/src/main/java/co/omise/android/models/SourceType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -140,29 +140,6 @@ sealed class SourceType(
data class Unknown(
@JsonValue override val name: String?,
) : Installment(name)

companion object {
fun availableTerms(installment: Installment): List<Int> =
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ internal class PaymentChooserFragment : OmiseListFragment<PaymentMethodResource>
-> 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()
}
}
Expand Down
Loading

0 comments on commit 3c3be5c

Please sign in to comment.