react-paypal-js
react-paypal-js copied to clipboard
[BUG]: React skipped hostedField
Is there an existing issue for this?
- [X] I have searched the existing issues.
π Describe the Bug
Referring to my other bug report on #304 , I tried another way using code example as a base. I did not get any error but it seems like React skipped hostedfield.cardFields.submit()
I was unable to find any solutions for this issue but I found a few recent posts on stackoverflow to address the same issue.
My code:
App.js
import { useState, useEffect, useRef } from "react";
import {
PayPalScriptProvider,
PayPalHostedFieldsProvider,
PayPalHostedField,
usePayPalHostedFields,
} from "@paypal/react-paypal-js";
const CUSTOM_FIELD_STYLE = {"border":"1px solid #606060","boxShadow":"2px 2px 10px 2px rgba(0,0,0,0.1)"};
const INVALID_COLOR = {
color: "#dc3545",
};
// Example of custom component to handle form submit
const SubmitPayment = ({ customStyle, mr, sh }) => {
const [paying, setPaying] = useState(false);
const cardHolderName = useRef(null);
const hostedField = usePayPalHostedFields();
const baseUrl = "http://local.payment.web"
const handleClick = () => {
if (!hostedField?.cardFields) {
const childErrorMessage = 'Unable to find any child components in the <PayPalHostedFieldsProvider />';
// eslint-disable-next-line no-undef
action(ERROR)(childErrorMessage);
throw new Error(childErrorMessage);
}
const isFormInvalid =
Object.values(hostedField.cardFields.getState().fields).some(
(field) => !field.isValid
) || !cardHolderName?.current?.value;
if (isFormInvalid) {
return alert(
"The payment form is invalid"
);
}
setPaying(true);
hostedField.cardFields
.submit({
cardholderName: cardHolderName?.current?.value,
})
.then(async () => {
return await fetch(`${baseUrl}/api/Ppal/ReactApi/CaptureOrder`, {
mode: "cors",
method: "post",
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
merchantRef: mr,
sessionHash: sh
})
})
.then(res => res.json())
.then((data) => {
console.log("data from pay button : ", data);
if(data.isApproved) alert("Payment successful")
if(!data.isApproved) alert("Payment declined")
if(data.isExpired) alert("Payment expired")
})
.catch(err => console.log("err : ", err))
.finally(() => setPaying(false))
})
.catch(err => console.log("Error catch : ", err))
};
return (
<>
<label title="This represents the full name as shown in the card">
Card Holder Name
<input
id="card-holder"
ref={cardHolderName}
className="card-field"
style={{ ...customStyle, outline: "none" }}
type="text"
placeholder="Full name"
/>
</label>
<button
className={`btn${paying ? "" : " btn-primary"}`}
style={{ float: "right" }}
onClick={handleClick}
>
{paying ? <div className="spinner tiny" /> : "Pay"}
</button>
</>
);
};
export default function App() {
const [clientId, setClientId] = useState(null)
const [clientToken, setClientToken] = useState(null)
const [merchantRef, setMerchantRef] = useState(null)
const [sessionHash, setSessionHash] = useState(null)
const [orderId, setOrderId] = useState(null)
const baseUrl = "http://local.payment.web"
useEffect(() => {
(async () => {
return await fetch(`${baseUrl}/api/Ppal/ReactApi/PrepareForPayment`, {
mode: "cors",
method: "post",
headers: {
'content-type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
"curCode": "USD",
"orderAmount": 500
})
}).then(res => {
console.log("fetch Data : ", res);
return res.json()
}).then(data => {
console.log("fetch Data : ", data);
setClientId(data.ClientId)
setClientToken(data.ClientToken)
if (data.prepareForPayment) {
setMerchantRef(data.merchantRef)
setSessionHash(data.sessionHash)
}
}).catch(err => console.log(err))
})();
}, []);
return (
<>
{clientToken ? (
<PayPalScriptProvider
options={{
"client-id": clientId,
components: "buttons,hosted-fields",
"data-client-token": clientToken,
intent: "capture",
vault: false,
}}
>
<PayPalHostedFieldsProvider
styles={{".valid":{"color":"#28a745"},".invalid":{"color":"#dc3545"},"input":{"font-family":"monospace","font-size":"16px"}}}
createOrder={async() => {
return await fetch(`${baseUrl}/api/Ppal/ReactApi/CreateOrder2`, {
method: 'post',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
merchantRef: merchantRef,
sessionHash: sessionHash
})
}).then(res => {
console.log("res from createOrder", res);
return res.json()
}).then(data => {
console.log("orderId from button : ", data?.orderId);
if (data?.createOrder) return setOrderId(data?.orderId)
})
}}
>
<label htmlFor="card-number">
Card Number
<span style={INVALID_COLOR}>*</span>
</label>
<PayPalHostedField
id="card-number"
className="card-field"
style={CUSTOM_FIELD_STYLE}
hostedFieldType="number"
options={{
selector: "#card-number",
placeholder: "4111 1111 1111 1111",
}}
/>
<label htmlFor="cvv">
CVV<span style={INVALID_COLOR}>*</span>
</label>
<PayPalHostedField
id="cvv"
className="card-field"
style={CUSTOM_FIELD_STYLE}
hostedFieldType="cvv"
options={{
selector: "#cvv",
placeholder: "123",
maskInput: true,
}}
/>
<label htmlFor="expiration-date">
Expiration Date
<span style={INVALID_COLOR}>*</span>
</label>
<PayPalHostedField
id="expiration-date"
className="card-field"
style={CUSTOM_FIELD_STYLE}
hostedFieldType="expirationDate"
options={{
selector: "#expiration-date",
placeholder: "MM/YYYY",
}}
/>
<SubmitPayment customStyle={{"border":"1px solid #606060","boxShadow":"2px 2px 10px 2px rgba(0,0,0,0.1)"}} />
</PayPalHostedFieldsProvider>
</PayPalScriptProvider>
) : (
<h1>Loading token...</h1>
)}
</>
);
}
π Current Behavior
No error and Alert
tag render but on Network tab, it did not fetch capture
api as screenshot below
π€ Expected Behavior
It should call capture
api and render alert tag whether the payment was successful or declined or log any errors
π¬ Minimal Reproduction
No response
π Environment
| Software | Version(s) |
| ---------------- | ---------- |
| react-paypal-js | ^7.8.1 |
| Browser | Chrome |
| Operating System | Window10 |
Relevant log output
No log or output, React skipped `Hostedfield.cardfield.submit()`
Code of Conduct
- [X] I agree to follow this project's Code of Conduct
β Anything else?
No response
@westeezy Any update on this?
Not yet we have to triage and schedule it. I'll post updates once we have someone assigned to it.
Not yet we have to triage and schedule it. I'll post updates once we have someone assigned to it.
Ok well noted
Hi @tengkuzulfadli is this still a problem for you? We have a working example of Hosted Fields on Storybook here: https://paypal.github.io/react-paypal-js/?path=/story/paypal-paypalhostedfields--default. That one does call submit() as expected.
If this is still a problem, can you try removing the loading state around paying
/setPaying
and see if that helps?
Hi @gregjopa Yes it is. I did remove the loading state and avoid to use any additional state to my code. But I'm still having problem with Hostedfield
. Everytime I run the code, it skipped hostedfield.cardFields.submit()
Okay thanks for confirming.
Letβs verify your eligibility. With the Hosted Fields component merchants have to onboard to be eligible.
What does this command return when you run it in the console?
paypal.HostedFields.isEligible()
https://developer.paypal.com/docs/checkout/advanced/integrate/#link-fullstackexamplenodejs
@gregjopa sorry for the late response. I will get back to you soon today with my implementations using 2 ways; PaypalButton and Hostedfields.cardfield to emphasis the issue with hostedfields. I will place my code to sandbox
For PaypalButtons
, it seems like you have updated your docs and removed hostedfields
of components
object inside options
of PaypalScriptProvider
. This was one of issue I was facing before. The paypal buttons was working fine after I removed hostedfields
and only define buttons
. So, for this, it is resolved and I will not provide sample code in sandbox. However, we do not want to use PaypalButtons
, instead we want to use PayPalHostedFieldsProvider
.
For PayPalHostedFieldsProvider
, the docs that you suggested, is not using React-Paypal-Js package, I did log paypal.HostedFields.isEligible()
and it returns true
. I implemented it before and it was working. However, again we want to use PayPalHostedFieldsProvider
from React-Paypal-js package. When I run my code above, onClick
event, browser skipped hostedfields.cardfields.submit()
Just to clarify, you can use the components
prop to load both buttons and hosted fields or just one component. Ex:
<PayPalScriptProvider options={{ "client-id": "test", components: "buttons,hosted-fields" }}>
...
</PayPalScriptProvider>
Please provide a url to the reproduction of the hosted fields issue you are experiencing related to submit()
not firing and we can debug further.
Please see my implementation below if any mistakes from our side.
- The form was finally rendered because I did not use
hostedFields.cardfields
method on this implementation - When I click
Submit
button, it only loggedError
,submit()
andfetch
did not fire. I replaced path with our own server path built with .net framework and you can test using yours - If I remove
PaypalScriptProvider
that wrappingform
tag, I've got an error thatusePayPalScriptReducer
needs to be wrapped insidePaypalScriptProvider
import React, { useCallback, useEffect, useState } from "react";
import { PayPalScriptProvider, usePayPalScriptReducer } from "@paypal/react-paypal-js";
const PaypalForm = () => {
const paypal = window.paypal;
const [{ isResolved, options }] = usePayPalScriptReducer();
const [getHostedFields, setGetHostedFields] = useState(null);
const [renderForm, setRenderForm] = useState();
const initPaypal = useCallback(async () => {
if (typeof getHostedFields === "boolean" && !!getHostedFields) {
const renderFormFields = await paypal.HostedFields.render({
createOrder: function (data, actions) {
return ""
// return fetch("create-order-server", {
// method: "post"
// }).then((res) => {
// return res.json()
// }).then((data) => {
// return console.log(data.id);
// })
},
styles: {
input: {
"font-size": "16pt",
color: "#3A3A3A"
},
".number": {
"font-family": "monospace"
},
".valid": {
color: "green"
}
},
fields: {
number: {
selector: "#card-number",
placeholder: "Credit Card Number"
},
cvv: {
selector: "#cvv",
placeholder: "CVV"
},
expirationDate: {
selector: "#expiration-date",
placeholder: "MM/YYYY"
}
}
});
setRenderForm(renderFormFields);
}
}, [paypal, getHostedFields]);
useEffect(() => {
if(isResolved) {
console.log("isResolved : ", isResolved)
setGetHostedFields(paypal.HostedFields.isEligible());
console.log("Hostedfields eligible : ", paypal.HostedFields.isEligible());
}
}, [setGetHostedFields, isResolved, paypal])
useEffect(() => {
initPaypal();
}, [initPaypal])
const handleFormSubmit = (e) => {
e.preventDefault();
renderForm
.submit()
.then((payload) => {
console.log("Payload : ", payload);
// return fetch(`server-to-handle-capture-payment/${payload.orderId}`, {
// method: "post"
// }).then((res) => {
// if(!res.ok) {
// alert("Payment unable to capture")
// }
// });
return payload;
}).catch((err) => console.log("Error", err))
}
if (!getHostedFields) return null;
return(
<PayPalScriptProvider
options={{
"client-id": "use your own client id",
"data-client-token": "use your own token",
components: "hosted-fields",
intent: "capture",
vault: false
}}
>
<form id="my-sample-form" onSubmit={handleFormSubmit}>
<label htmlFor="card-number">Card Number</label>
<div id="card-number"></div>
<label htmlFor="expiration-date">Expiration Date</label>
<div id="expiration-date"></div>
<label htmlFor="cvv">CVV</label>
<div id="cvv"></div>
<button value="submit" id="submit" className="btn">
Pay with Card
</button>
</form>
</PayPalScriptProvider>
)
}
const App = () => {
return(
<PayPalScriptProvider
options={{
"client-id": "use your own client id",
"data-client-token": "use your own token",
components: "hosted-fields",
intent: "capture",
vault: false
}}
>
<PaypalForm />
</PayPalScriptProvider>
)
}
export default App;
I did create PaypalForm
as a separate component and removed PaypalScriptProvider
that wrapping form
tag, the isResolved
was false
and I have 4 errors: -
-
TypeError: Cannot read properties of undefined (reading 'HostedFields')
-
react_devtools_backend.js:4026 Failed to load the PayPal JS SDK script. Error: Expected client-id to be passed (debug id: f212034907ac9)
-
react-paypal-js.js:520 GET https://www.paypal.com/sdk/js?components=buttons,funding-eligibility,hosted-fields&intent=capture&vault=false net::ERR_ABORTED 400
-
react-paypal-js.js:679 GET https://www.paypal.com/sdk/js?components=buttons,funding-eligibility,hosted-fields&intent=capture&vault=false 400
Hi @tengkuzulfadli check the network tab to see why the JS SDK script is failing to load. From what I can tell is it's missing hte client-id
which is a required param.
@gregjopa Thank you for pointing it out. I managed to fix it by passing the client-id
. However, I'm still getting the same errors for hostedfields as screenshot below:
Here is my implementation:
import React, { useCallback, useEffect, useState } from "react";
import { PayPalScriptProvider, usePayPalScriptReducer } from "@paypal/react-paypal-js";
const baseUrl = "http://local.payment.web"
const PaymentForm = ({ merchantRef, sessionHash }) => {
const [orderId, setOrderId] = useState();
const paypal = window.paypal;
const [{ isResolved, options }] = usePayPalScriptReducer();
const [getHostedFields, setGetHostedFields] = useState(null);
const [renderForm, setRenderForm] = useState();
const initPaypal = useCallback(async () => {
const renderFormFields = await paypal.HostedFields?.render({
createOrder: function (data, actions) {
return fetch(`${baseUrl}/api/Ppal/ReactApi/CreateOrder2`, {
method: 'post',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
merchantRef: merchantRef,
sessionHash: sessionHash
})
}).then(res => {
console.log("res from createOrder", res);
return res.json()
}).then(data => {
console.log("orderId from button : ", data?.orderId);
if (data?.createOrder) return setOrderId(data?.orderId)
})
},
styles: {
input: {
"font-size": "16pt",
color: "#3A3A3A"
},
".number": {
"font-family": "monospace"
},
".valid": {
color: "green"
}
},
fields: {
number: {
selector: "#card-number",
placeholder: "Credit Card Number"
},
cvv: {
selector: "#cvv",
placeholder: "CVV"
},
expirationDate: {
selector: "#expiration-date",
placeholder: "MM/YYYY"
}
}
})
setRenderForm(renderFormFields);
}, [paypal, getHostedFields]);
useEffect(() => {
if(isResolved) {
console.log("isResolved : ", isResolved)
setGetHostedFields(paypal.HostedFields?.isEligible());
console.log("Hostedfields eligible : ", paypal.HostedFields?.isEligible());
}
}, [setGetHostedFields, options, isResolved, paypal])
useEffect(() => {
initPaypal();
}, [initPaypal])
const handleFormSubmit = (e) => {
e.preventDefault();
renderForm
.submit()
.then(async (payload) => {
console.log("Payload : ", payload);
return await fetch(`${baseUrl}/api/Ppal/ReactApi/CaptureOrder`, {
mode: "cors",
method: "post",
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
merchantRef: merchantRef,
sessionHash: sessionHash
})
}).then((response) => response.json()).then((data) => {
console.log("data from pay button : ", data);
if(data.isApproved) {
alert("Payment successful")
}
if(!data.isApproved) alert("Payment declined")
if(data.isExpired) alert("Payment expired")
}).catch((err) => console.log("Error here : ", err));
})}
if (!getHostedFields) {
console.log("Something wrong");
}
return(
<>
{
isResolved && (
<form id="my-sample-form" onSubmit={handleFormSubmit}>
<label htmlFor="card-number">Card Number</label>
<div id="card-number"></div>
<label htmlFor="expiration-date">Expiration Date</label>
<div id="expiration-date"></div>
<label htmlFor="cvv">CVV</label>
<div id="cvv"></div>
<button value="submit" id="submit" className="btn">
Pay with Card
</button>
</form>
)
}
</>
)
}
export default PaymentForm
@gregjopa I tried another implementation using an example from this package. The code is missing hostedFields?.cardFields?.submit({})
, not hostedFields.submit({})
to make the submit()
event fire when it's triggered. However, I have an error of invalid token and require me to pass orderId
in hostedfields and asking for access token. Why do you need an access token? Since our back end is preparing all required data such as clientId, clientToken, orderId, etc.. I couldn't able to find a good answer for that. Our back end is built with .net Framework. How can I pass the orderId on capture payment? Your docs is not really clear. We are using the same back end for Angular site and it's working well on Angular.
Here is the screenshot:
This is the implementation:
import React, { useState, useEffect } from "react";
import {
PayPalScriptProvider,
PayPalHostedFieldsProvider,
PayPalHostedField,
usePayPalHostedFields,
} from "@paypal/react-paypal-js";
const baseUrl = "http://local.payment.web"
const SubmitPayment = ({ merchantRefData, sessionHashData, orderId }) => {
const hostedFields = usePayPalHostedFields();
console.log('Merchant Ref : ', merchantRefData);
console.log('Session Hash : ', sessionHashData);
console.log('order Id : ', orderId);
const submitHandler = () => {
if (typeof hostedFields?.cardFields?.submit !== "function") {
console.log("Submit something wrong ");
}
hostedFields.cardFields?.submit({
// The full name as shown in the card and billing address
cardholderName: "John Wick",
})
.then(async (order) => {
return await fetch(
`${baseUrl}/api/Ppal/ReactApi/CaptureOrder`, {
mode: "cors",
method: "post",
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
merchantRef: merchantRefData,
sessionHash: sessionHashData
})
}
)
.then((response) => response.json())
.then((data) => {
console.log("data from pay button : ", data);
if(data.isApproved) {
alert("Payment successful")
}
if(!data.isApproved) alert("Payment declined")
if(data.isExpired) alert("Payment expired")
})
.catch((err) => {
console.log("Error here : ", err)
});
});
};
return <button onClick={submitHandler}>Pay</button>;
};
export default function App() {
const [clientId, setClientId] = useState(null);
const [clientToken, setClientToken] = useState(null);
const [merchantRef, setMerchantRef] = useState(null);
const [sessionHash, setSessionHash] = useState(null);
const [orderId, setOrderId] = useState(null);
const prepareForPayment = async () => {
return await fetch(`${baseUrl}/api/Ppal/ReactApi/PrepareForPayment`, {
mode: "cors",
method: "post",
headers: {
'content-type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
"curCode": "USD",
"orderAmount": 500
})
}).then(res => {
console.log("fetch Data : ", res);
return res.json()
}).then(data => {
console.log("fetch Data : ", data);
setClientId(data.ClientId)
setClientToken(data.ClientToken)
if (data.prepareForPayment) {
setMerchantRef(data.merchantRef)
setSessionHash(data.sessionHash)
}
}).catch(err => console.log(err))
}
console.log("Client id after click : ", clientId);
console.log("Client token after click : ", clientToken);
return (
<>
<button onClick={prepareForPayment}>Prepare for payment</button>
{
clientToken ?
<PayPalScriptProvider
options={{
"client-id": clientId,
"data-client-token": clientToken,
components: "hosted-fields",
vault: false
}}
>
<PayPalHostedFieldsProvider
createOrder={async () => {
return await fetch(`${baseUrl}/api/Ppal/ReactApi/CreateOrder2`, {
method: "post",
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
merchantRef: merchantRef,
sessionHash: sessionHash
})
}).then(res => {
console.log("res from createOrder", res);
return res.json()
}).then(data => {
console.log("orderId from button : ", data?.orderId);
if (data?.createOrder) return setOrderId(data?.orderId)
return data.orderId;
}).catch(err => console.log(err))
}}
>
<span style={{ "backgroundColor": "red" }}><PayPalHostedField
id="card-number"
hostedFieldType="number"
options={{ selector: "#card-number" }}
/></span>
<span style={{ "backgroundColor": "blue" }}><PayPalHostedField
id="cvv"
hostedFieldType="cvv"
options={{ selector: "#cvv" }}
/></span>
<span style={{ "backgroundColor": "violet" }}><PayPalHostedField
id="expiration-date"
hostedFieldType="expirationDate"
options={{
selector: "#expiration-date",
placeholder: "MM/YY",
}}
/></span>
<SubmitPayment merchantRefData={merchantRef} sessionHashData={sessionHash} orderId={orderId} />
</PayPalHostedFieldsProvider>
</PayPalScriptProvider> :
<div>Loading.........</div>
}
</>
);
}
Hi @tengkuzulfadli, one thing I realized is you didn't return the orderId. Could you please update the createOrder callback and make it return the orderId?
createOrder={async () => {
return await fetch(`${baseUrl}/api/Ppal/ReactApi/CreateOrder2`, {
method: "post",
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
merchantRef: merchantRef,
sessionHash: sessionHash
})
}).then(res => {
console.log("res from createOrder", res);
return res.json()
.then((data) => {
if (data?.createOrder) {
setOrderId(data?. orderId)
//need to return orderId
return data.orderId;
}
}).catch(err => console.log(err))
}}
Hey @xuanwan05 , thank you for pointing out. Yeah I missed that one and just updated to my code. I'm still getting the same error with invalid access token
. I already checked our access token, it's all good. Is there anything that I'm missing right now?
Hey @tengkuzulfadli, Could you please follow the steps and send me the screenshot?
- open browser inspect and click on Network tab
- find "fetch-client-token" which throws "invalid access token" error
- Click on the name
- Send me the screen shot for Headers and Payload
the screen shots should look like:
@xuanwan05 I don't have the fetch-client-token
but I do have confirm-payment-source
of hosted-fields-payments-sdk-tokenization
Hey @tengkuzulfadli, It seems like you didn't get the orderId correctly. Could you please add console.log(data.orderId)
before the added row return data.orderId;
? I want to check what the orderId value is.
@xuanwan05 You're right. It didn't get the orderId on createOrder event. However, on SubmitPayment
component, it is returning an orderId.
See below screenshots:
@xuanwan05 update from above comment, I placed a log before if statement on createOrder event, it is returning the same orderId, but it did not return orderId after if statement. Screenshot below:
I removed if statement
, now it is returning the orderId
but still getting the same error both in console and network
@xuanwan05 further updates:
I changed from return order?.id
to return order.id
, now it's returning orderId
but still having the same invalid token issue.
@gregjopa It seems like it's not firing hostedFields.cardFields.submit()
Screenshots below:
@tengkuzulfadli Could you please send a screenshot of Preview tab which is next to Payload?
@xuanwan05 Please see attached screenshot. I think my bad, it is firing hostedFields.cardFields.submit()
because it captured the cardholder details but it didn't call the ${baseUrl}/api/Ppal/ReactApi/CaptureOrder
This is our access token call in postman
Could you please check if Authorization from Request Headers has the same value as the one from postman?
@xuanwan05 Yeah now I'm getting where it went wrong. I have totally different authorization token.
But why is this happening? The clientId that I've got when preparing an order (That I've got from our back end) is the same as clientId that I have in postman.
I just want to double check, Could you please
- From Postman, make the call one more time
- From web browser, fresh the page and click Pay button after fill the card fields and capture the Authorization value
- Compare the access tokens
I want to see if the access tokens are still different.
Yes, still different
@xuanwan05 But in the request header, the path is v2
but in Postman, I have v1
. Are these related to capture a payment and access-token?