datatrans / secure-fields-sample

Visually pleasing credit card forms with Datatrans Payment Gateway and PCI Proxy.
https://datatrans.github.io/secure-fields-sample/
MIT License
7 stars 7 forks source link

Documentation for types of fields #19

Closed princefishthrower closed 2 years ago

princefishthrower commented 2 years ago

Is there a link to the full documentation for secure fields? I have only found examples with the card number and CVV, but what about the expiration date and month? Are they not needed for this method of payment process?

dmengelt commented 2 years ago

hi @princefishthrower the expiry month and expiry year field can be your own <input> fields. Secure Fields only creates iframes for cardno and cvv.

Also, when using browser autofill you can listen for the autofill event to automatically fill your own expiry fields: https://docs.datatrans.ch/docs/secure-fields-events#section-browser-autofill

princefishthrower commented 2 years ago

Hi @dmengelt

Ah I see now those fields are not needed to be through an iframe.

Additionally, from what I understand I would first need a transaction Id to properly initialize the secure fields. But where do I pass this transaction ID? In this.initSecureFields(), i.e. this.initSecureFields(transactionId)?

Transaction IDs are not "secret", right? Because I would need to pass that to our web payment page with something like ourwebsite.com/pay?transactionID=abc123... ultimately we plan to call this web payment portal through a WebView on a React Native app. Is this a pattern you have seen or could recommend?

Thanks again for the help.

dmengelt commented 2 years ago

@princefishthrower did you manage to read our documentation on https://docs.datatrans.ch/docs/secure-fields?

You will get the transactionId from the initial server to server API call and the you would initialize Secure Fields as follows:

var secureFields = new SecureFields();
secureFields.init( {{transactionId}}, {
      cardNumber: "cardNumberPlaceholder",
      cvv: "cvvPlaceholder",
  });

Correct. They are not a secret. No, we are currently not aware of any integrations using Secure Fields within a WebView on a React Native app.

princefishthrower commented 2 years ago

@dmengelt - hmmm... why is it then I only see this.initSecureFields() in the React example: https://github.com/datatrans/secure-fields-sample/blob/c7ed9a73015bd2faaa1b9de00f8d22774c13c2c1/react-example/src/SecureFields.js#L41

and also this.secureFields.initTokenize(): https://github.com/datatrans/secure-fields-sample/blob/c7ed9a73015bd2faaa1b9de00f8d22774c13c2c1/react-example/src/SecureFields.js#L91

I can't find documentation on either of these functions, especially the fields and options values for initTokenize()...

Also, is there any example or someone I could talk to about possible integrations with React Native? I also contacted Datatrans through email, the contacts I spoke to are also unaware of advising for React Native...

dmengelt commented 2 years ago

@princefishthrower

this.initSecureFields is a class method of our sample React component.

However, sorry for the confusion. In our GitHub examples we use initTokenize. However you should only use initTokenize if you are are a PCI-Proxy customer. Is this the case for you? If you want to do actual payments with Secure Fields you should follow https://docs.pci-proxy.com/collect-and-store-cards/capture-iframes

fields values can be cardNumber and cvv:

secureFields.init(
    {{transactionId}},
    {
        cardNumber: "cardNumberPlaceholder",
        cvv:  {
            placeholderElementId: "cvvPlaceholder",
            inputType: "tel",
            placeholder: "enter your cvv"
        }
    },{
        styles: styles,
        focus: "cvv"
    }
);

https://docs.datatrans.ch/docs/secure-fields-options#section-further-initialization-settings

options values can be styles, focus and paymentMethods. You can find some examples here: https://docs.datatrans.ch/docs/secure-fields-options#section-further-initialization-settings

Regarding react native: we don't have any specific examples on how to integrate this properly. I'm sure there are best practices. Please make sure to check whether our Mobile SDK could be helpful in your case.

princefishthrower commented 2 years ago

Do I need to be a customer of PCI-Proxy to use Secure Fields? I assumed secured fields would work out of the box as it is mentioned directly in the Datatrans API docs as a possible method of customer initialized payments.

