Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 75 additions & 71 deletions src/components/credit-card/credit-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
CreditCardBase,
CreditCardChildren,
CreditCardFunctionChildren,
CreditCardNoButton,
CreditCardPayButtonProps,
CreditCardProps,
} from './credit-card.types';
Expand All @@ -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,
Expand All @@ -48,10 +50,7 @@ function CreditCard({
style,
...props
}: CreditCardProps) {
const [card, setCard] = React.useState<Square.Card | undefined>(() => undefined);
const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
const buttonRef = React.useRef<HTMLButtonElement>(null);
const { cardTokenizeResponseReceived, payments } = useForm();
const { payments, card, setCard } = useForm();

const options: Square.CardOptions = React.useMemo(() => {
const baseOptions = {
Expand All @@ -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;
Expand Down Expand Up @@ -162,6 +119,68 @@ function CreditCard({
recalculateSize(card?.recalculateSize);
}

return (
<>
<div {...props} data-testid="rswps-card-container" id={id} style={{ minHeight: 89 }}>
{!card && <LoadingCard />}
</div>

{props.hideButton ? null : typeof render === 'function' ? (
render(CreditCardButton)
) : (
<CreditCardButton {...buttonProps}>{children ?? 'Pay'}</CreditCardButton>
)}
</>
);
}

function CreditCardButton({ children, isLoading, render, ...props }: CreditCardPayButtonProps) {
const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);
const buttonRef = React.useRef<HTMLButtonElement>(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',
Expand All @@ -171,35 +190,20 @@ function CreditCard({
},
});

const Button = ({ children, isLoading, ...props }: CreditCardPayButtonProps) => {
const id = 'rswp-card-button';
const disabled = isLoading || !card || isSubmitting;

return (
<PayButton
{...props}
aria-disabled={disabled}
css={props?.css}
disabled={disabled}
id={id}
ref={buttonRef}
type="button"
>
{children ?? 'Pay'}
</PayButton>
);
};
const id = 'rswp-card-button';
const disabled = isLoading || !card || isSubmitting;

return (
<>
<div {...props} data-testid="rswps-card-container" id={id} style={{ minHeight: 89 }}>
{!card && <LoadingCard />}
</div>
if (render) {
return render({ isSubmitting, handlePayment, buttonRef });
}

{typeof render === 'function' ? render(Button) : <Button {...buttonProps}>{children ?? 'Pay'}</Button>}
</>
return (
<PayButton {...props} aria-disabled={disabled} disabled={disabled} id={id} ref={buttonRef} type="button">
{children ?? 'Pay'}
</PayButton>
);
}

export default CreditCard;
export { CreditCardButton };
export * from './credit-card.types';
30 changes: 30 additions & 0 deletions src/components/credit-card/credit-card.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ export type CreditCardPayButtonProps = Omit<
css?: Stitches.ComponentProps<typeof PayButton>['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<HTMLElement>;
}) => React.ReactElement;
};

export type CreditCardFunctionChildrenProps = {
Expand Down Expand Up @@ -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
* `<CreditCardButton />` 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 `<button>` element. The following props are not
Expand All @@ -159,4 +183,10 @@ export interface CreditCardProps extends CreditCardBase {
* @param Button - The button component
*/
render?(Button: (props: CreditCardPayButtonProps) => React.ReactElement | null): React.ReactNode;
/**
* Make it possible to render the button outside of the component using
* `<CreditCardButton />` If `hideButton` is set to `true` then the button
* will not be rendered and `render`, `children` and `buttonProps` will be ignored.
*/
hideButton?: boolean;
}
1 change: 1 addition & 0 deletions src/components/payment-form/payment-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ function RenderPaymentForm(
const PaymentForm = React.forwardRef<HTMLDivElement, PaymentFormProps>(RenderPaymentForm);

export default PaymentForm;
export { useForm } from '~/contexts/form';
export * from './payment-form.types';
3 changes: 2 additions & 1 deletion src/components/payment-form/payment-form.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export type PaymentFormProps = {
*/
cardTokenizeResponseReceived: (
props: Square.TokenResult,
verifiedBuyer?: Square.VerifyBuyerResponseDetails | null
verifiedBuyer?: Square.VerifyBuyerResponseDetails | null,
payments?: Square.Payments
) => void;
children: React.ReactNode;
/**
Expand Down
7 changes: 6 additions & 1 deletion src/contexts/form/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ const FormContext = React.createContext<FormContextType>({
cardTokenizeResponseReceived: null as unknown as () => Promise<void>,
createPaymentRequest: null as unknown as Square.PaymentRequestOptions,
payments: null as unknown as Square.Payments,
card: undefined,
setCard: () => undefined,
});

function FormProvider({ applicationId, locationId, children, overrides, ...props }: FormProviderProps) {
const [instance, setInstance] = React.useState<Square.Payments>();
const [createPaymentRequest] = React.useState<undefined | Square.PaymentRequestOptions>(() =>
props.createPaymentRequest?.()
);
const [card, setCard] = React.useState<Square.Card | undefined>(() => undefined);

React.useEffect(() => {
const abortController = new AbortController();
Expand Down Expand Up @@ -61,7 +64,7 @@ function FormProvider({ applicationId, locationId, children, overrides, ...props

const verifyBuyerResults = await instance?.verifyBuyer(String(rest.token), props.createVerificationDetails());

await props.cardTokenizeResponseReceived(rest, verifyBuyerResults);
await props.cardTokenizeResponseReceived(rest, verifyBuyerResults, instance);
};

// Fixes stale closure issue with using React Hooks & SqPaymentForm callback functions
Expand All @@ -78,6 +81,8 @@ function FormProvider({ applicationId, locationId, children, overrides, ...props
cardTokenizeResponseReceived: cardTokenizeResponseReceivedCallback,
createPaymentRequest,
payments: instance,
card,
setCard,
};

return <FormContext.Provider value={context}>{children}</FormContext.Provider>;
Expand Down
5 changes: 4 additions & 1 deletion src/contexts/form/form.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export type FormContextType = {
* Invoked when a digital wallet payment button is clicked.
*/
createPaymentRequest?: Square.PaymentRequestOptions;
card?: Square.Card;
setCard: React.Dispatch<React.SetStateAction<Square.Card | undefined>>;
};

export type FormProviderProps = {
Expand All @@ -44,7 +46,8 @@ export type FormProviderProps = {
*/
cardTokenizeResponseReceived: (
token: Square.TokenResult,
verifiedBuyer?: Square.VerifyBuyerResponseDetails | null
verifiedBuyer?: Square.VerifyBuyerResponseDetails | null,
payments?: Square.Payments
) => void | Promise<void>;
children: React.ReactNode;
/**
Expand Down