jam-cloud/jam-ui/src/components/page/JKPaymentMethod.js

509 lines
19 KiB
JavaScript

import React, { useState, useEffect, useRef } from 'react';
import {
Card,
CardBody,
Col,
Button,
Row,
CustomInput,
Label
} from 'reactstrap';
import Flex from '../common/Flex';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import iconPaymentMethodsGrid from '../../assets/img/icons/icon-payment-methods-grid.png';
import iconPaypalFull from '../../assets/img/icons/icon-paypal-full.png';
import { toast } from 'react-toastify';
import { updatePayment } from '../../helpers/rest';
import { useAuth } from '../../context/UserAuth';
import { getBillingInfo, updateBillingInfo, getUserDetail, getCountries } from '../../helpers/rest';
import { useForm, Controller } from 'react-hook-form';
import Select from 'react-select';
import { useResponsive } from '@farfetch/react-context-responsive';
const JKPaymentMethod = () => {
const { t } = useTranslation('account');
const [billingInfo, setBillingInfo] = useState({});
const [hasStoredCreditCard, setHasStoredCreditCard] = useState(false);
const [paymentMethod, setPaymentMethod] = useState('credit-card');
const { currentUser } = useAuth();
const [countries, setCountries] = useState([]);
const labelClassName = 'ls text-600 font-weight-semi-bold mb-0';
const [submitting, setSubmitting] = useState(false);
const [billingDataLoaded, setBillingDataLoaded] = useState(false);
const [isCardValid, setIsCardValid] = useState(false);
const { greaterThan } = useResponsive();
const elementsRef = useRef(null);
const formRef = useRef(null);
const recurlyConfigured = useRef(false);
const paypal = useRef(null);
const {
register,
control,
handleSubmit,
setValue,
formState: { errors }
} = useForm({
defaultValues: {
first_name: '',
last_name: '',
address1: '',
address2: '',
city: '',
state: '',
zip: '',
country: 'US',
}
});
useEffect(() => {
if (currentUser) {
populateUserData();
}
}, [currentUser]);
const populateUserData = async () => {
const options = {
id: currentUser.id
};
try {
const userResp = await getUserDetail(options);
const userData = await userResp.json();
if (userData.has_recurly_account) {
setHasStoredCreditCard(userData['has_stored_credit_card?']);
await populateBillingAddress();
setBillingDataLoaded(true);
}
} catch (error) {
console.error('Failed to get user details:', error);
}
};
const populateBillingAddress = async () => {
try {
const resp = await getBillingInfo();
const data = await resp.json();
const bi = data.billing_info;
setBillingInfo(bi);
} catch (error) {
console.error('Failed to get billing info:', error);
}
};
useEffect(() => {
if (currentUser) {
fetchCountries();
}
}, [currentUser]);
const fetchCountries = () => {
getCountries()
.then(response => {
if (response.ok) {
return response.json();
}
})
.then(data => {
setCountries(data.countriesx);
})
.catch(error => console.log(error));
};
useEffect(() => {
if (billingInfo) {
setValue('first_name', billingInfo.first_name || '');
setValue('last_name', billingInfo.last_name || '');
setValue('address1', billingInfo.address1 || '');
setValue('address2', billingInfo.address2 || '');
setValue('city', billingInfo.city || '');
setValue('state', billingInfo.state || '');
setValue('zip', billingInfo.zip || '');
setValue('country', billingInfo.country || 'US');
}
}, [billingInfo, setValue]);
const handleCountryChange = selectedOption => {
setValue('country', selectedOption.value);
};
useEffect(() => {
if (!window.recurly) return;
if (recurlyConfigured.current) return;
const interval = setInterval(() => {
const container = document.querySelector('#recurly-elements');
console.log('Checking for Recurly Elements container:', container);
if (container && window.recurly) {
console.log('Initializing Recurly Elements...');
window.recurly.configure({ publicKey: process.env.REACT_APP_RECURLY_PUBLIC_API_KEY });
const elements = window.recurly.Elements();
const cardElement = elements.CardElement();
cardElement.attach('#recurly-elements');
cardElement.on('change', (event) => {
if (event.complete) {
setIsCardValid(true);
} else if (event.error) {
setIsCardValid(false);
} else {
setIsCardValid(false);
}
});
//then load paypal:
const paypalInst = window.recurly.PayPal({ braintree: { clientAuthorization: process.env.REACT_APP_BRAINTREE_TOKEN } })
paypal.current = paypalInst;
paypal.current.on('error', onPayPalError);
paypal.current.on('token', onPayPalToken);
elementsRef.current = elements;
recurlyConfigured.current = true;
clearInterval(interval);
}
}, 100);
return () => clearInterval(interval);
}, []);
const onPayPalError = (error) => {
console.error('PayPal Error:', error);
toast.error('PayPal Error: ' + (error.message || t('payment_method.alerts.try_again')));
setSubmitting(false);
}
const onPayPalToken = (token) => {
handleUpdatePayment(token);
}
const handleUpdatePayment = (token) => {
updatePayment({ recurly_token: token.id }).then((response) => {
setHasStoredCreditCard(true);
toast.success(t('payment_method.alerts.payment_method_updated'));
}).catch((error) => {
console.error('Error updating payment with PayPal token:', error);
if (error.response && error.response.data && error.response.data.message) {
toast.error(error.response.data.message);
} else {
console.error('Error updating payment with PayPal token:', error);
toast.error(t('payment_method.alerts.card_update_error'));
}
}).finally(() => {
setSubmitting(false);
});
};
const onSubmit = async (data) => {
//first update billing address
setSubmitting(true);
const resp = await updateBillingInfo(data)
if (!resp.ok) {
setSubmitting(false);
const errorData = await resp.json();
console.error('Error updating billing info:', errorData);
toast.error(errorData.message || t('payment_method.alerts.billing_update_error'));
return;
}
if (paymentMethod === 'paypal') {
handoverToPaypal();
return;
} else {
if (!elementsRef.current) {
console.error('Recurly elementsRef.current is not ready');
setSubmitting(false);
return;
}
if (!formRef.current) {
console.error('formRef.current is not ready');
setSubmitting(false);
return;
}
// if (!isCardValid) {
// console.error('Card is not valid');
// toast.error(t('payment_method.validations.card.invalid'));
// setSubmitting(false);
// return;
// }
window.recurly.token(elementsRef.current, formRef.current, (err, token) => {
if (err) {
console.error('Recurly token error:', err);
toast.error(err.message || t('payment_method.alerts.card_processing_error'));
setSubmitting(false);
} else {
console.log('Recurly token:', token.id);
// send token.id to backend
handleUpdatePayment(token);
}
});
}
};
const handoverToPaypal = () => {
// Handover to Paypal
setSubmitting(true);
paypal.current.start()
};
return (
<Card>
<FalconCardHeader title={t('payment_method.page_title')} titleClass="font-weight-bold" />
<CardBody className="pt-3" style={{ backgroundColor: '#edf2f9' }}>
<div className='mb-3'>
{hasStoredCreditCard ? (
<span>
<strong>{t('payment_method.help_text_has_card')}</strong>
</span>
) : (
<span>
<strong>{t('payment_method.help_text_no_card')}</strong>
</span>
)}
{t('payment_method.help_text')}
</div>
<form onSubmit={handleSubmit(onSubmit)} ref={formRef}>
<Card style={{ width: greaterThan.sm ? "90%" : '100%' }} className='mx-auto'>
<FalconCardHeader title={t('payment_method.header')} titleTag="h5" />
<CardBody>
<Row>
<Col className="mb-2" xs={12} md={6}>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="first_name" className={labelClassName}>
{t('payment_method.first_name')}
</Label>
</Col>
<Col>
<input {...register('first_name', { required: t('payment_method.validations.first_name.required') })} className="form-control" data-recurly="first_name" />
{errors.first_name && (
<div className="text-danger">
<small>{errors.first_name.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="last_name" className={labelClassName}>
{t('payment_method.last_name')}
</Label>
</Col>
<Col>
<input {...register('last_name', { required: t('payment_method.validations.last_name.required') })} className="form-control" data-recurly="last_name" />
{errors.last_name && (
<div className="text-danger">
<small>{errors.last_name.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="address1" className={labelClassName}>
{t('payment_method.address1')}
</Label>
</Col>
<Col>
<input {...register('address1', { required: t('payment_method.validations.address1.required') })} className="form-control" data-recurly="address1" />
{errors.address1 && (
<div className="text-danger">
<small>{errors.address1.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="address2" className={labelClassName}>
{t('payment_method.address2')}
</Label>
</Col>
<Col>
<input {...register('address2')} className="form-control" data-recurly="address2" />
{errors.address2 && (
<div className="text-danger">
<small>{errors.address2.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="city" className={labelClassName}>
{t('payment_method.city')}
</Label>
</Col>
<Col>
<input {...register('city', { required: t('payment_method.validations.city.required') })} className="form-control" data-recurly="city" />
{errors.city && (
<div className="text-danger">
<small>{errors.city.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="state" className={labelClassName}>
{t('payment_method.state')}
</Label>
</Col>
<Col>
<input {...register('state', { required: t('payment_method.validations.state.required') })} className="form-control" data-recurly="state" />
{errors.state && (
<div className="text-danger">
<small>{errors.state.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="zip" className={labelClassName}>
{t('payment_method.zip_code')}
</Label>
</Col>
<Col>
<input
{...register('zip', { required: t('payment_method.validations.zip_code.required') })}
className="form-control" data-recurly="postal_code"
/>
{errors.zip && (
<div className="text-danger">
<small>{errors.zip.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="country" className={labelClassName}>
{t('payment_method.country')}
</Label>
</Col>
<Col>
<Controller
name="country"
control={control}
rules={{ required: t('payment_method.validations.country.required') }}
render={({ field: { onChange, value } }) => {
const country = countries.find(country => country.countrycode === value);
if (!country) {
return (
<Select
data-testid="countrySelect"
data-recurly="country"
onChange={handleCountryChange}
options={countries.map(c => {
return { value: c.countrycode, label: c.countryname };
})}
/>
);
}
return (
<Select
data-testid="countrySelect"
data-recurly="country"
value={{ value: country.countrycode, label: country.countryname }}
onChange={handleCountryChange}
options={countries.map(c => {
return { value: c.countrycode, label: c.countryname };
})}
/>
);
}}
/>
<input type="hidden" name="country" data-recurly="country" {...register('country')} />
{errors.country && (
<div className="text-danger">
<small>{errors.country.message}</small>
</div>
)}
</Col>
</Row>
</Col>
<Col xs={12} md={6} className="mb-2 pl-5">
<Row>
<Col xs={12}>
<CustomInput
label={
<Flex align="center" className="mb-2 fs-1">
<span>{t('payment_method.credit_card')}</span>
</Flex>
}
id="credit-card"
value="credit-card"
checked={paymentMethod === 'credit-card'}
onChange={({ target }) => setPaymentMethod(target.value)}
type="radio"
/>
</Col>
</Row>
<Row>
<Col sm={8}>
<div id="recurly-elements"></div>
{!isCardValid && errors.recurly && (
<div className="text-danger">
<small>{errors.recurly.message}</small>
</div>
)}
<input type="hidden" name="recurly-token" data-recurly="token" />
</Col>
<div className="col-4 text-center pt-2 d-none d-sm-block">
<div className="rounded p-2 mt-3 bg-100">
<div className="text-uppercase fs--2 font-weight-bold">{t('payment_method.we_accept')}</div>
<img src={iconPaymentMethodsGrid} alt="" width="120" />
</div>
</div>
</Row>
<hr />
<Row className="mt-3">
<Col xs={12}>
<CustomInput
label={<img className="pull-right" src={iconPaypalFull} height="20" alt="" />}
id="paypal"
value="paypal"
checked={paymentMethod === 'paypal'}
onChange={({ target }) => setPaymentMethod(target.value)}
type="radio"
/>
</Col>
</Row>
<div className="d-flex justify-content-center">
<Button
color="primary"
type="submit"
disabled={submitting || !billingDataLoaded }
className="mt-3"
>
{submitting ? t('payment_method.submitting') : t('payment_method.save_payment_info')}
</Button>
</div>
<div className="text-center">
<p className="fs--1 mt-3 mb-0">
{t('payment_method.aggreement.text1')} <strong>{t('payment_method.aggreement.text2')} </strong>{t('payment_method.aggreement.text3')}{' '}
<br />
<a href="https://www.jamkazam.com/corp/terms" target='_blank'>{t('payment_method.aggreement.terms_of_service')}</a>
</p>
</div>
</Col>
</Row>
</CardBody>
</Card>
</form>
</CardBody>
</Card>
);
};
export default JKPaymentMethod;