dmengelt commented 2 years ago

You don't πŸ˜‰

What I'm trying to say is: PCI-Proxy customer (they mostly "only" do tokenization): Use initTokenize Datarans Payment customer: 1. initial server to server call to get a transactionId, 2. Use init(trxId)

princefishthrower commented 2 years ago

Ok, it's clear now! When I run my example locally, I unfortunately get a cross-origin error:

Uncaught Error: A cross-origin error was thrown. React doesn't have access to the actual error object in development. See https://reactjs.org/link/crossorigin-error for more information.
    at Object.invokeGuardedCallbackDev (webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:4005:19)
    at invokeGuardedCallback (webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:4056:31)
    at invokeGuardedCallbackAndCatchFirstError (webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:4070:25)
    at executeDispatch (webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:8243:3)
    at processDispatchQueueItemsInOrder (webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:8275:7)
    at processDispatchQueue (webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:8288:5)
    at dispatchEventsForPlugins (webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:8299:3)
    at eval (webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:8508:12)
    at batchedEventUpdates$1 (webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:22391:12)
    at batchedEventUpdates (webpack-internal:///./node_modules/react-dom/cjs/react-dom.development.js:3745:12)

I assume that is because I am making requests from localhost. Is there a sort of dev mode flag I can switch on or will I have to proxy myself?

Furthermore, when it is run even on a real website, I still get a variety of errors:

react-dom.production.min.js:101 Uncaught TypeError: Cannot read properties of undefined (reading 'focus')
    at Object.B.focus (secure-fields-2.0.0.min.js:1:4298)
    at callback (SecureFields.tsx:191:49)
    at onClick (SecureField.tsx:14:93)
    at Object.We (react-dom.production.min.js:52:317)
    at Ye (react-dom.production.min.js:52:471)
    at react-dom.production.min.js:53:35
    at _r (react-dom.production.min.js:100:68)
    at Cr (react-dom.production.min.js:101:380)
    at react-dom.production.min.js:113:65
    at De (react-dom.production.min.js:292:189)

when trying to click either the credit card or CVV fields, and when clicking the pay button, I get:

Uncaught TypeError: Cannot read properties of undefined (reading 'postMessage')
    at v (secure-fields-2.0.0.min.js:1:4239)
    at Object.B.submit (secure-fields-2.0.0.min.js:1:3600)
    at onClick (SecureFields.tsx:218:48)
    at Object.We (react-dom.production.min.js:52:317)
    at Ye (react-dom.production.min.js:52:471)
    at react-dom.production.min.js:53:35
    at _r (react-dom.production.min.js:100:68)
    at Cr (react-dom.production.min.js:101:380)
    at react-dom.production.min.js:113:65
    at De (react-dom.production.min.js:292:189)

is there an example floating around somewhere that uses both React and the init() method? Or should it theoretically just be able to swap out of initTokenize for the init() function to have it work?

One last requirement I would like is for the customer to have the option to to save their card (in Datatrans lingo I guess make an "alias" for the card). Can this also be done through the secured fields api? Didn't see this option in the docs anywhere. (https://docs.datatrans.ch/docs/secure-fields)

princefishthrower commented 2 years ago

This is quite strange... as a sanity check I copied the src/ folder of react-example directly to a fresh create-react-app just to see if it works there. Immediately on page load I get:

Uncaught TypeError: Cannot read properties of undefined (reading 'placeholderElementId')
    at b (secure-fields-2.0.0.min.js:1:1702)
    at n (secure-fields-2.0.0.min.js:1:1648)
    at Object.B.initTokenize (secure-fields-2.0.0.min.js:1:820)
    at n.initSecureFields (SecureFields.js:91:23)
    at HTMLScriptElement.n.onload (SecureFields.js:48:12)

as well as the same focus and postMessage issues above when trying to interact with the form. Looking at the react example provided by you guys, I can't see webpack doing anything special that makes these errors go away, but I guess I need to look harder, as there is obviously a difference between them.

EDIT: found the issue, buried in the example/src/index.js, I see an example config is passed to the component when calling ReactDOM.render:

ReactDOM.render(<SecureFields config={{
  merchantID: '1100007006',
  fields:{
    cardNumber: {
      placeholderElementId: 'card-number',
      inputType: 'tel'
    },
    cvv: {
      placeholderElementId: 'cvv-number',
      inputType: 'tel'
    }
  },
  options: {}
}} />, document.getElementById('root'))
pstadler commented 2 years ago

Uncaught Error: A cross-origin error was thrown. React doesn't have access to the actual error object in development. See https://reactjs.org/link/crossorigin-error for more information.

That sounds like a general problem with React.

What happens if you run the React example app? I just tested it and it works fine for me. Could it be that you're trying to load and initialize the secure fields script on your server vs. in the browser? You should load it only in the browser.

If this doesn't help, please share your code in a repo, gist or similar.

dmengelt commented 2 years ago

One last requirement I would like is for the customer to have the option to to save their card (in Datatrans lingo I guess make an "alias" for the card). Can this also be done through the secured fields api? Didn't see this option in the docs anywhere.

Good input. We have to add this to our docs.

So once you finished the payment (don't forget to do the final server to server authorization call) you can get the alias by calling our Status API. You will find the alias within the card.alias property.

princefishthrower commented 2 years ago

@dmengelt - so the alias is generated regardless?

@pstadler - more interesting findings. When I use the example merchant ID (1100007006), both credit card and CVV fields work. However, when I pass ours (1100034100), only the credit card field works. Then I realized this was because we are not PCI Proxy customers, so instead of using initTokenize, I passed the following:

this.secureFields.init("220121091819405816", {
      cardNumber: "card-number",
      cvv: "cvv-number"
    })

The credit card field works this way, but I still get the same error why I try to click the CVV field:

Uncaught TypeError: Cannot read properties of undefined (reading 'focus')
    at Object.B.focus (secure-fields-2.0.0.min.js:1:4298)
    at callback (SecureFields.js:75:1)
    at onClick (SecureField.js:8:1)
    at HTMLUnknownElement.callCallback (react-dom.development.js:3945:1)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:3994:1)
    at invokeGuardedCallback (react-dom.development.js:4056:1)
    at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js:4070:1)
    at executeDispatch (react-dom.development.js:8243:1)
    at processDispatchQueueItemsInOrder (react-dom.development.js:8275:1)
    at processDispatchQueue (react-dom.development.js:8288:1)

