PayPal React shows extra buttons after changing amount - reactjs

WITHOUT react-paypal-button-v2 ~~~has an ovehead of 60KB
Similar question here but they suggest react-paypal-button-v2
I'm Trying to make a React PayPal button that changes the billing amount on props change.
I call the following component with props price and every time the price change i would like to re-render the button to update the actual price. WITHOUT react-paypal-button-v2
const PaypalForm = props => {
let paypalRef = useRef();
useEffect(() => {
window.paypal
.Buttons({
createOrder: (data, actions) => {
return actions.order.create({
purchase_units: [
{
description: "test",
amount: {
currency_code: "USD",
value: props.price
}
}
]
});
},
onApprove: async (data, actions) => {
const order = await actions.order.capture();
console.log(order);
},
onError: err => {
setError(err);
console.error(err);
}
})
.render(paypalRef.current);
}, [props.price]);
return (
<Row className="justify-content-center">
{error && <div>Uh oh, an error occurred! {error.message}</div>}
<div ref={paypalRef} />
</Row>
);
};
Everything is working except that a new button is created and added in the bottom of old one at each props change. I would like my new button to replace the old one. Without using react-paypal-button-v2

Something like:
useEffect(() => {
if(window.myButton) window.myButton.close();
window.myButton = window.paypal
.Buttons({
createOrder: (data, actions) => {
return actions.order.create({
purchase_units: [
{
description: "test",
amount: {
currency_code: "USD",
value: props.price
}
}
]
});
},
onApprove: async (data, actions) => {
const order = await actions.order.capture();
console.log(order);
},
onError: err => {
setError(err);
console.error(err);
}
});
window.myButton.render(paypalRef.current);
However, you do not actually need to re-render the button on price change!
You can do value: document.getElementById('...').value or similar (or whatever variable or function call you need)
In your example, if props.price returns the (new/current) desired value when the button is clicked, then that value will be used.
Basically, the createOrder function isn't called until you click a button.

Related

React component uses old data from previous API call

I am using React Query to fetch data from an API I have built. The component is rendering the old data from the previous api call and not updating with new the data from the new api call.
The new data is only rendering when I refresh the page.
Component:
export const ProfilePageStats = (props: {
user: User;
id: number;
}) => {
const { chatId } = useParams();
const { status: subscribeStatus, data: subscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"SUBSCRIBE"
);
const { status: unsubscribeStatus, data: unsubscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"UNSUBSCRIBE"
);
if (unsubscribeStatus == "success" && subscribeStatus == "success") {
console.log("Working", unsubscribeData);
return (
<ProfilePageStatsWithData
user={props.user}
subscribed={Object.keys(subscribeData).length}
unsubscribed={Object.keys(unsubscribeData).length}
/>
);
}
if (unsubscribeStatus == "error" && subscribeStatus == "error") {
console.log("error");
return <ProfilePageStatsLoading />;
}
if (unsubscribeStatus == "loading" && subscribeStatus == "loading") {
console.log("loading");
return <ProfilePageStatsLoading />;
}
return <ProfilePageStatsLoading />;
};
export const useSubscriptionsWithType = (
chatId: string,
id: number,
userId: number,
type: string
) => {
return useQuery(
["subscriptionsWithType"],
async () => {
const { data } = await api.get(
`${chatId}/subscriptions/${id}/${userId}?type=${type}`
);
return data;
},
{
enabled: chatId > 0 && userId > 0,
refetchOnWindowFocus: false,
}
);
};
The component should update to show the new user values but shows the previous user values. If I click out and select a different user entirely it then shows the values for the previously clicked user.
I can see that React Query is fetching with the correct values for the query but the component still renders the old user data?
It turns out that the fetchStatus value is changing to "fetching" but it not actually calling the api. Hence, why its only using the old values?
Your key part of the useQuery is what tells the hook when to update.
You only use ["subscriptionsWithType"] as key, so it will never know that you need to refetch something.
If you add userId there, it will update when that changes.
So, using
return useQuery(
["subscriptionsWithType", userId],
async () => {
...
will work.
It is likely, that you want all the params, that you use in the url, to be added there.
I solved it by adding a useEffect and refetching based on the changing user id.
useEffect(() => {
refetch();
}, [props.user.id]);

Why my setState returns an array, but the state is a promise when the component rerender?

The code below try to check if an url is reachable or not.
The urls to check are stored in a state called trackedUrls
I update this state with an async function checkAll.
The object just before being updated seems fine, but when the component rerender, it contains a promise !
Why ?
What I should change to my code ?
import React from "react"
export default function App() {
const [trackedUrls, setTrackedUrls] = React.useState([])
// 1st call, empty array, it's ok
// 2nd call, useEffect populate trackedUrls with the correct value
// 3rd call, when checkAll is called, it contains a Promise :/
console.log("trackedUrls :", trackedUrls)
const wrappedUrls = trackedUrls.map(urlObject => {
return (
<div key={urlObject.id}>
{urlObject.label}
</div>
)
})
// check if the url is reachable
// this works well if cors-anywhere is enable, click the button on the page
async function checkUrl(url) {
const corsUrl = "https://cors-anywhere.herokuapp.com/" + url
const result = await fetch(corsUrl)
.then(response => response.ok)
console.log(result)
return result
}
// Checks if every url in trackedUrls is reachable
// I check simultaneously the urls with Promise.all
async function checkAll() {
setTrackedUrls(async oldTrackedUrls => {
const newTrackedUrls = await Promise.all(oldTrackedUrls.map(async urlObject => {
let isReachable = await checkUrl(urlObject.url)
const newUrlObject = {
...urlObject,
isReachable: isReachable
}
return newUrlObject
}))
// checkAll works quite well ! the object returned seems fine
// (2) [{…}, {…}]
// { id: '1', label: 'google', url: 'https://www.google.Fr', isReachable: true }
// { id: '2', label: 'whatever', url: 'https://qmsjfqsmjfq.com', isReachable: false }
console.log(newTrackedUrls)
return newTrackedUrls
})
}
React.useEffect(() => {
setTrackedUrls([
{ id: "1", label: "google", url: "https://www.google.Fr" },
{ id: "2", label: "whatever", url: "https://qmsjfqsmjfq.com" }
])
}, [])
return (
<div>
<button onClick={checkAll}>Check all !</button>
<div>
{wrappedUrls}
</div>
</div>
);
}
Konrad helped me to grasp the problem.
This works and it's less cumbersome.
If anyone has a solution with passing a function to setTrackedUrls, I'm interested just for educational purpose.
async function checkAll() {
const newTrackedUrls = await Promise.all(trackedUrls.map(async urlObject => {
let isReachable = await checkUrl(urlObject.url)
const newUrlObject = {
...urlObject,
isReachable: isReachable
}
return newUrlObject
}))
setTrackedUrls(newTrackedUrls)
}
You can only put data into setState.

Patch multiple id's with one request with React Query

I have a very basic prototype of app that allows to book a seat. User selects the seat/seats, clicks book, patch request with available: false is sent to the fake api (json-server) with React Query, library invalidates the request and immediately shows response from the server.
Database structure looks like this:
{
"hallA": [
{
"id": 1,
"seat": 1,
"available": false
},
{
"id": 2,
"seat": 2,
"available": true
},
{
"id": 3,
"seat": 3,
"available": false
}
]
}
and the logic for selecting, booking seats looks like this:
const App = () => {
const { data, isLoading } = useGetHallLayout("hallA");
const [selected, setSelected] = useState<
{ id: number; seat: number; available: boolean }[]
>([]);
const handleSelect = useCallback(
(seat: { id: number; seat: number; available: boolean }) => {
const itemIdx = selected.findIndex((element) => element.id === seat.id);
if (itemIdx === -1) {
setSelected((prevState) => [
...prevState,
{ id: seat.id, seat: seat.seat, available: !seat.available },
]);
} else {
setSelected((prevState) =>
prevState.filter((element) => element.id !== seat.id)
);
}
},
[selected]
);
const takeSeat = useTakeSeat({
onSuccess: () => {
useGetHallLayout.invalidate();
},
});
const sendRequest = useCallback(() => {
selected.forEach((item) =>
takeSeat.mutateAsync({ id: item.id, hall: "hallA" })
);
setSelected([]);
}, [selected, takeSeat]);
return (
<>
{!isLoading && (
<ConcertHall
layout={data}
onSeatSelect={handleSelect}
activeSeats={selected}
/>
)}
<button disabled={isLoading} onClick={sendRequest}>
Take selected
</button>
</>
);
};
Queries look like this:
export const useGetHallLayout = (hall: string) => {
const { data, isLoading } = useQuery(["hall"], () =>
axios.get(`http://localhost:3000/${hall}`).then((res) => res.data)
);
return { data, isLoading };
};
export const useTakeSeat = (options?: UseMutationOptions<unknown, any, any>) =>
useMutation(
(data: { hall: string; id: number }) =>
axios.patch(`http://localhost:3000/${data.hall}/${data.id}`, {
available: false,
}),
{
...options,
}
);
useGetHallLayout.invalidate = () => {
return queryClient.invalidateQueries("hall");
};
The problem of the above code is that I perform very expensive operation of updating each id in a for each loop (to available: false) and query invalidates it after each change not once all of them are updated.
The question is: is there any better way to do this taking into account the limitations of json-server? Any batch update instead of sending request to each and every id seperately? Maybe some changes in a logic?
Thanks in advance
You can certainly make one mutation that fires of multiple requests, and returns the result with Promise.all or Promise.allSettled. Something like:
useMutation((seats) => {
return Promise.allSettled(seats.map((seat) => axios.patch(...))
})
then, you would have one "lifecycle" (loading / error / success) for all queries together, and onSuccess will only be called once.
Another gotcha I'm seeing is that you'd really want the hall string to be part of the query key:
- useQuery(["hall"], () =>
+ useQuery(["hall", hall], () =>

Paypal integration Bad request / Order could not be captured

I am trying to implement paypal with credit/Debit card in react.js
Here, I am using references for this : enter link description here
& Here is my code references for paypal button:
createOrder = (data, actions) => {
// console.log('action',actions.order);
return actions.order.create({
purchase_units: [
{
description: +"Mercedes G-Wagon",
amount: {
currency_code: "USD",
value: 200,
// intent: 'capture',
},
},
],
});
};
onApprove = (data, actions) => {
actions.order.capture().then(details => {
const paymentData = {
payerID: data.payerID,
orderID: data.orderID
};
console.log("Payment Approved: ", paymentData);
});
};
Here is the code of paypal button:
<PayPalButton
createOrder={(data, actions) => this.createOrder(data, actions)}
onApprove={(data, actions) => this.onApprove(data, actions)}
/>
Also I added PAYPAL SDK Link
src="https://www.paypal.com/sdk/js?client-id=Ae-iRhlCvBlx2WS-YACHU3MAMbbDPTj9Cp-ynyMFi2qautDvNMynnDn1mG3qgqyeiCD6IAFG0MPHfBxj
"
I tried with make payment using credit/Debit card and choose India in county drop-down But everytime I got the same error that "Order could not be captured"
How I can resolve this error ??

PayPal Buttons don't close (Duplicates after re-rendering)

I'm implementing the PayPal Smart Payment Buttons with React, and every time my component re-renders I receive a duplicate of the buttons (with the one on the bottom holding the correct transaction information).
Clearly I need to close the buttons, if I try so I receive the error that window.paypal.close()is not a function.
I tried to follow this example: Paypal React shows extra buttons after changing amount
Here is my code, I'm using Redux for state management and I need to rerender the component if items in the shopping cart are removed (to update the item information of the transaction):
useEffect(() => {
if (window.myButton) {
window.myButton.close()
}
window.myButton = window.paypal
.Buttons({
createOrder: (data, actions) => {
return actions.order.create({
purchase_units: [
{
description: "test transaction",
amount: {
currency_code: "USD",
value: document.getElementById("totalAmount").innerHTML,
breakdown: {
item_total: {
currency_code: "USD",
value: document.getElementById("totalAmount").innerHTML
}
}
}
,
items: itemsInCart.map(item => {
console.log(item.value)
return {
name: item.name,
unit_amount: {
currency_code: "USD",
value: String(item.price)
},
quantity: "1"
}
})
}
]
});
},
onApprove: async (data, actions) => {
const order = await actions.order.capture();
}
.catch(function(error) {
console.error("Error writing document: ", error);
});
},
onError: err => {
// setError(err);
console.error(err);
}
})
.render(paypalRef.current)
}, [itemsInCart]);
})
.render(paypalRef.current)
The problem is you are setting myButton to the .render() promise result, not the Button itself.
You need to store a reference to the actual Button (before rendereing it), and only then .render() it -- so that later you can call .close() on the reference. Basically:
let myButton = paypal.Buttons(
....
});
myButton.render(paypalRef.current)
// and at some later point in time...
myButton.close();

Resources