#mick23_code
1 messages ยท Page 1 of 1 (latest)
๐ Welcome to your new thread!
โฒ๏ธ We'll be here soon! Typically we respond in a few minutes, but sometimes we might take a bit longer if the server is busy or if you have a particularly tricky question.
โฑ๏ธ We close idle threads, which makes them read-only. Once a thread is closed it won't be reopened, but you can always start a new thread if you have another question.
๐ This thread will always be available, even after it's closed. You can find it again using Discord's search, or you can save this link: https://discord.com/channels/841573134531821608/1229504346895679518
๐ Have more to share? Add more details, code, screenshots, videos, etc. below.
Below are links to other discussions we've had with you in the past week in case you want to review that information. If your question is related to one of these previous discussions, please provide a comprehensive summary of the current state and what you need help with now. We help many users simultaneously, so a summary allows us to resolve your issue as soon as possible.
- mick_java-sdk-confirmationtokens, 1 hour ago, 12 messages
- mick23_code, 2 hours ago, 64 messages
- mick23_code, 5 days ago, 85 messages
- mick23_code, 5 days ago, 24 messages
- mick23_code, 6 days ago, 84 messages
Ok, so I've got working;
- SaaS Add Customer --> Stripe Create Customer
- SaaS Add Card to Account via Stripe Elements Payment Element
- Got ConfirmationToken for card details, so I can check for duplicate Card Fingerprints in SaaS DB
Now I'm trying to get the next step working, actually adding card to the customer's account in Stripe
I had this working previously before 1, 2, 3, so just on it's own using the Setup Intent which was working fine. So I thought, great, just copy and paste that last bit of code where I need it and job done.
Oh, if only things were that simple ๐
Here's the error message I'm getting;
"Payment details were collected through Stripe Elements in payment mode and cannot be confirmed with a Setup Intent."
That specific error is happening because you are specifying payment as the mode when setting up Stripe.js. For SetupIntents you should specify setup instead https://docs.stripe.com/js/elements_object/create_without_intent#stripe_elements_no_intent-options-mode
This is the #4 bit of code for reference;
if (cardAlreadyExistsJson.isCardExists) {
console.log("Card Already Exists");
} else {
console.log("Card is New, Craete Setup Intent (I think)");
// Create the SetupIntent and obtain clientSecret
// Locally on Server, Not a Stripe API
// const res = await fetch("/create-intent", {
const responseFromCreateSetupIntent = await fetch("/api/stripe/customer-create-setup-intent", {
method: "POST",
});
// Get CustomerSetupIntentClientSecret
const {client_secret: clientSecret} = await responseFromCreateSetupIntent.json();
// Confirm the SetupIntent using the details collected by the Payment Element
const {error} = await stripe.confirmSetup({
elements,
clientSecret,
confirmParams: {
return_url: 'https://example.com/order/123/complete',
},
});
if (error) {
// This point is only reached if there's an immediate error when
// confirming the setup. Show the error to your customer (for example, payment details incomplete)
handleError(error);
} else {
// Your customer is redirected to your `return_url`. For some payment
// methods like iDEAL, your customer is redirected to an intermediate
// site first to authorize the payment, then redirected to the `return_url`.
}
}
Right ok. That makes sense. But, then if that's the case, there is something incorrect in the architecture here.
What am I missing?
One option (really bad user experience) would be to redirect the user ifCardNotExists, to a new page and instead use the 'payment' as the setup mode but then they would have to re-enter their card details again.
I'm sorry but I don't quite follow that question. Taking a quick step back, can you tell me about what your user is doing when this code is invoked? Are they trying to make a payment or are they only trying to save their payment method for future payments?
Yeah sure
Future Payments
So there is a page within the "My Account" page in SaaS application. Button for "Add Card for Future Payment". Presented with Card Payment Element UI. With the ultimate goal of "Save Card to Account so I can subscribe to a product at some point in the future"
That's essentially what the user it trying to achieve.
So my first attempt at all this was purely to use the Setup Intent stuff. But what I found was that this allowed the user to enter duplicate cards which is naturally something to avoid. So my second attempt is what I described above with the duplicate card check approach via the ConfirmationToken stuff. Then this resulted in the error I mentioned above. So feels as though this second attempt is not quite going to work either.
Gotcha, in that case SetupIntents and setup mode are what you want to use. PaymentIntents and payment mode are only for when you are charging or placing a hold on funds right now. When there isn't an immediate payment, SetupIntents are the way to go.
Checking the newly saved card's fingerprint against the fingerprints of previously saved card(s) for the user is the best way to deduplicate. Are you running in to issues once a new card has successfully been created?
Ah ok. So might be just that all the logic is correct, I just need to change that mode.
Let me give that a quick go, one min
So this is what I'm just trying now;
const options = {
// mode: 'payment',
mode: 'setup',
// amount: 1099,
currency: 'gbp',
paymentMethodCreation: 'manual', // future payments
// Fully customizable with appearance API.
appearance: {/*...*/},
};
// Set up Stripe.js and Elements to use in checkout form
const elements = stripe.elements(options);
Looks fine to me, let me know if you run in to issues
Managed to create card. Now I'll try again to see if it blocks the duplicate.
Think I've found a missing gap in my code, actually saving the Card Fingerprint ID to the SaaS DB, let me get that added in
Where it is best to do this? Server Side /my-setup-intent? or Client Side Response from /my-setup-intent? Or makes no difference either way?
Can't remember without checking which bit gives me back the Card Fingerprint ID
Wondering if I need to build the /my-api-save-card-fingerprint-id-to-database kind of thing
It would make more sense to check this on your server. If you are doing it on your client, it is possible for it to lose connection before telling your server about the duplicate. A bad actor could also have the client tell your server the wrong thing about there being a duplicate which would have your server act incorrectly.
True
Just had a look at the Response Body from the Setup Intent. Doesn't look like it has the Card Fingerprint in there, but it does have the Payment Method ID in there, which I can then use to get the Payment Method, which gives me the Fingerprint.
Let me implement that now and check, feels I'm at the last 1% now to get this finally working
Yep, that is expected. If you are retrieving the SetupIntent you can expand its payment_method property to see the card's fingerprint as part of the object that comes back from the retrieve setup intent call
https://docs.stripe.com/api/expanding_objects
Complete reference documentation for the Stripe API. Includes code snippets and examples for our Python, Java, PHP, Node.js, Go, Ruby, and .NET libraries.
Yeah I used that on my /sumarize-payment API
So i've just tried this bit of code on my /SetupIntent API;
String paymentMethodId = setupIntent.getPaymentMethod();
PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodId);
String cardFingerprintId = paymentMethod.getCard().getFingerprint();
But I'm getting a strange error here;
" com.stripe.exception.InvalidRequestException: Invalid null ID found for url path formatting. This can be because your string ID argument to the API method is null, or the ID field in your stripe object instance is null. Please contact support@stripe.com on the latter case. "
Reads as a rookie error not passing the data through, but I can't figure out why that would be null
Do you have the ID of the request ID where you got that error? It will be in your dashboard logs if you just got that https://support.stripe.com/?contact=true
So this bit of code which is just before this correctly gives me the info;
setupIntent.getClientSecret()
Yeah, let me grab that now
req_v4CMW3yh0Yp5yL
Also have you tried printing out paymentMethodId to see if it is properly populated?
That request ID looks to be for a different call to create a SetupIntent. If this error isn't in your dashboard, it may be a thing that our library throws that never touches Stripe's servers
Ah is it this one you're looking for, seti_1P5vGEE9qTMxDj1EsmLty2gK
Hmm. Something different between the above request and this one: "id": "seti_1P5v21E9qTMxDj1EKLnMZaPb",
Can you show me how you are retrieving that SetupIntent?
Also, if you are expanding that payment method object within the setup intent retrieve call, you actually shouldn't need to make this second retrieve call. You can get the fingerprint from the full payment method object that is returned within the SetupIntent object
Managed to replicate working/not working examples now. So let me explain. My gut feel is right now is that Stripe.js must be doing something with the response from /setup-intent to actually confirm things.
So when I add in this bit of code into /setup-intent before sending the response back, then it's not showing the card in the Stripe Logs;
String paymentMethodId = setupIntent.getPaymentMethod();
PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodId);
String cardFingerprintId = paymentMethod.getCard().getFingerprint();
But when I do this, and allow the /setup-intent to complete, it looks like I can see in the Stripe Logs that there is actually another request to Stripe to Confirm Setup Intent;
(most recent at the top)
200 OK
POST
/v1/setup_intents/seti_1P5v21E9qTMxDj1EKLnMZaPb/confirm
20:17:33
200 OK
POST
/v1/setup_intents
20:17:33
And that can only be via the abstracted stuff within Stripe.js as it's not something I'm explicitly doing in my code.
Given that, the only option I can see right now to Save Card Fingerprint ID to DB is having another SaaS API for that to pass in the Fingerprint ID
This error is happening because that paymentMethodId variable is empty in your server code. Stripe.js likely is not the issue here as long as you are getting an ID from it and passing it to your backend properly
Can you show me your code for how you set the setupIntent object from the setupIntent.getPaymentMethod() call?
The client side bit?
Just checked client and server side, and I don't have any uncommented code to do the getPaymentMethod() call at all.
But let me grab the bit when it's uncommented to show the surrounding code
SetupIntent setupIntent;
try {
setupIntent = SetupIntent.create(params);
Map<String, String> map = new HashMap();
map.put("client_secret", setupIntent.getClientSecret());
System.out.println("setupIntent.getClientSecret(): " + setupIntent.getClientSecret());
System.out.println("Card Fingerprint: " + setupIntent.getPaymentMethodObject().getId()); // Get Card Fingerprint
String paymentMethodId = setupIntent.getPaymentMethod();
System.out.println("setupIntent.getPaymentMethod(): " + setupIntent.getPaymentMethod());
PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodId);
String cardFingerprintId = paymentMethod.getCard().getFingerprint();
// Save Card Fingerprint ID to Database
// HERE
So the above code is in /my-api/create-setup-attempt
Which is just before sending the Response back on the HTTP Requset
So that bit of code is essentially running.....
200 OK
POST
/v1/setup_intents/seti_1P5v21E9qTMxDj1EKLnMZaPb/confirm
20:17:33
HERE i.e. before the above URL is called, /v1/setup_intents/seti_1P5v21E9qTMxDj1EKLnMZaPb/confirm
200 OK
POST
/v1/setup_intents
20:17:33
So on this request - /v1/setup_intents
The payment_method is null
Yet on this request - /v1/setup_intents/seti_1P5v21E9qTMxDj1EKLnMZaPb/confirm
The payment_method has data in
Hence my theory about about Stripe.js actually doing this bit under the hood calling this URL /v1/setup_intents/seti_1P5v21E9qTMxDj1EKLnMZaPb/confirm
Once it has recieved a Response from /my-api/create-setup-intent
Because all my code is doing on the client side is this;
const responseFromCreateSetupIntent = await fetch("/api/stripe/customer-create-setup-intent", {
method: "POST",
});
// Get CustomerSetupIntentClientSecret
const {client_secret: clientSecret} = await responseFromCreateSetupIntent.json();
// Confirm the SetupIntent using the details collected by the Payment Element
const {error} = await stripe.confirmSetup({
elements,
clientSecret,
confirmParams: {
return_url: 'https://example.com/order/123/complete',
},
});
if (error) {
// This point is only reached if there's an immediate error when
// confirming the setup. Show the error to your customer (for example, payment details incomplete)
handleError(error);
} else {
// Your customer is redirected to your `return_url`. For some payment
// methods like iDEAL, your customer is redirected to an intermediate
// site first to authorize the payment, then redirected to the `return_url`.
}
So this call setupIntent = SetupIntent.create(params); returns a new SetupIntent, you aren't passing in any payment method info so it doesn't have a payment method assosciated with it.
Ah ok
The PaymentMethod won't exist until after that confirmSetup call succeeds. After that you can ask your server to check the payment method's fingerprint
This is the bit of code missing for that bit;
SetupIntentCreateParams params
= SetupIntentCreateParams
.builder()
.setCustomer(tenant.getStripeCustomerId())
// In the latest version of the API, specifying the `automatic_payment_methods` parameter is optional because Stripe enables its functionality by default.
.setAutomaticPaymentMethods(
SetupIntentCreateParams.AutomaticPaymentMethods.builder().setEnabled(true).build()
)
.build();
Right ok, that's confirmed that then
That means then, I'm going to have to have another /my-api/add-card-fingerprint-to-saas-db. Seems only option
So something along the lines of;
// Confirm the SetupIntent using the details collected by the Payment Element
const {error} = await stripe.confirmSetup({
elements,
clientSecret,
confirmParams: {
return_url: 'https://example.com/order/123/complete',
},
});
if (error) {
// This point is only reached if there's an immediate error when
// confirming the setup. Show the error to your customer (for example, payment details incomplete)
handleError(error);
} else {
// Your customer is redirected to your `return_url`. For some payment
// methods like iDEAL, your customer is redirected to an intermediate
// site first to authorize the payment, then redirected to the `return_url`.
*****************callSaaSAPIToAddCardFingerprintIdToDatabase(cardFingerprintId);
}
Or, would I get the main ID from this;
const {error} = await stripe.confirmSetup({
And then use the ID from that to call /my-api/add-card-fingerprint-to-database?setupIntentId = 123
Then server side, I'd use the Retrieve Setup Intent, https://docs.stripe.com/api/setup_intents/retrieve , to actually get the Payment Method ID, and then ultimately the Card Fingerprint ID
Yes, that second option now I type it out is what I think you were referring too earlier actually.
Complete reference documentation for the Stripe API. Includes code snippets and examples for our Python, Java, PHP, Node.js, Go, Ruby, and .NET libraries.
Terminology is so easy to cross wires when we have the same terms used across client/server/sdk/my-api/stripe-api
Yep that is what I was talking about. And when you retrieve the setup intent after the confirm, you can tell stripe to give you the full payment method object back with it, which would let you check the fingerprint without having to make a separate API call to retrieve the payment method.
SetupIntentRetrieveParams.builder().addExpand("payment_method").build();
SetupIntent setupIntent = SetupIntent.retrieve("seti_1234", params, null);```
I see, that makes sense with what you were saying now with that code snippet.
Right, off to knock up another API to do this last 1% bit and feels like this should, fingers crossed, get me over the line now.
This stuff is genuinely challenging. Not for the faint-hearted.
Where do I get the 123 bit from?
const {error} = await stripe.confirmSetup({
elements,
clientSecret,
confirmParams: {
return_url: 'https://example.com/order/123/complete',
},
});
in the return URL
That's probably the easiest way to do this last step I think if I can get the data
123 is just a placeholder, you would use your actual setup intent ID, and probably from a variable instead of hardcoded
Ah yes, just spotted that when this is actually running, there are 3x parameters added on to that return_url when it is actually called anyhow for;
https://example.com/order/123/complete
?setup_intent=seti_1P5wHqE9qTMxDj1EA9M7b3Ji
&setup_intent_client_secret=abc123
&redirect_status=succeeded
So I can use that for what I need
@tall agate my teammate had to step away. Let me know if you're all set for now!
Thanks
Just in the last steps now, should hopefully be testing it in the next 10 mins
What's the bit to get the fingerprint from this code;
SetupIntent setupIntent = SetupIntent.retrieve(setupIntentId, params, null);
(Java)
Might have randomly figured it out actually;
setupIntent.getPaymentMethodObject().getCard().getFingerprint()
I'm not a Java expert but the above means setupIntent will be an object. You'll need to look a this object's payment_method, then look at that PaymentMethod's card.fingerprint
Going to give this bit a go now, fingers crossed!
Error, bit confused by this one;
POST https://api.stripe.com/v1/setup_intents/seti_1P5xFaE9qTMxDj1EeVAC7SSB/confirm 400 (Bad Request)
Ah, the return_url you set is incomplete/invalid
confirmParams: {
// return_url: 'https://example.com/order/123/complete',
return_url: '/account/billing/card/add-new/complete',
},
Assuming needs to be an Absolute URL, not a Relative URL?
Brill, that's sorted now
Just the last 0.1% now
When I get an error back that the card already exists, how to I refresh the page with an error (I've probably answered by question here)
Is there any best practice here?
I'm thinking it's probably easier to just do something like a JavaScript Page Refresh with ?error=card-already-exists, and reloading the page with that error outside the Payment Element UI stuff for ease, then the user can simply re-try.
I'm sure I've got some handy code somewhere for that.
That'll be up to you, right. You'll need to decide what to show to a customer and if they should take any action to re-render the PaymentElement
Yeah true, I'll have a play with a few options.
hmm, interestingly, what I was expecting to work hasn't
if (error) {
// This point is only reached if there's an immediate error when
// confirming the setup. Show the error to your customer (for example, payment details incomplete)
window.location.href = "http://www.w3schools.com";
handleError(error);
} else {
Code above for context;
const responseFromCreateSetupIntent = await fetch("/api/stripe/customer-create-setup-intent", {
method: "POST",
});
// Get CustomerSetupIntentClientSecret
const {client_secret: clientSecret} = await responseFromCreateSetupIntent.json();
// Confirm the SetupIntent using the details collected by the Payment Element
// NOTE: setip.confirmSetup is what actually adds the card to the Customer's account within Stripe.
// Before this, the card doesn't actually exist
const {error} = await stripe.confirmSetup({
elements,
clientSecret,
confirmParams: {
// return_url: 'https://example.com/order/123/complete',
return_url: 'http://localhost:8084' + '/account/billing/add/submit/complete',
},
});
Probably be easier with a full snippet;
const cardAlreadyExistsJson = await cardAlreadyExists.json();
if (cardAlreadyExistsJson.isCardExists) {
console.log("Card Already Exists");
} else {
console.log("Card is New, Craete Setup Intent (I think)");
// const res = await fetch("/create-intent", {
const responseFromCreateSetupIntent = await fetch("/api/stripe/customer-create-setup-intent", {
method: "POST",
});
// Get CustomerSetupIntentClientSecret
const {client_secret: clientSecret} = await responseFromCreateSetupIntent.json();
const {error} = await stripe.confirmSetup({
elements,
clientSecret,
confirmParams: {
// return_url: 'https://example.com/order/123/complete',
return_url: 'http://localhost:8084' + '/account/billing/add/submit/complete',
},
});
if (error) {
window.location.href = "http://www.w3schools.com";
handleError(error);
} else {
}
}
};
Sorry, it's kind of hard to follow the conditionals here. In this case, you're pointing your window to http://www.w3schools.com only if there's an error with confirming the SetupIntent, right?
Ah, I think I might need to re-structure this so I dynamically generate the redirect_url based on the if/else for the isCardExists logic
const {error} = await stripe.confirmSetup({
elements,
clientSecret,
confirmParams: {
// return_url: 'https://example.com/order/123/complete',
return_url: 'http://localhost:8084' + '/account/billing/add/submit/complete',
},
});
Hmm. That didn't work either actually;
const {error} = await stripe.confirmSetup({
elements,
clientSecret,
confirmParams: {
// return_url: 'https://example.com/order/123/complete',
// return_url: 'http://localhost:8084' + '/account/billing/add/submit/complete',
return_url: redirectUrl,
},
});
Payment Element just froze, which always means I've done something wrong in the JS
Hold on. If a card already exists, you don't want to go into the code block that creates and confirms a SetupIntent at all, right?
Think it's a stupid copy and paste error, one min
All sorted
Now I just need to understand the JS logic of how it's all flowing ๐ But that's something I can hopefully figure out now.
Thanks for all the help as always. Genuinely couldn't have achieved this via the docs alone. Understanding the basic HTTP Request/Responses through all the different layers along the way is just an absolute minefield to navigate.
I'm sure I'll be back in a few days when I get onto actually trying to charge the card ๐