This is curious, because upon inspecting my DOM, I indeed have a <div> with id="cvv-number"

The last thing I can guess is that I'm no longer able to pass the inputType: 'tel' that was passed in the config for the initTokenize call, maybe that is causing the problem?

EDIT: Okay, saw that it is possible to pass this input type (and even placholder!) so I tried this:

this.secureFields.init("220121091819405816", {
      cardNumber: {
        placeholderElementId: "card-number",
        inputType: "tel",
        placeholder: "Credit card"
      },
      cvv: {
        placeholderElementId: "cvv-number",
        inputType: "tel",
        placeholder: "CVV"
      }
    },{
      focus: 'cardNumber'
    })

Credit card field still fine, CVV still throwing errors. I'm going to just fork the repository and show you what is going on.

dmengelt commented 2 years ago

so the alias is generated regardless?

Yes.

I just tried with your merchantId. And this works for me:

 secureFields.init(
        '220121094931809683',
        {
            cardNumber: {
                placeholderElementId: 'card-number',
                inputType: "tel",
            },
            cvv: {
                placeholderElementId: 'cvv-number',
                inputType: "tel",
            }
        },
        {
            focus: "cardNumber"
        }
    );
princefishthrower commented 2 years ago

@dmengelt Great news for the alias feature!

Hmmm what do you mean "with your merchantId" - is there a place to provide it somewhere in the configuration?

