I have just implemented Stripe into Android for the first time and the library is very clean, but I am having an issue when launching the AddPaymentActivity using:
AddPaymentMethodActivityStarter(this).startForResult(
AddPaymentMethodActivityStarter.Args.Builder()
.setShouldAttachToCustomer(true)
.build())
When I use shouldAttachToCustomer(false) the Activity immediately finishes when the tick is pressed and my callback in onActivityResult is triggered in the calling Activity. However when I use shouldAttachToCustomer(true) to save the card information, when entering card details and clicking on the tick the progress bar just continually shows a loading state.
However when I press the back button (after the tick has already been pressed and the progress bar is stuck loading) the card is added to the account, I have verified this by launching STPPaymentOptionsViewController on our iOS version to view all attached cards. The card is not saved when the tick is clicked, but is immediately saved when the back button is pressed after the tick icon. I have verified this with a log statement in the return from my call to our APIs ephemeral key endpoint - the call to fetch an ephemeral key is not made until the back button is pressed.
In my Application onCreate():
class CustomApplication : Application() {
override fun onCreate() {
super.onCreate()
setupPaymentSession()
}
private fun setupPaymentSession() {
PaymentConfiguration.init(
applicationContext,
BuildConfig.STRIPE_KEY
)
}
}
In the calling Activity:
class DashboardActivity : AppCompatActivity() {
private lateinit var mBinding: ActivityDashboardBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_dashboard)
mBinding.lifecycleOwner = this
if (intent.hasExtra(SHOULD_SETUP_STRIPE_PAYMENTS)) {
if (intent.getBooleanExtra(SHOULD_SETUP_STRIPE_PAYMENTS, false)) addStripePaymentMethod()
}
}
private fun addStripePaymentMethod() {
setupPaymentSession()
AddPaymentMethodActivityStarter(this).startForResult(
AddPaymentMethodActivityStarter.Args.Builder()
.setShouldAttachToCustomer(true)
.build())
}
private fun setupPaymentSession() {
CustomerSession.initCustomerSession(
this,
StripeKeyProvider(this),
true
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == AddPaymentMethodActivityStarter.REQUEST_CODE &&
resultCode == Activity.RESULT_OK && data != null) {
val paymentMethod: PaymentMethod? =
AddPaymentMethodActivityStarter.Result.fromIntent(data)?.paymentMethod
Log.v("Debug", "Card returned: $paymentMethod")
CardAddedAlertDialog(this, 5000).show()
}
}
}
The EphemeralKeyProvider:
class StripeKeyProvider(private val lifecycleOwner: LifecycleOwner) : EphemeralKeyProvider {
override fun createEphemeralKey(
apiVersion: String,
keyUpdateListener: EphemeralKeyUpdateListener
) {
ServiceManager.stripeService.getEphemeralKey(apiVersion).observe(lifecycleOwner, Observer { response ->
if (response.status == Status.SUCCESS) {
response.data?.let { responseData ->
Log.v("Debug", "$responseData")
keyUpdateListener.onKeyUpdate(responseData)
} ?: run {
keyUpdateListener.onKeyUpdateFailure(response.code ?: 99, "Server returned no data")
}
} else if (response.status == Status.ERROR) {
keyUpdateListener.onKeyUpdateFailure(response.code ?: 99, "Failed to retrieve ephemeral key: ${response.msg ?: "No Message"}")
}
})
}
}
Any help will be much appreciated, I have a feeling maybe there is something I may have missed but have been battling with this for several hours now and haven't made any progress at all.
The only similar issue I have found is #534, however the solution for that was to return the body as a String from the request to the ephemeral key provider. Given that the card is added to the account (albeit not when I would expect it to be) I am assuming this issue is none related, as I am passing our response from the API as a String:
interface StripeAPI {
@Multipart
@POST("...")
suspend fun getEphemeralKey(@Part("api_version") apiVersion: String): Response<String>
}
Android 10, Samsung Galaxy S9+
Using gradle:
implementation 'com.stripe:stripe-android:14.2.1'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
minSdkVersion 24
targetSdkVersion 29
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
dataBinding {
enabled = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
@thebillington I was able to verify that the scenario you described worked on my end. The response from your backend should look something like the JSON below. Can you verify that this is what you're receiving?
{
"id": "ephkey_1GUuzgL6pqDH",
"object": "ephemeral_key",
"associated_objects": [{
"id": "cus_H311yR",
"type": "customer"
}],
"created": 1586179684,
"expires": 1586183284,
"livemode": false,
"secret": "ek_test_YWNjdF8xRzZPbmpMNnBxREgyZkRKLHB2UtraGk_00kLG0lmgZ"
}
@mshafrir-stripe I've tested the response from the ephemeral key endpoint and can verify that I'm receiving back:
{
"id": "ephkey_1GUv7rFoOa6jmIikDHXkxzcR",
"object": "ephemeral_key",
"associated_objects": [
{
"id": "cus_H1q3Vl0WXZEUlG",
"type": "customer"
}
],
"created": 1586180191,
"expires": 1586183791,
"livemode": false,
"secret": "ek_test_YWNjdF8xQWVRUmxGb09hNmptSWlrLHQ0RHpqY2JYRmM1UHRKNXhXNlJ2bkF2cmNwR2phN3Q_00v6YerEp7"
}
I've done a lot more testing through the day and have been inspecting network traffic for the request. I have found that the first time I login on a brand new account the request is successful to add a card, however on any further requests the previously described issue occurs again. This means the issue could at least in part be affected by our backend.
In addition if I logout of an account and leave it for a period of time, when I log back in later and add a new card the first attempt will succeed, followed by failed attempts as previously described.
For a successful attempt the network calls look like this:
api.stripe.com/v1/payment_methods
Followed by 3 concurrent calls:
q.stripe.com?publishable_key...
q.stripe.com?app_name...
api.stripe.com/v1/payment_methods/<key>/attach
When the call fails:
api.stripe.com/v1/payment_methods
Followed by:
q.stripe.com?publishable_key
Then when the back button is pressed and the card is added to the account:
our-server.io/call-to-get-ephemeral-key
api.stripe.com/v1/customers/<key>
q.stripe.com?app_name
When the call to get the ephemeral key from our server succeeds:
q.stripe.com?app_name
api.stripe.com/v1/payment_methods/<key>/attach
When the issue occurs and the back button is pressed there is a call made to the ephemeral key endpoint, whereas when the card is added straight away without the issue occurring there is no call to the endpoint.
I have checked and when the card is added successfully the ephemeral key endpoint is called on app startup, but when the card fails to add the ephemeral key endpoint is not called until the back button is pressed. This is the code that handles login:
class LoginActivity : AppCompatActivity() {
private lateinit var mBinding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_login)
mBinding.lifecycleOwner = this
if (SharedPrefsManager.getAuthToken() != null) navigateToDashboard()
}
fun navigateToDashboard() {
val intent = Intent(this, DashboardActivity::class.java)
intent.putExtra(SHOULD_SETUP_STRIPE_PAYMENTS, true)
startActivity(intent)
finish()
}
}
Any more help much appreciated.
I have found a way to consistently get this to work by:
I initially thought that the issue may be on the server with using the same auth token (as an auth token refresh seems to do the trick), but if I simply logout and log back in the issue is still there. The only way to get it to work is to remove the auth token and wipe the database, then restart the app.
This gives me the suspicion that it may be linked to the context used for configuring the PaymentConfiguration, however having moved the call to initialise the PaymentConfiguration into the calling Activity (using either application or activity level context), the issue is still present:
private fun setupPaymentSession() {
PaymentConfiguration.init(
applicationContext,
BuildConfig.STRIPE_KEY
)
CustomerSession.initCustomerSession(
this,
StripeKeyProvider(this),
true
)
}
OR
private fun setupPaymentSession() {
PaymentConfiguration.init(
this,
BuildConfig.STRIPE_KEY
)
CustomerSession.initCustomerSession(
this,
StripeKeyProvider(this),
true
)
}
If I logout on its own (refresh OAuth), or force close the app on its own the issue is still present. The only way to get it to work is to logout and force close. I am still looking into this, any insights you can offer to help diagnose would be much appreciated.
Can you try setting up your CustomerSession in your Application subclass?
@mshafrir-stripe my architecture uses coroutines and LiveData to observe the api call meaning my endpoint to get the ephemeral key requires a lifecycle owner:
ServiceManager.stripeService.getEphemeralKey(apiVersion).observe(lifecycleOwner, Observer { response ->
if (response.status == Status.SUCCESS) {
response.data?.let { responseData ->
keyUpdateListener.onKeyUpdate(responseData)
} ?: run {
keyUpdateListener.onKeyUpdateFailure(response.code ?: 99, "Server returned no data")
}
} else if (response.status == Status.ERROR) {
keyUpdateListener.onKeyUpdateFailure(response.code ?: 99, "Failed to retrieve ephemeral key: ${response.msg ?: "No Message"}")
}
})
I will remove the need for the call to be lifecycle managed and try setting up the CustomerSession in my Application subclass as you suggested. I'll let you know the result.
@mshafrir-stripe your solution worked.
Setting up the CustomerSession in the Application subclass was what was needed to ensure the ephemeral key is always fetched. To get it to work I had to change my call to the ephemeral key endpoint to no longer be a suspended coroutine call and return type Call<String> instead:
interface StripeAPI {
@Multipart
@POST("...")
fun getEphemeralKey(@Part("api_version") apiVersion: String): Call<String>
}
Then I updated the EphemeralKeyProvider:
class StripeKeyProvider() : EphemeralKeyProvider {
override fun createEphemeralKey(
apiVersion: String,
keyUpdateListener: EphemeralKeyUpdateListener
) {
val call = RetrofitClient.createService(StripeAPI::class.java).getEphemeralKey(apiVersion)
call.enqueue(object: Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
response.body()?.let {
keyUpdateListener.onKeyUpdate(it)
} ?: run {
keyUpdateListener.onKeyUpdateFailure(99, "Server returned no data")
}
}
override fun onFailure(call: Call<String>, t: Throwable) {
keyUpdateListener.onKeyUpdateFailure(99, "Failed to retrieve ephemeral key: $t")
}
})
}
}
Lastly I moved the call to initialise the CustomerSession in my Application subclass:
class CustomApplication : Application() {
init {
instance = this
}
override fun onCreate() {
super.onCreate()
setupPaymentSession()
}
private fun setupPaymentSession() {
if (SharedPrefsManager.getAuthToken() != null) {
PaymentConfiguration.init(
applicationContext,
BuildConfig.STRIPE_KEY
)
CustomerSession.initCustomerSession(
this,
StripeKeyProvider(),
true
)
}
}
companion object {
private var instance: ContextAwareApplication? = null
fun setupPaymentSession() {
instance?.setupPaymentSession()
}
}
}
When logging in and setting the OAuth token I called CustomApplication.setupPaymentSession() to initialise the payment session (the ephemeral key endpoint only returns a value if the OAuth token is set):
fun setAuthToken(authToken: String?) {
...
ContextAwareApplication.setupPaymentSession()
}
Thanks so much for your help in debugging, I really appreciate your time.
@thebillington I'm glad to hear you figured it out
I am facing the same issue, I have tried with the application class object also.
private void setUpPaymentSession () {
PaymentConfiguration.init(getApplicationContext(), Constants.STRIPE_PUBLISH_KEY);
CustomerSession.initCustomerSession(this, new ExampleEphemeralKeyProvider(), true);
}
ExampleEphemeralKeyProvider class
public class ExampleEphemeralKeyProvider implements EphemeralKeyProvider {
@Override
public void createEphemeralKey(@NonNull @Size(min = 4) String apiVersion, @NonNull final EphemeralKeyUpdateListener keyUpdateListener) {
callGetEphemeralKey(apiVersion);
}
private void callGetEphemeralKey(String mApiVersion) {
final ModelEphemeralKeyRequest mModelEphemeralKeyRequest = new ModelEphemeralKeyRequest();
mModelEphemeralKeyRequest.setApp_version(mApiVersion);
Observable<ModelEphemeralKeyResponse> observable = MyApp.getInstance().getServiceWrapperInstance().getEphemeralKey(mModelEphemeralKeyRequest).compose(MyApp.getInstance().applyObservableAsync());
observable.subscribe(getEphemeralKey());
}
private Observer<ModelEphemeralKeyResponse> getEphemeralKey() {
return new Observer<ModelEphemeralKeyResponse>() {
@Override
public void onSubscribe(@NonNull Disposable mDisposable) {
}
@Override
public void onNext(@NonNull ModelEphemeralKeyResponse mEphemeralKeyResponse) {
}
@Override
public void onError(@NonNull Throwable mThrowable) {
}
@Override
public void onComplete() {
}
};
}
}
@Mihir3646 in what class does this code reside?
private void setUpPaymentSession () {
PaymentConfiguration.init(getApplicationContext(), Constants.STRIPE_PUBLISH_KEY);
CustomerSession.initCustomerSession(this, new ExampleEphemeralKeyProvider(), true);
}
@mshafrir-stripe Thank you for the quick reply.
The code resides in the Application class.
@Mihir3646 what is the exact issue you're experiencing?
@mshafrir-stripe While I am calling the presentPaymentMethodSelection method it will open the PaymentMethodsActivity and then open AddPaymentMethodActivity, after adding the card info when I try to add that it continuously loading.
Here is the video.
https://drive.google.com/file/d/1QtbBuBesNRpbXepEc0wzymEinwqahrZ3/view?usp=sharing
@mshafrir-stripe Any solution? I also contact the support still not get any proper solution. If resolve soon so, It's a great help to me.
@Mihir3646 what version of the SDK are you using?
@msaviano-stripe I am using the 16.0.1 version SDK.
@Mihir3646 when you run adb logcat, do you see anything in the logs that indicates an issue?
@mshafrir-stripe Nothing. Is ther any specific tag that I can track?
Just this logs when adding card detail, but no log after "right image click"
W/IInputConnectionWrapper: getSelectedText on inactive InputConnection
W/IInputConnectionWrapper: getTextAfterCursor on inactive InputConnection
W/IInputConnectionWrapper: sendKeyEvent on inactive InputConnection
W/IInputConnectionWrapper: sendKeyEvent on inactive InputConnection
W/IInputConnectionWrapper: getTextBeforeCursor on inactive InputConnection
@mshafrir-stripe Any update or any changes I can do? Still not able to resolve the issue.
@Mihir3646 is it possible that you aren't calling onKeyUpdate() or onKeyUpdateFailure() in your EphemeralKeyProvider implementation? I don't see that happening in your code.
@Mihir3646 can you share more code examples of how you're using PaymentSession?
@mshafrir-stripe Here is all the thing what I did
Payment Session which I am setting in the Application class before landing on the payment page of my app.
public void setUpPaymentSession (String publisherKey) {
PaymentConfiguration.init(getApplicationContext(), publisherKey);
CustomerSession.initCustomerSession(this, new ExampleEphemeralKeyProvider(), false);
}
ExampleEphemeralKeyProvider class
public class ExampleEphemeralKeyProvider implements EphemeralKeyProvider, EphemeralKeyUpdateListener {
EphemeralKeyUpdateListener mEphemeralKeyUpdateListener;
@Override
public void createEphemeralKey(@NonNull @Size(min = 4) String apiVersion,
@NonNull final EphemeralKeyUpdateListener keyUpdateListener) {
callGetEphemeralKey(apiVersion);
mEphemeralKeyUpdateListener = keyUpdateListener;
}
private void callGetEphemeralKey(String mApiVersion) {
final ModelEphemeralKeyRequest mModelEphemeralKeyRequest = new ModelEphemeralKeyRequest();
mModelEphemeralKeyRequest.setApp_version(mApiVersion);
Observable<ModelEphemeralKeyResponse> observable =
getServiceWrapperInstance().getEphemeralKey(mModelEphemeralKeyRequest)
.compose(applyObservableAsync());
observable.subscribe(getEphemeralKey());
}
private Observer<ModelEphemeralKeyResponse> getEphemeralKey() {
return new Observer<ModelEphemeralKeyResponse>() {
@Override
public void onSubscribe(@NonNull Disposable mDisposable) {
}
@Override
public void onNext(@NonNull ModelEphemeralKeyResponse mEphemeralKeyResponse) {
// here is the response json
/*
{
"id": "ephkey_id",
"object": "ephemeral_key",
"associated_objects": [
{
"id": "cus_id",
"type": "customer"
}
],
"created": 1606233260,
"expires": 1606236860,
"livemode": false,
"secret": "ek_test_secrate"
}
*/
}
@Override
public void onError(@NonNull Throwable mThrowable) {
}
@Override
public void onComplete() {
}
};
}
@Override
public void onKeyUpdate(@NotNull String mS) {
}
@Override
public void onKeyUpdateFailure(int mI, @NotNull String mS) {
}
}
On my Application checkout page's onCreate I am calling the below thing
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
callGetPaymentIntent(boTyPoint);
paymentSession = new PaymentSession(
this, new PaymentSessionConfig.Builder()
.setShippingInfoRequired(false)
.setShippingMethodsRequired(false)
.setBillingAddressFields(BillingAddressFields.None)
.setShouldShowGooglePay(true)
.build());
setupPaymentSession();
}
Calling the PaymentIntent API
private void callGetPaymentIntent(String points) {
if (Network avability check) {
final ModelPaymentIntentRequest mModelPaymentIntentRequest = new ModelPaymentIntentRequest();
mModelPaymentIntentRequest.setPoints(points);
Observable<ModelPaymentIntentResponse> observable =
getServiceWrapperInstance().getPaymentIntent(mModelPaymentIntentRequest)
.compose(applyObservableAsync());
observable.subscribe(getPaymentIntent());
} else {
DisplayDialog.getInstance().dismissProgressDialog();
showSnackBar(binding.btnCheckout, getString(R.string.no_internet_connection), 0);
}
}
private Observer<ModelPaymentIntentResponse> getPaymentIntent() {
return new Observer<ModelPaymentIntentResponse>() {
@Override
public void onSubscribe(@NonNull Disposable mDisposable) {
mCompositeDisposable.add(mDisposable);
DisplayDialog.getInstance().showProgressDialog(getContext());
}
@Override
public void onNext(@NonNull ModelPaymentIntentResponse mModelPaymentIntentResponse) {
DisplayDialog.getInstance().dismissProgressDialog();
// set data in UI from mModelPaymentIntentResponse
// Also getting the client_secret
binding.tvSelectPaymentMethod.setVisibility(View.VISIBLE);
}
@Override
public void onError(@NonNull Throwable mThrowable) {
showSnackBar(binding.btnCheckout, Utils.getInstance().handleErrorResponse(mThrowable), 0);
DisplayDialog.getInstance().dismissProgressDialog();
Log.e("onError :", "Error");
}
@Override
public void onComplete() {
DisplayDialog.getInstance().dismissProgressDialog();
Log.i("onComplete :", "");
}
};
}
Payment Setup Method
private void setupPaymentSession() {
paymentSession.init(new PaymentSession.PaymentSessionListener() {
@Override
public void onCommunicatingStateChanged(boolean isCommunicating) {
if (isCommunicating) {
// update UI to indicate that network communication is in progress
} else {
// update UI to indicate that network communication has completed
}
Log.d("CommStateChange", "isCommunicating: " + isCommunicating);
}
@Override
public void onError(int errorCode, @NotNull String errorMessage) {
Log.d("StripePayment", "Error Code:" + errorCode + "\nError Msg:" + errorMessage);
}
@Override
public void onPaymentSessionDataChanged(@NonNull PaymentSessionData data) {
if (data.getUseGooglePay()) {
// customer intends to pay with Google Pay
} else {
final PaymentMethod paymentMethod = data.getPaymentMethod();
if (paymentMethod != null) {
// Display information about the selected payment method
if (paymentMethod.card != null) {
// ******* Updateing the things here
}
}
}
// Update your UI here with other data
if (data.isPaymentReadyToCharge()) {
// Use the data to complete your charge - see below.
binding.btnCheckout.setEnabled(true);
}
}
});
}
onActivity Result
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data != null) {
paymentSession.handlePaymentData(requestCode, resultCode, data);
}
}
Where do you call mEphemeralKeyUpdateListener in ExampleEphemeralKeyProvider?
Actually, not calling. What is the use of it and when I need to call?
@Mihir3646 please take a look at our documentation for how to implement an EphemeralKeyProvider
https://stripe.com/docs/mobile/android/basic#set-up-ephemeral-key-client-side
Tons of Thanks @mshafrir-stripe
I missed a very important line of code. Here are the changes what I made in my ExampleEphemeralKeyProvider class and It's working
public class ExampleEphemeralKeyProvider implements EphemeralKeyProvider {
EphemeralKeyUpdateListener mEphemeralKeyUpdateListener;
@Override
public void createEphemeralKey(@NonNull @Size(min = 4) String apiVersion,
@NonNull final EphemeralKeyUpdateListener keyUpdateListener) {
callGetEphemeralKey(apiVersion);
mEphemeralKeyUpdateListener = keyUpdateListener;
}
private void callGetEphemeralKey(String mApiVersion) {
final ModelEphemeralKeyRequest mModelEphemeralKeyRequest = new ModelEphemeralKeyRequest();
mModelEphemeralKeyRequest.setApp_version(mApiVersion);
Observable<ModelEphemeralKeyResponse> observable =
getServiceWrapperInstance().getEphemeralKey(mModelEphemeralKeyRequest)
.compose(applyObservableAsync());
observable.subscribe(getEphemeralKey());
}
private Observer<ModelEphemeralKeyResponse> getEphemeralKey() {
return new Observer<ModelEphemeralKeyResponse>() {
@Override
public void onSubscribe(@NonNull Disposable mDisposable) {
}
@Override
public void onNext(@NonNull ModelEphemeralKeyResponse mEphemeralKeyResponse) {
// Very most important line of code, and I missed my bad 馃憥
// After calling onKeyUpdate listner I am able to get error popup if EphemeralKey is wrong
// *********************************************************************************************
mEphemeralKeyUpdateListener.onKeyUpdate(new Gson().toJson(mEphemeralKeyResponse));
// *********************************************************************************************
}
@Override
public void onError(@NonNull Throwable mThrowable) {
}
@Override
public void onComplete() {
}
};
}
}
Most helpful comment
Where do you call
mEphemeralKeyUpdateListenerinExampleEphemeralKeyProvider?