#benkass-paymentmethod-clone
1 messages ยท Page 1 of 1 (latest)
HI ๐
I'm not going to be able to review all your code without more information
- What is the error thrown
- What is the specific line of code throwing the error
- What is the API request you are making when the error is thrown
ok yeah, let me clarify...
I have been working on this implemenation following the guide of several of your colleagues here.
The idea is that we're creating a charge on connected standard accounts. When the user clicks the pay button we initPaymentSheet which returns a paymentMethod. We clone it, and create a paymentIntent with it while passing the connected account ID.
The error I am getting is from the connected account because it doesn't recognize the customer (makes sense).
So what do we need to adjust in the code?
- What type of connected accounts are you using
- Have you cloned Customers along with the payment method?
- Standard
- No. Do we need to? I guess it makes sense
- No. Do we need to? I guess it makes sense
Yes that is what is required so you can charge a Customer object that exists on the Standard account. You can find the documentation here https://stripe.com/docs/connect/cloning-customers-across-accounts
OK, so I think that what is missing in my code is this:
Once I clone the payment method, I should create a token, and then clone a customer following this: https://stripe.com/docs/connect/cloning-customers-across-accounts#creating-tokens and this: https://stripe.com/docs/connect/cloning-customers-across-accounts#creating-charges
and run:
...
// Clone the Payment Method to a connected account
const clonedPaymentMethod = await stripe.paymentMethods.create({
customer,
payment_method,
}, {
stripeAccount: connected_account_id,
});
// Create a token for the connected account
const token = await stripe.tokens.create(
{
customer,
},
{
stripeAccount: connected_account_id,
}
);
// Clone the CUSTOMER on the connected account
const clonedCustomer = await stripe.customers.create(
{
source: token.id,
},
{
stripeAccount: connected_account_id,
}
);
And then finally create the paymentIntent:
const paymentIntentObject = {
currency: "usd",
amount,
customer: clonedCustomer.id,
description,
payment_method: clonedPaymentMethod.id,
confirm: true // Automatically confirm the payment
}
const paymentIntent = await stripe.paymentIntents.create(
paymentIntentObject, {
stripeAccount: connected_account_id,
}
);
Did I get this right?
This makes sense to me but I would definitely run it in test mode to verify the correct behavior
๐ there's absolutely no reason to create a "Token" in your flow at all
benkass-paymentmethod-clone
https://dev.to/stripe/single-slider-direct-charges-on-multiple-accounts-with-cloning-ic5 I highly recommend watching this first
ok hey @tiny agate I was just about to suggested writing @fading willow who initially suggested this integration to make sure it's correct ๐
I know and I appreciate it ๐
Tokens and Cards and Sources were deprecated back in 2018 and there's no reason to use that Token API unless you misunderstood something
I am only following on the link you guys send me. Let me look at your last link. One moment please
I already watched it
How do I put this... we started with your suggestion and I started implementing just that. Then at some point, I noticed that the price was not showing on the payment sheet. So I reached out and Rubeus suggested a slightly different approach that would let us display the payment sheet with a price and then clone the payment method, and so on
Sure sure but again absolutely no reason to use Token here
I don't want to use tokens
Let's take a quick step back: Why do you even want a Customer on the connected account? This is fully optional, so why are you using a Customer? Do you want that connected account to also see a Customer and charge that card whenever they want?
I don't want to use anything ๐ I just want this to work. hehe
Ah, exactly the right question. So I don't want to clone the customer at all. Scratch all that. Just please pull up the code I shared at the very top and just look through it quickly. It all works, but the last part where it complains that the customer doesn't exist. Which makes sense because it's trying to charge the customer on the connected account who is not aware of that cusomer
Eventhough we ARE passing the customer ID alone with the cloned payment method
yeah I looked at your code already fully. I'm sorry I know you "just want this to work" but you clearly are lost with it all despite the complete end to end video explaining cloning. And if you don't fully understand it, it will basically never work.
So please: explain if you do want a Customer on the connected account or if you don't care about Customers at all there, you only want to save card details on the platform
I do care about having the customer record and we do have customer records on the platform. And we save new setupIntents for them when needed.
I am not lost at all. I don't understand why you think I am lost. It's not very complicated. I have a customer. They initiate an intent to buy. It returns a payment method. I clone it to be used on a connected standard account for one of our merchants, and then I need to charge the customer via the connected account. As for the exact details on how to do it, that's what I need
Do you want a Customer on each connected account. Yes or no?
If I can have the connected account charge the customer on our platform without having the customer on their account, then great. No need to clone the customer on their account at all. If I must clone them so that the connected account will be able to charge them, then please tell me
I'm really sorry, this is extremely subtle and you aren't really answering my question unfortunately. I know you think you are but it's like you add multiple buts to your answer.
I don't want a customer on each connected account
perfect, drastically easier
I want the customers on our platform account
1/ Clone the PaymentMethod (first call in your code)
2/ Create/confirm a PaymentIntent (fourth call in your code) but do not pass customer at all
Oh this is drastically easier
testing
OK so this time it worked. It created the charge for the guest. Great! However, the paymentIntent returns a client_secret and I send it back to the app (initPaymentSheet's confirmHandler intentCreationCallback), but it shows No such payment_intent ....
1/ Why are you returning the client secret back at that point, it should already have succeeded
True, it did succeed. I am following on the docs for confirmHandler where it's written "Call the intentCreationCallback with the client secret or error"
Also, I'm using stripe-react-native which shows the payment dialog (bottom sheet). After clicking on "Pay $xx", it shows "processing..." At some point it should show a success. I assume this is what intentCreationCallback should do?
I'm sorry this is really confusing me and it still feels like you have it all mixed up again
Because of your complex set up you are initialized the ReactNative SDK on your own platform account because you are collecting and storing card details there first with a SetupInent. Is that correct?
yes
Okay so you do that, you get a complete/succeeded SetupIntent that leaves you with a PaymentIntent and Customer on the platform. At that point you are done client-side, the UI is resolved, everything else happens server-side
No. I don't believe you are correct. We create a setupIntent only when a customer adds a new credit card.
The paymentSheet creates a paymentMethod. if paymentMethod has a customerId, then there's no need to create a setupIntent for the customer.
Next, we clone the payment method, and with that create a paymentIntent on the connected account
sorry a SetupIntent and a Customer, damn I can't type today
The paymentSheet creates a paymentMethod. if paymentMethod has a customerId, then there's no need to create a setupIntent for the customer.
Next, we clone the payment method, and with that create a paymentIntent on the connected account
I'm sorry but that doesn't make sense to me at all right now.
Please keep in mind that I am following the instructions of you and several other of your colleagues
The only reason to use paymentSheet is to collect card details once, why are you using it for future payments with an already saved card?
SO please chat with Rubeus. He is the one who suggested this setup or put him on here and we can have him weigh in
I am not. I said we only create a setupIntent if it's needed. If the customer is using a new credit card
Okay I did sync with Rubeus who confirmed they gave you the same advice I did earlier.
thanks. So what advise?
So ultimately you have to understand which account your object(s) live on and configure your mobile SDK with the right one. If you do something on the platform -> Confirm on the platform. If you do something on the connected account -> Confirm on the connected account
This means your RN SDK has to be configured with the right info and you are right now mixing them up
When you initialize your PaymentSheet you have to properly configure it either for your platform or for the connected account depending on what you're doing
OK I understand what you're writing, but let me get this straight. We have our app (the platform). Think Uber Eats. We have our users (customers). They want to place an order.
We want to have the merchants get paid and we take our cut.
If we initiate the paymentSheet with the merchant's connected account, then we will be essentially creating customers across many stripe accounts. It also means the customers will be forced to add credit card for every restaurant they order from (different connected accounts).
What do you suggest we do in order to, simply, show the customer the payment sheet. have them click pay, then process the payment via the connected account?
see that's why I keep saying you are unfortunately not understanding the cloning flow at all at the moment. It's clearly not "clicking" for you and so you keep mixing them up
Existing customer comes to your app to order from restaurant A
1/ Clone their PM pm_ABC from the platform to the connected account, get pm_XXX back
2/ Create a PaymentIntent on the connected account and attach the cloned PM pm_XXX back
3/ Return that PaymentIntent's client_secret to your app
Where you are stuck is that if the PaymentSheet has been configured with the platform then it says "no such PaymentIntent" which is normal since it's on A not the platform. So you need to configure the React Native SDK with the right connected account upfront because you know it's an existing customer and they are using their previously saved card.
it is definitely really complex to do what you are trying. It's really are to do that kind of business model with Standard accounts really. That's what makes this so hard for you
it's ok. checking the code and trying to adjust
ok, so it seems I do everything your wrote above (point 1 to 3). What I don't totally get it So you need to configure the React Native SDK with the right connected account upfront because you know it's an existing customer and they are using their previously saved card
I believe you are saying I should initPaymentSheet and pass the connected_account_id to it. Is that correct?
This is how I init it currently:
const { error: initPaymentSheetError } = await initPaymentSheet({
merchantDisplayName: 'xx',
customerId: user?.stripe?.customer,
customerEphemeralKeySecret: ephemeralKey,
intentConfiguration: {
mode: {
amount,
currencyCode: 'USD',
},
confirmHandler,
},
returnURL: 'xxx://xx-xx', // TODO: Without it some payment method will be hidden. Right now it's ok.
});
this is separate from PaymentSheet in ReactNative so it's really when you initialize your app
We can only init stripe with our account. We cannot init endless amount of stripe instances
That is not correct. You can't do that, and you have to do exactly what I said and that is the only and correct way
None of this matters, there's nothing "endless", it's just code, you initialize your provider with the right info based on context that's all
Are you talking about initStripe call provided by '@stripe/stripe-react-native'?
I'm talking about the exact code linked https://stripe.com/docs/connect/authentication?client=create-client-react-native#adding-the-connected-account-id-to-a-client-side-application is that not how you're using the SDK? If it's not, please shared details context around your whole code client-side for me to help you get unblocked
One moment checking
You can use https://stripe.dev/stripe-react-native/api-reference/interfaces/InitStripeParams.html#stripeAccountId otherwise maybe?
Going to run by @void ruin can help if you have follow up questions!
Thanks.
๐
So yes, I wrapped the initPaymentSheet in a Stripe provider and passed the connected account id. However, now I'm getting No such customer
Where are you encountering "no such customer" and can you share the customer ID and account ID you're using?
Which sort of brings us back to the point where if we init the call with the connected account, we would need to create the customers on the connected accounts, which beats the purpose of doing what we're doing
sure
When initializing with a connected account context, the customer would need ot exist on that connected account rather than your platform account
Getting No such customer: 'cus_Ofti1Ac6MSleVP' when running:
initPaymentSheet({
merchantDisplayName: 'xxxx',
customerId: user?.stripe?.customer,
customerEphemeralKeySecret: ephemeralKey,
intentConfiguration: {
mode: {
amount,
currencyCode: 'USD',
},
confirmHandler,
},
});
Which is wrapped in
<StripeProvider
publishableKey={Constants.expoConfig?.extra?.stripePublishableKey}
stripeAccountId="acct_1NkveLENrXFoOICw"
>
exactly why I would not want to do that
Yes, so that customer exists on your platform, no that connected account
We're trying to init a payment sheet on our patform which will create a PM. We then clone it and create a paymentIntent on the connected account.
So it can't be used with the way you're initializing the provider
This works so far. It charges a "guest" on the connected account, but returns a client secret with a PM which the initial call to initPaymentSheet doesn't recognize
I mean, initPaymentSheet is expecting the client secret back to pass to confirmHandler intentCreationCallback
Where is the client secret coming into play? Are you referring to the ephemeral key?
nope
initPaymentSheet is expecting the client secret back to pass to confirmHandler intentCreationCallback
So when we complete the paymentIntent on the connected account, it returns a paymentIntent object with client_secret
Expecting it back from where/what?
When you initPaymentSheet it has a confirmHandler with a callback function intentCreationCallback that expects a client_secret
You can see my entire code at the very top of this thread
the only thing we changed in it so far is that we don't pass a customer with paymentIntentObject sent to paymentIntents.create so that the connected account will be able to charge the customer
Ok, i've read the backscroll to make sure i have the context here, and I remain confused about the flow here. Earlier, we tried to clarify setting up a new card from using a previously saved card and that the payment sheet should only be used for the first case.
If you use setup intents for card collection, and you're encountering payment intent errors, is it safe to assume you're dealin with the second scenario (an already-saved card)?
I'm starting to get more confused then you guys ๐
I initPaymentSheet (suggested by Rubeus) that will allow showing a dialog where the customer can add credit cards and click on Pay.
confirmHandler returns paymentMethod. If paymentMethod.customerId exists, this means a new credit card was added (so I was told).
Next, if paymentMethod.customerId is missing, we want to attach the new credit card to the user with stripe.setupIntents.create({ customer })
So far correct?
If paymentMethod.customerId exists, this means a new credit card was added (so I was told)
I don't understand why you'd need to be guessing this from outcomes, are you not controlling the new card vs existing card flow? ie, you'd know this ahead of time already, right?
Not really no
We display the payment sheet to the customer on react native. There they can either select an existing card or add another
how exactly would I know if they decide to add a card before they do?
Ohhhh I see, you're depending on the customer sheet to present those saved payment methods for user selection or entry
Which requires it to be initialized as your platform
Ok, sure, so then however that happens, new or existing, you have a PM on a platform customer
then what was described earlier involves you using that PM on your server to clone to some connected account and confirm a payment intent
yes
So I don't understand how the payment sheet and a payment intent client secret comes into play, that doesn't sound like part of the flow
and once we confirmed the PI, it returns a client secret which mismatches with what the payment sheet intentCreationCallback expects
ok so when we press pay on the payment sheet, it waits for something and, I believe, you complete the call with intentCreationCallback, no?
You should not be paying with the payment sheet using you flow
and should not be returning a payment intent client secret to your client
What we've all been discussing would only ever involve a setup intent client secret for saving a new payment method
The payment part is entirely server side
Wait, did I write that I return the client secret from the setupIntent? I meant from paymentIntents
No, I did write paymentIntents
I do not return a client secret from a setupIntent
I only use SetupIntent to store a card for a customer on the platform
ok, so what do I return to the payment sheet so it completes the transaction?
In most cases, nothing, ideally. The server should ideally complete that payment without customer interaction after the payment method has been set up.
However, sometimes the payment intent will require action
I can see it on the connected account dashboard
Sign in to the Stripe Dashboard to manage business payments and operations in your account. Manage payments and refunds, respond to disputes and more.
So perhaps I should return the client_secret from the original PM that was created by the paymentSheet?
in those requires_action cases only, you'd need the client_secret on your client to confirm the payment and handle those actions. If you need to do that, the SDK must be initialized with the the connected account context matching the payment intent, and without a customer, since the connected account payment intents are not associated with a customer
That isn't a thing, PMs don't have client secrets
oh
ok, so if I don't use intentCreationCallback - how do I show a completed payment in the payment sheet once it succedded?
currently the Pay button, when clicked, shows "Processing..."
It's waiting for something
You don't if you're completing the payment server side
But how do I show the clients that it worked? Just close the sheet forcefully and show an alert? ๐
is there a way to make the payment sheet show a "completed" status somehow?
Im looking at your code. I see you can init the payment sheet with customFlow and then confirmPaymentSheetPayment on your own?
Is that how?
No. Once I add customFlow: true it hides the pay button
Give us a few minutes, we're discussing over here
ok thank you
Trying to narrow down the issue here, were you able to get things working in the case where the customer is entering a new payment method by returning the setup intent client secret to the payment sheet callback?
yes
wait
wait
no. I haven't tried a new payment method test actally. Let me try
ok, so I tried again with a new card. Indeed, the PM returned by the payment sheet had no customer, so I ran const newCard = await stripe.setupIntents.create({ customer }); But I don't see how that would add a payment method for the customer. It doesn't.
It returned:
newCard: {
id: 'seti_1NuhwlCg0bBEGiBd25MIsyEd',
object: 'setup_intent',
application: null,
automatic_payment_methods: null,
cancellation_reason: null,
client_secret: 'seti_1NuhwlCg0bBEGiBd25MIsyEd_secret_Oi8D3jpbwgjkjqrjDV7fXEFjQwVMp06',
created: 1695760411,
customer: 'cus_Ofti1Ac6MSleVP',
description: null,
flow_directions: null,
last_setup_error: null,
latest_attempt: null,
livemode: false,
mandate: null,
metadata: {},
next_action: null,
on_behalf_of: null,
payment_method: null,
payment_method_configuration_details: null,
payment_method_options: { card: [Object] },
payment_method_types: [ 'card' ],
single_use_mandate: null,
status: 'requires_payment_method',
usage: 'off_session'
}
Anyway, the call did not fail. I see the charge with this card on the merchant connected account, and it returned the secret from that paymentintent
Maybe I'm supposed to pass the PM to the setupIntent?
OK I tried now passing the PM to setupIntents.create and this time got:
newCard: {
id: 'seti_1Nui1bCg0bBEGiBdJdTPLH7V',
object: 'setup_intent',
application: null,
automatic_payment_methods: null,
cancellation_reason: null,
client_secret: 'seti_1Nui1bCg0bBEGiBdJdTPLH7V_secret_Oi8ISZyWuYE82mZdg4N4KDADn7pgKch',
created: 1695760711,
customer: 'cus_Ofti1Ac6MSleVP',
description: null,
flow_directions: null,
last_setup_error: null,
latest_attempt: null,
livemode: false,
mandate: null,
metadata: {},
next_action: null,
on_behalf_of: null,
payment_method: 'pm_1Nui1aCg0bBEGiBds5h1FieL',
payment_method_configuration_details: null,
payment_method_options: { card: [Object] },
payment_method_types: [ 'card' ],
single_use_mandate: null,
status: 'requires_confirmation',
usage: 'off_session'
}
requires_confirmation
Adding confirm: true to it ๐
Yes, you would need to confirm that when setting up a new payment method
We're still talking about this, haven't forgotten about you
Ah, this time I got status: 'succeeded',
And yes, the new card is added to the payment sheet
Ok, trying to tie things together here
๐
There's a mismatch in the way PaymentSheet works in payment mode and your payment flow.
When setting up a new payment method, you can do this because you'd be able to return the platform client secret to the payment sheet callback, while completing the payments on the connected accounts on your server.
That's kinda what I suggested above, no?
For existing payment methods, though, the callback expects a client secret from a payment intent, but you're not creating a payment on the platform, only the connected accounts
And I think that's where you're encountering the issue with the client secrets, because of the context mismatch of platform vs connected account
Yes, and I appreciate the patience, it's a complex flow with a complex opinionated UI library, so we need to unwind what was happening and why.
I understand, no worries
I think the core issue is that the flow you are trying to achieve is not supported by the payment sheet, and you're going to have trouble making it work. The good news is that you have two options, and you will likely want to do do one then the other.
1/ Don't rely on Payment Sheet for listing existing PMs. Build your own UI for displaying "Pick a PM or Setup a new one" using the customer payment method list API to get the PM details. When the customer selects a saved PM (in your UI) you send that PM ID to your server and do the connected account payment intents and don't need to worry about the callback and client secret (only handling potential actions)
2/ We have a new "Customer Sheet" UI that is more like what you expect: it allows customers to manage their payment methods, selecting one or setting up a new one: https://stripe.com/docs/elements/customer-sheet
This is currently in beta for React Native, but you can write in to support to ask for it to be enabled on your account.
This will be a much better fit, because you get to have the payment method management/selection for the paltform customer, and you can handle the connected account payments based on that selection.
Will that customer sheet integrate both the PMs and the Pay button?
If it's only a bottom sheet for managing the payment methods, then it's only partial solution. we would need to spend time building the payment part. Kinda hoping to just be able to resolve the call from the one I already spent days working on
Just today we started chatting at 9:15am
No, it would only be the payment method setup/selection, you'd add your own "pay" button using that selection, but its what you're already doing effectively.
Building a custom integration altogether is def not something Id like to do
You're already sending that payment method to the server to complete the connected account payments, it would be the same as that
true, but it would be more time developing the interface
In fact, I believe that it is already possible to handle payment methods alone with the existing paymentsheet
by passing custom: true
The fundamental mismatch you have right now is the platform context needed for the customer payment methods vs the connected account payment intents
I am not sure exactly how the save button would be generated, but I believe there's a way to customize it
hmmmmmm
What if we created a dummy PI on the platform and simply generate a valid client_secret and pass that back to the callback function?
I mean, you could, but that could be getting confirmed by the SDK too, then
Or maybe pass a valid static client secret and not create any PI?
just so we can get the illusion of a successful payment for the client
I'm not sure what that would do, to be honest. You'd need to test what happens if you did that.
let me try something. One sec
I need ot step away, but @rapid wing is keeping an eye on things here. After extensive discussion the above is what we came up with as the two best options.
thanks! I imagine those are indeed valid approaches, and if I cannot make the one I have already work, I'll sadly need to look at those
@rapid wing give me a moment while I test this pls
haha. It works
I just return a PI client_secret that I generated on the server, as often as I want, and it shows payment success and closes the payment sheet
Amazing!
hang on. Let's see if it works when I use another PM
yes it does
Next, adding a new PM
yes, works
So are you guys hiring? ๐
Glad it worked! I know you've been working on this for a few hours with several teammates of mine. It's not immediately clear what to do next with complex integrations like this so I'm glad you persisted.
Oh yeah, days spent on this. I really would recommend you add a way to send back a manual confirmation to confirmHandler so such a hack would not be needed
And I was half kidding about hiring. I really like your support team. Always a pleasure chatting with you folks. Knowledgable, patient, kind, and super helpful.
I am thinking I will need to use a PI from the live account when returning the secret because it looks like it actually checks for the pi on Stripe. It's not an ideal solution, but should be ok until, hopefully, you add an option to manually confirm vs using intentCreationCallback.
Is there an API call to retrieve a client secret?
Not exactly. You can retrieve a PaymentIntent and inspect its client_secret, or retrieve an Invoice and expand the associated PaymentIntent to view the client_secret
cool. thanks