Open sawyermade opened 1 year ago
I switched to using the xml stuff instead of the compose and it worked. Does this not work with compose?
@sawyermade this looks related to this issue: https://github.com/braintree/braintree-android-drop-in/issues/413. We do not offer explicit support for JetPack Compose, but it should work. The SDK needs access to the activity to launch the DropIn Activity in the same task as the host Activity.
Okay, thanks for the response. I wanted to start updating are UI to using compose but maybe I will hold off on that for a while, I appreciate the response and thanks!
I had the same issue. Initializing DropInClient dropInClient = new DropInClient(this, "sandbox_xxx_token_key");
and then calling dropInClient.launchDropIn(dropInRequest);
resulted in an error and the application crashing:
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.app.example....PaymentActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void androidx.activity.result.ActivityResultLauncher.launch(java.lang.Object)' on a null object reference
From what I understand based on observations and testing, the issue lies in the fact that initializing DropInClient takes some time (likely asynchronously verifying the passed key in the background). Therefore, the line dropInClient.launchDropIn(dropInRequest);
is executed before the DropInClient is fully initialized, resulting in a NullPointerException.
My solution was to move dropInClient.launchDropIn(dropInRequest); inside onResume, and the problem was resolved:
@Override
protected void onResume() {
super.onResume();
dropInClient.launchDropIn(dropInRequest);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.payment_ly);
createDropInRequest();
dropInClient = new DropInClient(this, "sandbox_6mgp9yhd_cq3sptm5w7kvss4n");
dropInClient.setListener(this);
}
This ensures that the launchDropIn method is called after the DropInClient is fully initialized, preventing the NullPointerException.
The solution is to introduce a slight delay using any technique to give the dropInClient enough time to initialize before calling launchDropIn().
I hope this will help someone because due to the desperately poor and unclear documentation, I wasted half of a day just trying to initialize dropInClient. With 10 different versions to navigate through until you reach the current one that works, and even on the official website of the current version, you don't have all the necessary information, people lose a couple of days trying to implement a simple payment, and some even give up.
@bolebolo thanks for your writeup of what you're experiencing. You're correct in your understanding, originally we made an implicit assumption that launchDropIn()
would be called in response to some user interaction made within the Merchant's host activity. When called in response to a user action, the Activity it presumably in the onResume()
state.
There are several reasons why the SDK API is the way it is today, and we are currently at work to improve the SDK so that you won't need this workaround. We're working on a next-major-version beta of our braintree/braintree_android core SDK. Once the Core SDK beta is released, we will subsequently release new beta versions of our DropIn library to remove these restrictions. We don't have a specific timeline for when these new versions will be available, but we are aware of challenges like these and we're planning changes that will make the SDK less opinonated.
Feedback like this helps improve the SDK tremendously. If there's anything else you'd like to see change, please feel free to open a discussion with a detailed description of a feature (or pattern) that you'd like us to consider in the next major version.
I was able to get it to work in compose by creating a compose activity then changing the inherited class from component activity to fragment activity and I can then launch the drop in from a composable. Works pretty well but yeah, it’s a little hacky.
This is how I was able to get it working. Another thing you guys need to do is fix the Client Token Provider to be able to work with Ktor too instead of just retrofit, I had to write a little retrofit for this specific module where the rest of them are using the Ktor for my API calls. Not terrible but definitely not so cut and dry when using full kotlin with kotlin Gradle, compose, code, etc. Still works great though atm as long as you know how to get it to pop up bc your documentation pretty much sucks, no offense.
package com.sjursolutions.creditcard
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.fragment.app.FragmentActivity
import com.braintreepayments.api.ClientTokenProvider
import com.braintreepayments.api.DropInClient
import com.braintreepayments.api.DropInListener
import com.braintreepayments.api.DropInRequest
import com.braintreepayments.api.DropInResult
import com.sjursolutions.creditcard.ui.theme.Vip2Theme
import com.sjursolutions.myprefs.DataStoreManager
import kotlinx.coroutines.runBlocking
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
// Constants
private const val TAG = "CreditCardActivity"
class CreditCardActivity : FragmentActivity(), DropInListener {
// Class vars
private lateinit var dropInClient: DropInClient
private lateinit var dropInRequest: DropInRequest
private lateinit var mContext: Context
private lateinit var dataStore: DataStoreManager
private lateinit var baseUrl: String
private lateinit var spToken: String
private lateinit var retrofit: Retrofit
private lateinit var firstname: String
private lateinit var lastname: String
private lateinit var address1: String
private lateinit var address2: String
private lateinit var city: String
private lateinit var state: String
private lateinit var zip: String
private lateinit var phone: String
private lateinit var email: String
private var contractorId: Int = -1
private val buttonLoadingString = "Loading, Please Wait..."
private val buttonLiveString = "Try Again"
// Dynamic vars for composable
private val buttonLoading = mutableStateOf(true)
private val buttonText = mutableStateOf(buttonLoadingString)
private val inDropIn = mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Log start
Log.d(TAG, "onCreate STARTED")
// Load Context, DataStore, and vars needed
mContext = this
dataStore = DataStoreManager(mContext)
runBlocking {
baseUrl = dataStore.getBaseUrl()
spToken = dataStore.getSpToken()
firstname = dataStore.getFirstname()
lastname = dataStore.getLastname()
email = dataStore.getEmail()
phone = dataStore.getPhoneNumber()
address1 = ""
address2 = ""
city = ""
state = ""
zip = ""
contractorId = dataStore.getContractorId()
}
Log.d(TAG, "baseUrl, spToken: ${baseUrl}, $spToken")
// Retrofit builder setup with base url
retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
// Setup drop in and listener
dropInRequest = DropInRequest()
dropInClient = DropInClient(this, getClientTokenProvider())
dropInClient.setListener(this)
// Start drop in
dropInClient.launchDropIn(dropInRequest)
// Composable start
setContent {
Vip2Theme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
// Calls main composable function
Main()
}
}
}
}
// Main functions UI
@Composable
private fun Main() {
// Local vars
val uriHandler = LocalUriHandler.current
// Main UIX column on screen
LazyColumn(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
// Try again, loading button and spinner
item {
// Font size
val fontSize = 28.sp
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
// Loading text button
if (buttonLoading.value && !inDropIn.value) {
TextButton(
onClick = {},
enabled = true
) {
Text(text = buttonText.value, fontSize = fontSize)
}
} else if (!inDropIn.value) {
// Try again button, relaunches drop in
Button(onClick = {
// Change button and launch drop in
buttonText.value = buttonLoadingString
buttonLoading.value = true
inDropIn.value = true
dropInClient.launchDropIn(dropInRequest)
},
modifier = Modifier
.weight(1f)
.padding(start = 40.dp, end = 40.dp, top = 10.dp)
) {
Text(text = buttonText.value, fontSize = fontSize)
}
}
}
}
// Call for help button
item {
// Font size and spacer
val fontSize = 28.sp
Spacer(modifier = Modifier.height(40.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
// Call button if not loading
if (!buttonLoading.value && !inDropIn.value) {
// Text button to call for support
Button(onClick = {
val uriPhone = "tel:18888201180"
uriHandler.openUri(uriPhone)
},
modifier = Modifier
.weight(1f)
.padding(start = 40.dp, end = 40.dp, top = 10.dp)
) {
Text(text = "Call", fontSize = fontSize)
}
} else if (!inDropIn.value) {
// Circular indicator if loading drop in
CircularProgressIndicator()
}
}
}
}
}
// Client token provider async call to get nonce from Braintree
private fun getClientTokenProvider(): ClientTokenProvider {
return ClientTokenProvider {
// Creates request interface
val request = retrofit.create(VipApiRF::class.java)
// Setup async call with spToken
val call: Call<String> = request.getClientToken(
"application/json",
"Bearer $spToken"
)
// Queue async call and get client token from VIP API servers
call.enqueue(object : Callback<String?> {
override fun onResponse(call: Call<String?>, response: Response<String?>) {
// Response http status code
val status: Int = response.code()
// DEBUG
Log.d(TAG, "getClientToken onResponse: status, response: $status, $response")
// If request successful, sends token to drop-in client callback
if (response.isSuccessful) {
inDropIn.value = true
buttonLoading.value = false
buttonText.value = buttonLiveString
it.onSuccess(response.body()!!)
Log.d(TAG, "clientToken: ${response.body()}")
} else {
inDropIn.value = false
buttonLoading.value = false
buttonText.value = buttonLiveString
Toast.makeText(mContext, "Getting Nonce Failed", Toast.LENGTH_LONG).show()
}
}
// Server connect fail
override fun onFailure(call: Call<String?>, t: Throwable) {
inDropIn.value = false
buttonLoading.value = false
buttonText.value = buttonLiveString
Toast.makeText(mContext, "Getting Nonce Failed", Toast.LENGTH_LONG).show()
}
})
}
}
// Drop in success on Braintree side, tries to posts to VIP API
override fun onDropInSuccess(dropInResult: DropInResult) {
//TODO("Not yet implemented")
// Get payment method nonce
val paymentMethodNonce = dropInResult.paymentMethodNonce?.string
Log.d(TAG, "paymentMethodNonce: $paymentMethodNonce")
if (paymentMethodNonce != null) {
// Post data class for VPI API
val postBraintree = PostBraintree(
firstname,
lastname,
address1,
address2,
city,
state,
zip,
phone,
email,
paymentMethodNonce,
contractorId
)
// Create VIP API interface
val postAPIData = retrofit.create(VipApiRF::class.java)
// Send to VIP API call
val call = postAPIData.postBraintree(
"application/json",
"Bearer $spToken",
postBraintree
)
// Queue async call and get client token from VIP API servers
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(call: Call<ResponseBody?>, response: Response<ResponseBody?>) {
// Response http status code
val status: Int = response.code()
// DEBUG
Log.d(TAG, "PostBraintree status, response: $status, $response")
// Completed show message and finish activity
if (status == 200) {
Toast.makeText(mContext, "Card Added Successfully!", Toast.LENGTH_SHORT)
.show()
this@CreditCardActivity.finish()
} else {
// Failed, show error messages
Toast.makeText(mContext, "PostBraintree Failed: $status", Toast.LENGTH_LONG).show()
// Change button to try again if fails
buttonLoading.value = false
buttonText.value = buttonLiveString
inDropIn.value = false
}
}
// Server connect fail
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
// Error message
Toast.makeText(mContext, "PostBraintree Failed", Toast.LENGTH_LONG).show()
// Change button to try again if fails
buttonLoading.value = false
buttonText.value = buttonLiveString
inDropIn.value = false
}
})
} else {
// Error message
Toast.makeText(this, "Adding Card Failed", Toast.LENGTH_SHORT).show()
// Change button to try again if fails
buttonLoading.value = false
buttonText.value = buttonLiveString
inDropIn.value = false
}
}
// Drop in failed to connect or start up
override fun onDropInFailure(error: Exception) {
// Error message
Toast.makeText(this, "Adding Card Failed", Toast.LENGTH_SHORT).show()
// Change button to try again if it fails
buttonLoading.value = false
buttonText.value = buttonLiveString
inDropIn.value = false
}
}
Here's my retrofit code I call for the api too for anyone that is having probs...
package com.sjursolutions.creditcard
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
interface VipApiRF {
@GET("api/Braintree/token")
fun getClientToken(
@Header("Content-Type") contentType: String,
@Header("Authorization") authorization: String
): Call<String>
@POST("api/Braintree/SetUpCustomer")
fun postBraintree(
@Header("Content-Type") contentType: String,
@Header("Authorization") authorization: String,
@Body postBraintree: PostBraintree
): Call<ResponseBody>
}
// PostBraintree data class for post to VIP
data class PostBraintree(
private var firstName: String,
private var lastName: String,
private var address1: String,
private var address2: String,
private var city: String,
private var state: String,
private var zip: String,
private var phone: String,
private var email: String,
private var paymentMethodNonce: String,
private var contractorId: Int
)
Hi @sshropshire, and Thank you for submitted work and effort to facilitate the use of this library whose documentation is so confusing (so many "Migration documentation", inactive and outdated versions, but still present through links and documentation even official).
I have a question that is not directly related to this problem, and i will delete a comment later if necessary
The Question is : Is there a way to refresh (reinitialize - recreate) a DropInClient that has already initialized? (without recreating activity)
The situation is :
I initialized a DropInClient successfully, performed the transaction for the first time when the user did not have saved payment methods and customer Id.
Got "customer Id" from first successfully performed transaction response (save that id in DBase for that user), and it works perfectly,
JSONObject transactionJsonObj = new JSONObject(transactionString);
String customerId = transactionJsonObj.getJSONObject("customer").getString("id");
but if I stay on that activity and want to perform another transaction, when displaying a new Drop-in, the previously entered method won't be suggested because Drop-in was initialized without a customer ID. Is there a way to refresh the DropInClient after a successful transaction without recreating activity (recreate());?
I tried to reinitialize DropInClient and listener, but in that case, the application crashes because it is already connected to the lifecycle of the activity.
dropInClient =null;
dropInClient = new DropInClient(this, new BraintreeClientTokenProvider(customerId));
dropInClient.setListener(this);
But this results in a error.
What is the right way to recreate DropInClient after receiving the customer id?
Thank you in advance
Don’t create a new dropInClient and just use the one you already have and call dropInClient.launchDropIn(dropInRequest) again, should work.
Thanks for your reply.
That is how I currently operate.... calling again dropInClient.launchDropIn(dropInRequest)
However, after a successful first transaction, the payment method, I already used, doesn't appear unless I exit or recreate the activity. If I exit and re-enter the activity or recreate it, everything works as expected, and the payment method I used for the first payment appears.
I think the problem lies in the fact that when the user first initialize Drop-in client the customerId is an empty string because it doesn't exist in the database yet.
customerId = ""
dropInClient = new DropInClient(this, new BraintreeClientTokenProvider(customerId));
dropInClient.setListener(this);
And after the first transaction, I get the customerId:
JSONObject transactionJsonObj = new JSONObject(transactionString);
String customerId = transactionJsonObj.getJSONObject("customer").getString("id");
so now customerId is some number :
customerId = "some_customer_id_11111"
When I launch dropInClient.launchDropIn(dropInRequest) again, it doesn't return that method because dropInClient is already initialized with an empty customerId , and he doesn't know the new customerId.
I think that at that moment I should somehow inform and recreate dropInClientthat with that new customerId = "some_customer_id_11111"
If I reload the page, dropInClient is initialized with the customerId pulled from the database:
customerId = "some_customer_id_11111"
In that case, it works as expected.
So, the problem only occurs if I stay on the same activity after the first successful transaction.
Instead of setting the old one to null, have you tried just creating a new dropInClient2 or something, then getting the client token again, and launching it from there?
Yes yes of course, I tried with setting to null and without.... in both cases I get the same error:
java.lang.IllegalStateException: LifecycleOwner com.app.xxxxxx.activities.PaymentActivity@2da6df8 is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED.
I’m not sure then bud, I did mine all in kotlin with compose, the views stuff drives me bonkers but that’s definitely an asynchronous type problem with the views lifecycle. Did you try calling register for activity or whatever it’s called?
Thank you for trying to help me, but i will have to wait for @sshropshire to reply or someone who works at braintree.
Until then, my solution is to check is customerId an empty string after the transaction is completed, and if yes, then recreate Activity :)
Okay, I reopened this, it was set to closed haha.
In 20 years of programming I have never come across an SDK so poorly designed and dysfunctional.
Had to jump through massive hoops to get this to work. If you have any other option, use another payment provider.
In 20 years of programming I have never come across an SDK so poorly designed and dysfunctional.
Had to jump through massive hoops to get this to work. If you have any other option, use another payment provider.
@jktestaccount As you know within the past 10 years the Android landscape has changed significantly. We are constantly evolving our SDK and seeking the best possible solution that fits the myriad of possible architectures that exist today. Feedback is the only way forward, and if you could share the pain points of your integration it would be greatly appreciated. Otherwise thank you for your business and we hope you consider our product in the future.
@bolebolo @sawyermade apologies for such a late response and thank you for your patience in general. This issue seems to have gotten swept up in the weeds. We are looking to deprecate ClientTokenProvider
in an upcoming release. There are some logistical hurdles we need to clear, but we know now through trial and error that it's better to decrease the opinionated nature of our SDKs to enable more integration flexibility. We don't have an exact timeline, but the next major version of our Core SDK is in beta. A DropIn update will soon follow.
Braintree SDK Version
6.9.0
Environment
Sandbox
Android Version & Device
Android 12
Braintree dependencies
implementation 'com.braintreepayments.api:drop-in:6.9.0'
Describe the bug
Very simple program to test the drop in. Cant even initialize it, it says it needs a FragmenActivity or Fragment, not sure why this isn't working? The line with the ***s is the problem, says function definition doesn't exist??? I followed the guide on your website (which is a pretty bad compared to this guthub, they should just link here haha) and can't even get this thing to init. Any help would be appreciated.
***dropInClient = DropInClient(this@MainActivity, clientToken)
class MainActivity : ComponentActivity(), DropInListener { private val clientToken: String = "bob" private var dropInClient: DropInClient? = null private var dropInRequest: DropInRequest? = null private var REQUEST_CODE = 6969
}
To reproduce
Try to initialize the drop in client.
Expected behavior
It should have initialized.
Screenshots
No response