Skip to content

Commit 241f0e8

Browse files
MOB-183 - Fix transaction initialisation using access code. (#150)
* Fix transaction initialisation using access code. * Call notifyAll on singleton instance object in AuthActivity * Add tests for initiateTransaction * Add optional charge parameters to initialize request (#151)
1 parent 61343e5 commit 241f0e8

File tree

10 files changed

+192
-13
lines changed

10 files changed

+192
-13
lines changed

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ ext {
6464
compileSdkVersion = 29
6565
minSdkVersion = 16
6666
targetSdkVersion = 29
67-
versionCode = 19
67+
versionCode = 21
6868

6969
buildToolsVersion = "29.0.2"
70-
versionName = "3.2.0-alpha02"
70+
versionName = "3.3.0-alpha02"
7171
}
7272

7373
Object getEnvOrDefault(String propertyName, Object defaultValue) {

gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ android.useAndroidX=true
2525
org.gradle.daemon=true
2626
org.gradle.jvmargs=-Xmx2560m
2727
GROUP=co.paystack.android
28-
VERSION_NAME=3.2.0-alpha02
28+
VERSION_NAME=3.3.0-alpha01
2929
POM_DESCRIPTION=Android SDK for Paystack
3030
POM_URL=https://github.com/PaystackHQ/paystack-android
3131
POM_SCM_URL=https://github.com/PaystackHQ/paystack-android
@@ -37,4 +37,4 @@ POM_LICENCE_DIST=repo
3737
POM_DEVELOPER_ID=paystack
3838
POM_DEVELOPER_NAME=Paystack
3939
POM_DEVELOPER_EMAIL=[email protected]
40-
org.gradle.unsafe.configuration-cache=true
40+
org.gradle.unsafe.configuration-cache=false

paystack/src/main/java/co/paystack/android/TransactionManager.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package co.paystack.android;
22

3+
import static co.paystack.android.Transaction.EMPTY_TRANSACTION;
4+
35
import android.app.Activity;
46
import android.content.Intent;
57
import android.os.AsyncTask;
68
import android.os.CountDownTimer;
79
import android.provider.Settings;
810
import android.util.Log;
911

12+
import androidx.annotation.VisibleForTesting;
13+
1014
import org.jetbrains.annotations.NotNull;
1115

1216
import co.paystack.android.api.ApiCallback;
@@ -34,8 +38,6 @@
3438
import co.paystack.android.utils.Crypto;
3539
import co.paystack.android.utils.StringUtils;
3640

37-
import static co.paystack.android.Transaction.EMPTY_TRANSACTION;
38-
3941
class TransactionManager {
4042

4143
private static final String LOG_TAG = TransactionManager.class.getSimpleName();
@@ -117,8 +119,9 @@ private void validateCardThenInitTransaction(String publicKey, Charge charge) {
117119
}
118120
}
119121

120-
private void initiateTransaction(String publicKey, Charge charge, String deviceId) {
121-
paystackRepository.initializeTransaction(publicKey, charge, deviceId, new ApiCallback<TransactionInitResponse>() {
122+
@VisibleForTesting
123+
void initiateTransaction(String publicKey, Charge charge, String deviceId) {
124+
ApiCallback<TransactionInitResponse> callback = new ApiCallback<TransactionInitResponse>() {
122125
@Override
123126
public void onSuccess(TransactionInitResponse data) {
124127
Card card = charge.getCard();
@@ -132,12 +135,19 @@ public void onSuccess(TransactionInitResponse data) {
132135
);
133136
paystackRepository.processCardCharge(params, cardProcessCallback);
134137
}
138+
135139
@Override
136140
public void onError(@NotNull Throwable exception) {
137141
Log.e(LOG_TAG, exception.getMessage(), exception);
138142
notifyProcessingError(exception);
139143
}
140-
});
144+
};
145+
146+
if (charge.getAccessCode() == null || charge.getAccessCode().isEmpty()) {
147+
paystackRepository.initializeTransaction(publicKey, charge, deviceId, callback);
148+
} else {
149+
paystackRepository.getTransactionWithAccessCode(charge.getAccessCode(), callback);
150+
}
141151
}
142152

143153
private void processChargeResponse(ChargeParams chargeParams, ChargeResponse chargeResponse) {

paystack/src/main/java/co/paystack/android/api/PaystackRepository.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,9 @@ interface PaystackRepository {
1616
fun validateAddress(chargeParams: ChargeParams, address: Address, callback: ChargeApiCallback)
1717

1818
fun requeryTransaction(chargeParams: ChargeParams, callback: ChargeApiCallback)
19+
20+
fun getTransactionWithAccessCode(
21+
accessCode: String,
22+
callback: ApiCallback<TransactionInitResponse>
23+
)
1924
}

paystack/src/main/java/co/paystack/android/api/PaystackRepositoryImpl.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ internal class PaystackRepositoryImpl(private val apiService: PaystackApiService
2121
currency = charge.currency,
2222
metadata = charge.metadata,
2323
device = deviceId,
24+
reference = charge.reference,
25+
subAccount = charge.subaccount,
26+
transactionCharge = charge.transactionCharge,
27+
plan = charge.plan,
28+
bearer = charge.bearer,
29+
additionalParameters = charge.additionalParameters,
2430
).toRequestMap()
2531

2632
makeApiRequest(
@@ -71,6 +77,14 @@ internal class PaystackRepositoryImpl(private val apiService: PaystackApiService
7177
)
7278
}
7379

80+
override fun getTransactionWithAccessCode(accessCode: String, callback: ApiCallback<TransactionInitResponse>) {
81+
makeApiRequest(
82+
onSuccess = { data -> callback.onSuccess(data) },
83+
onError = { throwable -> callback.onError(throwable) },
84+
apiCall = { apiService.getTransaction(accessCode) }
85+
)
86+
}
87+
7488
private fun <T> makeApiRequest(apiCall: () -> Call<T>, onSuccess: (T) -> Unit, onError: (Throwable) -> Unit) {
7589
val retrofitCallback = object : Callback<T> {
7690
override fun onResponse(call: Call<T>, response: Response<T>) {

paystack/src/main/java/co/paystack/android/api/request/TransactionInitRequestBody.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package co.paystack.android.api.request
22

33
import co.paystack.android.api.utils.pruneNullValues
4+
import co.paystack.android.model.Charge.Bearer
45

56
data class TransactionInitRequestBody(
67
val publicKey: String,
@@ -9,14 +10,25 @@ data class TransactionInitRequestBody(
910
val currency: String?,
1011
val metadata: String?,
1112
val device: String,
13+
val reference: String?,
14+
val subAccount: String?,
15+
val transactionCharge: Int?,
16+
val plan: String?,
17+
val bearer: Bearer?,
18+
val additionalParameters: Map<String, String>,
1219
) {
13-
fun toRequestMap() = mapOf(
20+
fun toRequestMap() = additionalParameters + mapOf(
1421
FIELD_KEY to publicKey,
1522
FIELD_EMAIL to email,
1623
FIELD_AMOUNT to amount,
1724
FIELD_CURRENCY to currency,
1825
FIELD_METADATA to metadata,
1926
FIELD_DEVICE to device,
27+
FIELD_REFERENCE to reference,
28+
FIELD_SUBACCOUNT to subAccount,
29+
FIELD_TRANSACTION_CHARGE to transactionCharge,
30+
FIELD_BEARER to bearer?.name,
31+
FIELD_PLAN to plan,
2032
).pruneNullValues()
2133

2234
companion object {
@@ -26,5 +38,11 @@ data class TransactionInitRequestBody(
2638
const val FIELD_CURRENCY = "currency"
2739
const val FIELD_METADATA = "metadata"
2840
const val FIELD_DEVICE = "device"
41+
42+
const val FIELD_REFERENCE = "reference";
43+
const val FIELD_SUBACCOUNT = "subaccount";
44+
const val FIELD_TRANSACTION_CHARGE = "transaction_charge";
45+
const val FIELD_BEARER = "bearer";
46+
const val FIELD_PLAN = "plan";
2947
}
3048
}

paystack/src/main/java/co/paystack/android/api/service/PaystackApiService.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ internal interface PaystackApiService {
1414
@GET("/checkout/request_inline")
1515
fun initializeTransaction(@QueryMap params: Map<String, @JvmSuppressWildcards Any>): Call<TransactionInitResponse>
1616

17+
@GET("/transaction/verify_access_code/{accessCode}")
18+
fun getTransaction(@Path("accessCode") accessCode: String): Call<TransactionInitResponse>
19+
1720
@FormUrlEncoded
1821
@POST("/checkout/card/charge")
1922
@NoWrap

paystack/src/main/java/co/paystack/android/ui/AuthActivity.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,20 @@ public void handleResponse() {
6565
}
6666
synchronized (si) {
6767
si.setResponseJson(responseJson);
68-
si.notify();
68+
si.notifyAll();
6969
}
7070
finish();
7171
}
7272

7373
protected void setupWebview() {
7474
setContentView(R.layout.co_paystack_android____activity_auth);
7575

76-
findViewById(R.id.iv_close).setOnClickListener(v -> finish());
76+
findViewById(R.id.iv_close).setOnClickListener(v -> {
77+
synchronized (si) {
78+
si.notify();
79+
}
80+
finish();
81+
});
7782

7883
webView = findViewById(R.id.webView);
7984
webView.setKeepScreenOn(true);
@@ -133,7 +138,7 @@ public void onLoadResource(WebView view, String url) {
133138
}
134139

135140
public void onDestroy() {
136-
pusher.unsubscribe(channelName);
141+
pusher.disconnect();
137142
super.onDestroy();
138143
if (webView != null) {
139144
webView.stopLoading();

paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
package co.paystack.android
22

3+
import android.util.Log
34
import androidx.test.core.app.ActivityScenario
5+
import co.paystack.android.api.ApiCallback
6+
import co.paystack.android.api.ChargeApiCallback
47
import co.paystack.android.api.PaystackRepository
8+
import co.paystack.android.api.model.TransactionInitResponse
9+
import co.paystack.android.api.request.ChargeParams
510
import co.paystack.android.model.Card
611
import co.paystack.android.model.Charge
12+
import com.nhaarman.mockitokotlin2.any
713
import com.nhaarman.mockitokotlin2.isA
814
import com.nhaarman.mockitokotlin2.verify
15+
import com.nhaarman.mockitokotlin2.whenever
916
import org.junit.Before
1017
import org.junit.Test
1118
import org.junit.runner.RunWith
1219
import org.mockito.Mock
20+
import org.mockito.Mockito.anyString
1321
import org.mockito.Mockito.mock
1422
import org.mockito.MockitoAnnotations
1523
import org.robolectric.RobolectricTestRunner
@@ -44,7 +52,78 @@ class TransactionManagerTest {
4452
}
4553
}
4654

55+
@Test
56+
fun initiateTransactionIsCalled_ChargeAccessCodeIsNull_callInitializeTransactionOnPaystackRepository() {
57+
val publicKey = "pk_test_key"
58+
val deviceId = "android_702948084"
59+
val charge = Charge().apply {
60+
card = TEST_CARD
61+
accessCode = null
62+
}
63+
whenever(paystackRepository.initializeTransaction(anyString(), any(), anyString(), any()))
64+
.thenAnswer { Log.i(TAG, "initializeTransaction called") }
65+
66+
val transactionManager = TransactionManager(paystackRepository)
67+
transactionManager.initiateTransaction(publicKey, charge, deviceId)
68+
69+
verify(paystackRepository).initializeTransaction(
70+
isA<String>(),
71+
isA<Charge>(),
72+
isA<String>(),
73+
isA<ApiCallback<TransactionInitResponse>>()
74+
)
75+
}
76+
77+
@Test
78+
fun initiateTransactionIsCalled_ChargeAccessCodeIsNotNull_call_getTransactionWithAccessCode_on_PaystackRepository() {
79+
val publicKey = "pk_test_key"
80+
val deviceId = "android_702948084"
81+
val transAccessCode = "transaction_access_code"
82+
val charge = Charge().apply {
83+
card = TEST_CARD
84+
accessCode = transAccessCode
85+
}
86+
87+
whenever(paystackRepository.getTransactionWithAccessCode(anyString(), any()))
88+
.thenAnswer { Log.i(TAG, "getTransactionWithAccessCode called") }
89+
90+
val transactionManager = TransactionManager(paystackRepository)
91+
transactionManager.initiateTransaction(publicKey, charge, deviceId)
92+
93+
verify(paystackRepository).getTransactionWithAccessCode(
94+
isA<String>(),
95+
isA<ApiCallback<TransactionInitResponse>>()
96+
)
97+
}
98+
99+
100+
@Test
101+
fun initiateTransactionIsCalled_transactionInitializationSucceeds_call_processCardCharge_OnPaystackRepository() {
102+
val publicKey = "pk_test_key"
103+
val deviceId = "android_702948084"
104+
val charge = Charge().apply {
105+
card = TEST_CARD
106+
accessCode = null
107+
}
108+
109+
whenever(paystackRepository.initializeTransaction(anyString(), any(), anyString(), any()))
110+
.thenAnswer {
111+
val callback = it.arguments[3] as ApiCallback<TransactionInitResponse>
112+
callback.onSuccess(TransactionInitResponse("success", "trans_id"))
113+
}
114+
115+
val transactionManager = TransactionManager(paystackRepository)
116+
transactionManager.initiateTransaction(publicKey, charge, deviceId)
117+
118+
verify(paystackRepository).processCardCharge(
119+
isA<ChargeParams>(),
120+
isA<ChargeApiCallback>()
121+
)
122+
}
123+
124+
47125
companion object {
126+
private const val TAG = "TransactionManagerTest"
48127
val TEST_CARD = Card("5105105105105100", 2, 2024, "123")
49128
}
50129
}

paystack/src/test/java/co/paystack/android/api/PaystackRepositoryImplTest.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
package co.paystack.android.api
22

33
import co.paystack.android.api.model.ChargeResponse
4+
import co.paystack.android.api.model.TransactionInitResponse
45
import co.paystack.android.api.request.ChargeParams
6+
import co.paystack.android.api.request.TransactionInitRequestBody
57
import co.paystack.android.api.service.PaystackApiService
8+
import co.paystack.android.model.Charge
69
import com.nhaarman.mockitokotlin2.mock
710
import com.nhaarman.mockitokotlin2.times
811
import com.nhaarman.mockitokotlin2.verify
912
import com.nhaarman.mockitokotlin2.whenever
1013
import org.junit.Test
14+
import org.junit.runner.RunWith
15+
import org.mockito.ArgumentMatchers.anyMap
16+
import org.robolectric.RobolectricTestRunner
1117

18+
@RunWith(RobolectricTestRunner::class)
1219
class PaystackRepositoryImplTest {
1320
private val apiService: PaystackApiService = mock()
1421

@@ -24,13 +31,51 @@ class PaystackRepositoryImplTest {
2431
verify(apiService, times(1)).chargeCard(TEST_CHARGE_PARAMS.toRequestMap())
2532
}
2633

34+
@Test
35+
fun `initializeTransaction calls PaystackApiService with correct params`() {
36+
whenever(apiService.initializeTransaction(anyMap()))
37+
.thenReturn(FakeCall.success(TransactionInitResponse("success", "trans_id")))
38+
val repository = PaystackRepositoryImpl(apiService)
39+
val apiCallback = mock<ApiCallback<TransactionInitResponse>>()
40+
val testMetadata = "internal_tag" to "tag_internal_example"
41+
val charge = Charge().apply {
42+
email = testEmail
43+
amount = testAmount
44+
currency = testCurrency
45+
reference = testReference
46+
subaccount = "subacc_13850248"
47+
transactionCharge = 1000
48+
plan = "PLN_123456789"
49+
bearer = Charge.Bearer.subaccount
50+
putMetadata(testMetadata.first, testMetadata.second)
51+
}
52+
repository.initializeTransaction(testPublicKey, charge, testDeviceId, apiCallback)
53+
54+
val expectedApiCallMap = mapOf(
55+
TransactionInitRequestBody.FIELD_KEY to testPublicKey,
56+
TransactionInitRequestBody.FIELD_EMAIL to charge.email,
57+
TransactionInitRequestBody.FIELD_AMOUNT to charge.amount,
58+
TransactionInitRequestBody.FIELD_CURRENCY to charge.currency,
59+
TransactionInitRequestBody.FIELD_METADATA to charge.metadata,
60+
TransactionInitRequestBody.FIELD_DEVICE to testDeviceId,
61+
TransactionInitRequestBody.FIELD_REFERENCE to charge.reference,
62+
TransactionInitRequestBody.FIELD_SUBACCOUNT to charge.subaccount,
63+
TransactionInitRequestBody.FIELD_TRANSACTION_CHARGE to charge.transactionCharge,
64+
TransactionInitRequestBody.FIELD_BEARER to charge.bearer.name,
65+
TransactionInitRequestBody.FIELD_PLAN to charge.plan,
66+
)
67+
68+
verify(apiService).initializeTransaction(expectedApiCallMap)
69+
}
70+
2771
companion object {
2872
const val testPublicKey = "pk_live_123445677555"
2973
const val testEmail = "[email protected]"
3074
const val testDeviceId = "test_device_id"
3175
const val testAmount = 10000
3276
const val testCurrency = "NGN"
3377
const val testTransactionId = "123458685949"
78+
const val testReference = "ref_123458685949"
3479

3580
val TEST_CHARGE_PARAMS = ChargeParams(
3681
clientData = "encryptedClientData",

0 commit comments

Comments
 (0)