#benkass-paymentmethod-clone

1 messages ยท Page 1 of 1 (latest)

rancid raftBOT
violet adder
#

sorry, it's long

worthy hinge
#

HI ๐Ÿ‘‹

violet adder
#

Hey @worthy hinge

#

good morning

worthy hinge
#

I'm not going to be able to review all your code without more information

#
  1. What is the error thrown
#
  1. What is the specific line of code throwing the error
#
  1. What is the API request you are making when the error is thrown
violet adder
#

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?

worthy hinge
#
  1. What type of connected accounts are you using
  2. Have you cloned Customers along with the payment method?
violet adder
#
  1. Standard
  2. No. Do we need to? I guess it makes sense
worthy hinge
rancid raftBOT
violet adder
#

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?

worthy hinge
#

This makes sense to me but I would definitely run it in test mode to verify the correct behavior

tiny agate
#

๐Ÿ‘‹ there's absolutely no reason to create a "Token" in your flow at all

#

benkass-paymentmethod-clone

violet adder
#

ok hey @tiny agate I was just about to suggested writing @fading willow who initially suggested this integration to make sure it's correct ๐Ÿ™‚

tiny agate
#

@fading willow is not around, we're the ones helping you already!

violet adder
#

I know and I appreciate it ๐Ÿ™‚

tiny agate
#

Tokens and Cards and Sources were deprecated back in 2018 and there's no reason to use that Token API unless you misunderstood something

violet adder
#

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

tiny agate
#

Sure sure but again absolutely no reason to use Token here

violet adder
#

I don't want to use tokens

tiny agate
#

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?

violet adder
#

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

tiny agate
#

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

violet adder
#

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

tiny agate
#

Do you want a Customer on each connected account. Yes or no?

violet adder
#

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

tiny agate
#

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.

violet adder
#

I don't want a customer on each connected account

tiny agate
#

perfect, drastically easier

violet adder
#

I want the customers on our platform account

tiny agate
#

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

violet adder
#

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

tiny agate
#

1/ Why are you returning the client secret back at that point, it should already have succeeded

rancid raftBOT
violet adder
#

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?

tiny agate
#

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?

violet adder
#

yes

tiny agate
#

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

violet adder
#

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

tiny agate
#

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.

violet adder
#

Please keep in mind that I am following the instructions of you and several other of your colleagues

tiny agate
#

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?

violet adder
#

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

violet adder
tiny agate
#

Okay I did sync with Rubeus who confirmed they gave you the same advice I did earlier.

violet adder
#

thanks. So what advise?

tiny agate
#

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

violet adder
#

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?

tiny agate
#

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

violet adder
#

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.
      });
tiny agate
#

this is separate from PaymentSheet in ReactNative so it's really when you initialize your app

violet adder
#

We can only init stripe with our account. We cannot init endless amount of stripe instances

tiny agate
#

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

violet adder
#

Are you talking about initStripe call provided by '@stripe/stripe-react-native'?

tiny agate
violet adder
#

One moment checking

violet adder
#

If that would do the same, then sure

#

Let me try that

rancid raftBOT
tiny agate
#

Going to run by @void ruin can help if you have follow up questions!

violet adder
#

Thanks.

void ruin
#

๐Ÿ‘‹

violet adder
#

So yes, I wrapped the initPaymentSheet in a Stripe provider and passed the connected account id. However, now I'm getting No such customer

void ruin
#

Where are you encountering "no such customer" and can you share the customer ID and account ID you're using?

violet adder
#

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

void ruin
#

When initializing with a connected account context, the customer would need ot exist on that connected account rather than your platform account

violet adder
#

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"
        >
violet adder
void ruin
#

Yes, so that customer exists on your platform, no that connected account

violet adder
#

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.

void ruin
#

So it can't be used with the way you're initializing the provider

violet adder
#

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

void ruin
#

Where is the client secret coming into play? Are you referring to the ephemeral key?

violet adder
#

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

void ruin
#

Expecting it back from where/what?

violet adder
#

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

void ruin
#

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

violet adder
#

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?

void ruin
#

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?

violet adder
#

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?

void ruin
#

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

violet adder
#

yes

#

that's how the payment sheet with rn works. At least that's what I see here

void ruin
#

Ok, sure, so then however that happens, new or existing, you have a PM on a platform customer

violet adder
#

yes

#

which we then clone on the connected account

void ruin
#

then what was described earlier involves you using that PM on your server to clone to some connected account and confirm a payment intent

violet adder
#

yes

void ruin
#

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

violet adder
#

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?

void ruin
#

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

rancid raftBOT
void ruin
#

The payment part is entirely server side

violet adder
#

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?

void ruin
#

In most cases, nothing, ideally. The server should ideally complete that payment without customer interaction after the payment method has been set up.

violet adder
#

It does

#

the payment completed

void ruin
#

However, sometimes the payment intent will require action

violet adder
#

I can see it on the connected account dashboard

#

So perhaps I should return the client_secret from the original PM that was created by the paymentSheet?

void ruin
#

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

void ruin
violet adder
#

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

void ruin
#

You don't if you're completing the payment server side

violet adder
#

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

void ruin
#

Give us a few minutes, we're discussing over here

violet adder
#

ok thank you

void ruin
#

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?

violet adder
#

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 ๐Ÿ™‚

void ruin
#

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

violet adder
#

Ah, this time I got status: 'succeeded',

#

And yes, the new card is added to the payment sheet

rancid raftBOT
void ruin
#

Ok, trying to tie things together here

violet adder
#

๐Ÿ‘

void ruin
#

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.

violet adder
#

That's kinda what I suggested above, no?

void ruin
#

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

violet adder
#

I thought the client_secret is not returned with the PM

#

indeed

void ruin
#

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

violet adder
#

yes, it is

#

I've been trying to say this all along ๐Ÿ˜„

void ruin
#

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.

violet adder
#

I understand, no worries

void ruin
#

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.

violet adder
#

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

void ruin
#

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.

violet adder
#

Building a custom integration altogether is def not something Id like to do

void ruin
#

You're already sending that payment method to the server to complete the connected account payments, it would be the same as that

violet adder
#

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

void ruin
#

The fundamental mismatch you have right now is the platform context needed for the customer payment methods vs the connected account payment intents

violet adder
#

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?

void ruin
#

I mean, you could, but that could be getting confirmed by the SDK too, then

violet adder
#

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

void ruin
#

I'm not sure what that would do, to be honest. You'd need to test what happens if you did that.

violet adder
#

let me try something. One sec

void ruin
violet adder
#

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

rapid wing
#

Amazing!

violet adder
#

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? ๐Ÿ˜‰

rapid wing
#

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.

violet adder
#

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.

violet adder
#

Is there an API call to retrieve a client secret?

rapid wing
#

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

violet adder
#

cool. thanks