From 61343e57b919a27aa925e1be9b10962708ef898d Mon Sep 17 00:00:00 2001 From: Michael Obi <58430556+michael-paystack@users.noreply.github.com> Date: Fri, 3 Feb 2023 09:40:58 +0100 Subject: [PATCH 1/8] Migrate to checkout endpoints (#149) * Update Android Gradle Plugin to 7.1.3 - Older versions don't work on ARM computers * Add close button to 3DS popup * Update dependencies to fix Jacoco reports * Correct version name * Autoclose 3DS window when complete * TEMP: WEB MESSAGE EXPLORATION * Merge master into update-api-endpoints * Fix TransactionManagerTest.kt * Move pusher library version to ext * Change `NO_TRANSACTION` name to `EMPTY_TRANSACTION` --- build.gradle | 29 +- example/build.gradle | 2 +- .../co/paystack/example/MainActivity.java | 2 +- gradle.properties | 3 +- paystack/build.gradle | 4 +- .../main/java/co/paystack/android/AuthType.kt | 9 + .../java/co/paystack/android/Paystack.java | 2 +- .../paystack/android/PaystackSdkComponent.kt | 2 +- .../java/co/paystack/android/Transaction.java | 10 + .../paystack/android/TransactionManager.java | 350 +++++++++--------- .../paystack/android/api/di/ApiComponent.kt | 26 +- .../android/api/model/ChargeResponse.kt | 23 +- .../converter/WrappedResponseConverter.kt | 9 +- .../co/paystack/android/ui/AuthActivity.java | 43 ++- .../co/paystack/android/ui/AuthSingleton.java | 10 + .../co/paystack/android/utils/Crypto.java | 4 +- .../android/TransactionManagerTest.kt | 16 +- 17 files changed, 334 insertions(+), 210 deletions(-) create mode 100644 paystack/src/main/java/co/paystack/android/AuthType.kt diff --git a/build.gradle b/build.gradle index be99336..b4f854c 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,7 @@ buildscript { ext.versions = [ 'kotlin' : '1.7.22', 'coroutines' : '1.4.2', + 'pusher' : '2.2.1', 'robolectric' : '4.4', 'mockito' : '3.8.0', 'mockito_kotlin': '2.2.0', @@ -24,7 +25,7 @@ buildscript { } } plugins { - id "org.sonarqube" version "3.1.1" + id "org.sonarqube" version "3.5.0.2730" } sonarqube { @@ -67,4 +68,28 @@ ext { buildToolsVersion = "29.0.2" versionName = "3.2.0-alpha02" -} \ No newline at end of file +} + +Object getEnvOrDefault(String propertyName, Object defaultValue) { + // Check 'local.properties' first + String propertyValue + + def propFile = file('local.properties') + if (propFile.exists()) { + Properties localProps = new Properties() + localProps.load(propFile.newDataInputStream()) + propertyValue = localProps.getProperty(propertyName) + if (propertyValue != null) { + return propertyValue + } + } + + propertyValue = project.properties[propertyName] + + if (propertyValue == null) { + logger.error("Build property named {} not defined. Falling back to default value.", propertyName) + return defaultValue + } + + return propertyValue +} diff --git a/example/build.gradle b/example/build.gradle index ce93785..3c8ee60 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -42,7 +42,7 @@ android { } buildTypes { debug { - testCoverageEnabled true + testCoverageEnabled false } release { minifyEnabled false diff --git a/example/src/main/java/co/paystack/example/MainActivity.java b/example/src/main/java/co/paystack/example/MainActivity.java index 7b4d627..4e02cef 100644 --- a/example/src/main/java/co/paystack/example/MainActivity.java +++ b/example/src/main/java/co/paystack/example/MainActivity.java @@ -35,7 +35,7 @@ public class MainActivity extends AppCompatActivity { // Step 3. Login with your heroku credentials or create a free heroku account // Step 4. Provide your secret key and an email with which to start all test transactions // Step 5. Copy the url generated by heroku (format https://some-url.herokuapp.com) into the space below - String backend_url = "https://infinite-peak-60063.herokuapp.com"; + String backend_url = "https://charge-sample-service.onrender.com"; // Set this to a public key that matches the secret key you supplied while creating the heroku instance String paystack_public_key = "pk_test_9eb0263ed776c4c892e0281348aee4136cd0dd52"; diff --git a/gradle.properties b/gradle.properties index e104b22..69fbc7b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -36,4 +36,5 @@ POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt POM_LICENCE_DIST=repo POM_DEVELOPER_ID=paystack POM_DEVELOPER_NAME=Paystack -POM_DEVELOPER_EMAIL=developers@paystack.co \ No newline at end of file +POM_DEVELOPER_EMAIL=developers@paystack.co +org.gradle.unsafe.configuration-cache=true \ No newline at end of file diff --git a/paystack/build.gradle b/paystack/build.gradle index a202a60..a84717f 100644 --- a/paystack/build.gradle +++ b/paystack/build.gradle @@ -44,6 +44,7 @@ android { buildConfigField 'int', 'VERSION_CODE', "${rootProject.ext.versionCode}" buildConfigField 'String', 'VERSION_NAME', "\"${rootProject.ext.versionName}\"" + buildConfigField 'String', 'PUSHER_KEY', "\"${getEnvOrDefault("PUSHER_KEY", "")}\"" } buildTypes { debug { @@ -74,12 +75,13 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.coroutines" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutines" + implementation "com.pusher:pusher-java-client:$versions.pusher" + testImplementation "org.robolectric:robolectric:$versions.robolectric" testImplementation "org.mockito:mockito-core:$versions.mockito" testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$versions.mockito_kotlin" testImplementation "androidx.test.ext:junit-ktx:$versions.junit_ext" testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin" - } apply from: "https://raw.githubusercontent.com/PaystackHQ/publish-mavencentral/main/maven-publish.gradle" diff --git a/paystack/src/main/java/co/paystack/android/AuthType.kt b/paystack/src/main/java/co/paystack/android/AuthType.kt new file mode 100644 index 0000000..31bda25 --- /dev/null +++ b/paystack/src/main/java/co/paystack/android/AuthType.kt @@ -0,0 +1,9 @@ +package co.paystack.android + +object AuthType { + const val PIN = "pin" + const val OTP = "otp" + const val THREE_DS = "3DS" + const val PHONE = "phone" + const val ADDRESS_VERIFICATION = "avs" +} \ No newline at end of file diff --git a/paystack/src/main/java/co/paystack/android/Paystack.java b/paystack/src/main/java/co/paystack/android/Paystack.java index 1002b90..ad308f9 100644 --- a/paystack/src/main/java/co/paystack/android/Paystack.java +++ b/paystack/src/main/java/co/paystack/android/Paystack.java @@ -68,7 +68,7 @@ private void chargeCard(Activity activity, Charge charge, String publicKey, Tran .getTransactionManagerFactory() .create(); - transactionManager.chargeCard(activity, charge, transactionCallback); + transactionManager.chargeCard(activity, PaystackSdk.getPublicKey(), charge, transactionCallback); } catch (Exception ae) { assert transactionCallback != null; diff --git a/paystack/src/main/java/co/paystack/android/PaystackSdkComponent.kt b/paystack/src/main/java/co/paystack/android/PaystackSdkComponent.kt index f39225d..d797350 100644 --- a/paystack/src/main/java/co/paystack/android/PaystackSdkComponent.kt +++ b/paystack/src/main/java/co/paystack/android/PaystackSdkComponent.kt @@ -12,7 +12,7 @@ internal object PaystackSdkModule : PaystackSdkComponent { override val transactionManagerFactory: Factory = object : Factory { override fun create(): TransactionManager { - return TransactionManager(apiComponent().apiService) + return TransactionManager(apiComponent().paystackRepository) } } } \ No newline at end of file diff --git a/paystack/src/main/java/co/paystack/android/Transaction.java b/paystack/src/main/java/co/paystack/android/Transaction.java index 0e3a350..4b94bfc 100644 --- a/paystack/src/main/java/co/paystack/android/Transaction.java +++ b/paystack/src/main/java/co/paystack/android/Transaction.java @@ -6,6 +6,8 @@ public class Transaction { private String id; private String reference; + public static Transaction EMPTY_TRANSACTION = new Transaction(); + void loadFromResponse(TransactionApiResponse t) { if (t.hasValidReferenceAndTrans()) { this.reference = t.reference; @@ -21,6 +23,14 @@ public String getReference() { return reference; } + void setReference(String reference) { + this.reference = reference; + } + + void setId(String id) { + this.id = id; + } + boolean hasStartedOnServer() { return (reference != null) && (id != null); } diff --git a/paystack/src/main/java/co/paystack/android/TransactionManager.java b/paystack/src/main/java/co/paystack/android/TransactionManager.java index fb874de..52bbc65 100644 --- a/paystack/src/main/java/co/paystack/android/TransactionManager.java +++ b/paystack/src/main/java/co/paystack/android/TransactionManager.java @@ -7,18 +7,16 @@ import android.provider.Settings; import android.util.Log; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.util.HashMap; - -import co.paystack.android.api.model.TransactionApiResponse; -import co.paystack.android.api.request.ChargeRequestBody; -import co.paystack.android.api.request.ValidateRequestBody; -import co.paystack.android.api.service.ApiService; +import org.jetbrains.annotations.NotNull; + +import co.paystack.android.api.ApiCallback; +import co.paystack.android.api.ChargeApiCallback; +import co.paystack.android.api.PaystackRepository; +import co.paystack.android.api.model.ChargeResponse; +import co.paystack.android.api.model.TransactionInitResponse; +import co.paystack.android.api.request.ChargeParams; import co.paystack.android.exceptions.CardException; import co.paystack.android.exceptions.ChargeException; -import co.paystack.android.exceptions.ExpiredAccessCodeException; import co.paystack.android.exceptions.ProcessingException; import co.paystack.android.model.Card; import co.paystack.android.model.Charge; @@ -33,64 +31,46 @@ import co.paystack.android.ui.OtpSingleton; import co.paystack.android.ui.PinActivity; import co.paystack.android.ui.PinSingleton; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; +import co.paystack.android.utils.Crypto; +import co.paystack.android.utils.StringUtils; + +import static co.paystack.android.Transaction.EMPTY_TRANSACTION; class TransactionManager { private static final String LOG_TAG = TransactionManager.class.getSimpleName(); private static boolean PROCESSING = false; - - private Charge charge; private Activity activity; - private Transaction transaction; private Paystack.TransactionCallback transactionCallback; - private final CardSingleton cns = CardSingleton.getInstance(); private final PinSingleton psi = PinSingleton.getInstance(); private final OtpSingleton osi = OtpSingleton.getInstance(); private final AuthSingleton asi = AuthSingleton.getInstance(); private final AddressHolder addressHolder = AddressHolder.getInstance(); + private final PaystackRepository paystackRepository; - private ChargeRequestBody chargeRequestBody; - private ValidateRequestBody validateRequestBody; - private ApiService apiService; - - private int invalidDataSentRetries = 0; - - private final Callback serverCallback = new Callback() { + ChargeApiCallback cardProcessCallback = new ChargeApiCallback() { @Override - public void onResponse(Call call, Response response) { - handleApiResponse(response.body()); + public void onSuccess(@NotNull ChargeParams params, @NotNull ChargeResponse data) { + processChargeResponse(params, data); transactionCallback.showLoading(false); } @Override - public void onFailure(Call call, Throwable t) { - Log.e(LOG_TAG, t.getMessage()); - notifyProcessingError(t); + public void onError(@NotNull Throwable e) { + Log.e(LOG_TAG, e.getMessage(), e); + notifyProcessingError(e); } }; - TransactionManager(ApiService apiService) { - this.apiService = apiService; + TransactionManager(PaystackRepository paystackRepository) { + this.paystackRepository = paystackRepository; } - private void initiate() throws ProcessingException { - if (TransactionManager.PROCESSING) { - throw new ProcessingException(); - } - setProcessingOn(); - String deviceId = "androidsdk_" + Settings.Secure.getString(activity.getContentResolver(), Settings.Secure.ANDROID_ID); - chargeRequestBody = new ChargeRequestBody(charge, deviceId); - validateRequestBody = new ValidateRequestBody(deviceId); - } - - void chargeCard(Activity activity, Charge charge, Paystack.TransactionCallback transactionCallback) { + void chargeCard(Activity activity, String publicKey, Charge charge, Paystack.TransactionCallback transactionCallback) { if (BuildConfig.DEBUG && (activity == null)) { throw new AssertionError("activity must not be null"); } @@ -105,180 +85,171 @@ void chargeCard(Activity activity, Charge charge, Paystack.TransactionCallback t } this.activity = activity; - this.charge = charge; this.transactionCallback = transactionCallback; - this.transaction = new Transaction(); - charge(); + validateCardThenInitTransaction(publicKey, charge); } - private void charge() { + private void validateCardThenInitTransaction(String publicKey, Charge charge) { try { if (charge.getCard() == null || !charge.getCard().isValid()) { final CardSingleton si = CardSingleton.getInstance(); synchronized (si) { si.setCard(charge.getCard()); } - new CardAsyncTask().execute(); + new CardAsyncTask(publicKey, charge).execute(); } else { - initiate(); - sendChargeToServer(); + if (TransactionManager.PROCESSING) { + throw new ProcessingException(); + } + + setProcessingOn(); + + String deviceId = "androidsdk_" + Settings.Secure.getString(activity.getContentResolver(), Settings.Secure.ANDROID_ID); + initiateTransaction(publicKey, charge, deviceId); } } catch (Exception ce) { Log.e(LOG_TAG, ce.getMessage(), ce); if (!(ce instanceof ProcessingException)) { setProcessingOff(); } - transactionCallback.onError(ce, transaction); + transactionCallback.onError(ce, EMPTY_TRANSACTION); } } - private void sendChargeToServer() { - try { - initiateChargeOnServer(); - } catch (Exception ce) { - Log.e(LOG_TAG, ce.getMessage(), ce); - notifyProcessingError(ce); - } + private void initiateTransaction(String publicKey, Charge charge, String deviceId) { + paystackRepository.initializeTransaction(publicKey, charge, deviceId, new ApiCallback() { + @Override + public void onSuccess(TransactionInitResponse data) { + Card card = charge.getCard(); + String transactionId = data.getTransactionId(); + ChargeParams params = new ChargeParams( + Crypto.encrypt(StringUtils.concatenateCardFields(card)), + transactionId, + card.getLast4digits(), + deviceId, + null + ); + paystackRepository.processCardCharge(params, cardProcessCallback); + } + @Override + public void onError(@NotNull Throwable exception) { + Log.e(LOG_TAG, exception.getMessage(), exception); + notifyProcessingError(exception); + } + }); } - private void validate() { - try { - validateChargeOnServer(); - } catch (Exception ce) { - Log.e(LOG_TAG, ce.getMessage(), ce); - notifyProcessingError(ce); + private void processChargeResponse(ChargeParams chargeParams, ChargeResponse chargeResponse) { + if (chargeResponse == null) { + notifyProcessingError(new ChargeException("Unknown server response")); + return; } - } - - private void reQuery() { - try { - reQueryChargeOnServer(); - } catch (Exception ce) { - Log.e(LOG_TAG, ce.getMessage(), ce); - notifyProcessingError(ce); - } + 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(); + transactionCallback.onSuccess(transaction); + return; + } + if (status.equalsIgnoreCase("pending")) { + reQueryChargeOnServer(chargeParams); + return; + } - private void chargeWithAvs(Address address) { - HashMap fields = address.toHashMap(); - fields.put("trans", transaction.getId()); - try { - this.transactionCallback.showLoading(true); - Call call = apiService.submitCardAddress(fields); - call.enqueue(serverCallback); - } catch (Exception e) { - Log.e(LOG_TAG, e.getMessage(), e); - notifyProcessingError(e); + if (status.equalsIgnoreCase("requery")) { + reQueryChargeOnServer(chargeParams); + return; + } } - } - private void validateChargeOnServer() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException { - this.transactionCallback.showLoading(true); - HashMap params = validateRequestBody.getParamsHashMap(); - Call call = apiService.validateCharge(params); - call.enqueue(serverCallback); - } - - private void reQueryChargeOnServer() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException { - this.transactionCallback.showLoading(true); - Call call = apiService.requeryTransaction(transaction.getId()); - call.enqueue(serverCallback); - } - - private void initiateChargeOnServer() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException { - this.transactionCallback.showLoading(true); - Call call = apiService.charge(chargeRequestBody.getParamsHashMap()); - call.enqueue(serverCallback); + if (chargeResponse.getAuth() != null && !chargeResponse.getAuth().equalsIgnoreCase("none")) { + authenticateTransaction(chargeParams, chargeResponse, transaction); + return; + } + setProcessingOff(); + notifyProcessingError(new ChargeException(chargeResponse.getMessage())); } - private void handleApiResponse(TransactionApiResponse transactionApiResponse) { - if (transactionApiResponse == null) { - transactionApiResponse = TransactionApiResponse.unknownServerResponse(); - } + private void authenticateTransaction(ChargeParams chargeParams, ChargeResponse chargeResponse, Transaction transaction) { + String authType = chargeResponse.getAuth(); + assert authType != null; - // The AVS charge endpoint sends an "errors" object when address verification fails - if (transactionApiResponse.hasErrors) { - notifyProcessingError(new ChargeException(transactionApiResponse.message)); - return; + String authMessage = "Authentication required"; + if (chargeResponse.getOtpMessage() != null) { + authMessage = chargeResponse.getOtpMessage(); + } else if (chargeResponse.getMessage() != null) { + authMessage = chargeResponse.getMessage(); } - transaction.loadFromResponse(transactionApiResponse); - if (transactionApiResponse.status.equalsIgnoreCase("1") || transactionApiResponse.status.equalsIgnoreCase("success")) { + if (authType.equalsIgnoreCase(AuthType.ADDRESS_VERIFICATION)) { + new AddressVerificationAsyncTask(chargeParams).execute(chargeResponse.getCountryCode()); + } else if (authType.equalsIgnoreCase(AuthType.PIN)) { + new PinAsyncTask(chargeParams).execute(); + } else if (authType.equalsIgnoreCase(AuthType.OTP) || authType.equalsIgnoreCase(AuthType.PHONE)) { + transactionCallback.beforeValidate(transaction); + osi.setOtpMessage(authMessage); + new OtpAsyncTask(chargeParams).execute(); + } else if (authType.equalsIgnoreCase(AuthType.THREE_DS)) { + transactionCallback.beforeValidate(transaction); + asi.setTransactionId(chargeParams.getTransactionId()); + asi.setUrl(authMessage); + new AuthAsyncTask(chargeParams).execute(); + } else { setProcessingOff(); - transactionCallback.onSuccess(transaction); - return; + notifyProcessingError(new RuntimeException("Unknown authentication method required. Please contact Paystack"), transaction); } + } - if (transactionApiResponse.status.equalsIgnoreCase("2") && transactionApiResponse.hasValidAuth() && transactionApiResponse.auth.equalsIgnoreCase("avs")) { - new AddressVerificationAsyncTask().execute(transactionApiResponse.avsCountryCode); - return; + private void validateOtp(String token, ChargeParams chargeParams) { + try { + paystackRepository.validateTransaction(chargeParams, token, cardProcessCallback); + } catch (Exception ce) { + Log.e(LOG_TAG, ce.getMessage(), ce); + notifyProcessingError(ce); } + } - if (transactionApiResponse.status.equalsIgnoreCase("2") || (transactionApiResponse.hasValidAuth() && (transactionApiResponse.auth.equalsIgnoreCase("pin")))) { - new PinAsyncTask().execute(); - return; + private void chargeWithAvs(Address address, ChargeParams chargeParams) { + try { + paystackRepository.validateAddress(chargeParams, address, cardProcessCallback); + } catch (Exception e) { + Log.e(LOG_TAG, e.getMessage(), e); + notifyProcessingError(e); } + } - if (transactionApiResponse.status.equalsIgnoreCase("3") && transactionApiResponse.hasValidReferenceAndTrans()) { - transactionCallback.beforeValidate(transaction); - validateRequestBody.setTrans(transactionApiResponse.trans); - osi.setOtpMessage(transactionApiResponse.message); - new OtpAsyncTask().execute(); - return; - } + private void reQueryChargeOnServer(ChargeParams chargeParams) { + try { + new CountDownTimer(5000, 5000) { + public void onFinish() { + paystackRepository.requeryTransaction(chargeParams, cardProcessCallback); + } - if (transaction.hasStartedOnServer()) { - if (transactionApiResponse.status.equalsIgnoreCase("requery")) { - transactionCallback.beforeValidate(transaction); - new CountDownTimer(5000, 5000) { - public void onFinish() { - reQuery(); - } - - public void onTick(long millisUntilFinished) { - } - }.start(); - return; - } - if (transactionApiResponse.hasValidAuth() && (transactionApiResponse.auth.equalsIgnoreCase("3DS")) && transactionApiResponse.hasValidUrl()) { - transactionCallback.beforeValidate(transaction); - asi.setUrl(transactionApiResponse.otpmessage); - new AuthAsyncTask().execute(); - return; - } - if (transactionApiResponse.hasValidAuth() && (transactionApiResponse.auth.equalsIgnoreCase("otp") || transactionApiResponse.auth.equalsIgnoreCase("phone")) && transactionApiResponse.hasValidOtpMessage()) { - transactionCallback.beforeValidate(transaction); - validateRequestBody.setTrans(transaction.getId()); - osi.setOtpMessage(transactionApiResponse.otpmessage); - new OtpAsyncTask().execute(); - return; - } + public void onTick(long millisUntilFinished) { + } + }.start(); + } catch (Exception ce) { + Log.e(LOG_TAG, ce.getMessage(), ce); + notifyProcessingError(ce); } + } - if (transactionApiResponse.status.equalsIgnoreCase("0") || transactionApiResponse.status.equalsIgnoreCase("error")) { - //throw an error - if (transactionApiResponse.message.equalsIgnoreCase("Invalid Data Sent") && invalidDataSentRetries < 3) { - invalidDataSentRetries++; - TransactionManager.this.sendChargeToServer(); - return; - } - if (transactionApiResponse.message.equalsIgnoreCase("Access code has expired")) { - notifyProcessingError(new ExpiredAccessCodeException(transactionApiResponse.message)); - return; - } - notifyProcessingError(new ChargeException(transactionApiResponse.message)); - return; - } + private void notifyProcessingError(Throwable t) { + setProcessingOff(); + transactionCallback.showLoading(false); - notifyProcessingError(new RuntimeException("Unknown server response")); + transactionCallback.onError(t, EMPTY_TRANSACTION); } - private void notifyProcessingError(Throwable t) { + private void notifyProcessingError(Throwable t, Transaction transaction) { setProcessingOff(); transactionCallback.showLoading(false); transactionCallback.onError(t, transaction); @@ -294,6 +265,14 @@ private void setProcessingOn() { private class CardAsyncTask extends AsyncTask { + private final String publicKey; + private final Charge charge; + + CardAsyncTask(String publicKey, Charge charge) { + this.publicKey = publicKey; + this.charge = charge; + } + @Override protected Card doInBackground(Void... params) { Intent i = new Intent(activity, CardActivity.class); @@ -316,12 +295,17 @@ protected void onPostExecute(Card cns) { notifyProcessingError(new CardException("Invalid card parameters")); } else { charge.setCard(cns); - TransactionManager.this.charge(); + validateCardThenInitTransaction(publicKey, charge); } } } private class PinAsyncTask extends AsyncTask { + private final ChargeParams chargeParams; + + PinAsyncTask(ChargeParams chargeParams) { + this.chargeParams = chargeParams; + } @Override protected String doInBackground(Void... params) { @@ -344,8 +328,7 @@ protected String doInBackground(Void... params) { protected void onPostExecute(String pin) { super.onPostExecute(pin); if (pin != null && (4 == pin.length())) { - chargeRequestBody.addPin(pin); - TransactionManager.this.sendChargeToServer(); + paystackRepository.processCardCharge(chargeParams.addPin(Crypto.encrypt(pin)), cardProcessCallback); } else { notifyProcessingError(new Exception("PIN must be exactly 4 digits")); } @@ -353,6 +336,11 @@ protected void onPostExecute(String pin) { } private class OtpAsyncTask extends AsyncTask { + private final ChargeParams chargeParams; + + public OtpAsyncTask(ChargeParams chargeParams) { + this.chargeParams = chargeParams; + } @Override protected String doInBackground(Void... params) { @@ -374,8 +362,7 @@ protected String doInBackground(Void... params) { protected void onPostExecute(String otp) { super.onPostExecute(otp); if (otp != null) { - validateRequestBody.setToken(otp); - TransactionManager.this.validate(); + validateOtp(otp, chargeParams); } else { notifyProcessingError(new Exception("You did not provide an OTP")); } @@ -384,6 +371,11 @@ protected void onPostExecute(String otp) { private class AuthAsyncTask extends AsyncTask { + private final ChargeParams chargeParams; + + AuthAsyncTask(ChargeParams chargeParams) { + this.chargeParams = chargeParams; + } @Override protected String doInBackground(Void... params) { @@ -404,13 +396,17 @@ protected String doInBackground(Void... params) { @Override protected void onPostExecute(String responseJson) { super.onPostExecute(responseJson); - TransactionApiResponse transactionApiResponse = TransactionApiResponse.fromJsonString(responseJson); - handleApiResponse(transactionApiResponse); + ChargeResponse chargeResponse = ChargeResponse.Companion.fromJsonString(responseJson); + processChargeResponse(chargeParams, chargeResponse); } } private class AddressVerificationAsyncTask extends AsyncTask { + private final ChargeParams chargeParams; + public AddressVerificationAsyncTask(ChargeParams chargeParams) { + this.chargeParams = chargeParams; + } @Override protected Address doInBackground(String... params) { @@ -433,9 +429,7 @@ protected void onPostExecute(Address address) { super.onPostExecute(address); if (address != null) { - Log.e("AVS_ADDRESS", address.toString()); - chargeWithAvs(address); - + chargeWithAvs(address, chargeParams); } else { notifyProcessingError(new Exception("No address provided")); } diff --git a/paystack/src/main/java/co/paystack/android/api/di/ApiComponent.kt b/paystack/src/main/java/co/paystack/android/api/di/ApiComponent.kt index 98e7988..4ceeef9 100644 --- a/paystack/src/main/java/co/paystack/android/api/di/ApiComponent.kt +++ b/paystack/src/main/java/co/paystack/android/api/di/ApiComponent.kt @@ -2,7 +2,11 @@ package co.paystack.android.api.di import android.os.Build import co.paystack.android.BuildConfig +import co.paystack.android.api.PaystackRepository +import co.paystack.android.api.PaystackRepositoryImpl 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 @@ -17,11 +21,14 @@ internal interface ApiComponent { val gson: Gson val tlsV1point2factory: TLSSocketFactory val okHttpClient: OkHttpClient - val apiService: ApiService + val legacyApiService: ApiService + val paystackApiService: PaystackApiService + val paystackRepository: PaystackRepository } internal object ApiModule : ApiComponent { - const val API_URL = "https://standard.paystack.co/" + 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'") @@ -47,11 +54,22 @@ internal object ApiModule : ApiComponent { .writeTimeout(5, TimeUnit.MINUTES) .build() - override val apiService = Retrofit.Builder() - .baseUrl(API_URL) + + override val legacyApiService: ApiService = Retrofit.Builder() + .baseUrl(LEGACY_API_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .build() .create(ApiService::class.java) + + override val paystackApiService: PaystackApiService = Retrofit.Builder() + .baseUrl(PAYSTACK_API_URL) + .client(okHttpClient) + .addConverterFactory(WrappedResponseConverter.Factory()) + .addConverterFactory(GsonConverterFactory.create(gson)) + .build() + .create(PaystackApiService::class.java) + + override val paystackRepository: PaystackRepository = PaystackRepositoryImpl(paystackApiService) } diff --git a/paystack/src/main/java/co/paystack/android/api/model/ChargeResponse.kt b/paystack/src/main/java/co/paystack/android/api/model/ChargeResponse.kt index 179ff29..48eba6c 100644 --- a/paystack/src/main/java/co/paystack/android/api/model/ChargeResponse.kt +++ b/paystack/src/main/java/co/paystack/android/api/model/ChargeResponse.kt @@ -2,6 +2,7 @@ package co.paystack.android.api.model import androidx.annotation.Keep +import com.google.gson.Gson import com.google.gson.annotations.SerializedName @Keep @@ -22,6 +23,22 @@ data class ChargeResponse( val auth: String? = null, @SerializedName("countryCode") - val countryCode: String? = null - -) \ No newline at end of file + val countryCode: String? = null, + + ) { + + companion object { + fun fromJsonString(jsonString: String?): ChargeResponse { + return try { + Gson().fromJson(jsonString, ChargeResponse::class.java) + } catch (e: Exception) { + ChargeResponse( + status = "0", + transactionId = "", + reference = "", + message = e.message ?: "An error occurred while reading Auth data" + ) + } + } + } +} \ No newline at end of file diff --git a/paystack/src/main/java/co/paystack/android/api/service/converter/WrappedResponseConverter.kt b/paystack/src/main/java/co/paystack/android/api/service/converter/WrappedResponseConverter.kt index 0a73699..44053b1 100644 --- a/paystack/src/main/java/co/paystack/android/api/service/converter/WrappedResponseConverter.kt +++ b/paystack/src/main/java/co/paystack/android/api/service/converter/WrappedResponseConverter.kt @@ -5,6 +5,7 @@ import retrofit2.Converter import retrofit2.Retrofit import java.lang.reflect.ParameterizedType import java.lang.reflect.Type +import kotlin.annotation.AnnotationTarget.FUNCTION class WrappedResponseConverter( private val delegate: Converter> @@ -21,6 +22,12 @@ class WrappedResponseConverter( annotations: Array, retrofit: Retrofit ): Converter? { + + // Should not use this converter if function is annotated with [NoWrap] + if (annotations.any { it is NoWrap }) { + return null + } + val wrappedType: Type = object : ParameterizedType { override fun getRawType(): Type { return WrappedResponse::class.java @@ -46,4 +53,4 @@ class WrappedResponseConverter( val message: String, val status: Boolean ) -} \ No newline at end of file +} diff --git a/paystack/src/main/java/co/paystack/android/ui/AuthActivity.java b/paystack/src/main/java/co/paystack/android/ui/AuthActivity.java index 5306e45..9d40c69 100644 --- a/paystack/src/main/java/co/paystack/android/ui/AuthActivity.java +++ b/paystack/src/main/java/co/paystack/android/ui/AuthActivity.java @@ -3,13 +3,17 @@ import android.app.Activity; import android.os.Build; import android.os.Bundle; -import android.view.View; +import android.util.Log; import android.view.WindowManager; import android.webkit.JavascriptInterface; import android.webkit.WebView; import android.webkit.WebViewClient; -import android.widget.ImageView; +import com.pusher.client.Pusher; +import com.pusher.client.PusherOptions; +import com.pusher.client.channel.Channel; + +import co.paystack.android.BuildConfig; import co.paystack.android.R; import co.paystack.android.api.di.ApiModule; @@ -18,6 +22,11 @@ public class AuthActivity extends Activity { final AuthSingleton si = AuthSingleton.getInstance(); private WebView webView; private String responseJson; + private static final String TAG = "AuthActivity"; + + private Pusher pusher; + private Channel channel; + private String channelName = "3DS_" + si.getTransactionId(); @Override protected void onCreate(Bundle savedInstanceState) { @@ -27,7 +36,27 @@ protected void onCreate(Bundle savedInstanceState) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); webView = findViewById(R.id.webView); - setup(); + setupWebview(); + + listenForChargeEvents(); + } + + private void listenForChargeEvents() { + PusherOptions options = new PusherOptions(); + options.setCluster("eu"); + + pusher = new Pusher(BuildConfig.PUSHER_KEY, options); + pusher.connect(); + + channel = pusher.subscribe(channelName); + + channel.bind("pusher:subscription_succeeded", event -> Log.d(TAG, event.toString())); + channel.bind("pusher:subscription_error", event -> Log.e(TAG, "Pusher subscription error: " + event.toString())); + channel.bind("response", event -> { + Log.i(TAG, event.toString()); + responseJson = event.getData(); + handleResponse(); + }); } public void handleResponse() { @@ -41,7 +70,7 @@ public void handleResponse() { finish(); } - protected void setup() { + protected void setupWebview() { setContentView(R.layout.co_paystack_android____activity_auth); findViewById(R.id.iv_close).setOnClickListener(v -> finish()); @@ -79,7 +108,6 @@ private AuthResponseJI getJI() { } else { return new AuthResponseLegacyJI(); } - } } @@ -90,7 +118,7 @@ private AuthResponseJI getJI() { webView.setWebViewClient(new WebViewClient() { @Override public void onPageFinished(WebView view, String url) { - if (url.contains(ApiModule.API_URL + "charge/three_d_response/")) { + if (url.contains(ApiModule.LEGACY_API_URL + "charge/three_d_response/")) { view.loadUrl("javascript:window.INTERFACE.processContent(document.getElementById('return').innerText);"); } } @@ -105,12 +133,11 @@ public void onLoadResource(WebView view, String url) { } public void onDestroy() { + pusher.unsubscribe(channelName); super.onDestroy(); if (webView != null) { webView.stopLoading(); webView.removeJavascriptInterface("INTERFACE"); } - handleResponse(); } - } diff --git a/paystack/src/main/java/co/paystack/android/ui/AuthSingleton.java b/paystack/src/main/java/co/paystack/android/ui/AuthSingleton.java index 3e849fe..493b664 100644 --- a/paystack/src/main/java/co/paystack/android/ui/AuthSingleton.java +++ b/paystack/src/main/java/co/paystack/android/ui/AuthSingleton.java @@ -5,6 +5,8 @@ public class AuthSingleton { private String responseJson = "{\"status\":\"requery\",\"message\":\"Reaffirm Transaction Status on Server\"}"; private String url = ""; + private String transactionId = ""; + private AuthSingleton() { } @@ -29,4 +31,12 @@ AuthSingleton setResponseJson(String responseJson) { this.responseJson = responseJson; return this; } + + public String getTransactionId() { + return transactionId; + } + + public void setTransactionId(String transactionId) { + this.transactionId = transactionId; + } } \ No newline at end of file diff --git a/paystack/src/main/java/co/paystack/android/utils/Crypto.java b/paystack/src/main/java/co/paystack/android/utils/Crypto.java index bf70d55..e830bf1 100644 --- a/paystack/src/main/java/co/paystack/android/utils/Crypto.java +++ b/paystack/src/main/java/co/paystack/android/utils/Crypto.java @@ -20,9 +20,7 @@ * @author {androidsupport@paystack.co} on 8/10/15. */ public class Crypto { - - private static final String PAYSTACK_RSA_PUBLIC_KEY = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANIsL+RHqfkBiKGn/D1y1QnNrMkKzxWP" + - "2wkeSokw2OJrCI+d6YGJPrHHx+nmb/Qn885/R01Gw6d7M824qofmCvkCAwEAAQ=="; + private static final String PAYSTACK_RSA_PUBLIC_KEY = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALhZs/7hP0g0+hrqTq0hFyGVxgco0NMxZD8nPS6ihxap0yNFjzdyUuZED6P4/aK9Ezl5ajEI9pcx5/1BrEE+F3kCAwEAAQ=="; private static String ALGORITHM = "RSA"; private static String CIPHER = "RSA/ECB/PKCS1Padding"; diff --git a/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt b/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt index 5a49809..b7294b3 100644 --- a/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt +++ b/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt @@ -1,7 +1,7 @@ package co.paystack.android import androidx.test.core.app.ActivityScenario -import co.paystack.android.api.service.ApiService +import co.paystack.android.api.PaystackRepository import co.paystack.android.model.Card import co.paystack.android.model.Charge import com.nhaarman.mockitokotlin2.isA @@ -17,7 +17,7 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class TransactionManagerTest { @Mock - lateinit var apiService: ApiService + lateinit var paystackRepository: PaystackRepository @Before fun setup() { @@ -29,11 +29,17 @@ class TransactionManagerTest { ActivityScenario.launch(TestActivity::class.java).use { scenario -> scenario.onActivity { activity -> // Initialize TransactionManager - val transactionManager = TransactionManager(apiService) + val transactionManager = TransactionManager(paystackRepository) val charge = Charge().setCard(TEST_CARD) - transactionManager.chargeCard(activity, charge, mock(Paystack.TransactionCallback::class.java)) - verify(apiService).charge(isA()) + val publicKey = "public_key" + transactionManager.chargeCard( + activity, + publicKey, + charge, + mock(Paystack.TransactionCallback::class.java) + ) + verify(paystackRepository).initializeTransaction(isA(), isA(), isA(), isA()) } } } From 241f0e81f32bd3269d3beb389e09d12d4a45f18d Mon Sep 17 00:00:00 2001 From: Michael Obi <58430556+michael-paystack@users.noreply.github.com> Date: Wed, 8 Feb 2023 10:50:09 +0100 Subject: [PATCH 2/8] 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) --- build.gradle | 4 +- gradle.properties | 4 +- .../paystack/android/TransactionManager.java | 20 +++-- .../android/api/PaystackRepository.kt | 5 ++ .../android/api/PaystackRepositoryImpl.kt | 14 ++++ .../api/request/TransactionInitRequestBody.kt | 20 ++++- .../android/api/service/PaystackApiService.kt | 3 + .../co/paystack/android/ui/AuthActivity.java | 11 ++- .../android/TransactionManagerTest.kt | 79 +++++++++++++++++++ .../android/api/PaystackRepositoryImplTest.kt | 45 +++++++++++ 10 files changed, 192 insertions(+), 13 deletions(-) diff --git a/build.gradle b/build.gradle index b4f854c..f5471a1 100644 --- a/build.gradle +++ b/build.gradle @@ -64,10 +64,10 @@ ext { compileSdkVersion = 29 minSdkVersion = 16 targetSdkVersion = 29 - versionCode = 19 + versionCode = 21 buildToolsVersion = "29.0.2" - versionName = "3.2.0-alpha02" + versionName = "3.3.0-alpha02" } Object getEnvOrDefault(String propertyName, Object defaultValue) { diff --git a/gradle.properties b/gradle.properties index 69fbc7b..a614749 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,7 +25,7 @@ android.useAndroidX=true org.gradle.daemon=true org.gradle.jvmargs=-Xmx2560m GROUP=co.paystack.android -VERSION_NAME=3.2.0-alpha02 +VERSION_NAME=3.3.0-alpha01 POM_DESCRIPTION=Android SDK for Paystack POM_URL=https://github.com/PaystackHQ/paystack-android POM_SCM_URL=https://github.com/PaystackHQ/paystack-android @@ -37,4 +37,4 @@ POM_LICENCE_DIST=repo POM_DEVELOPER_ID=paystack POM_DEVELOPER_NAME=Paystack POM_DEVELOPER_EMAIL=developers@paystack.co -org.gradle.unsafe.configuration-cache=true \ No newline at end of file +org.gradle.unsafe.configuration-cache=false \ No newline at end of file diff --git a/paystack/src/main/java/co/paystack/android/TransactionManager.java b/paystack/src/main/java/co/paystack/android/TransactionManager.java index 52bbc65..6c9e308 100644 --- a/paystack/src/main/java/co/paystack/android/TransactionManager.java +++ b/paystack/src/main/java/co/paystack/android/TransactionManager.java @@ -1,5 +1,7 @@ package co.paystack.android; +import static co.paystack.android.Transaction.EMPTY_TRANSACTION; + import android.app.Activity; import android.content.Intent; import android.os.AsyncTask; @@ -7,6 +9,8 @@ import android.provider.Settings; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import org.jetbrains.annotations.NotNull; import co.paystack.android.api.ApiCallback; @@ -34,8 +38,6 @@ import co.paystack.android.utils.Crypto; import co.paystack.android.utils.StringUtils; -import static co.paystack.android.Transaction.EMPTY_TRANSACTION; - class TransactionManager { private static final String LOG_TAG = TransactionManager.class.getSimpleName(); @@ -117,8 +119,9 @@ private void validateCardThenInitTransaction(String publicKey, Charge charge) { } } - private void initiateTransaction(String publicKey, Charge charge, String deviceId) { - paystackRepository.initializeTransaction(publicKey, charge, deviceId, new ApiCallback() { + @VisibleForTesting + void initiateTransaction(String publicKey, Charge charge, String deviceId) { + ApiCallback callback = new ApiCallback() { @Override public void onSuccess(TransactionInitResponse data) { Card card = charge.getCard(); @@ -132,12 +135,19 @@ public void onSuccess(TransactionInitResponse data) { ); paystackRepository.processCardCharge(params, cardProcessCallback); } + @Override public void onError(@NotNull Throwable exception) { Log.e(LOG_TAG, exception.getMessage(), exception); notifyProcessingError(exception); } - }); + }; + + if (charge.getAccessCode() == null || charge.getAccessCode().isEmpty()) { + paystackRepository.initializeTransaction(publicKey, charge, deviceId, callback); + } else { + paystackRepository.getTransactionWithAccessCode(charge.getAccessCode(), callback); + } } private void processChargeResponse(ChargeParams chargeParams, ChargeResponse chargeResponse) { diff --git a/paystack/src/main/java/co/paystack/android/api/PaystackRepository.kt b/paystack/src/main/java/co/paystack/android/api/PaystackRepository.kt index ec1881e..c95916c 100644 --- a/paystack/src/main/java/co/paystack/android/api/PaystackRepository.kt +++ b/paystack/src/main/java/co/paystack/android/api/PaystackRepository.kt @@ -16,4 +16,9 @@ interface PaystackRepository { fun validateAddress(chargeParams: ChargeParams, address: Address, callback: ChargeApiCallback) fun requeryTransaction(chargeParams: ChargeParams, callback: ChargeApiCallback) + + fun getTransactionWithAccessCode( + accessCode: String, + callback: ApiCallback + ) } \ No newline at end of file diff --git a/paystack/src/main/java/co/paystack/android/api/PaystackRepositoryImpl.kt b/paystack/src/main/java/co/paystack/android/api/PaystackRepositoryImpl.kt index 519db7b..60e6603 100644 --- a/paystack/src/main/java/co/paystack/android/api/PaystackRepositoryImpl.kt +++ b/paystack/src/main/java/co/paystack/android/api/PaystackRepositoryImpl.kt @@ -21,6 +21,12 @@ internal class PaystackRepositoryImpl(private val apiService: PaystackApiService currency = charge.currency, metadata = charge.metadata, device = deviceId, + reference = charge.reference, + subAccount = charge.subaccount, + transactionCharge = charge.transactionCharge, + plan = charge.plan, + bearer = charge.bearer, + additionalParameters = charge.additionalParameters, ).toRequestMap() makeApiRequest( @@ -71,6 +77,14 @@ internal class PaystackRepositoryImpl(private val apiService: PaystackApiService ) } + override fun getTransactionWithAccessCode(accessCode: String, callback: ApiCallback) { + makeApiRequest( + onSuccess = { data -> callback.onSuccess(data) }, + onError = { throwable -> callback.onError(throwable) }, + apiCall = { apiService.getTransaction(accessCode) } + ) + } + private fun makeApiRequest(apiCall: () -> Call, onSuccess: (T) -> Unit, onError: (Throwable) -> Unit) { val retrofitCallback = object : Callback { override fun onResponse(call: Call, response: Response) { diff --git a/paystack/src/main/java/co/paystack/android/api/request/TransactionInitRequestBody.kt b/paystack/src/main/java/co/paystack/android/api/request/TransactionInitRequestBody.kt index c11e517..9b618a0 100644 --- a/paystack/src/main/java/co/paystack/android/api/request/TransactionInitRequestBody.kt +++ b/paystack/src/main/java/co/paystack/android/api/request/TransactionInitRequestBody.kt @@ -1,6 +1,7 @@ package co.paystack.android.api.request import co.paystack.android.api.utils.pruneNullValues +import co.paystack.android.model.Charge.Bearer data class TransactionInitRequestBody( val publicKey: String, @@ -9,14 +10,25 @@ data class TransactionInitRequestBody( val currency: String?, val metadata: String?, val device: String, + val reference: String?, + val subAccount: String?, + val transactionCharge: Int?, + val plan: String?, + val bearer: Bearer?, + val additionalParameters: Map, ) { - fun toRequestMap() = mapOf( + fun toRequestMap() = additionalParameters + mapOf( FIELD_KEY to publicKey, FIELD_EMAIL to email, FIELD_AMOUNT to amount, FIELD_CURRENCY to currency, FIELD_METADATA to metadata, FIELD_DEVICE to device, + FIELD_REFERENCE to reference, + FIELD_SUBACCOUNT to subAccount, + FIELD_TRANSACTION_CHARGE to transactionCharge, + FIELD_BEARER to bearer?.name, + FIELD_PLAN to plan, ).pruneNullValues() companion object { @@ -26,5 +38,11 @@ data class TransactionInitRequestBody( const val FIELD_CURRENCY = "currency" const val FIELD_METADATA = "metadata" const val FIELD_DEVICE = "device" + + const val FIELD_REFERENCE = "reference"; + const val FIELD_SUBACCOUNT = "subaccount"; + const val FIELD_TRANSACTION_CHARGE = "transaction_charge"; + const val FIELD_BEARER = "bearer"; + const val FIELD_PLAN = "plan"; } } diff --git a/paystack/src/main/java/co/paystack/android/api/service/PaystackApiService.kt b/paystack/src/main/java/co/paystack/android/api/service/PaystackApiService.kt index c7a7942..376885d 100644 --- a/paystack/src/main/java/co/paystack/android/api/service/PaystackApiService.kt +++ b/paystack/src/main/java/co/paystack/android/api/service/PaystackApiService.kt @@ -14,6 +14,9 @@ internal interface PaystackApiService { @GET("/checkout/request_inline") fun initializeTransaction(@QueryMap params: Map): Call + @GET("/transaction/verify_access_code/{accessCode}") + fun getTransaction(@Path("accessCode") accessCode: String): Call + @FormUrlEncoded @POST("/checkout/card/charge") @NoWrap diff --git a/paystack/src/main/java/co/paystack/android/ui/AuthActivity.java b/paystack/src/main/java/co/paystack/android/ui/AuthActivity.java index 9d40c69..82ea939 100644 --- a/paystack/src/main/java/co/paystack/android/ui/AuthActivity.java +++ b/paystack/src/main/java/co/paystack/android/ui/AuthActivity.java @@ -65,7 +65,7 @@ public void handleResponse() { } synchronized (si) { si.setResponseJson(responseJson); - si.notify(); + si.notifyAll(); } finish(); } @@ -73,7 +73,12 @@ public void handleResponse() { protected void setupWebview() { setContentView(R.layout.co_paystack_android____activity_auth); - findViewById(R.id.iv_close).setOnClickListener(v -> finish()); + findViewById(R.id.iv_close).setOnClickListener(v -> { + synchronized (si) { + si.notify(); + } + finish(); + }); webView = findViewById(R.id.webView); webView.setKeepScreenOn(true); @@ -133,7 +138,7 @@ public void onLoadResource(WebView view, String url) { } public void onDestroy() { - pusher.unsubscribe(channelName); + pusher.disconnect(); super.onDestroy(); if (webView != null) { webView.stopLoading(); diff --git a/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt b/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt index b7294b3..fd7cfa1 100644 --- a/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt +++ b/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt @@ -1,15 +1,23 @@ package co.paystack.android +import android.util.Log import androidx.test.core.app.ActivityScenario +import co.paystack.android.api.ApiCallback +import co.paystack.android.api.ChargeApiCallback import co.paystack.android.api.PaystackRepository +import co.paystack.android.api.model.TransactionInitResponse +import co.paystack.android.api.request.ChargeParams import co.paystack.android.model.Card import co.paystack.android.model.Charge +import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.isA import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.anyString import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations import org.robolectric.RobolectricTestRunner @@ -44,7 +52,78 @@ class TransactionManagerTest { } } + @Test + fun initiateTransactionIsCalled_ChargeAccessCodeIsNull_callInitializeTransactionOnPaystackRepository() { + val publicKey = "pk_test_key" + val deviceId = "android_702948084" + val charge = Charge().apply { + card = TEST_CARD + accessCode = null + } + whenever(paystackRepository.initializeTransaction(anyString(), any(), anyString(), any())) + .thenAnswer { Log.i(TAG, "initializeTransaction called") } + + val transactionManager = TransactionManager(paystackRepository) + transactionManager.initiateTransaction(publicKey, charge, deviceId) + + verify(paystackRepository).initializeTransaction( + isA(), + isA(), + isA(), + isA>() + ) + } + + @Test + fun initiateTransactionIsCalled_ChargeAccessCodeIsNotNull_call_getTransactionWithAccessCode_on_PaystackRepository() { + val publicKey = "pk_test_key" + val deviceId = "android_702948084" + val transAccessCode = "transaction_access_code" + val charge = Charge().apply { + card = TEST_CARD + accessCode = transAccessCode + } + + whenever(paystackRepository.getTransactionWithAccessCode(anyString(), any())) + .thenAnswer { Log.i(TAG, "getTransactionWithAccessCode called") } + + val transactionManager = TransactionManager(paystackRepository) + transactionManager.initiateTransaction(publicKey, charge, deviceId) + + verify(paystackRepository).getTransactionWithAccessCode( + isA(), + isA>() + ) + } + + + @Test + fun initiateTransactionIsCalled_transactionInitializationSucceeds_call_processCardCharge_OnPaystackRepository() { + val publicKey = "pk_test_key" + val deviceId = "android_702948084" + val charge = Charge().apply { + card = TEST_CARD + accessCode = null + } + + whenever(paystackRepository.initializeTransaction(anyString(), any(), anyString(), any())) + .thenAnswer { + val callback = it.arguments[3] as ApiCallback + callback.onSuccess(TransactionInitResponse("success", "trans_id")) + } + + val transactionManager = TransactionManager(paystackRepository) + transactionManager.initiateTransaction(publicKey, charge, deviceId) + + verify(paystackRepository).processCardCharge( + isA(), + isA() + ) + } + + companion object { + private const val TAG = "TransactionManagerTest" val TEST_CARD = Card("5105105105105100", 2, 2024, "123") } } \ No newline at end of file diff --git a/paystack/src/test/java/co/paystack/android/api/PaystackRepositoryImplTest.kt b/paystack/src/test/java/co/paystack/android/api/PaystackRepositoryImplTest.kt index ee09cad..b18422b 100644 --- a/paystack/src/test/java/co/paystack/android/api/PaystackRepositoryImplTest.kt +++ b/paystack/src/test/java/co/paystack/android/api/PaystackRepositoryImplTest.kt @@ -1,14 +1,21 @@ package co.paystack.android.api import co.paystack.android.api.model.ChargeResponse +import co.paystack.android.api.model.TransactionInitResponse import co.paystack.android.api.request.ChargeParams +import co.paystack.android.api.request.TransactionInitRequestBody import co.paystack.android.api.service.PaystackApiService +import co.paystack.android.model.Charge import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyMap +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class PaystackRepositoryImplTest { private val apiService: PaystackApiService = mock() @@ -24,6 +31,43 @@ class PaystackRepositoryImplTest { verify(apiService, times(1)).chargeCard(TEST_CHARGE_PARAMS.toRequestMap()) } + @Test + fun `initializeTransaction calls PaystackApiService with correct params`() { + whenever(apiService.initializeTransaction(anyMap())) + .thenReturn(FakeCall.success(TransactionInitResponse("success", "trans_id"))) + val repository = PaystackRepositoryImpl(apiService) + val apiCallback = mock>() + val testMetadata = "internal_tag" to "tag_internal_example" + val charge = Charge().apply { + email = testEmail + amount = testAmount + currency = testCurrency + reference = testReference + subaccount = "subacc_13850248" + transactionCharge = 1000 + plan = "PLN_123456789" + bearer = Charge.Bearer.subaccount + putMetadata(testMetadata.first, testMetadata.second) + } + repository.initializeTransaction(testPublicKey, charge, testDeviceId, apiCallback) + + val expectedApiCallMap = mapOf( + TransactionInitRequestBody.FIELD_KEY to testPublicKey, + TransactionInitRequestBody.FIELD_EMAIL to charge.email, + TransactionInitRequestBody.FIELD_AMOUNT to charge.amount, + TransactionInitRequestBody.FIELD_CURRENCY to charge.currency, + TransactionInitRequestBody.FIELD_METADATA to charge.metadata, + TransactionInitRequestBody.FIELD_DEVICE to testDeviceId, + TransactionInitRequestBody.FIELD_REFERENCE to charge.reference, + TransactionInitRequestBody.FIELD_SUBACCOUNT to charge.subaccount, + TransactionInitRequestBody.FIELD_TRANSACTION_CHARGE to charge.transactionCharge, + TransactionInitRequestBody.FIELD_BEARER to charge.bearer.name, + TransactionInitRequestBody.FIELD_PLAN to charge.plan, + ) + + verify(apiService).initializeTransaction(expectedApiCallMap) + } + companion object { const val testPublicKey = "pk_live_123445677555" const val testEmail = "michael@paystack.com" @@ -31,6 +75,7 @@ class PaystackRepositoryImplTest { const val testAmount = 10000 const val testCurrency = "NGN" const val testTransactionId = "123458685949" + const val testReference = "ref_123458685949" val TEST_CHARGE_PARAMS = ChargeParams( clientData = "encryptedClientData", From 28af0bd5c63cb818c42013c6277e7d219d12b056 Mon Sep 17 00:00:00 2001 From: Michael Obi <58430556+michael-paystack@users.noreply.github.com> Date: Fri, 10 Feb 2023 13:16:52 +0100 Subject: [PATCH 3/8] Update version name and code (#153) --- build.gradle | 4 ++-- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index f5471a1..afac200 100644 --- a/build.gradle +++ b/build.gradle @@ -64,10 +64,10 @@ ext { compileSdkVersion = 29 minSdkVersion = 16 targetSdkVersion = 29 - versionCode = 21 + versionCode = 22 buildToolsVersion = "29.0.2" - versionName = "3.3.0-alpha02" + versionName = "3.3.0" } Object getEnvOrDefault(String propertyName, Object defaultValue) { diff --git a/gradle.properties b/gradle.properties index a614749..e75a0fe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,7 +25,7 @@ android.useAndroidX=true org.gradle.daemon=true org.gradle.jvmargs=-Xmx2560m GROUP=co.paystack.android -VERSION_NAME=3.3.0-alpha01 +VERSION_NAME=3.3.0 POM_DESCRIPTION=Android SDK for Paystack POM_URL=https://github.com/PaystackHQ/paystack-android POM_SCM_URL=https://github.com/PaystackHQ/paystack-android From bf2734769b5db97f3808253560a13db042d7d2b1 Mon Sep 17 00:00:00 2001 From: Michael Obi <58430556+michael-paystack@users.noreply.github.com> Date: Fri, 10 Feb 2023 13:17:22 +0100 Subject: [PATCH 4/8] Add AvsState to proguard rules (#152) --- paystack/proguard-rules.pro | 1 + 1 file changed, 1 insertion(+) diff --git a/paystack/proguard-rules.pro b/paystack/proguard-rules.pro index ae35fb8..77e0b4b 100644 --- a/paystack/proguard-rules.pro +++ b/paystack/proguard-rules.pro @@ -1 +1,2 @@ -keepclassmembers class co.paystack.android.api.model.** { ; } +-keepclassmembers class co.paystack.android.model.AvsState { ; } From 166b2591fb749359045519e4aa066093ca233f1b Mon Sep 17 00:00:00 2001 From: Michael Obi <58430556+michael-paystack@users.noreply.github.com> Date: Thu, 16 Feb 2023 14:40:53 +0100 Subject: [PATCH 5/8] Call `showLoading` on `TransactionCallback` when starting communicating with server (#154) * Call `showLoading()` on `TransactionCallback` when starting communicating with server * Bump version to 3.3.1 (23) --- build.gradle | 4 ++-- .../co/paystack/example/MainActivity.java | 4 +++- gradle.properties | 2 +- .../paystack/android/TransactionManager.java | 21 +++++++++++++++---- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index afac200..5cce453 100644 --- a/build.gradle +++ b/build.gradle @@ -64,10 +64,10 @@ ext { compileSdkVersion = 29 minSdkVersion = 16 targetSdkVersion = 29 - versionCode = 22 + versionCode = 23 buildToolsVersion = "29.0.2" - versionName = "3.3.0" + versionName = "3.3.1" } Object getEnvOrDefault(String propertyName, Object defaultValue) { diff --git a/example/src/main/java/co/paystack/example/MainActivity.java b/example/src/main/java/co/paystack/example/MainActivity.java index 4e02cef..ec7e989 100644 --- a/example/src/main/java/co/paystack/example/MainActivity.java +++ b/example/src/main/java/co/paystack/example/MainActivity.java @@ -3,6 +3,7 @@ import android.app.ProgressDialog; import android.os.AsyncTask; import android.os.Bundle; +import android.util.Log; import android.view.Menu; import android.view.View; import android.widget.Button; @@ -51,7 +52,7 @@ public class MainActivity extends AppCompatActivity { private TextView mTextReference; private Charge charge; private Transaction transaction; - + private static final String TAG = "MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { @@ -213,6 +214,7 @@ public void beforeValidate(Transaction transaction) { @Override public void showLoading(Boolean isProcessing) { + Log.i(TAG, "Paystack SDK loading: " + isProcessing); if (isProcessing) { Toast.makeText(MainActivity.this, "Processing...", Toast.LENGTH_LONG).show(); } diff --git a/gradle.properties b/gradle.properties index e75a0fe..11599ff 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,7 +25,7 @@ android.useAndroidX=true org.gradle.daemon=true org.gradle.jvmargs=-Xmx2560m GROUP=co.paystack.android -VERSION_NAME=3.3.0 +VERSION_NAME=3.3.1 POM_DESCRIPTION=Android SDK for Paystack POM_URL=https://github.com/PaystackHQ/paystack-android POM_SCM_URL=https://github.com/PaystackHQ/paystack-android diff --git a/paystack/src/main/java/co/paystack/android/TransactionManager.java b/paystack/src/main/java/co/paystack/android/TransactionManager.java index 6c9e308..8e738be 100644 --- a/paystack/src/main/java/co/paystack/android/TransactionManager.java +++ b/paystack/src/main/java/co/paystack/android/TransactionManager.java @@ -133,7 +133,7 @@ public void onSuccess(TransactionInitResponse data) { deviceId, null ); - paystackRepository.processCardCharge(params, cardProcessCallback); + processCharge(params); } @Override @@ -143,6 +143,7 @@ public void onError(@NotNull Throwable exception) { } }; + transactionCallback.showLoading(true); if (charge.getAccessCode() == null || charge.getAccessCode().isEmpty()) { paystackRepository.initializeTransaction(publicKey, charge, deviceId, callback); } else { @@ -220,6 +221,7 @@ private void authenticateTransaction(ChargeParams chargeParams, ChargeResponse c private void validateOtp(String token, ChargeParams chargeParams) { try { + transactionCallback.showLoading(true); paystackRepository.validateTransaction(chargeParams, token, cardProcessCallback); } catch (Exception ce) { Log.e(LOG_TAG, ce.getMessage(), ce); @@ -229,6 +231,7 @@ private void validateOtp(String token, ChargeParams chargeParams) { private void chargeWithAvs(Address address, ChargeParams chargeParams) { try { + transactionCallback.showLoading(true); paystackRepository.validateAddress(chargeParams, address, cardProcessCallback); } catch (Exception e) { Log.e(LOG_TAG, e.getMessage(), e); @@ -240,7 +243,7 @@ private void reQueryChargeOnServer(ChargeParams chargeParams) { try { new CountDownTimer(5000, 5000) { public void onFinish() { - paystackRepository.requeryTransaction(chargeParams, cardProcessCallback); + requeryTransaction(chargeParams); } public void onTick(long millisUntilFinished) { @@ -252,10 +255,19 @@ public void onTick(long millisUntilFinished) { } } + private void requeryTransaction(ChargeParams chargeParams) { + transactionCallback.showLoading(true); + paystackRepository.requeryTransaction(chargeParams, cardProcessCallback); + } + + private void processCharge(ChargeParams params) { + transactionCallback.showLoading(true); + paystackRepository.processCardCharge(params, cardProcessCallback); + } + private void notifyProcessingError(Throwable t) { setProcessingOff(); transactionCallback.showLoading(false); - transactionCallback.onError(t, EMPTY_TRANSACTION); } @@ -338,7 +350,8 @@ protected String doInBackground(Void... params) { protected void onPostExecute(String pin) { super.onPostExecute(pin); if (pin != null && (4 == pin.length())) { - paystackRepository.processCardCharge(chargeParams.addPin(Crypto.encrypt(pin)), cardProcessCallback); + ChargeParams params = chargeParams.addPin(Crypto.encrypt(pin)); + processCharge(params); } else { notifyProcessingError(new Exception("PIN must be exactly 4 digits")); } From 68a4a8469a0747ee51cdb41c74f420d0c1dde7c0 Mon Sep 17 00:00:00 2001 From: Michael Obi <58430556+michael-paystack@users.noreply.github.com> Date: Sat, 25 Feb 2023 07:36:30 +0100 Subject: [PATCH 6/8] Fix TransactionManager Robolectric tests (#156) --- build.gradle | 2 +- .../paystack/android/TransactionManager.java | 6 ++++++ .../paystack/android/TransactionManagerTest.kt | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5cce453..f5d9aa4 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { 'kotlin' : '1.7.22', 'coroutines' : '1.4.2', 'pusher' : '2.2.1', - 'robolectric' : '4.4', + 'robolectric' : '4.9.2', 'mockito' : '3.8.0', 'mockito_kotlin': '2.2.0', 'junit_ext' : '1.1.2', diff --git a/paystack/src/main/java/co/paystack/android/TransactionManager.java b/paystack/src/main/java/co/paystack/android/TransactionManager.java index 8e738be..21ff125 100644 --- a/paystack/src/main/java/co/paystack/android/TransactionManager.java +++ b/paystack/src/main/java/co/paystack/android/TransactionManager.java @@ -285,6 +285,12 @@ private void setProcessingOn() { TransactionManager.PROCESSING = true; } + + @VisibleForTesting + void setTransactionCallback(Paystack.TransactionCallback transactionCallback) { + this.transactionCallback = transactionCallback; + } + private class CardAsyncTask extends AsyncTask { private final String publicKey; diff --git a/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt b/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt index fd7cfa1..85ea895 100644 --- a/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt +++ b/paystack/src/test/java/co/paystack/android/TransactionManagerTest.kt @@ -2,6 +2,7 @@ package co.paystack.android import android.util.Log import androidx.test.core.app.ActivityScenario +import co.paystack.android.Paystack.TransactionCallback import co.paystack.android.api.ApiCallback import co.paystack.android.api.ChargeApiCallback import co.paystack.android.api.PaystackRepository @@ -27,6 +28,20 @@ class TransactionManagerTest { @Mock lateinit var paystackRepository: PaystackRepository + private val emptyTransactionCallback = object : TransactionCallback { + override fun onSuccess(transaction: Transaction?) { + } + + override fun beforeValidate(transaction: Transaction?) { + } + + override fun showLoading(isProcessing: Boolean?) { + } + + override fun onError(error: Throwable?, transaction: Transaction?) { + } + } + @Before fun setup() { MockitoAnnotations.openMocks(this) @@ -64,6 +79,7 @@ class TransactionManagerTest { .thenAnswer { Log.i(TAG, "initializeTransaction called") } val transactionManager = TransactionManager(paystackRepository) + transactionManager.setTransactionCallback(emptyTransactionCallback) transactionManager.initiateTransaction(publicKey, charge, deviceId) verify(paystackRepository).initializeTransaction( @@ -88,6 +104,7 @@ class TransactionManagerTest { .thenAnswer { Log.i(TAG, "getTransactionWithAccessCode called") } val transactionManager = TransactionManager(paystackRepository) + transactionManager.setTransactionCallback(emptyTransactionCallback) transactionManager.initiateTransaction(publicKey, charge, deviceId) verify(paystackRepository).getTransactionWithAccessCode( @@ -113,6 +130,7 @@ class TransactionManagerTest { } val transactionManager = TransactionManager(paystackRepository) + transactionManager.setTransactionCallback(emptyTransactionCallback) transactionManager.initiateTransaction(publicKey, charge, deviceId) verify(paystackRepository).processCardCharge( From 1620c62079fa209a3548b6c6eae3333ab99b2c39 Mon Sep 17 00:00:00 2001 From: Michael Obi <58430556+michael-paystack@users.noreply.github.com> Date: Mon, 6 Mar 2023 09:49:39 +0100 Subject: [PATCH 7/8] Add `showLoading()` to `TransactionCallback` docs (#155) * Add `showLoading()` to `TransactionCallback` docs * Update README.md Co-authored-by: nathan-paystack <82086670+nathanstasin@users.noreply.github.com> --------- Co-authored-by: nathan-paystack <82086670+nathanstasin@users.noreply.github.com> --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 1d71e1f..74594be 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,12 @@ public class MainActivity extends AppCompatActivity { // Save reference so you may send to server. If // error occurs with OTP, you should still verify on server. } + + @Override + public void showLoading(Boolean isProcessing) { + // This is called whenever the SDK makes network requests. + // Use this to display loading indicators in your application UI + } @Override public void onError(Throwable error, Transaction transaction) { From 25a23359c0f1a86c603763b90cbd6df2a1e7bb70 Mon Sep 17 00:00:00 2001 From: Michael Obi <58430556+michael-paystack@users.noreply.github.com> Date: Wed, 29 Mar 2023 15:45:28 +0100 Subject: [PATCH 8/8] 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 --- build.gradle | 6 +- gradle.properties | 2 +- paystack/build.gradle | 7 +- paystack/proguard-rules.pro | 81 +++++++++- .../java/co/paystack/android/Transaction.java | 4 +- .../paystack/android/TransactionManager.java | 30 ++-- .../paystack/android/api/ChargeApiCallback.kt | 2 +- .../android/api/PaystackRepositoryImpl.kt | 8 +- .../paystack/android/api/di/ApiComponent.kt | 30 ++-- .../android/api/model/ApiResponse.java | 18 --- .../android/api/model/BaseApiModel.java | 7 - .../android/api/model/ChargeResponse.kt | 23 +-- .../api/model/TransactionApiResponse.java | 64 -------- .../api/model/TransactionApiResponse.kt | 47 ++++++ .../api/model/TransactionInitResponse.kt | 11 +- .../android/api/request/BaseRequestBody.java | 22 --- .../android/api/request/ChargeParams.kt | 14 +- .../api/request/ChargeRequestBody.java | 143 ------------------ .../api/request/TransactionInitRequestBody.kt | 2 + .../api/request/ValidateRequestBody.java | 51 ------- .../api/request/ValidateTransactionParams.kt | 2 + .../android/api/service/PaystackApiFactory.kt | 20 ++- .../converter/WrappedResponseConverter.kt | 15 +- .../co/paystack/android/model/AvsState.kt | 3 + 24 files changed, 229 insertions(+), 383 deletions(-) delete mode 100644 paystack/src/main/java/co/paystack/android/api/model/ApiResponse.java delete mode 100644 paystack/src/main/java/co/paystack/android/api/model/BaseApiModel.java delete mode 100644 paystack/src/main/java/co/paystack/android/api/model/TransactionApiResponse.java create mode 100644 paystack/src/main/java/co/paystack/android/api/model/TransactionApiResponse.kt delete mode 100644 paystack/src/main/java/co/paystack/android/api/request/BaseRequestBody.java delete mode 100644 paystack/src/main/java/co/paystack/android/api/request/ChargeRequestBody.java delete mode 100644 paystack/src/main/java/co/paystack/android/api/request/ValidateRequestBody.java diff --git a/build.gradle b/build.gradle index f5d9aa4..e04e25b 100644 --- a/build.gradle +++ b/build.gradle @@ -43,9 +43,9 @@ sonarqube { allprojects { repositories { - jcenter() - google() mavenCentral() + google() + jcenter() } configurations.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) { diff --git a/gradle.properties b/gradle.properties index 11599ff..8677e85 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/paystack/build.gradle b/paystack/build.gradle index a84717f..ab17091 100644 --- a/paystack/build.gradle +++ b/paystack/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'jacoco' +apply plugin: 'kotlin-kapt' jacoco { toolVersion = "$jacocoVersion" @@ -65,9 +66,8 @@ 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' @@ -75,6 +75,9 @@ dependencies { 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" diff --git a/paystack/proguard-rules.pro b/paystack/proguard-rules.pro index 77e0b4b..76fa3a7 100644 --- a/paystack/proguard-rules.pro +++ b/paystack/proguard-rules.pro @@ -1,2 +1,81 @@ -keepclassmembers class co.paystack.android.api.model.** { ; } --keepclassmembers class co.paystack.android.model.AvsState { ; } +-keepclassmembers class co.paystack.android.model.** { ; } + + +# 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.* ; +} + + +##MOSHI +# JSR 305 annotations are for embedding nullability information. +-dontwarn javax.annotation.** + +-keepclasseswithmembers class * { + @com.squareup.moshi.* ; +} + +-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 { + ; +} + +# 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 (...); +} + +# Retain generated JsonAdapters if annotated type is retained. +-if @com.squareup.moshi.JsonClass class * +-keep class <1>JsonAdapter { + (...); + ; +} +-if @com.squareup.moshi.JsonClass class **$* +-keep class <1>_<2>JsonAdapter { + (...); + ; +} +-if @com.squareup.moshi.JsonClass class **$*$* +-keep class <1>_<2>_<3>JsonAdapter { + (...); + ; +} +-if @com.squareup.moshi.JsonClass class **$*$*$* +-keep class <1>_<2>_<3>_<4>JsonAdapter { + (...); + ; +} +-if @com.squareup.moshi.JsonClass class **$*$*$*$* +-keep class <1>_<2>_<3>_<4>_<5>JsonAdapter { + (...); + ; +} +-if @com.squareup.moshi.JsonClass class **$*$*$*$*$* +-keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter { + (...); + ; +} + +-keepclassmembers class kotlin.Metadata { + public ; +} \ No newline at end of file diff --git a/paystack/src/main/java/co/paystack/android/Transaction.java b/paystack/src/main/java/co/paystack/android/Transaction.java index 4b94bfc..5a50e92 100644 --- a/paystack/src/main/java/co/paystack/android/Transaction.java +++ b/paystack/src/main/java/co/paystack/android/Transaction.java @@ -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; } diff --git a/paystack/src/main/java/co/paystack/android/TransactionManager.java b/paystack/src/main/java/co/paystack/android/TransactionManager.java index 21ff125..801e7bb 100644 --- a/paystack/src/main/java/co/paystack/android/TransactionManager.java +++ b/paystack/src/main/java/co/paystack/android/TransactionManager.java @@ -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; @@ -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); } }; @@ -131,6 +134,7 @@ public void onSuccess(TransactionInitResponse data) { transactionId, card.getLast4digits(), deviceId, + data.getReference(), null ); processCharge(params); @@ -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; } @@ -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) { @@ -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()); } } @@ -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()); } } @@ -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()); } } @@ -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()); } } } diff --git a/paystack/src/main/java/co/paystack/android/api/ChargeApiCallback.kt b/paystack/src/main/java/co/paystack/android/api/ChargeApiCallback.kt index 2bb5c86..d83bcbf 100644 --- a/paystack/src/main/java/co/paystack/android/api/ChargeApiCallback.kt +++ b/paystack/src/main/java/co/paystack/android/api/ChargeApiCallback.kt @@ -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?) } \ No newline at end of file diff --git a/paystack/src/main/java/co/paystack/android/api/PaystackRepositoryImpl.kt b/paystack/src/main/java/co/paystack/android/api/PaystackRepositoryImpl.kt index 60e6603..74995a7 100644 --- a/paystack/src/main/java/co/paystack/android/api/PaystackRepositoryImpl.kt +++ b/paystack/src/main/java/co/paystack/android/api/PaystackRepositoryImpl.kt @@ -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()) } ) } @@ -54,7 +54,7 @@ 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) }, ) } @@ -62,7 +62,7 @@ internal class PaystackRepositoryImpl(private val apiService: PaystackApiService makeApiRequest( apiCall = { apiService.requeryTransaction(chargeParams.transactionId) }, onSuccess = { data -> callback.onSuccess(chargeParams, data) }, - onError = { throwable -> callback.onError(throwable) }, + onError = { throwable -> callback.onError(throwable, chargeParams.reference) }, ) } @@ -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) }, ) } diff --git a/paystack/src/main/java/co/paystack/android/api/di/ApiComponent.kt b/paystack/src/main/java/co/paystack/android/api/di/ApiComponent.kt index 4ceeef9..e0da8d3 100644 --- a/paystack/src/main/java/co/paystack/android/api/di/ApiComponent.kt +++ b/paystack/src/main/java/co/paystack/android/api/di/ApiComponent.kt @@ -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()) @@ -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) diff --git a/paystack/src/main/java/co/paystack/android/api/model/ApiResponse.java b/paystack/src/main/java/co/paystack/android/api/model/ApiResponse.java deleted file mode 100644 index 11ea72a..0000000 --- a/paystack/src/main/java/co/paystack/android/api/model/ApiResponse.java +++ /dev/null @@ -1,18 +0,0 @@ -package co.paystack.android.api.model; - -import com.google.gson.annotations.SerializedName; - -/** - * An API response always includes a status and a message - */ -public class ApiResponse extends BaseApiModel { - - @SerializedName("status") - public String status; - - @SerializedName("message") - public String message; - - @SerializedName("errors") - public boolean hasErrors = false; -} diff --git a/paystack/src/main/java/co/paystack/android/api/model/BaseApiModel.java b/paystack/src/main/java/co/paystack/android/api/model/BaseApiModel.java deleted file mode 100644 index a81526e..0000000 --- a/paystack/src/main/java/co/paystack/android/api/model/BaseApiModel.java +++ /dev/null @@ -1,7 +0,0 @@ -package co.paystack.android.api.model; - -/** - * Created by {androidsupport@paystack.co} on 9/17/15. - */ -public abstract class BaseApiModel { -} diff --git a/paystack/src/main/java/co/paystack/android/api/model/ChargeResponse.kt b/paystack/src/main/java/co/paystack/android/api/model/ChargeResponse.kt index 48eba6c..cbe3a05 100644 --- a/paystack/src/main/java/co/paystack/android/api/model/ChargeResponse.kt +++ b/paystack/src/main/java/co/paystack/android/api/model/ChargeResponse.kt @@ -2,35 +2,38 @@ package co.paystack.android.api.model import androidx.annotation.Keep -import com.google.gson.Gson -import com.google.gson.annotations.SerializedName +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi @Keep +@JsonClass(generateAdapter = true) data class ChargeResponse( val status: String?, - @SerializedName("trans") - val transactionId: String, + @Json(name = "trans") + val transactionId: String?, - val reference: String, + val reference: String?, val message: String?, - @SerializedName("otpmessage") + @Json(name = "otpmessage") val otpMessage: String? = null, val auth: String? = null, - @SerializedName("countryCode") + @Json(name = "countryCode") val countryCode: String? = null, - - ) { +) { companion object { fun fromJsonString(jsonString: String?): ChargeResponse { return try { - Gson().fromJson(jsonString, ChargeResponse::class.java) + Moshi.Builder().build() + .adapter(ChargeResponse::class.java) + .fromJson(jsonString) ?: error("Failed to parse charge response") } catch (e: Exception) { ChargeResponse( status = "0", diff --git a/paystack/src/main/java/co/paystack/android/api/model/TransactionApiResponse.java b/paystack/src/main/java/co/paystack/android/api/model/TransactionApiResponse.java deleted file mode 100644 index dc6e1ae..0000000 --- a/paystack/src/main/java/co/paystack/android/api/model/TransactionApiResponse.java +++ /dev/null @@ -1,64 +0,0 @@ -package co.paystack.android.api.model; - -import android.webkit.URLUtil; - -import com.google.gson.Gson; -import com.google.gson.annotations.SerializedName; - -import java.io.Serializable; - -/** - * 3DS would give a redirect url at which we can conclude payment - */ -public class TransactionApiResponse extends ApiResponse implements Serializable { - - @SerializedName("reference") - public String reference; - - @SerializedName("trans") - public String trans; - - @SerializedName("auth") - public String auth; - - @SerializedName("otpmessage") - public String otpmessage; - - @SerializedName("countryCode") - public String avsCountryCode; // Country code for Address Verification on supported international cards - - public static TransactionApiResponse unknownServerResponse() { - TransactionApiResponse t = new TransactionApiResponse(); - t.status = "0"; - t.message = "Unknown server response"; - return t; - } - - public static TransactionApiResponse fromJsonString(String jsonString) { - try { - return new Gson().fromJson(jsonString, TransactionApiResponse.class); - } catch (Exception e) { - TransactionApiResponse t = new TransactionApiResponse(); - t.status = "0"; - t.message = e.getMessage(); - return t; - } - } - - public boolean hasValidReferenceAndTrans() { - return (reference != null) && (trans != null); - } - - public boolean hasValidUrl() { - return otpmessage != null && URLUtil.isValidUrl(otpmessage); - } - - public boolean hasValidOtpMessage() { - return otpmessage != null; - } - - public boolean hasValidAuth() { - return auth != null; - } - -} diff --git a/paystack/src/main/java/co/paystack/android/api/model/TransactionApiResponse.kt b/paystack/src/main/java/co/paystack/android/api/model/TransactionApiResponse.kt new file mode 100644 index 0000000..bd6bf60 --- /dev/null +++ b/paystack/src/main/java/co/paystack/android/api/model/TransactionApiResponse.kt @@ -0,0 +1,47 @@ +package co.paystack.android.api.model + +import android.webkit.URLUtil +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TransactionApiResponse( + @Json(name = "status") + val status: String? = null, + + @Json(name = "message") + val message: String? = null, + + @JvmField + @Json(name = "reference") + val reference: String? = null, + + @JvmField + @Json(name = "trans") + val trans: String? = null, + + @Json(name = "auth") + val auth: String? = null, + + @Json(name = "otpmessage") + val otpmessage: String? = null, + + @Json(name = "countryCode") + val avsCountryCode: String? = null, +) { + fun hasValidReferenceAndTrans(): Boolean { + return reference != null && trans != null + } + + fun hasValidUrl(): Boolean { + return otpmessage != null && URLUtil.isValidUrl(otpmessage) + } + + fun hasValidOtpMessage(): Boolean { + return otpmessage != null + } + + fun hasValidAuth(): Boolean { + return auth != null + } +} \ No newline at end of file diff --git a/paystack/src/main/java/co/paystack/android/api/model/TransactionInitResponse.kt b/paystack/src/main/java/co/paystack/android/api/model/TransactionInitResponse.kt index f55b26e..ccb7806 100644 --- a/paystack/src/main/java/co/paystack/android/api/model/TransactionInitResponse.kt +++ b/paystack/src/main/java/co/paystack/android/api/model/TransactionInitResponse.kt @@ -1,10 +1,13 @@ package co.paystack.android.api.model -import com.google.gson.annotations.SerializedName +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class TransactionInitResponse( - val status: String, + @Json(name = "id") + val transactionId: String, - @SerializedName("id") - val transactionId: String + @Json(name = "reference") + val reference: String? ) diff --git a/paystack/src/main/java/co/paystack/android/api/request/BaseRequestBody.java b/paystack/src/main/java/co/paystack/android/api/request/BaseRequestBody.java deleted file mode 100644 index 6d12788..0000000 --- a/paystack/src/main/java/co/paystack/android/api/request/BaseRequestBody.java +++ /dev/null @@ -1,22 +0,0 @@ -package co.paystack.android.api.request; - -import com.google.gson.annotations.SerializedName; - -import java.util.HashMap; - -/** - * A base for all request bodies - */ -abstract class BaseRequestBody { - static final String FIELD_DEVICE = "device"; - @SerializedName(FIELD_DEVICE) - String device; - - public abstract HashMap getParamsHashMap(); - - protected BaseRequestBody(String deviceId) { - this.device = deviceId; - } - - -} diff --git a/paystack/src/main/java/co/paystack/android/api/request/ChargeParams.kt b/paystack/src/main/java/co/paystack/android/api/request/ChargeParams.kt index 4cbae1b..278a579 100644 --- a/paystack/src/main/java/co/paystack/android/api/request/ChargeParams.kt +++ b/paystack/src/main/java/co/paystack/android/api/request/ChargeParams.kt @@ -1,16 +1,19 @@ package co.paystack.android.api.request +import co.paystack.android.Transaction import co.paystack.android.api.utils.pruneNullValues +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class ChargeParams( val clientData: String, val transactionId: String, val last4: String, val deviceId: String, - val handle: String? + val reference: String?, + val handle: String? = null ) { fun toRequestMap() = mapOf( - FIELD_CLIENT_DATA to clientData, FIELD_HANDLE to handle, FIELD_TRANS to transactionId, @@ -21,6 +24,13 @@ data class ChargeParams( fun addPin(pin: String) = copy(handle = pin) + fun getTransaction(): Transaction { + val transaction = Transaction() + transaction.setId(transactionId) + transaction.reference = reference + return transaction + } + companion object { const val FIELD_CLIENT_DATA = "clientdata" const val FIELD_HANDLE = "handle" diff --git a/paystack/src/main/java/co/paystack/android/api/request/ChargeRequestBody.java b/paystack/src/main/java/co/paystack/android/api/request/ChargeRequestBody.java deleted file mode 100644 index 8be3bc4..0000000 --- a/paystack/src/main/java/co/paystack/android/api/request/ChargeRequestBody.java +++ /dev/null @@ -1,143 +0,0 @@ -package co.paystack.android.api.request; - -import com.google.gson.annotations.SerializedName; - -import java.util.HashMap; - -import co.paystack.android.PaystackSdk; -import co.paystack.android.model.Charge; -import co.paystack.android.utils.Crypto; -import co.paystack.android.utils.StringUtils; - -/** - * Charge Request Body - */ -public class ChargeRequestBody extends BaseRequestBody { - - private static final String FIELD_CLIENT_DATA = "clientdata"; - private static final String FIELD_LAST4 = "last4"; - private static final String FIELD_ACCESS_CODE = "access_code"; - private static final String FIELD_PUBLIC_KEY = "public_key"; - private static final String FIELD_EMAIL = "email"; - private static final String FIELD_AMOUNT = "amount"; - private static final String FIELD_REFERENCE = "reference"; - private static final String FIELD_SUBACCOUNT = "subaccount"; - private static final String FIELD_TRANSACTION_CHARGE = "transaction_charge"; - private static final String FIELD_BEARER = "bearer"; - private static final String FIELD_HANDLE = "handle"; - private static final String FIELD_METADATA = "metadata"; - private static final String FIELD_CURRENCY = "currency"; - private static final String FIELD_PLAN = "plan"; - - @SerializedName(FIELD_CLIENT_DATA) - private final String clientData; - - @SerializedName(FIELD_LAST4) - private final String last4; - - @SerializedName(FIELD_PUBLIC_KEY) - private final String public_key; - - @SerializedName(FIELD_ACCESS_CODE) - private final String access_code; - - @SerializedName(FIELD_EMAIL) - private final String email; - - @SerializedName(FIELD_AMOUNT) - private final String amount; - - @SerializedName(FIELD_REFERENCE) - private final String reference; - - @SerializedName(FIELD_SUBACCOUNT) - private final String subaccount; - - @SerializedName(FIELD_TRANSACTION_CHARGE) - private final String transaction_charge; - - @SerializedName(FIELD_BEARER) - private final String bearer; - - @SerializedName(FIELD_HANDLE) - private String handle; - - @SerializedName(FIELD_METADATA) - private String metadata; - - @SerializedName(FIELD_CURRENCY) - private String currency; - - @SerializedName(FIELD_PLAN) - private String plan; - private HashMap additionalParameters; - - public ChargeRequestBody(Charge charge, String deviceId) { - super(deviceId); - this.clientData = Crypto.encrypt(StringUtils.concatenateCardFields(charge.getCard())); - this.last4 = charge.getCard().getLast4digits(); - this.public_key = PaystackSdk.getPublicKey(); - this.email = charge.getEmail(); - this.amount = Integer.toString(charge.getAmount()); - this.reference = charge.getReference(); - this.subaccount = charge.getSubaccount(); - this.transaction_charge = charge.getTransactionCharge() > 0 ? Integer.toString(charge.getTransactionCharge()) : null; - this.bearer = charge.getBearer() != null ? charge.getBearer().name() : null; - this.metadata = charge.getMetadata(); - this.plan = charge.getPlan(); - this.currency = charge.getCurrency(); - this.access_code = charge.getAccessCode(); - this.additionalParameters = charge.getAdditionalParameters(); - } - - public ChargeRequestBody addPin(String pin) { - this.handle = Crypto.encrypt(pin); - return this; - } - - @Override - public HashMap getParamsHashMap() { - // set values will override additional params provided - HashMap params = additionalParameters; - params.put(FIELD_PUBLIC_KEY, public_key); - params.put(FIELD_CLIENT_DATA, clientData); - params.put(FIELD_LAST4, last4); - if (access_code != null) { - params.put(FIELD_ACCESS_CODE, access_code); - } - if (email != null) { - params.put(FIELD_EMAIL, email); - } - if (amount != null) { - params.put(FIELD_AMOUNT, amount); - } - if (handle != null) { - params.put(FIELD_HANDLE, handle); - } - if (reference != null) { - params.put(FIELD_REFERENCE, reference); - } - if (subaccount != null) { - params.put(FIELD_SUBACCOUNT, subaccount); - } - if (transaction_charge != null) { - params.put(FIELD_TRANSACTION_CHARGE, transaction_charge); - } - if (bearer != null) { - params.put(FIELD_BEARER, bearer); - } - if (metadata != null) { - params.put(FIELD_METADATA, metadata); - } - if (plan != null) { - params.put(FIELD_PLAN, plan); - } - if (currency != null) { - params.put(FIELD_CURRENCY, currency); - } - if (device != null) { - params.put(FIELD_DEVICE, device); - } - return params; - } -} diff --git a/paystack/src/main/java/co/paystack/android/api/request/TransactionInitRequestBody.kt b/paystack/src/main/java/co/paystack/android/api/request/TransactionInitRequestBody.kt index 9b618a0..ad72d9e 100644 --- a/paystack/src/main/java/co/paystack/android/api/request/TransactionInitRequestBody.kt +++ b/paystack/src/main/java/co/paystack/android/api/request/TransactionInitRequestBody.kt @@ -2,7 +2,9 @@ package co.paystack.android.api.request import co.paystack.android.api.utils.pruneNullValues import co.paystack.android.model.Charge.Bearer +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class TransactionInitRequestBody( val publicKey: String, val email: String, diff --git a/paystack/src/main/java/co/paystack/android/api/request/ValidateRequestBody.java b/paystack/src/main/java/co/paystack/android/api/request/ValidateRequestBody.java deleted file mode 100644 index bcfb015..0000000 --- a/paystack/src/main/java/co/paystack/android/api/request/ValidateRequestBody.java +++ /dev/null @@ -1,51 +0,0 @@ -package co.paystack.android.api.request; - -import com.google.gson.annotations.SerializedName; - -import java.io.Serializable; -import java.util.HashMap; - -public class ValidateRequestBody extends BaseRequestBody implements Serializable { - - private static final String FIELD_TRANS = "trans"; - private static final String FIELD_TOKEN = "token"; - - @SerializedName(FIELD_TRANS) - private String trans; - - @SerializedName(FIELD_TOKEN) - private String token; - - public ValidateRequestBody(String deviceId) { - super(deviceId); - } - - private String getTrans() { - return trans; - } - - public ValidateRequestBody setTrans(String trans) { - this.trans = trans; - return this; - } - - private String getToken() { - return token; - } - - public ValidateRequestBody setToken(String token) { - this.token = token; - return this; - } - - @Override - public HashMap getParamsHashMap() { - HashMap params = new HashMap<>(); - params.put(FIELD_TRANS, getTrans()); - params.put(FIELD_TOKEN, getToken()); - if (device != null) { - params.put(FIELD_DEVICE, device); - } - return params; - } -} \ No newline at end of file diff --git a/paystack/src/main/java/co/paystack/android/api/request/ValidateTransactionParams.kt b/paystack/src/main/java/co/paystack/android/api/request/ValidateTransactionParams.kt index da728e2..b82448e 100644 --- a/paystack/src/main/java/co/paystack/android/api/request/ValidateTransactionParams.kt +++ b/paystack/src/main/java/co/paystack/android/api/request/ValidateTransactionParams.kt @@ -1,7 +1,9 @@ package co.paystack.android.api.request import co.paystack.android.api.utils.pruneNullValues +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class ValidateTransactionParams( val transactionId: String, val token: String? = null, diff --git a/paystack/src/main/java/co/paystack/android/api/service/PaystackApiFactory.kt b/paystack/src/main/java/co/paystack/android/api/service/PaystackApiFactory.kt index 2973d6f..ddec85d 100644 --- a/paystack/src/main/java/co/paystack/android/api/service/PaystackApiFactory.kt +++ b/paystack/src/main/java/co/paystack/android/api/service/PaystackApiFactory.kt @@ -5,10 +5,10 @@ import co.paystack.android.BuildConfig 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.GsonBuilder +import com.squareup.moshi.Moshi import okhttp3.OkHttpClient import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.moshi.MoshiConverterFactory import java.security.KeyManagementException import java.security.KeyStoreException import java.security.NoSuchAlgorithmException @@ -20,11 +20,12 @@ import java.util.concurrent.TimeUnit internal object PaystackApiFactory { private const val BASE_URL = "https://api.paystack.co/" - @Throws(NoSuchAlgorithmException::class, KeyManagementException::class, KeyStoreException::class) + @Throws( + NoSuchAlgorithmException::class, + KeyManagementException::class, + KeyStoreException::class + ) fun createRetrofitService(): PaystackApiService { - val gson = GsonBuilder() - .setDateFormat("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'") - .create() val tlsV1point2factory = TLSSocketFactory() val okHttpClient = OkHttpClient.Builder() @@ -32,7 +33,10 @@ internal object PaystackApiFactory { 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()) @@ -49,7 +53,7 @@ internal object PaystackApiFactory { .baseUrl(BASE_URL) .client(okHttpClient) .addConverterFactory(WrappedResponseConverter.Factory()) - .addConverterFactory(GsonConverterFactory.create(gson)) + .addConverterFactory(MoshiConverterFactory.create(Moshi.Builder().build())) .build() return retrofit.create(PaystackApiService::class.java) diff --git a/paystack/src/main/java/co/paystack/android/api/service/converter/WrappedResponseConverter.kt b/paystack/src/main/java/co/paystack/android/api/service/converter/WrappedResponseConverter.kt index 44053b1..02980c9 100644 --- a/paystack/src/main/java/co/paystack/android/api/service/converter/WrappedResponseConverter.kt +++ b/paystack/src/main/java/co/paystack/android/api/service/converter/WrappedResponseConverter.kt @@ -1,11 +1,11 @@ package co.paystack.android.api.service.converter +import com.squareup.moshi.JsonClass import okhttp3.ResponseBody import retrofit2.Converter import retrofit2.Retrofit import java.lang.reflect.ParameterizedType import java.lang.reflect.Type -import kotlin.annotation.AnnotationTarget.FUNCTION class WrappedResponseConverter( private val delegate: Converter> @@ -47,10 +47,11 @@ class WrappedResponseConverter( } } - - open class WrappedResponse( - val `data`: T, - val message: String, - val status: Boolean - ) } + +@JsonClass(generateAdapter = true) +open class WrappedResponse( + val `data`: T, + val message: String, + val status: Boolean +) \ No newline at end of file diff --git a/paystack/src/main/java/co/paystack/android/model/AvsState.kt b/paystack/src/main/java/co/paystack/android/model/AvsState.kt index 06e6813..7ae8398 100644 --- a/paystack/src/main/java/co/paystack/android/model/AvsState.kt +++ b/paystack/src/main/java/co/paystack/android/model/AvsState.kt @@ -1,5 +1,8 @@ package co.paystack.android.model +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) data class AvsState( val name: String, val slug: String,