I have this implementation of the PayPal smart buttons in React:
function PayPalButtonComponent(props: PayPalButtonProps) {
const [show, set_show] = useState(false);
const [error, set_error] = useState<string>();
const create_order = (_: any, actions: any) => {
return actions.order.create({
purchase_units: [
{
amount: {
currency: props.currency || "EUR",
value: props.total
}
}
]
});
};
const handle_approve = (_: any, actions: any) => {
return actions.order.capture().then((details: any) => {
if (props.onSuccess) props.onSuccess(details);
});
};
const handle_cancel = () => {
if (props.onCancel) props.onCancel();
};
const handle_error = () => {
if (props.onError) props.onError();
};
const render_button = () => {
const Button = paypal.Buttons.driver("react", { React, ReactDOM });
return (
<Button
style={{
layout: "horizontal",
size: "responsive",
shape: "rect",
color: "gold",
tagline: false
}}
funding={{
allowed: [paypal.FUNDING.CARD, paypal.FUNDING.PAYPAL],
disallowed: [paypal.FUNDING.CREDIT]
}}
createOrder={create_order}
onApprove={handle_approve}
onError={handle_error}
onCancel={handle_cancel}
/>
);
};
useEffect(() => {
if (props.isScriptLoaded) {
if (props.isScriptLoadSucceed) set_show(true);
else set_error("Unable to load the paypalscript");
}
}, [props.isScriptLoaded, props.isScriptLoadSucceed]);
if (error) return <p>{error}</p>;
if (!show) return <FakeButton />;
return render_button();
}
I have struggled to implement these buttons in react since there is no documentation, I have found and copied some code from here and trying to guess other stuff. But I can't understand how to disable the button.
In this guide they state that one can call the disable() method on the actions object but can't figure out how I can accomplish that with my configuration.
Have you ever try something similar? Do you know any documentation one can follow?
edit
What I'm trying to accomplish is to set the button in a disable state during the payment. I know there is the paypal overlay, but when the transaction completes I change the app route and since it happen when onSuccess is called, due to the apparent async nature of actions.order.capture() this can't happen instantaneously, and so there is a moment when one can click again on the paypal button. If i can disable the button i have solve the problem.
The onInit implementation allows you to disable/enable the button before clicking on it, useful for some sort of validation before the checkout (like terms checked) but doesn't apply to my case. I have also tried to call actions.disable() in create_order but that breaks the button.
Have same situation here and the onInit example from documentation is not that straight forward as some think... You would need to create hidden imput and assign somsort of variable to it when payment made in order to achieve buttons to be disabled after payment. To bad no one solve this one.
Related
import { confirmAlert } from "react-confirm-alert"; // Import
import "react-confirm-alert/src/react-confirm-alert.css"; // Import css
export default function App() {
const handleButtonPress = (event) => {
if (event.key === "Enter") {
alert("Click Yes");
}
};
const handleClick = (e) => {
confirmAlert({
title: "Confirm to submit",
message: "Are you sure to do this.",
buttons: [
{
label: "Yes",
onClick: () => {
alert("Click Yes");
},
onKeyPress: () => {
handleButtonPress();
}
},
{
label: "No",
onClick: () => {
alert("Click No");
}
}
]
});
};
return (
<div className="App">
<button
onClick={() => {
handleClick();
}}
>
Click
</button>
</div>
);
}
I'm testing react-confirm-alert.
I'm trying to handle Button Yes by pressing Enter. Both function onClick() for yes and no are working good, but press enter is not working.
Can someone let me know if I did something wrong?
There's several issues. This react-confirm-alert library looks like a poor library, I'd pick a different one if you can find one that suits your needs.
You're calling handleButtonPress() with no arguments, and then you're trying to read from the event object, which you don't pass.
const handleButtonPress = (event) => {
if (event.key === "Enter") {
The event.key line should be throwing an error since event is undefined. Since you're not seeing errors, it's clear this line isn't getting called. You should be using console.log or the debugger to double check what code is getting called.
You should also get in the habit of reading documentation. In this case, the documentation shows that onKeyPress is a top level setting, while you've incorrectly put it in buttons.
Either way, react-confirm-alert doesn't pass the event to the onKeyPress callback: https://github.com/GA-MO/react-confirm-alert/#options so it doesn't seem like this API should exist. It doesn't have any use.
I would either pick a different library, otherwise you'll need to add your own keypress listener to the document, and manually handle the enter key there.
so im creating a small webApp using reactjs with material ui,
its basically the settings to customize the color, family, weight of any button or text
so im using the Select to display a list of google font families and when the user picks one it gets added to the overall settings object
when i click on the list it takes a few seconds(~4 to 8 to load Im not sure of the issue nor the solution.
So i was wondering if this is due to my internet connection(3Mbs XD) or might it be something else.
a solution may be setting the list to local storage then it only slow loads once or even have a local version of google fonts...
const [googleFonts, setGoogleFonts] = useState();
useEffect(() => {
let isMounted = true;
let cancelToken = axios.CancelToken.source();
const fetchData = async () => {
try {
let res = await client.get();
if (isMounted) {
setGoogleFonts(res.data.items);
}
} catch (e) {
console.error(e);
};
}
fetchData();
return () => {
isMounted = false;
cancelToken.cancel();
}
}, []);
<Select
labelId="FontFamily"
id="FontFamilySelect"
name="FontFamily"
style={{ width: '188px' }}
defaultValue='PT Sans'
value={widgetFont}
onChange={(e) => setWidgetFont(e.target.value)}
>
{googleFonts?.map((option, index) => (
<MenuItem key={index} value={option.family} >
{option.family}
</MenuItem>
))}
</Select>
Thank you
I don't know about the internet connection, because your user experience could vary. But i think there's some issue in this code.
const [googleFonts, setGoogleFonts] = useState();
You code could break at the beginning, so it's better to anticipate some bad outcome. For example,
const [googleFonts, setGoogleFonts] = useState([]);
Make the initial value an array [] might help in your case.
Or add a spinner if it's not loaded try displaying something else.
The reason this is different than your case is that suppose your loading takes 1s. What the user do around that time? And more importantly your UI has to still functional, ex.
spinner drive user attention somewhere else
empty, so not allow user to do anything, but can't be jammed. Maybe your Select is jammed during transition, i don't know.
Follow this code, your UI render when font loaded
import React, { useEffect, useState } from 'react';
const MyComponent = ( ) => {
const [loading, setLoading] = useState(true);
useEffect( ()=> {
document.fonts.load("20px FontFamilyName").then( () => { setLoading(false) } );
}, [])
return (
<React.Fragment>
{ loading
? <div>Loading...</div>
: <main id="mainWrapper">
Reset of Eelements
</main>
}
</React.Fragment>
);
};
export default MyComponent;
I created a hook to use a confirm dialog, this hook provides the properties to the component to use them like this:
const { setIsDialogOpen, dialogProps } = useConfirmDialog({
title: "Are you sure you want to delete this group?",
text: "This process is not reversible.",
buttons: {
confirm: {
onPress: onDeleteGroup,
},
},
width: "360px",
});
<ConfirmDialog {...dialogProps} />
This works fine, but also I want to give the option to change these properties whenever is needed without declaring extra states in the component where is used and in order to achieve this what I did was to save these properties in a state inside the hook and this way provide another function to change them if needed before showing the dialog:
interface IState {
isDialogOpen: boolean;
dialogProps: TDialogProps;
}
export const useConfirmDialog = (props?: TDialogProps) => {
const [state, setState] = useState<IState>({
isDialogOpen: false,
dialogProps: {
...props,
},
});
const setIsDialogOpen = (isOpen = true) => {
setState((prevState) => ({
...prevState,
isDialogOpen: isOpen,
}));
};
// Change dialog props optionally before showing it
const showConfirmDialog = (dialogProps?: TDialogProps) => {
if (dialogProps) {
const updatedProps = { ...state.dialogProps, ...dialogProps };
setState((prevState) => ({
...prevState,
dialogProps: updatedProps,
}));
}
setIsDialogOpen(true);
};
return {
setIsDialogOpen,
showConfirmDialog,
dialogProps: {
isOpen: state.isDialogOpen,
onClose: () => setIsDialogOpen(false),
...state.dialogProps,
},
};
};
But the problem here is the following:
Arguments are passed by reference so if I pass a function to the button (i.e onDeleteGroup) i will keep the function updated to its latest state to perform the correct deletion if a group id changes inside of it.
But as I'm saving the properties inside a state the reference is lost and now I only have the function with the state which it was declared at the beginning.
I tried to add an useEffect to update the hook state when arguments change but this is causing an infinite re render:
useEffect(() => {
setState((prevState) => ({
...prevState,
dialogProps: props || {},
}));
}, [props]);
I know I can call showConfirmDialog and pass the function to update the state with the latest function state but I'm looking for a way to just call the hook, declare the props and not touch the dialog props if isn't needed.
Any answer is welcome, thank you for reading.
You should really consider not doing this, this is not a good coding pattern, this unnecessarily complicates your hook and can cause hard to debug problems. Also this goes against the "single source of truth" principle. I mean a situation like the following
const Component = ({title}: {title?: string}) => {
const {showConfirmDialog} = useConfirmDialog({
title,
// ...
})
useEffect(() => {
// Here you expect the title to be "title"
if(something) showConfirmDialog()
}, [])
useEffect(() => {
// Here you expect the title to be "Foo bar?"
if(somethingElse) showConfirmDialog({title: 'Foo bar?'})
}, [])
// But if the second dialog is opened, then the first, the title will be
// "Foo bar?" in both cases
}
So please think twice before implementing this, sometimes it's better to write a little more code but it will save you a lot debugging.
As for the answer, I would store the props in a ref and update them on every render somehow like this
/** Assign properties from obj2 to obj1 that are not already equal */
const assignChanged = <T extends Record<string, unknown>>(obj1: T, obj2: Partial<T>, deleteExcess = true): T => {
if(obj1 === obj2) return obj1
const result = {...obj1}
Object.keys(obj2).forEach(key => {
if(obj1[key] !== obj2[key]) {
result[key] = obj2[key]
}
})
if(deleteExcess) {
// Remove properties that are not present on obj2 but present on obj1
Object.keys(obj1).forEach(key => {
if(!obj2.hasOwnProperty(key)) delete result[key]
})
}
return result
}
const useConfirmDialog = (props) => {
const localProps = useRef(props)
localProps.current = assignChanged(localProps.current, props)
const showConfirmDialog = (changedProps?: Partial<TDialogProps>) => {
localProps.current = assignChanged(localProps.current, changedProps, false)
// ...
}
// ...
}
This is in case you have some optional properties in TDialogProps and you want to accept Partial properties in showConfirmDialog. If this is not the case, you could simplify the logic a little by removing this deleteExcess part.
You see that it greatly complicates your code, and adds a performance overhead (although it's insignificant, considering you only have 4-5 fields in your dialog props), so I really recommend against doing this and just letting the caller of useConfirmDialog have its own state that it can change. Or maybe you could remove props from useConfirmDialog in the first place and force the user to always pass them to showConfirmDialog, although in this case this hook becomes kinda useless. Maybe you don't need this hook at all, if it only contains the logic that you have actually shown in the answer? It seems like pretty much the only thing it does is setting isDialogOpen to true/false. Whatever, it's your choice, but I think it's not the best idea
Hi guys) I have a strange question may be, but I'm at a dead end.
I have my own custom hook.
const useModal = (Content?: ReactNode, options?: ModalOptions) => {
const { isOpen, close: contextClose, open: contextOpen, setContent } = useContext(
ModalContext,
)
const [customOpenContent, setCustomOpenContent] = useState<ReactNode>()
const showModal = useCallback(
(customContent?: ReactNode) => {
if (!isNil(customContent)) {
setCustomOpenContent(customContent)
contextOpen(customContent, options)
} else contextOpen(Content, options)
},
[contextOpen, Content, options],
)
const hideModal = useCallback(() => {
contextClose()
}, [contextClose])
return { isOpen, close: hideModal, open: showModal, setContent }
}
It is quite simple.
Also i have component which uses this hook
const App: React.FC = () => {
const [loading, setLoading] = useState(false)
const { open } = useModal(null, { deps: [loading] })
useEffect(() => {
setTimeout(() => {
setLoading(true)
}, 10000)
})
const buttonCallback = useCallback(() => {
open(<Button disabled={!loading}>Loading: {loading.toString()}</Button>)
}, [loading, open])
return (
<Page title="App">
<Button onClick={buttonCallback}>Open Modal</Button>
</Page>
)
}
Main problem is - Button didn't became enabled because useModal hook doesn't know anything about changes.
May be you have an idea how to update this component while it's props are updated? And how to do it handsomely ))
Context isn't the best solution to this problem. What you want is a Portal instead. Portals are React's solution to rendering outside of the current React component hierarchy. How to use React Portal? is a basic example, but as you can see, just going with the base React.Portal just gives you the location to render.
Here's a library that does a lot of the heavy lifting for you: https://github.com/wellyshen/react-cool-portal. It has typescript definitions and provides an easy API to work with.
Here's your example using react-cool-portal.
import usePortal from "react-cool-portal";
const App = () => {
const [loading, setLoading] = useState(false);
const { Portal, isShow, toggle } = usePortal({ defaultShow: false });
useEffect(() => {
setTimeout(() => {
setLoading(true);
}, 10000);
});
const buttonCallback = useCallback(() => {
toggle();
}, [toggle]);
return (
<div title="App" style={{ backgroundColor: "hotpink" }}>
<button onClick={buttonCallback}>
{isShow ? "Close" : "Open"} Modal
</button>
<Portal>
<button disabled={!loading}>Loading: {loading.toString()}</button>
</Portal>
<div>{loading.toString()}</div>
</div>
);
};
Basic CodeSandbox Example
There are more detailed ones within the react-cool-portal documentation.
For more detail of the issues with the Context solution you were trying, is that React Elements are just a javascript object. React then uses the object, it's location in the tree, and it's key to determine if they are the same element. React doesn't actually care or notice where you create the object, only it's location in the tree when it is rendered.
The disconnect in your solution is that when you pass the element to the open function in buttonCallback, the element is created at that point. It's a javascript object that then is set as the content in your context. At that point, the object is set and won't change until you called open again. If you set up your component to call open every time the relevant state changes, you could get it working that way. But as I mentioned earlier, context wasn't built for rendering components outside of the current component; hence why some really weird workarounds would be required to get it working.
I cant seem to figure out how to show a custom dialogue instead of using the normal window.confirm that routeWillLeave and history.listenBefore uses. Basically i have built a notification system and check if a form is dirty const { dispatch, history, dirty } = this.props;
if the form is dirty it means the user has unsaved changes. If they change route I would like to show my notification which will have two buttons STAY, IGNORE which can both take an onClick handler.
Ive spent a bit of time googling and havent come across any mention of how i might accomplish this using routeWillLeave. The closest thing i could find was to use history.listenBefore however there docs say that I need to do this.
let history = createHistory({
getUserConfirmation(message, callback) {
callback(window.confirm(message)) // The default behavior
}
})
But I am using browserHistory from react-router to initiate my store const history = syncHistoryWithStore(browserHistory, store);
How can I stop a route change after a link has been clicked, show a notification using my custom notification system and depending on which button is clicked either transition to the new route or stay?
Here is an example of how my notification system works and the direction ive headed in which obviously doesn't work because all this returns is a string to show in the window.confirm dialogue by default.
history.listenBefore((location) => {
if(this.props.dirty){
const acceptBtn = {
title: 'STAY',
handler: ignoreRouteChange() //This can be any function I want
}
const denyBtn = {
title: 'IGNORE',
handler: continueRouteChange() //This can be any function I want
}
return dispatch(addNotification({
message: 'You have unsaved changes!',
type: NOTIFICATION_TYPE_WARNING,
duration: 0,
canDismiss: false,
acceptBtn,
denyBtn
}));
return "Usaved changes!"
}
});
Thanks
Another solution that i have decided to use is to return false in the normal setRouterLeaveHook callback and then show my dialog and use the nextLocation passed to the callback to push the next route depending on button selection.
router.setRouteLeaveHook(route, this.routerWillLeave.bind(this));
routerWillLeave(nextLocation){
let { dirty, dispatch, resetForm } = this.props;
if (dirty) {
let dialog = {
id: Date.now(),
showTitle: true,
titleContent: 'Unsaved Changes',
titleIcon: 'fa fa-warning',
content: <span>You have <strong>unsaved</strong> changes! <strong>Discard</strong> them?</span>,
type: 'confirm',
handleCloseClick: (e) => {
e.preventDefault();
dispatch(closeDialog());
},
acceptBtn: {
title: 'Okay',
handler: (e) => {
e.preventDefault();
resetForm();
// Wait for call stack to unwind and then execute
// the following which will now have the updated values.
setTimeout(() => {
dispatch(push(nextLocation));
dispatch(closeDialog());
}, 0);
}
},
denyBtn: {
title: 'Deny',
handler: (e) => {
e.preventDefault();
dispatch(closeDialog());
}
}
}
dispatch(addDialogWindow(dialog));
dispatch(openDialog(false, (e) => dispatch(closeDialog()), false));
return false;
}
return true;
}