Skip to content

Commit

Permalink
Swap Moshi for GSON to address Type erasure problems in network reque…
Browse files Browse the repository at this point in the history
…sts (#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
  • Loading branch information
michael-paystack authored Mar 29, 2023
1 parent 1620c62 commit 25a2335
Show file tree
Hide file tree
Showing 24 changed files with 229 additions and 383 deletions.
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ sonarqube {

allprojects {
repositories {
jcenter()
google()
mavenCentral()
google()
jcenter()
}

configurations.all {
Expand All @@ -67,7 +67,7 @@ ext {
versionCode = 23

buildToolsVersion = "29.0.2"
versionName = "3.3.1"
versionName = "3.3.2"
}

Object getEnvOrDefault(String propertyName, Object defaultValue) {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ android.useAndroidX=true
org.gradle.daemon=true
org.gradle.jvmargs=-Xmx2560m
GROUP=co.paystack.android
VERSION_NAME=3.3.1
VERSION_NAME=3.3.2
POM_DESCRIPTION=Android SDK for Paystack
POM_URL=https://github.com/PaystackHQ/paystack-android
POM_SCM_URL=https://github.com/PaystackHQ/paystack-android
Expand Down
7 changes: 5 additions & 2 deletions paystack/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'jacoco'
apply plugin: 'kotlin-kapt'

jacoco {
toolVersion = "$jacocoVersion"
Expand Down Expand Up @@ -65,16 +66,18 @@ android {

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

implementation "com.squareup.moshi:moshi-kotlin:1.14.0"
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.14.0"

implementation "com.pusher:pusher-java-client:$versions.pusher"

testImplementation "org.robolectric:robolectric:$versions.robolectric"
Expand Down
81 changes: 80 additions & 1 deletion paystack/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -1,2 +1,81 @@
-keepclassmembers class co.paystack.android.api.model.** { <fields>; }
-keepclassmembers class co.paystack.android.model.AvsState { <fields>; }
-keepclassmembers class co.paystack.android.model.** { <fields>; }


# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
# EnclosingMethod is required to use InnerClasses.
-keepattributes Signature, InnerClasses, EnclosingMethod

# Retrofit does reflection on method and parameter annotations.
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations

# Retain service method parameters when optimizing.
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}


##MOSHI
# JSR 305 annotations are for embedding nullability information.
-dontwarn javax.annotation.**

-keepclasseswithmembers class * {
@com.squareup.moshi.* <methods>;
}

-keep @com.squareup.moshi.JsonQualifier interface *

# Enum field names are used by the integrated EnumJsonAdapter.
# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi.
-keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
<fields>;
}

# The name of @JsonClass types is used to look up the generated adapter.
-keepnames @com.squareup.moshi.JsonClass class *

# Retain generated target class's synthetic defaults constructor and keep DefaultConstructorMarker's
# name. We will look this up reflectively to invoke the type's constructor.
#
# We can't _just_ keep the defaults constructor because Proguard/R8's spec doesn't allow wildcard
# matching preceding parameters.
-keepnames class kotlin.jvm.internal.DefaultConstructorMarker
-keepclassmembers @com.squareup.moshi.JsonClass @kotlin.Metadata class * {
synthetic <init>(...);
}

# Retain generated JsonAdapters if annotated type is retained.
-if @com.squareup.moshi.JsonClass class *
-keep class <1>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*
-keep class <1>_<2>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*
-keep class <1>_<2>_<3>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*$*
-keep class <1>_<2>_<3>_<4>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*$*$*
-keep class <1>_<2>_<3>_<4>_<5>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*$*$*$*
-keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter {
<init>(...);
<fields>;
}

-keepclassmembers class kotlin.Metadata {
public <methods>;
}
4 changes: 2 additions & 2 deletions paystack/src/main/java/co/paystack/android/Transaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ public String getReference() {
return reference;
}

void setReference(String reference) {
public void setReference(String reference) {
this.reference = reference;
}

void setId(String id) {
public void setId(String id) {
this.id = id;
}

Expand Down
30 changes: 17 additions & 13 deletions paystack/src/main/java/co/paystack/android/TransactionManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import android.provider.Settings;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

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

@Override
public void onError(@NotNull Throwable e) {
public void onError(@NotNull Throwable e, @Nullable String reference) {
Log.e(LOG_TAG, e.getMessage(), e);
notifyProcessingError(e);
Transaction transaction = new Transaction();
transaction.setReference(reference);
notifyProcessingError(e, transaction);
}
};

Expand Down Expand Up @@ -131,6 +134,7 @@ public void onSuccess(TransactionInitResponse data) {
transactionId,
card.getLast4digits(),
deviceId,
data.getReference(),
null
);
processCharge(params);
Expand All @@ -153,18 +157,17 @@ public void onError(@NotNull Throwable exception) {

private void processChargeResponse(ChargeParams chargeParams, ChargeResponse chargeResponse) {
if (chargeResponse == null) {
notifyProcessingError(new ChargeException("Unknown server response"));
notifyProcessingError(new ChargeException("Unknown server response"), chargeParams.getTransaction());
return;
}

Transaction transaction = new Transaction();
transaction.setId(chargeResponse.getTransactionId());
transaction.setReference(chargeResponse.getReference());

String status = chargeResponse.getStatus();
if (status != null) {
if (status.equalsIgnoreCase("1") || status.equalsIgnoreCase("success")) {
setProcessingOff();
Transaction transaction = new Transaction();
transaction.setId(chargeResponse.getTransactionId());
transaction.setReference(chargeResponse.getReference());
transactionCallback.onSuccess(transaction);
return;
}
Expand All @@ -181,12 +184,12 @@ private void processChargeResponse(ChargeParams chargeParams, ChargeResponse cha
}

if (chargeResponse.getAuth() != null && !chargeResponse.getAuth().equalsIgnoreCase("none")) {
authenticateTransaction(chargeParams, chargeResponse, transaction);
authenticateTransaction(chargeParams, chargeResponse, chargeParams.getTransaction());
return;
}

setProcessingOff();
notifyProcessingError(new ChargeException(chargeResponse.getMessage()));
notifyProcessingError(new ChargeException(chargeResponse.getMessage()), chargeParams.getTransaction());
}

private void authenticateTransaction(ChargeParams chargeParams, ChargeResponse chargeResponse, Transaction transaction) {
Expand Down Expand Up @@ -225,7 +228,7 @@ private void validateOtp(String token, ChargeParams chargeParams) {
paystackRepository.validateTransaction(chargeParams, token, cardProcessCallback);
} catch (Exception ce) {
Log.e(LOG_TAG, ce.getMessage(), ce);
notifyProcessingError(ce);
notifyProcessingError(ce, chargeParams.getTransaction());
}
}

Expand All @@ -235,7 +238,7 @@ private void chargeWithAvs(Address address, ChargeParams chargeParams) {
paystackRepository.validateAddress(chargeParams, address, cardProcessCallback);
} catch (Exception e) {
Log.e(LOG_TAG, e.getMessage(), e);
notifyProcessingError(e);
notifyProcessingError(e, chargeParams.getTransaction());
}
}

Expand All @@ -251,7 +254,7 @@ public void onTick(long millisUntilFinished) {
}.start();
} catch (Exception ce) {
Log.e(LOG_TAG, ce.getMessage(), ce);
notifyProcessingError(ce);
notifyProcessingError(ce, chargeParams.getTransaction());
}
}

Expand Down Expand Up @@ -460,7 +463,8 @@ protected void onPostExecute(Address address) {
if (address != null) {
chargeWithAvs(address, chargeParams);
} else {
notifyProcessingError(new Exception("No address provided"));

notifyProcessingError(new Exception("No address provided"), chargeParams.getTransaction());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import co.paystack.android.api.request.ChargeParams
interface ChargeApiCallback {
fun onSuccess(params: ChargeParams, response: ChargeResponse)

fun onError(exception: Throwable)
fun onError(exception: Throwable, reference: String?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ internal class PaystackRepositoryImpl(private val apiService: PaystackApiService
override fun processCardCharge(chargeParams: ChargeParams, callback: ChargeApiCallback) {
makeApiRequest(
onSuccess = { data -> callback.onSuccess(chargeParams, data) },
onError = { throwable -> callback.onError(throwable) },
onError = { throwable -> callback.onError(throwable, chargeParams.reference) },
apiCall = { apiService.chargeCard(chargeParams.toRequestMap()) }
)
}
Expand All @@ -54,15 +54,15 @@ internal class PaystackRepositoryImpl(private val apiService: PaystackApiService
makeApiRequest(
apiCall = { apiService.validateOtp(requestBody) },
onSuccess = { data -> callback.onSuccess(chargeParams, data) },
onError = { throwable -> callback.onError(throwable) },
onError = { throwable -> callback.onError(throwable, chargeParams.reference) },
)
}

override fun requeryTransaction(chargeParams: ChargeParams, callback: ChargeApiCallback) {
makeApiRequest(
apiCall = { apiService.requeryTransaction(chargeParams.transactionId) },
onSuccess = { data -> callback.onSuccess(chargeParams, data) },
onError = { throwable -> callback.onError(throwable) },
onError = { throwable -> callback.onError(throwable, chargeParams.reference) },
)
}

Expand All @@ -73,7 +73,7 @@ internal class PaystackRepositoryImpl(private val apiService: PaystackApiService
makeApiRequest(
apiCall = { apiService.validateAddress(requestBody) },
onSuccess = { data -> callback.onSuccess(chargeParams, data) },
onError = { throwable -> callback.onError(throwable) },
onError = { throwable -> callback.onError(throwable, chargeParams.reference) },
)
}

Expand Down
30 changes: 10 additions & 20 deletions paystack/src/main/java/co/paystack/android/api/di/ApiComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,36 @@ import co.paystack.android.api.service.ApiService
import co.paystack.android.api.service.PaystackApiService
import co.paystack.android.api.service.converter.WrappedResponseConverter
import co.paystack.android.api.utils.TLSSocketFactory
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.squareup.moshi.Moshi
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit

internal fun apiComponent(): ApiComponent = ApiModule

internal interface ApiComponent {
val gson: Gson
val tlsV1point2factory: TLSSocketFactory
val okHttpClient: OkHttpClient
val legacyApiService: ApiService
val paystackApiService: PaystackApiService
val paystackRepository: PaystackRepository
}

internal object ApiModule : ApiComponent {
const val LEGACY_API_URL = "https://standard.paystack.co/"
const val PAYSTACK_API_URL = "https://api.paystack.co/"

override val gson = GsonBuilder()
.setDateFormat("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'")
.create()
private const val PAYSTACK_API_URL = "https://api.paystack.co/"

override val tlsV1point2factory = TLSSocketFactory()

override val okHttpClient = OkHttpClient.Builder()
override val okHttpClient: OkHttpClient = OkHttpClient.Builder()
.addInterceptor { chain ->
val original = chain.request()
// Add headers so we get Android version and Paystack Library version
val builder = original.newBuilder()
.header("User-Agent", "Android_" + Build.VERSION.SDK_INT + "_Paystack_" + BuildConfig.VERSION_NAME)
.header(
"User-Agent",
"Android_" + Build.VERSION.SDK_INT + "_Paystack_" + BuildConfig.VERSION_NAME
)
.header("X-Paystack-Build", BuildConfig.VERSION_CODE.toString())
.header("Accept", "application/json")
.method(original.method(), original.body())
Expand All @@ -54,19 +50,13 @@ internal object ApiModule : ApiComponent {
.writeTimeout(5, TimeUnit.MINUTES)
.build()


override val legacyApiService: ApiService = Retrofit.Builder()
.baseUrl(LEGACY_API_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(ApiService::class.java)
private val moshi = Moshi.Builder().build()

override val paystackApiService: PaystackApiService = Retrofit.Builder()
.baseUrl(PAYSTACK_API_URL)
.client(okHttpClient)
.addConverterFactory(WrappedResponseConverter.Factory())
.addConverterFactory(GsonConverterFactory.create(gson))
.addConverterFactory(MoshiConverterFactory.create(moshi).asLenient())
.build()
.create(PaystackApiService::class.java)

Expand Down

This file was deleted.

Loading

0 comments on commit 25a2335

Please sign in to comment.