Skip to content

Commit

Permalink
feat(auth): add checkout form
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristiaanScheermeijer committed Jul 23, 2021
1 parent a04b15e commit a1075d6
Show file tree
Hide file tree
Showing 20 changed files with 730 additions and 41 deletions.
173 changes: 173 additions & 0 deletions src/components/CheckoutForm/CheckoutForm.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
@use '../../styles/variables';
@use '../../styles/theme';

.backButton {
position: absolute;
right: 0;
left: 0;
}

.title {
margin-bottom: 24px;
font-family: theme.$body-alt-font-family;
font-weight: 700;
font-size: 24px;
}

.order {
display: flex;
align-items: center;
margin-bottom: 8px;
padding: 16px;
font-family: theme.$body-font-family;
background-color: rgba(variables.$white, 0.08);
border-radius: 4px;
}

.orderTitle {
margin: 0 0 4px;
font-weight: 700;
font-size: 14px;
}

.orderBillingDate {
margin: 0;
font-size: 14px;
}

.orderInfo {
flex: 1;
}

.orderPrice {
display: flex;
align-items: baseline;

> span {
font-weight: 700;
font-size: 24px;
}

> small {
margin-left: 4px;
font-size: 14px;
}
}

.couponForm {
margin-bottom: 24px;
text-align: right;
}

.redeemCoupon {
display: flex;
align-items: center;
width: 100%;
margin-bottom: 8px;
}

.couponInput {
flex: 1;
width: 100%;
height: 36px;
margin-right: 4px;
padding: 14px;

color: theme.$text-field-resting-color;

font-size: 16px;

background-color: theme.$text-field-bg-color;
border: 1px solid theme.$text-field-resting-border-color;
border-radius: 4px;
transition: border 0.2s ease;

&:focus-within {
color: theme.$text-field-active-color;
border-color: theme.$text-field-active-border-color;
}
}

.orderTotals {
width: 100%;
margin-bottom: 24px;
font-family: theme.$body-font-family;

td:first-child {
text-align: right;
}

td:last-child {
width: 100px;
text-align: right;
}

tfoot {
td {
padding-top: 8px;
font-weight: 700;
font-size: 24px;
}
}
}

.divider {
border: none;
border-top: 1px solid rgba(variables.$white, 0.34);
}

.paymentMethods {
display: flex;
margin: 0 -4px 24px;
}

.paymentMethod {
flex: 1;
margin: 0 4px;
}

.radio {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
white-space: nowrap;
clip: rect(0 0 0 0);
clip-path: inset(50%);

:focus,
:active {
+ .paymentMethodLabel {
border-color: variables.$white;
}
}

&:checked + .paymentMethodLabel {
color: variables.$black;
background-color: variables.$white;
border-color: variables.$white;

svg {
fill: variables.$black;
}
}
}

.paymentMethodLabel {
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
font-family: theme.$body-font-family;
font-weight: 700;
font-size: 20px;
background-color: rgba(variables.$black, 0.34);
border: 1px solid rgba(variables.$white, 0.34);
border-radius: 4px;
cursor: pointer;
transition: border 0.2s ease, background 0.2s ease;

> svg {
margin-right: 4px;
}
}
12 changes: 12 additions & 0 deletions src/components/CheckoutForm/CheckoutForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import { render } from '@testing-library/react';

import CheckoutForm from './CheckoutForm';

describe('<CheckoutForm>', () => {
test('renders and matches snapshot', () => {
const { container } = render(<CheckoutForm />);

expect(container).toMatchSnapshot();
});
});
186 changes: 186 additions & 0 deletions src/components/CheckoutForm/CheckoutForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import React from 'react';
import { useTranslation } from 'react-i18next';

import Button from '../Button/Button';
import type { Offer, Order, PaymentMethod } from '../../../types/checkout';
import IconButton from '../IconButton/IconButton';
import Exit from '../../icons/Exit';
import FormFeedback from '../FormFeedback/FormFeedback';
import { formatPrice } from '../../utils/formatting';
import Close from '../../icons/Close';

import styles from './CheckoutForm.module.scss';

type Props = {
paymentMethodId?: number;
onBackButtonClick: () => void;
paymentMethods?: PaymentMethod[];
onPaymentMethodChange: React.ChangeEventHandler<HTMLInputElement>;
onCouponFormSubmit: React.FormEventHandler<HTMLFormElement>;
onCouponInputChange: React.ChangeEventHandler<HTMLInputElement>;
onRedeemCouponButtonClick: () => void;
onCloseCouponFormClick: () => void;
couponFormOpen: boolean;
couponFormError?: boolean;
couponFormApplied?: boolean;
couponFormSubmitting?: boolean;
couponInputValue: string;
order: Order | null;
offer: Offer | null;
};

