PaystackHQ / paystack-android

Paystack SDK for Android. Accept payments on Android
Apache License 2.0
126 stars 101 forks source link

So many requests. #26

Closed solokingjec closed 7 years ago

solokingjec commented 7 years ago

I have been testing on this project but I noticed one big problem. The app fails and gives an error 'card can not be null'. then it starts posting wildly on my backed.

ibrahimlawal commented 7 years ago

@solokingjec I'm sorry... but you do know this information doesn't help us help you, right? What exactly are you doing? What did you expect?

solokingjec commented 7 years ago

My flow is exactly the way its described here.

  1. The android app requests the back end to initialize a transaction. This is happening right as my back end is returning a valid access_code to the android.
  2. The android app charges the card using the access_code. This is where its failing. I haven't modified the code to charge card. Am getting an error 'card can not be null' and the android app keeps sending an initialize transaction request to my back end.

The app was working fine and it just got messed up.

ibrahimlawal commented 7 years ago

What is your code? Why is the card being sent for charging null?

solokingjec commented 7 years ago

package app.merides.com.meridesapp.activities;

import android.app.ProgressDialog; import android.os.AsyncTask; import android.os.Bundle; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast;

import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.URL;

import app.merides.com.meridesapp.AppController; import app.merides.com.meridesapp.utilities.AppConstants; import co.paystack.android.Paystack; import co.paystack.android.PaystackSdk; import co.paystack.android.Transaction; import co.paystack.android.exceptions.ExpiredAccessCodeException; import co.paystack.android.model.Card; import co.paystack.android.model.Charge;

public class PayStackDetails extends AppCompatActivity { // To get started quickly, change this to your heroku deployment of // https://github.com/PaystackHQ/sample-charge-card-backend // Step 1. Visit https://github.com/PaystackHQ/sample-charge-card-backend // Step 2. Click "Deploy to heroku" // 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.heroku-app.com) into the space below String backend_url = AppConstants.getBaseUrl(); // Set this to a public key that matches the secret key you supplied while creating the heroku instance String paystack_public_key = "pktest***";

EditText mEditCardNum;
EditText mEditCVC;
EditText mEditExpiryMonth;
EditText mEditExpiryYear;

TextView mTextError;
TextView mTextBackendMessage;

Card card;

ProgressDialog dialog;
private TextView mTextReference;
private Charge charge;
private Transaction transaction;
private int amount;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_pay_stack_details);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    if (BuildConfig.DEBUG && (backend_url.equals(""))) {
        throw new AssertionError("Please set a backend url before running the sample");
    }
    if (BuildConfig.DEBUG && (paystack_public_key.equals(""))) {
        throw new AssertionError("Please set a public key before running the sample");
    }

    PaystackSdk.setPublicKey(paystack_public_key);

    Bundle extras = getIntent().getExtras();
    amount=extras.getInt("amount");
    mEditCardNum = (EditText) findViewById(R.id.edit_card_number);
    mEditCVC = (EditText) findViewById(R.id.edit_cvc);
    mEditExpiryMonth = (EditText) findViewById(R.id.edit_expiry_month);
    mEditExpiryYear = (EditText) findViewById(R.id.edit_expiry_year);

    Button mButtonPerformTransaction = (Button) findViewById(R.id.button_perform_transaction);

    mTextError = (TextView) findViewById(R.id.textview_error);
    mTextBackendMessage = (TextView) findViewById(R.id.textview_backend_message);
    mTextReference = (TextView) findViewById(R.id.textview_reference);

    //initialize sdk
    PaystackSdk.initialize(getApplicationContext());

    //set click listener
    mButtonPerformTransaction.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            //validate form
            validateCardForm();
            //check card validity
            if (card != null && card.isValid()) {
                dialog = new ProgressDialog(PayStackDetails.this);
                dialog.setMessage("Performing transaction... please wait");
                dialog.setCancelable(false);

                dialog.show();

                try {
                    startAFreshCharge();
                } catch (Exception e) {
                    PayStackDetails.this.mTextError.setText(String.format("An error occured hwile charging card: %s %s", e.getClass().getSimpleName(), e.getMessage()));
                }

            }
        }
    });
}

private void startAFreshCharge() {
    // initialize the charge
    charge = new Charge();
    // Perform transaction/initialize on our server to get an access code
    // documentation: https://developers.paystack.co/reference#initialize-a-transaction

    new fetchAccessCodeFromServer().execute(backend_url + "newaccesscode/" + AppController.apiKey + "/" + AppController.getUserCode()+"/"+amount);

}