I just did the same (forked from this repository) and it doesn't work. This commit was my only change: https://github.com/princefishthrower/secure-fields-sample/commit/4b459800fe01089dc64da813b31a1a3d58ab311b

If you build it and try it in the browser you'll see the CVV doesn't populate with the placeholder, and clicking it throws an error

dmengelt commented 2 years ago

@princefishthrower I meant I created a transactionId by using your merchantId. Sorry.

For your fork you now need to add the expiry handling.

If I run your fork I get:

Uncaught TypeError: Cannot read properties of undefined (reading 'expy')
dmengelt commented 2 years ago

Regarding Uncaught TypeError: Cannot read properties of undefined (reading 'focus'). This happens when you use secureFields.focus('cvv') but the transactionId is already consumed.

Once the form is submitted (secureFields.submit()) you need to create a new transactionId.

princefishthrower commented 2 years ago

@dmengelt - thanks for all your help, this is great stuff. I think I'm very close to getting it to work! I will investigate and return shortly...

princefishthrower commented 2 years ago

@dmengelt - thanks again for all your help! got everything working! (at least so far πŸ˜„)

Transactions are showing up in our dashboard πŸ‘

I have some other technical questions, however. πŸ˜„

  1. As you've told me, these initialized transactions can only be consumed once. Is this also the case for the authenticated transactions, as well as the authorized transactions? If this is the case, this would mean we would have to start all over if there is ever a problem at any point along the path to settlement.

  2. I notice that the data.transactionID returned in the case of success is identical to the initialized transaction - does this mean the transaction number itself remains the same through all stages of authentication and authorization? Same with the refno - should we keep this refno the same at each stage of the transaction process, or generate a new one at each stage (i.e. refno-init-123, refno-authenticated-123, refno-authorized-123)?

  3. The initialized transactions are documented as having a lifetime of only about 30 minutes. I assume the authenticated and authorized transactions are much longer lived?

  4. Would you recommend authorizing the authenticated transactions almost immediately after they are authenticated, or alternatively, authorizing them just before settling (or possibly cancelling) them? (Maybe the timing isn't all that important really, but I at least know from the documentation that a transaction has to be authorized at some point before settling or canceling it)

Again, thank you @dmengelt and @pstadler for all your help, you can probably guess from the spammy messages I'm under a tight deadline and your help has been extremely helpful!

dmengelt commented 2 years ago

got everything working! (at least so far πŸ˜„)

Sounds good! Could you maybe share a transactionId for us to verify and maybe also a screenshot of the Secure Fields integration. I'm curious how it looks. πŸ˜€

  1. No it is only about the client side Secure Fields integration. So you can only do secureFields.init(trxId) once. But retrying (make sure to use idempotency if you can) for example the final server to server authorization call is perfectly fine. If your backend process allows it you can also consider to use "autoSettle": true for the server to server authorzation call. So then no settlement call is needed.

  2. Yes correct. The transactionId remains the same throughout all stages. For the refno I would also keep the same.

  3. Correct. Authenticated and authorized transaction live "forever".

  4. I suggest to authorize almost immediately after the authentication.

Just one question from my side. Did you manage to handle the 3D process?

princefishthrower commented 2 years ago

@dmengelt - sure, here are some transactions from early this morning and more recently today (I haven't authorized any of them yet):

220122153250437362 220122124346213446 220122000520909081

And the screenshot of our actual page:

Screen Shot 2022-01-22 at 16 02 10

(Didn't realize until later that the denomination is in cents - so actually it should show "Pay CHF 1,23" to be correct.) It looks very mobile-ish because as I mentioned we open this in a WebView from our mobile app and this keeps the UI experience clean for our customers πŸ˜„

Unfortunately for now I think our business requirements require us to use autoSettle: false as we need to clear payments based on customer events at a slightly later time. (I'm also aware certain providers only keep pending transactions open for so long - this will be something for us to discover and refine as we go)

I haven't looked in to the 3D process yet. Actually I haven't done much work towards extra steps in general (error message styling, success etc.)- though I am working on this immediate authorization right now. The next step is then to figure out how to take the customer's choice to remember the alias or not - i guess this will be done from handling the success callback - a POST request should be sent to our success URL in that case, correct? Then i'll have to use the refno or something maybe to see if they chose to save the card data...

For the 3D process, Is there a way to know if a provider will trigger the 3D process? or is based on location, timing, amount, etc.?

By the way, I've built out my own initial TypeScript typings for the SecureFields library and have successfully refactored the SecureFields usage in a functional component with hooks - was thinking of commenting in the other open issue towards that, I can provide some pointers.

dmengelt commented 2 years ago

Nice! It looks good!

I'm also aware certain providers only keep pending transactions open for so long - this will be something for us to discover and refine as we go

Exactly. A general rule of thumb for credit cards is: Try to settle before 30 days have passed. Reach out to our support if you need detailed information about this as it could depend on the acquirer you are going to use.

The next step is then to figure out how to take the customer's choice to remember the alias or not - i guess this will be done from handling the success callback - a POST request should be sent to our success URL in that case, correct?

No. You have to do this on your own. A possible process could be:

  1. server to server init API call where you also specify a returnUrl. However, this returnUrl is only used when there is 3D involved.
  2. secureFields.init(trxId)
  3. cardholder enters cardno, cvv, expiry
  4. success callback (success event) is invoked.
  5. Once you decide to handle 3D this success event will contain a redirect property with a URL
  6. Redirect the cardholders browser to this URL
  7. Once the cardholder completes the 3D process he gets redirect back to the returnUrl from step 1.
  8. trx becomes authenticated
  9. Do the server to server authorization call
  10. If in step 3. the cardholder toggled "Save for next time" you do a Status API call where you will find the alias in the card.alias property.

Of course if you are not handling 3D you can omit steps 6. and 7.

For the 3D process, Is there a way to know if a provider will trigger the 3D process? or is based on location, timing, amount, etc.?

We will not return the redirect property in step 5. if 3D is not needed for the entered card (card not enrolled or 3D2 frictionless flow)

By the way, I've built out my own initial TypeScript typings for the SecureFields library and have successfully refactored the SecureFields usage in a functional component with hooks - was thinking of commenting in the other open issue towards that, I can provide some pointers.

Maybe @pstadler can comment on this.

pstadler commented 2 years ago

weβ€˜re also using hooks internally. please share your code if you can, always nice to have different angles on a solution! πŸ‘πŸ»

princefishthrower commented 2 years ago

@dmengelt , @pstadler - Thanks for all the information again. I will comment in the other thread with the code that I'm using. I'll probably make an example repository based on the react example again.

By now I understand fairly well how accepting payments from customers works, but I'm having trouble seeing how to do the reverse: paying out from our bank account to customer's IBANs. Is the only way of paying out to customers banks through Klarna? If that is the case, I see i need a previously registered alias for their account - I guess I will also need a secure fields site with an IBAN input, and I guess I could do just a 0 "payment" so really they would just be registered / stored with their IBAN so I can get an alias, right? I guess I don't understand how to tell the API to distinguish between a payment from a customer to us, to a payout from us to a customer.

princefishthrower commented 2 years ago

@dmengelt - one more thing with the 3D process, is the transaction ID also a part of the returnUrl in addition to the 3D result? Otherwise I guess I'll have to store the transaction ID in localstorage....

dmengelt commented 2 years ago

We don't have payout APIs. However, you might want to look into our marketplace solution.

one more thing with the 3D process, is the transaction ID also a part of the returnUrl in addition to the 3D result?

Yes. When redirecting back from the 3D process, we will do an application/x-www-form-urlencoded POST to the returnUrl which includes the uppTransactionId parameter.

princefishthrower commented 2 years ago

@pstadler - ok. For the 3D redirect, I assume in addition to the POST there is a normal GET redirect to returnUrl? Or what would the users see when the 3D is completed?

dmengelt commented 2 years ago

There is no additional GET. They only thing we do is:

<form name="returnForm" action="returnUrl" method="post">
  <input type=hidden name="uppTransactionId" value="xxx" />
</form>

So what the user sees at the end of the 3D process is the content of your returnUrl

princefishthrower commented 2 years ago

Interesting... the Secure Fields endpoint /v1/transactions/secureFields has no options for autoSettle, language, allowedPayments, refno and so on...

Actually refno I guess would be most important for us here... is that only something we would set when authorizing the authenticated transaction? Or is there a way to continue using the standard initialization endpoint (which we are currently using) and also provide a returnUrl?

dmengelt commented 2 years ago

is that only something we would set when authorizing the authenticated transaction?

Exactly.

secureFields.init(
    {{transactionId}},
    {
        cardNumber: "cardNumberPlaceholder",
        cvv:  "cvvPlaceholder"
    },{
        styles: styles,
        paymentMethods: ["ECA", "VIS"]  // allowing MC and Visa only
    }
);

Or is there a way to continue using the standard initialization endpoint.

No please use the Secure Fields initialization endpoint.

princefishthrower commented 2 years ago

@dmengelt - is the uppTransactionId the only thing that is POSTed? Don't we also need to know if the 3D process was actually successful or not?

dmengelt commented 2 years ago

Yes. Sorry I forgot to also mention that. Next to uppTransactionId we will also post status_3d to the returnUrl. https://docs.datatrans.ch/docs/secure-fields#section-success-3-d-secure.

However theoretically you don't need this parameter as you can always do a Status API call to figure out what the status of the transaction is.

dmengelt commented 2 years ago

@princefishthrower all good?

princefishthrower commented 2 years ago

@dmengelt Mostly πŸ˜„ I haven't had time to check the 3D flow yet, I'm also unsure from these docs: https://docs.datatrans.ch/docs/3d-secure#section-3-d-secure-response what is actually returned, is it the single character (Y, N, U, A, etc.), or the full code Authenticated, Enrolled etc., or both?

Getting the status of a transaction and saving aliases is also working. I have a remaining question here as well though - after we manage to get a card alias, will we be able to do the "authorize" transactions (i.e. the endpoint to issue payments without user interaction), without the user doing 3D again? In otherwords, is the alias enough, or will we always have to direct these customers with 3D credit cards to go through the 3D flow?

One other thing we did notice from our beta testing is that you can use the test cards provided (for example the master card 5200 0000 0000 0007), but with any CVV and any expiration date (not just the 123 and 06 / 25 respectively), and the payment is still able to be authenticated and authorized successfully. Is this a "feature" instead of a "bug"?

dmengelt commented 2 years ago

what is actually returned, is it the single character (Y, N, U, A, etc.), or the full code Authenticated, Enrolled etc., or both?

The single character.

In otherwords, is the alias enough, or will we always have to direct these customers with 3D credit cards to go through the 3D flow?

The alias (and the expiry) is enough if you want to do a merchant initiated transaction (MIT) (subscriptions or other use cases where the customer is not in session). If you have the customer "in session" you should trigger the 3D flow again. You would do that by using the "normal" initialisation API (not the Secure Fields one) and submit all the infos you have. alias, expiry, paymentMethod. Then you can redirect the customers browser to the URL we return (check the Location header of the initialisation API response) and we would automatically redirect to the 3D page of his bank.

Is this a "feature" instead of a "bug"?

Can you share a transactionId with me so that I can quickly look this up?

princefishthrower commented 2 years ago

@dmengelt sorry for the delay here, we've been waiting on a payment acquirer for card payments in production and as such haven't been doing any further tests. Turns out what I was saying about those authorized payments also wasn't true. I believe there has been more than enough discussion here and hopefully will be a helpful resource for anyone integrating secure fields in the future. If I run into trouble when integrating the 3D flow, I'll open up a separate ticket.