const CheckoutForm: React.FC<Props> = ({
paymentMethodId,
paymentMethods,
order,
offer,
onBackButtonClick,
onPaymentMethodChange,
couponFormOpen,
couponInputValue,
couponFormError,
couponFormApplied,
couponFormSubmitting,
onCouponInputChange,
onCloseCouponFormClick,
onCouponFormSubmit,
onRedeemCouponButtonClick,
}) => {
const { t } = useTranslation('account');

const getOfferPeriod = () => {
// t('periods.day')
// t('periods.week')
// t('periods.month')
// t('periods.year')
return offer ? t(`periods.${offer.period}`) : '';
};

const cardPaymentMethod = paymentMethods?.find((method) => method.methodName === 'card');
const paypalPaymentMethod = paymentMethods?.find((method) => method.methodName === 'paypal');

return (
<div>
<IconButton onClick={onBackButtonClick} className={styles.backButton} aria-label="Go back">
<Exit />
</IconButton>
{!order || !offer ? (
<FormFeedback variant="error">No order!</FormFeedback>
) : (
<React.Fragment>
<h2 className={styles.title}>{t('checkout.payment_method')}</h2>
<div className={styles.order}>
<div className={styles.orderInfo}>
<p className={styles.orderTitle}>{offer.period === 'month' ? t('checkout.monthly') : t('checkout.yearly')}</p>
{/*<p className={styles.orderBillingDate}>*/}
{/* Billing date is on <time>05-12-2021</time>*/}
{/*</p>*/}
</div>
<div className={styles.orderPrice}>
<span>{formatPrice(order.priceBreakdown.offerPrice, order.currency, offer.customerCountry)}</span>
<small>/{getOfferPeriod()}</small>
</div>
</div>
<div className={styles.couponForm}>
{couponFormOpen ? (
<form onSubmit={onCouponFormSubmit} noValidate>
<div className={styles.redeemCoupon}>
<IconButton aria-label="Close coupon form" onClick={onCloseCouponFormClick}>
<Close />
</IconButton>
<input
className={styles.couponInput}
name="couponCode"
type="text"
placeholder="Coupon code"
value={couponInputValue}
onChange={onCouponInputChange}
/>
<Button variant="outlined" label="Apply" type="submit" disabled={couponFormSubmitting} />
</div>
{couponFormError ? <FormFeedback variant="error">Coupon code error! :-(</FormFeedback> : null}
{couponFormApplied ? <FormFeedback variant="success">Coupon code applied!</FormFeedback> : null}
</form>
) : (
<Button variant="outlined" label={t('checkout.redeem_coupon')} onClick={onRedeemCouponButtonClick} />
)}
</div>
<div>
<table className={styles.orderTotals}>
<tbody>
<tr>
<td>{t('checkout.applicable_tax', { taxRate: Math.round(order.taxRate * 100) })}</td>
<td>{formatPrice(order.priceBreakdown.taxValue, order.currency, offer.customerCountry)}</td>
</tr>
<tr>
<td>{t('checkout.payment_method_fee')}</td>
<td>{formatPrice(order.priceBreakdown.paymentMethodFee, order.currency, offer.customerCountry)}</td>
</tr>
{order.discount.applied && order.discount.type === 'coupon' ? (
<tr>
<td>
<div>{t('checkout.coupon_discount')}</div>
<small>
({t('checkout.discount_period', {
count: order.discount.periods,
period: t(`periods.${offer.period}`, { count: order.discount.periods }),
})})
</small>
</td>
<td>{formatPrice(order.priceBreakdown.discountAmount, order.currency, offer.customerCountry)}</td>
</tr>
) : null}
</tbody>
<tfoot>
<tr>
<td>{t('checkout.total_price')}</td>
<td>{formatPrice(order.totalPrice, order.currency, offer.customerCountry)}</td>
</tr>
</tfoot>
</table>
</div>
<hr className={styles.divider} />
{order.requiredPaymentDetails ? (
<div className={styles.paymentMethods}>
{cardPaymentMethod ? (
<div className={styles.paymentMethod}>
<input
className={styles.radio}
type="radio"
name="paymentMethod"
value={cardPaymentMethod.id}
id="card"
checked={paymentMethodId === cardPaymentMethod.id}
onChange={onPaymentMethodChange}
/>
<label className={styles.paymentMethodLabel} htmlFor="card">
<Exit /> {t('checkout.credit_card')}
</label>
</div>
) : null}
{paypalPaymentMethod ? (
<div className={styles.paymentMethod}>
<input
className={styles.radio}
type="radio"
name="paymentMethod"
value={paypalPaymentMethod.id}
id="paypal"
checked={paymentMethodId === paypalPaymentMethod.id}
onChange={onPaymentMethodChange}
/>
<label className={styles.paymentMethodLabel} htmlFor="paypal">
<Exit /> {t('checkout.paypal')}
</label>
</div>
) : null}
</div>
) : null}
<Button label={t('checkout.continue')} variant="contained" color="primary" size="large" fullWidth />
</React.Fragment>
)}
</div>
);
};

export default CheckoutForm;
Loading

0 comments on commit a1075d6

Please sign in to comment.