/**
 * Method to validate the form, and set errors on the edittexts.
 */
private void validateCardForm() {
    //validate fields
    String cardNum = mEditCardNum.getText().toString().trim();

    if (isEmpty(cardNum)) {
        mEditCardNum.setError("Empty card number");
        return;
    }

    //build card object with ONLY the number, update the other fields later
    card = new Card.Builder(cardNum, 0, 0, "").build();
    if (!card.validNumber()) {
        mEditCardNum.setError("Invalid card number");
        return;
    }

    //validate cvc
    String cvc = mEditCVC.getText().toString().trim();
    if (isEmpty(cvc)) {
        mEditCVC.setError("Empty cvc");
        return;
    }
    //update the cvc field of the card
    card.setCvc(cvc);

    //check that it's valid
    if (!card.validCVC()) {
        mEditCVC.setError("Invalid cvc");
        return;
    }

    //validate expiry month;
    String sMonth = mEditExpiryMonth.getText().toString().trim();
    int month = -1;
    try {
        month = Integer.parseInt(sMonth);
    } catch (Exception ignored) {
    }

    if (month < 1) {
        mEditExpiryMonth.setError("Invalid month");
        return;
    }

    card.setExpiryMonth(month);

    String sYear = mEditExpiryYear.getText().toString().trim();
    int year = -1;
    try {
        year = Integer.parseInt(sYear);
    } catch (Exception ignored) {
    }

    if (year < 1) {
        mEditExpiryYear.setError("invalid year");
        return;
    }

    card.setExpiryYear(year);

    //validate expiry
    if (!card.validExpiryDate()) {
        mEditExpiryMonth.setError("Invalid expiry");
        mEditExpiryYear.setError("Invalid expiry");
    }
}

@Override
public void onPause() {
    super.onPause();

    if ((dialog != null) && dialog.isShowing()) {
        dialog.dismiss();
    }
    dialog = null;
}

private void chargeCard() {
    transaction = null;
    PaystackSdk.chargeCard(PayStackDetails.this, charge, new Paystack.TransactionCallback() {
        // This is called only after transaction is successful
        @Override
        public void onSuccess(Transaction transaction) {
            dismissDialog();

            PayStackDetails.this.transaction = transaction;
            mTextError.setText(" ");
            Toast.makeText(PayStackDetails.this, transaction.getReference(), Toast.LENGTH_LONG).show();
            updateTextViews();
            showDialogSuccess();
            //new verifyOnServer().execute(transaction.getReference());
        }

        // This is called only before requesting OTP
        // Save reference so you may send to server if
        // error occurs with OTP
        // No need to dismiss dialog
        @Override
        public void beforeValidate(Transaction transaction) {
            PayStackDetails.this.transaction = transaction;
            Toast.makeText(PayStackDetails.this, transaction.getReference(), Toast.LENGTH_LONG).show();
            updateTextViews();
        }

        @Override
        public void onError(Throwable error, Transaction transaction) {
            // If an access code has expired, simply ask your server for a new one
            // and restart the charge instead of displaying error
            PayStackDetails.this.transaction = transaction;
            if (error instanceof ExpiredAccessCodeException) {
                PayStackDetails.this.startAFreshCharge();
                PayStackDetails.this.chargeCard();
                return;
            }

            dismissDialog();

            if (transaction.getReference() != null) {
                Toast.makeText(PayStackDetails.this, transaction.getReference() + " concluded with error: " + error.getMessage(), Toast.LENGTH_LONG).show();
                mTextError.setText(String.format("%s  concluded with error: %s %s", transaction.getReference(), error.getClass().getSimpleName(), error.getMessage()));
                new verifyOnServer().execute(transaction.getReference());
            } else {
                Toast.makeText(PayStackDetails.this, error.getMessage(), Toast.LENGTH_LONG).show();
                mTextError.setText(String.format("Error: %s %s", error.getClass().getSimpleName(), error.getMessage()));
            }
            updateTextViews();
        }

    });
}

private AlertDialog d;
private void showDialogSuccess() {
    AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
    LayoutInflater inflater = getLayoutInflater();
    final View dialogView = inflater.inflate(R.layout.dialogconfirmed, null);
    dialogBuilder.setView(dialogView).setCancelable(false);
    TextView message = (TextView) dialogView.findViewById(R.id.textView54);
    message.setText("Your request has been received and its being processed. Thankyou for choosing Meride.");
    Button cancel=(Button)dialogView.findViewById(R.id.okay);
    cancel.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            d.dismiss();
            finish();
        }
    });
    d = dialogBuilder.create();
    d.show();
}

