diff --git a/src/components/credit-card/credit-card.tsx b/src/components/credit-card/credit-card.tsx index 181b59bb..fa643d39 100644 --- a/src/components/credit-card/credit-card.tsx +++ b/src/components/credit-card/credit-card.tsx @@ -10,6 +10,7 @@ import type { CreditCardBase, CreditCardChildren, CreditCardFunctionChildren, + CreditCardNoButton, CreditCardPayButtonProps, CreditCardProps, } from './credit-card.types'; @@ -35,6 +36,7 @@ import type { function CreditCard(props: CreditCardBase): JSX.Element; function CreditCard(props: CreditCardChildren): JSX.Element; function CreditCard(props: CreditCardFunctionChildren): JSX.Element; +function CreditCard(props: CreditCardNoButton): JSX.Element; function CreditCard({ buttonProps, callbacks, @@ -48,10 +50,7 @@ function CreditCard({ style, ...props }: CreditCardProps) { - const [card, setCard] = React.useState(() => undefined); - const [isSubmitting, setIsSubmitting] = React.useState(false); - const buttonRef = React.useRef(null); - const { cardTokenizeResponseReceived, payments } = useForm(); + const { payments, card, setCard } = useForm(); const options: Square.CardOptions = React.useMemo(() => { const baseOptions = { @@ -70,48 +69,6 @@ function CreditCard({ }, {}); }, [includeInputLabels, postalCode, style]); - /** - * Handle the on click of the Credit Card button click - * - * @param e An event which takes place in the DOM. - * @returns The data be sended to `cardTokenizeResponseReceived()` function, or an error - */ - const handlePayment = async (e: Event) => { - e.stopPropagation(); - - if (buttonProps?.isLoading) return; - - if (!card) { - console.warn('Credit Card button was clicked, but no Credit Card instance was found.'); - - return; - } - - setIsSubmitting(true); - - try { - const result = await card.tokenize(); - - if (result.status === 'OK') { - const tokenizedResult = await cardTokenizeResponseReceived(result); - return tokenizedResult; - } - - let message = `Tokenization failed with status: ${result.status}`; - if (result?.errors) { - message += ` and errors: ${JSON.stringify(result?.errors)}`; - - throw new Error(message); - } - - console.warn(message); - } catch (error) { - console.error(error); - } finally { - setIsSubmitting(false); - } - }; - React.useEffect(() => { const abortController = new AbortController(); const { signal } = abortController; @@ -162,6 +119,68 @@ function CreditCard({ recalculateSize(card?.recalculateSize); } + return ( + <> +
+ {!card && } +
+ + {props.hideButton ? null : typeof render === 'function' ? ( + render(CreditCardButton) + ) : ( + {children ?? 'Pay'} + )} + + ); +} + +function CreditCardButton({ children, isLoading, render, ...props }: CreditCardPayButtonProps) { + const [isSubmitting, setIsSubmitting] = React.useState(false); + const buttonRef = React.useRef(null); + const { cardTokenizeResponseReceived, card } = useForm(); + + /** + * Handle the on click of the Credit Card button click + * + * @param e An event which takes place in the DOM. + * @returns The data be sended to `cardTokenizeResponseReceived()` function, or an error + */ + const handlePayment = async (e: Event) => { + e.stopPropagation(); + + if (isLoading) return; + + if (!card) { + console.warn('Credit Card button was clicked, but no Credit Card instance was found.'); + + return; + } + + setIsSubmitting(true); + + try { + const result = await card.tokenize(); + + if (result.status === 'OK') { + const tokenizedResult = await cardTokenizeResponseReceived(result); + return tokenizedResult; + } + + let message = `Tokenization failed with status: ${result.status}`; + if (result?.errors) { + message += ` and errors: ${JSON.stringify(result?.errors)}`; + + throw new Error(message); + } + + console.warn(message); + } catch (error) { + console.error(error); + } finally { + setIsSubmitting(false); + } + }; + useEventListener({ listener: handlePayment, type: 'click', @@ -171,35 +190,20 @@ function CreditCard({ }, }); - const Button = ({ children, isLoading, ...props }: CreditCardPayButtonProps) => { - const id = 'rswp-card-button'; - const disabled = isLoading || !card || isSubmitting; - - return ( - - {children ?? 'Pay'} - - ); - }; + const id = 'rswp-card-button'; + const disabled = isLoading || !card || isSubmitting; - return ( - <> -
- {!card && } -
+ if (render) { + return render({ isSubmitting, handlePayment, buttonRef }); + } - {typeof render === 'function' ? render(Button) : } - + return ( + + {children ?? 'Pay'} + ); } export default CreditCard; +export { CreditCardButton }; export * from './credit-card.types'; diff --git a/src/components/credit-card/credit-card.types.ts b/src/components/credit-card/credit-card.types.ts index 48efd88f..62171c50 100644 --- a/src/components/credit-card/credit-card.types.ts +++ b/src/components/credit-card/credit-card.types.ts @@ -27,6 +27,21 @@ export type CreditCardPayButtonProps = Omit< css?: Stitches.ComponentProps['css']; /** Control the loading state of the button a.k.a disabling the button. */ isLoading?: boolean; + /** Render a custom button with full control. */ + render?: (props: { + /** If handlePayment is currently running. Use this to disable the button */ + isSubmitting: boolean; + /** + * Call this function onClick. Alternatively just set the `ref={buttonRef}` + * and it will be handled for you + */ + handlePayment: (e: Event) => void; + /** + * Set `ref={buttonRef}` for it to handle the payment on click. You may want + * to use `handlePayment` instead for more control, but don't use both. + */ + buttonRef: React.RefObject; + }) => React.ReactElement; }; export type CreditCardFunctionChildrenProps = { @@ -137,6 +152,15 @@ export interface CreditCardFunctionChildren extends CreditCardBase { render?(Button: (props: CreditCardPayButtonProps) => React.ReactElement): React.ReactNode; } +export interface CreditCardNoButton extends CreditCardBase { + /** + * Make it possible to render the button outside of the component using + * `` If `hideButton` is set to `true` then the button + * will not be rendered and `render`, `children` and `buttonProps` will be ignored. + */ + hideButton?: boolean; +} + export interface CreditCardProps extends CreditCardBase { /** * Props to be passed to the `