#mick23_code

1 messages ยท Page 1 of 1 (latest)

topaz pilotBOT
#

๐Ÿ‘‹ 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.

bronze laurelBOT
#

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.

tall agate
#

Ok, so I've got working;

  1. SaaS Add Customer --> Stripe Create Customer
  2. SaaS Add Card to Account via Stripe Elements Payment Element
  3. Got ConfirmationToken for card details, so I can check for duplicate Card Fingerprints in SaaS DB
bronze laurelBOT
tall agate
#

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."

plucky trout
tall agate
#

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`.
}
}
tall agate
#

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.

plucky trout
#

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?

tall agate
#

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.

plucky trout
#

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?

tall agate
#

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);
plucky trout
#

Looks fine to me, let me know if you run in to issues

tall agate
#

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

plucky trout
#

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.

tall agate
#

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

plucky trout
#

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

tall agate
#

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

plucky trout
tall agate
#

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

plucky trout
#

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

tall agate
#

Ah is it this one you're looking for, seti_1P5vGEE9qTMxDj1EsmLty2gK

#

Hmm. Something different between the above request and this one: "id": "seti_1P5v21E9qTMxDj1EKLnMZaPb",

plucky trout
#

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

tall agate
#

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

plucky trout
#

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?

tall agate
#

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`.
                                    
                                }
plucky trout
#

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.

tall agate
#

Ah ok

plucky trout
#

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

tall agate
#

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();
tall agate
#

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.

#

Terminology is so easy to cross wires when we have the same terms used across client/server/sdk/my-api/stripe-api

plucky trout
#

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);```
tall agate
#

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.

#

in the return URL

#

That's probably the easiest way to do this last step I think if I can get the data

plucky trout
#

123 is just a placeholder, you would use your actual setup intent ID, and probably from a variable instead of hardcoded

tall agate
#

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

topaz pilotBOT
brazen thicket
#

@tall agate my teammate had to step away. Let me know if you're all set for now!

tall agate
#

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()

brazen thicket
#

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

tall agate
#

Going to give this bit a go now, fingers crossed!

brazen thicket
#

Ah, the return_url you set is incomplete/invalid

tall agate
#

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.

brazen thicket
#

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

tall agate
#

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 {


                                }
                            }

                        };
brazen thicket
#

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?

tall agate
#

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',
},
});

#

Payment Element just froze, which always means I've done something wrong in the JS

brazen thicket
#

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?

tall agate
#

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.

topaz pilotBOT
tall agate
#

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 ๐Ÿ˜„