private void dismissDialog() {
    if ((dialog != null) && dialog.isShowing()) {
        dialog.dismiss();
    }
}

private void updateTextViews() {
    if (transaction.getReference() != null) {
        mTextReference.setText(String.format("Reference: %s", transaction.getReference()));
    } else {
        mTextReference.setText("No transaction");
    }
}

private boolean isEmpty(String s) {
    return s == null || s.length() < 1;
}

private class fetchAccessCodeFromServer extends AsyncTask<String, Void, String> {
    private String error;

    @Override
    protected void onPostExecute(String result) {
        super.onPostExecute(result);
        if (result != null) {
            charge.setAccessCode(result);
            charge.setCard(card);
            chargeCard();
        } else {
            PayStackDetails.this.mTextBackendMessage.setText(String.format("There was a problem getting a new access code form the backend: %s", error));
            dismissDialog();
        }
    }

    @Override
    protected String doInBackground(String... ac_url) {
        try {
            URL url = new URL(ac_url[0]);
            Log.e(AppController.TAG, url.toString());

            BufferedReader in = new BufferedReader(
                    new InputStreamReader(
                            url.openStream()));

            String inputLine;
            inputLine = in.readLine();
            in.close();
            return inputLine;
        } catch (Exception e) {
            error = e.getClass().getSimpleName() + ": " + e.getMessage();
        }
        return null;
    }
}

private class verifyOnServer extends AsyncTask<String, Void, String> {
    private String reference;
    private String error;

    @Override
    protected void onPostExecute(String result) {
        super.onPostExecute(result);
        if (result != null) {
            PayStackDetails.this.mTextBackendMessage.setText(String.format("Gateway response: %s", result));

        } else {
            PayStackDetails.this.mTextBackendMessage.setText(String.format("There was a problem verifying %s on the backend: %s ", this.reference, error));
            dismissDialog();
        }
    }

    @Override
    protected String doInBackground(String... reference) {
        try {
            this.reference = reference[0];
            URL url = new URL(backend_url + "verify/" + this.reference);
            BufferedReader in = new BufferedReader(
                    new InputStreamReader(
                            url.openStream()));

            String inputLine;
            inputLine = in.readLine();
            in.close();
            return inputLine;
        } catch (Exception e) {
            error = e.getClass().getSimpleName() + ": " + e.getMessage();
        }
        return null;
    }
}

}

solokingjec commented 7 years ago

Please if you are in a position, compile the example provided in this repo and see if you can reproduce the error.

solokingjec commented 7 years ago

screenshot from 2017-07-16 09-14-21 screenshot from 2017-07-16 09-13-59

Its failing on this line

PaystackSdk.chargeCard(PayStackDetails.this, charge, new Paystack.TransactionCallback() {

ibrahimlawal commented 7 years ago

@solokingjec It all goes back to answering one question : Why is card null? Running on my system doesn't give same error :(

solokingjec commented 7 years ago

I am doing this project for a friend of mine. I sent to him the apk to test and it worked just once and then it started showing that error on both end. How its giving that error I still don't understand. If you can send a working apk please do.

memail13 commented 7 years ago

@ibrahimlawal , we are on this issue since Friday and i tested the app it work fine, suddenly it keep post transaction on server when it keep give error 'card can not be null' and when i login to my paystack account it show many transactions which i only charge card once on test mode, https://dashboard.paystack.co/#/transactions

ibrahimlawal commented 7 years ago

Hey @memail13 that's something I can check.. one moment

ibrahimlawal commented 7 years ago

If you are on skype, we can step through your code together. My ID is skype:profibro

solokingjec commented 7 years ago

I have found the problem. I had to change the method charge card. 1.Set card again. 2.Also set the amount to charge and email. Viola!! it worked. @ibrahimlawal private void chargeCard() { transaction = null; charge.setCard(card); charge.setAmount(amount); charge.setEmail(AppController.getUserEmail()); Log.e(AppController.TAG,charge.getCard().getNumber().toString()); PaystackSdk.chargeCard(PayStackDetails.this, charge, new Paystack.TransactionCallback() {

I don't know how the card held by the object charge is being set to null.

ibrahimlawal commented 7 years ago

@solokingjec You are the man. God Bless you!

swananddesh commented 6 years ago

Hi Paystack Team,

I would like to ask you regarding token generation in latest version of Paystack SDK. How one should generate a token? In earlier version of Paystack (i.e. 1.0) their was one function PaystackSdk.createToken(card, new Paystack.TokenCallback() {}

Note : Currently I am using Paystack version 3.0.7.

Thanks in advance. ;)