Skip to content

Commit 25a2335

Browse files
Swap Moshi for GSON to address Type erasure problems in network requests (#157)
* Swap Moshi for GSON to address Type erasure problems in network requests * Add moshi converter factory dependency * Pass transaction reference and ID to callbacks for exception reporting
1 parent 1620c62 commit 25a2335

24 files changed

+229
-383
lines changed

build.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ sonarqube {
4343

4444
allprojects {
4545
repositories {
46-
jcenter()
47-
google()
4846
mavenCentral()
47+
google()
48+
jcenter()
4949
}
5050

5151
configurations.all {
@@ -67,7 +67,7 @@ ext {
6767
versionCode = 23
6868

6969
buildToolsVersion = "29.0.2"
70-
versionName = "3.3.1"
70+
versionName = "3.3.2"
7171
}
7272

7373
Object getEnvOrDefault(String propertyName, Object defaultValue) {

gradle.properties

Lines changed: 1 addition & 1 deletion
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.3.1
28+
VERSION_NAME=3.3.2
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

paystack/build.gradle

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
apply plugin: 'com.android.library'
22
apply plugin: 'kotlin-android'
33
apply plugin: 'jacoco'
4+
apply plugin: 'kotlin-kapt'
45

56
jacoco {
67
toolVersion = "$jacocoVersion"
@@ -65,16 +66,18 @@ android {
6566

6667
dependencies {
6768
implementation fileTree(dir: 'libs', include: ['*.jar'])
68-
implementation 'com.google.code.gson:gson:2.8.5'
6969
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
70-
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
70+
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
7171
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
7272
implementation 'androidx.appcompat:appcompat:1.2.0'
7373
implementation 'co.paystack.android.design.widget:pinpad:1.0.4'
7474
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
7575
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.coroutines"
7676
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutines"
7777

78+
implementation "com.squareup.moshi:moshi-kotlin:1.14.0"
79+
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.14.0"
80+
7881
implementation "com.pusher:pusher-java-client:$versions.pusher"
7982

8083
testImplementation "org.robolectric:robolectric:$versions.robolectric"

paystack/proguard-rules.pro

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,81 @@
11
-keepclassmembers class co.paystack.android.api.model.** { <fields>; }
2-
-keepclassmembers class co.paystack.android.model.AvsState { <fields>; }
2+
-keepclassmembers class co.paystack.android.model.** { <fields>; }
3+
4+
5+
# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
6+
# EnclosingMethod is required to use InnerClasses.
7+
-keepattributes Signature, InnerClasses, EnclosingMethod
8+
9+
# Retrofit does reflection on method and parameter annotations.
10+
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
11+
12+
# Retain service method parameters when optimizing.
13+
-keepclassmembers,allowshrinking,allowobfuscation interface * {
14+
@retrofit2.http.* <methods>;
15+
}
16+
17+
18+
##MOSHI
19+
# JSR 305 annotations are for embedding nullability information.
20+
-dontwarn javax.annotation.**
21+
22+
-keepclasseswithmembers class * {
23+
@com.squareup.moshi.* <methods>;
24+
}
25+
26+
-keep @com.squareup.moshi.JsonQualifier interface *
27+
28+
# Enum field names are used by the integrated EnumJsonAdapter.
29+
# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi.
30+
-keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
31+
<fields>;
32+
}
33+
34+
# The name of @JsonClass types is used to look up the generated adapter.
35+
-keepnames @com.squareup.moshi.JsonClass class *
36+
37+
# Retain generated target class's synthetic defaults constructor and keep DefaultConstructorMarker's
38+
# name. We will look this up reflectively to invoke the type's constructor.
39+
#
40+
# We can't _just_ keep the defaults constructor because Proguard/R8's spec doesn't allow wildcard
41+
# matching preceding parameters.
42+
-keepnames class kotlin.jvm.internal.DefaultConstructorMarker
43+
-keepclassmembers @com.squareup.moshi.JsonClass @kotlin.Metadata class * {
44+
synthetic <init>(...);
45+
}
46+
47+
# Retain generated JsonAdapters if annotated type is retained.
48+
-if @com.squareup.moshi.JsonClass class *
49+
-keep class <1>JsonAdapter {
50+
<init>(...);
51+
<fields>;
52+
}
53+
-if @com.squareup.moshi.JsonClass class **$*
54+
-keep class <1>_<2>JsonAdapter {
55+
<init>(...);
56+
<fields>;
57+
}
58+
-if @com.squareup.moshi.JsonClass class **$*$*
59+
-keep class <1>_<2>_<3>JsonAdapter {
60+
<init>(...);
61+
<fields>;
62+
}
63+
-if @com.squareup.moshi.JsonClass class **$*$*$*
64+
-keep class <1>_<2>_<3>_<4>JsonAdapter {
65+
<init>(...);
66+
<fields>;
67+
}
68+
-if @com.squareup.moshi.JsonClass class **$*$*$*$*
69+
-keep class <1>_<2>_<3>_<4>_<5>JsonAdapter {
70+
<init>(...);
71+
<fields>;
72+
}
73+
-if @com.squareup.moshi.JsonClass class **$*$*$*$*$*
74+
-keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter {
75+
<init>(...);
76+
<fields>;
77+
}
78+
79+
-keepclassmembers class kotlin.Metadata {
80+
public <methods>;
81+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ public String getReference() {
2323
return reference;
2424
}
2525

26-
void setReference(String reference) {
26+
public void setReference(String reference) {
2727
this.reference = reference;
2828
}
2929

30-
void setId(String id) {
30+
public void setId(String id) {
3131
this.id = id;
3232
}
3333

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

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import android.provider.Settings;
1010
import android.util.Log;
1111

12+
import androidx.annotation.Nullable;
1213
import androidx.annotation.VisibleForTesting;
1314

1415
import org.jetbrains.annotations.NotNull;
@@ -62,9 +63,11 @@ public void onSuccess(@NotNull ChargeParams params, @NotNull ChargeResponse data
6263
}
6364

6465
@Override
65-
public void onError(@NotNull Throwable e) {
66+
public void onError(@NotNull Throwable e, @Nullable String reference) {
6667
Log.e(LOG_TAG, e.getMessage(), e);
67-
notifyProcessingError(e);
68+
Transaction transaction = new Transaction();
69+
transaction.setReference(reference);
70+
notifyProcessingError(e, transaction);
6871
}
6972
};
7073

@@ -131,6 +134,7 @@ public void onSuccess(TransactionInitResponse data) {
131134
transactionId,
132135
card.getLast4digits(),
133136
deviceId,
137+
data.getReference(),
134138
null
135139
);
136140
processCharge(params);
@@ -153,18 +157,17 @@ public void onError(@NotNull Throwable exception) {
153157

154158
private void processChargeResponse(ChargeParams chargeParams, ChargeResponse chargeResponse) {
155159
if (chargeResponse == null) {
156-
notifyProcessingError(new ChargeException("Unknown server response"));
160+
notifyProcessingError(new ChargeException("Unknown server response"), chargeParams.getTransaction());
157161
return;
158162
}
159163

160-
Transaction transaction = new Transaction();
161-
transaction.setId(chargeResponse.getTransactionId());
162-
transaction.setReference(chargeResponse.getReference());
163-
164164
String status = chargeResponse.getStatus();
165165
if (status != null) {
166166
if (status.equalsIgnoreCase("1") || status.equalsIgnoreCase("success")) {
167167
setProcessingOff();
168+
Transaction transaction = new Transaction();
169+
transaction.setId(chargeResponse.getTransactionId());
170+
transaction.setReference(chargeResponse.getReference());
168171
transactionCallback.onSuccess(transaction);
169172
return;
170173
}
@@ -181,12 +184,12 @@ private void processChargeResponse(ChargeParams chargeParams, ChargeResponse cha
181184
}
182185

183186
if (chargeResponse.getAuth() != null && !chargeResponse.getAuth().equalsIgnoreCase("none")) {
184-
authenticateTransaction(chargeParams, chargeResponse, transaction);
187+
authenticateTransaction(chargeParams, chargeResponse, chargeParams.getTransaction());
185188
return;
186189
}
187190

188191
setProcessingOff();
189-
notifyProcessingError(new ChargeException(chargeResponse.getMessage()));
192+
notifyProcessingError(new ChargeException(chargeResponse.getMessage()), chargeParams.getTransaction());
190193
}
191194

192195
private void authenticateTransaction(ChargeParams chargeParams, ChargeResponse chargeResponse, Transaction transaction) {
@@ -225,7 +228,7 @@ private void validateOtp(String token, ChargeParams chargeParams) {
225228
paystackRepository.validateTransaction(chargeParams, token, cardProcessCallback);
226229
} catch (Exception ce) {
227230
Log.e(LOG_TAG, ce.getMessage(), ce);
228-
notifyProcessingError(ce);
231+
notifyProcessingError(ce, chargeParams.getTransaction());
229232
}
230233
}
231234

@@ -235,7 +238,7 @@ private void chargeWithAvs(Address address, ChargeParams chargeParams) {
235238
paystackRepository.validateAddress(chargeParams, address, cardProcessCallback);
236239
} catch (Exception e) {
237240
Log.e(LOG_TAG, e.getMessage(), e);
238-
notifyProcessingError(e);
241+
notifyProcessingError(e, chargeParams.getTransaction());
239242
}
240243
}
241244

@@ -251,7 +254,7 @@ public void onTick(long millisUntilFinished) {
251254
}.start();
252255
} catch (Exception ce) {
253256
Log.e(LOG_TAG, ce.getMessage(), ce);
254-
notifyProcessingError(ce);
257+
notifyProcessingError(ce, chargeParams.getTransaction());
255258
}
256259
}
257260

@@ -460,7 +463,8 @@ protected void onPostExecute(Address address) {
460463
if (address != null) {
461464
chargeWithAvs(address, chargeParams);
462465
} else {
463-
notifyProcessingError(new Exception("No address provided"));
466+
467+
notifyProcessingError(new Exception("No address provided"), chargeParams.getTransaction());
464468
}
465469
}
466470
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ import co.paystack.android.api.request.ChargeParams
66
interface ChargeApiCallback {
77
fun onSuccess(params: ChargeParams, response: ChargeResponse)
88

9-
fun onError(exception: Throwable)
9+
fun onError(exception: Throwable, reference: String?)
1010
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ internal class PaystackRepositoryImpl(private val apiService: PaystackApiService
3939
override fun processCardCharge(chargeParams: ChargeParams, callback: ChargeApiCallback) {
4040
makeApiRequest(
4141
onSuccess = { data -> callback.onSuccess(chargeParams, data) },
42-
onError = { throwable -> callback.onError(throwable) },
42+
onError = { throwable -> callback.onError(throwable, chargeParams.reference) },
4343
apiCall = { apiService.chargeCard(chargeParams.toRequestMap()) }
4444
)
4545
}
@@ -54,15 +54,15 @@ internal class PaystackRepositoryImpl(private val apiService: PaystackApiService
5454
makeApiRequest(
5555
apiCall = { apiService.validateOtp(requestBody) },
5656
onSuccess = { data -> callback.onSuccess(chargeParams, data) },
57-
onError = { throwable -> callback.onError(throwable) },
57+
onError = { throwable -> callback.onError(throwable, chargeParams.reference) },
5858
)
5959
}
6060

6161
override fun requeryTransaction(chargeParams: ChargeParams, callback: ChargeApiCallback) {
6262
makeApiRequest(
6363
apiCall = { apiService.requeryTransaction(chargeParams.transactionId) },
6464
onSuccess = { data -> callback.onSuccess(chargeParams, data) },
65-
onError = { throwable -> callback.onError(throwable) },
65+
onError = { throwable -> callback.onError(throwable, chargeParams.reference) },
6666
)
6767
}
6868

@@ -73,7 +73,7 @@ internal class PaystackRepositoryImpl(private val apiService: PaystackApiService
7373
makeApiRequest(
7474
apiCall = { apiService.validateAddress(requestBody) },
7575
onSuccess = { data -> callback.onSuccess(chargeParams, data) },
76-
onError = { throwable -> callback.onError(throwable) },
76+
onError = { throwable -> callback.onError(throwable, chargeParams.reference) },
7777
)
7878
}
7979

paystack/src/main/java/co/paystack/android/api/di/ApiComponent.kt

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,40 +8,36 @@ import co.paystack.android.api.service.ApiService
88
import co.paystack.android.api.service.PaystackApiService
99
import co.paystack.android.api.service.converter.WrappedResponseConverter
1010
import co.paystack.android.api.utils.TLSSocketFactory
11-
import com.google.gson.Gson
12-
import com.google.gson.GsonBuilder
11+
import com.squareup.moshi.Moshi
1312
import okhttp3.OkHttpClient
1413
import retrofit2.Retrofit
15-
import retrofit2.converter.gson.GsonConverterFactory
14+
import retrofit2.converter.moshi.MoshiConverterFactory
1615
import java.util.concurrent.TimeUnit
1716

1817
internal fun apiComponent(): ApiComponent = ApiModule
1918

2019
internal interface ApiComponent {
21-
val gson: Gson
2220
val tlsV1point2factory: TLSSocketFactory
2321
val okHttpClient: OkHttpClient
24-
val legacyApiService: ApiService
2522
val paystackApiService: PaystackApiService
2623
val paystackRepository: PaystackRepository
2724
}
2825

2926
internal object ApiModule : ApiComponent {
3027
const val LEGACY_API_URL = "https://standard.paystack.co/"
31-
const val PAYSTACK_API_URL = "https://api.paystack.co/"
32-
33-
override val gson = GsonBuilder()
34-
.setDateFormat("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'")
35-
.create()
28+
private const val PAYSTACK_API_URL = "https://api.paystack.co/"
3629

3730
override val tlsV1point2factory = TLSSocketFactory()
3831

39-
override val okHttpClient = OkHttpClient.Builder()
32+
override val okHttpClient: OkHttpClient = OkHttpClient.Builder()
4033
.addInterceptor { chain ->
4134
val original = chain.request()
4235
// Add headers so we get Android version and Paystack Library version
4336
val builder = original.newBuilder()
44-
.header("User-Agent", "Android_" + Build.VERSION.SDK_INT + "_Paystack_" + BuildConfig.VERSION_NAME)
37+
.header(
38+
"User-Agent",
39+
"Android_" + Build.VERSION.SDK_INT + "_Paystack_" + BuildConfig.VERSION_NAME
40+
)
4541
.header("X-Paystack-Build", BuildConfig.VERSION_CODE.toString())
4642
.header("Accept", "application/json")
4743
.method(original.method(), original.body())
@@ -54,19 +50,13 @@ internal object ApiModule : ApiComponent {
5450
.writeTimeout(5, TimeUnit.MINUTES)
5551
.build()
5652

57-
58-
override val legacyApiService: ApiService = Retrofit.Builder()
59-
.baseUrl(LEGACY_API_URL)
60-
.client(okHttpClient)
61-
.addConverterFactory(GsonConverterFactory.create(gson))
62-
.build()
63-
.create(ApiService::class.java)
53+
private val moshi = Moshi.Builder().build()
6454

6555
override val paystackApiService: PaystackApiService = Retrofit.Builder()
6656
.baseUrl(PAYSTACK_API_URL)
6757
.client(okHttpClient)
6858
.addConverterFactory(WrappedResponseConverter.Factory())
69-
.addConverterFactory(GsonConverterFactory.create(gson))
59+
.addConverterFactory(MoshiConverterFactory.create(moshi).asLenient())
7060
.build()
7161
.create(PaystackApiService::class.java)
7262

paystack/src/main/java/co/paystack/android/api/model/ApiResponse.java

Lines changed: 0 additions & 18 deletions
This file was deleted.

0 commit comments

Comments
 (0)