509 lines
19 KiB
JavaScript
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;
|