payment method page

new page to add user's payment method (credit card / paypal) alone
with billing address details
This commit is contained in:
Nuwan 2025-07-15 16:38:22 +05:30
parent c7ce60e0e4
commit a21dde88e0
17 changed files with 594 additions and 684 deletions

View File

@ -12,5 +12,5 @@ REACT_APP_SITE_KEY=6Let8dgSAAAAAFheKGWrs6iaq_hIlPOZ2f3Bb56B
REACT_APP_GOOGLE_ANALYTICS_ID=G-MC9BTWXWY4
PUBLIC_URL=
REACT_APP_COOKIE_DOMAIN=.jamkazam.local
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_9vO8ZnxBpb9Udb0paruV3qLv
REACT_APP_RECURRY_PUBLIC_API_KEY=ewr1-hvDV1xQxDw0HPaaRFP4KNE
REACT_APP_RECURLY_PUBLIC_API_KEY=ewr1-hvDV1xQxDw0HPaaRFP4KNE
REACT_APP_BRAINTREE_TOKEN=sandbox_pgjp8dvs_5v5rwm94m2vrfbms

View File

@ -9,5 +9,5 @@ REACT_APP_BITBUCKET_COMMIT=dev
REACT_APP_ENV=development
REACT_APP_COOKIE_DOMAIN=.jamkazam.com
REACT_APP_GOOGLE_ANALYTICS_ID=G-MC9BTWXWY4
REACT_APP_STRIPE_PUBLISHABLE_KEY=
REACT_APP_RECURRY_PUBLIC_API_KEY=
REACT_APP_RECURLY_PUBLIC_API_KEY=
REACT_APP_BRAINTREE_TOKEN=

View File

@ -9,5 +9,5 @@ REACT_APP_RECAPTCHA_ENABLED=true
REACT_APP_SITE_KEY=6Let8dgSAAAAAFheKGWrs6iaq_hIlPOZ2f3Bb56B
REACT_APP_COOKIE_DOMAIN=.jamkazam.com
REACT_APP_GOOGLE_ANALYTICS_ID=G-SPTNJRW7WB
REACT_APP_STRIPE_PUBLISHABLE_KEY=
REACT_APP_RECURRY_PUBLIC_API_KEY=
REACT_APP_RECURLY_PUBLIC_API_KEY=
REACT_APP_BRAINTREE_TOKEN=

View File

@ -9,5 +9,5 @@ REACT_APP_RECAPTCHA_ENABLED=false
REACT_APP_SITE_KEY=6Let8dgSAAAAAFheKGWrs6iaq_hIlPOZ2f3Bb56B
REACT_APP_COOKIE_DOMAIN=.staging.jamkazam.com
REACT_APP_GOOGLE_ANALYTICS_ID=G-8W0GTL53NT
REACT_APP_STRIPE_PUBLISHABLE_KEY=
REACT_APP_RECURRY_PUBLIC_API_KEY=
REACT_APP_RECURLY_PUBLIC_API_KEY=
REACT_APP_BRAINTREE_TOKEN=

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -278,7 +278,7 @@ function JKDashboardMain() {
useScript(`${process.env.REACT_APP_CLIENT_BASE_URL}/client_scripts`, initJKScripts);
useScript('https://js.recurly.com/v4/recurly.js', () => {
console.log('Recurly.js script loaded');
window.recurly.configure(process.env.REACT_APP_RECURRY_PUBLIC_API_KEY);
window.recurly.configure(process.env.REACT_APP_RECURLY_PUBLIC_API_KEY);
});
return (

View File

@ -1,29 +1,504 @@
import React, { useState, useEffect } from 'react';
import { Row, Col, Card, CardBody } from 'reactstrap';
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 JKBillingDetails from '../payments/JKBillingDetails';
import JKPaymentOptions from '../payments/JKPaymentOptions';
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) => {
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('subscription.page_title')} titleClass="font-weight-bold" />
<FalconCardHeader title={t('payment_method.page_title')} titleClass="font-weight-bold" />
<CardBody className="pt-3" style={{ backgroundColor: '#edf2f9' }}>
{true ? (
<Row>
<Col className="mb-2" xs={12} md={6} lg={5}>
<JKBillingDetails billingInfo={billingInfo} setBillingInfo={setBillingInfo} />
</Col>
<Col xs={12} md={6} lg={5}>
<JKPaymentOptions billingInfo={billingInfo} />
</Col>
<Col className='d-none d-sm-block' />
</Row>
) : 'Loading...' }
<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>
);

View File

@ -1,310 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Card,
CardBody,
Col,
Button,
Row,
Label
} from 'reactstrap';
import Select from 'react-select';
import FalconCardHeader from '../common/FalconCardHeader';
import { useForm, Controller } from 'react-hook-form';
import Flex from '../common/Flex';
import { getBillingInfo, updateBillingInfo, getUserDetail, getCountries } from '../../helpers/rest';
import { useAuth } from '../../context/UserAuth';
import { toast } from 'react-toastify';
const JKBillingDetails = ({ billingInfo, setBillingInfo}) => {
const { currentUser } = useAuth();
//const [billingInfo, setBillingInfo] = useState({});
const [countries, setCountries] = useState([]);
const [submitting, setSubmitting] = useState(false);
const labelClassName = 'ls text-600 font-weight-semi-bold mb-0';
const {
register,
control,
handleSubmit,
setValue,
setError,
formState: { errors }
} = useForm({
defaultValues: {
first_name: '',
last_name: '',
address1: '',
address2: '',
city: '',
state: '',
zip: '',
country: 'US',
}
});
useEffect(() => {
if (currentUser) {
fetchCountries();
populateData();
}
}, [currentUser]);
const fetchCountries = () => {
getCountries()
.then(response => {
if (response.ok) {
return response.json();
}
})
.then(data => {
setCountries(data.countriesx);
})
.catch(error => console.log(error));
};
const populateData = async () => {
const options = {
id: currentUser.id
};
try {
const userResp = await getUserDetail(options);
const userData = await userResp.json();
if (userData.has_recurly_account) {
await populateBillingAddress();
} else {
setValue('first_name', userData.first_name);
setValue('last_name', userData.last_name);
setValue('address1', userData.address1);
setValue('address2', userData.address2);
setValue('city', userData.city);
setValue('state', userData.state);
setValue('zip', userData.zip);
setValue('country', userData.country);
}
} 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;
setValue('first_name', bi.first_name);
setValue('last_name', bi.last_name);
setValue('address1', bi.address1);
setValue('address2', billingInfo.address2);
setValue('city', bi.city);
setValue('state', bi.state);
setValue('zip', bi.zip);
setValue('country', bi.country);
setBillingInfo(bi);
} catch (error) {
console.error('Failed to get billing info:', error);
}
};
const handleCountryChange = selectedOption => {
setValue('country', selectedOption.value);
};
const onSubmit = async data => {
setSubmitting(true);
try {
const response = await updateBillingInfo(data);
if (response.ok) {
toast.success('Billing details updated successfully');
const updatedData = await response.json();
setBillingInfo(updatedData.billing_info);
} else {
toast.error('Failed to update billing details');
}
} catch (error) {
console.error('Error updating billing details:', error);
toast.error('An error occurred while updating billing details');
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Card className="mb-3">
<FalconCardHeader title="Billing Address" titleTag="h5" />
<CardBody>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="first_name" className={labelClassName}>
First Name
</Label>
</Col>
<Col>
<input {...register('first_name', { required: 'First Name is required' })} className="form-control" />
{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}>
Last Name
</Label>
</Col>
<Col>
<input {...register('last_name', { required: 'Last Name is required' })} className="form-control" />
{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}>
Address 1
</Label>
</Col>
<Col>
<input {...register('address1', { required: 'Address is required' })} className="form-control" />
{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}>
Address 2
</Label>
</Col>
<Col>
<input {...register('address2')} className="form-control" />
{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}>
City
</Label>
</Col>
<Col>
<input {...register('city', { required: 'City is required' })} className="form-control" />
{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}>
State or Region
</Label>
</Col>
<Col>
<input {...register('state', { required: 'State or Region is required' })} className="form-control" />
{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}>
Zip or Postal Code
</Label>
</Col>
<Col>
<input
{...register('zip', { required: 'Zip or Postal Code is required' })}
className="form-control"
/>
{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}>
Country
</Label>
</Col>
<Col>
<Controller
name="country"
control={control}
rules={{ required: 'Country is required' }}
render={({ field: { onChange, value } }) => {
const country = countries.find(country => country.countrycode === value);
if (!country) {
return (
<Select
data-testid="countrySelect"
onChange={handleCountryChange}
options={countries.map(c => {
return { value: c.countrycode, label: c.countryname };
})}
/>
);
}
return (
<Select
data-testid="countrySelect"
value={{ value: country.countrycode, label: country.countryname }}
onChange={handleCountryChange}
options={countries.map(c => {
return { value: c.countrycode, label: c.countryname };
})}
/>
);
}}
/>
{errors.country && (
<div className="text-danger">
<small>{errors.country.message}</small>
</div>
)}
</Col>
</Row>
<div className="d-flex justify-content-end">
<Button
color="primary"
type="submit"
disabled={submitting}
className="mt-3"
>
{submitting ? 'Submitting...' : 'Save Address'}
</Button>
</div>
</CardBody>
</Card>
</form>
)
}
export default JKBillingDetails

View File

@ -1,340 +0,0 @@
import React, { useState, useContext, useEffect, useMemo } from 'react';
import AppContext from '../../context/Context';
import {
Card,
CardBody,
Col,
Button,
Row,
FormGroup,
CustomInput,
Label
} from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { useForm } from 'react-hook-form';
import { useHistory } from 'react-router-dom';
import Flex from '../common/Flex';
import iconPaymentMethodsGrid from '../../assets/img/icons/icon-payment-methods-grid.png';
import iconPaypalFull from '../../assets/img/icons/icon-paypal-full.png';
//import { useResponsive } from '@farfetch/react-context-responsive';
//import { useShoppingCart } from '../../hooks/useShoppingCart';
import { createRecurlyAccount, placeOrder, submitStripe } from '../../helpers/rest';
//import { useAuth } from '../../context/UserAuth';
import { isValid, isExpirationDateValid, isSecurityCodeValid, getCreditCardNameByNumber } from 'creditcard.js';
import { useCheckout } from '../../hooks/useCheckout';
import { toast } from 'react-toastify';
const JKPaymentOptions = ({ billingInfo }) => {
const history = useHistory();
const { setPreserveBillingInfo, refreshPreserveBillingInfo, shouldPreserveBillingInfo, deletePreserveBillingInfo } = useCheckout();
const [paymentMethod, setPaymentMethod] = useState('credit-card');
const [paymentErrorMessage, setPaymentErrorMessage] = useState('');
//const [orderErrorMessage, setOrderErrorMessage] = useState('');
const [cardNumber, setCardNumber] = useState('');
//const [billingInfo, setBillingInfo] = useState({});
const [submitting, setSubmitting] = useState(false);
const [reuseExistingCard, setReuseExistingCard] = useState(false);
const [saveThisCard, setSaveThisCard] = useState(false);
const {
register,
control,
handleSubmit,
setValue,
setError,
formState: { errors }
} = useForm({
defaultValues: {
number: '',
month: '',
year: '',
verification_value: ''
}
});
const disableCardFields = useMemo(() => {
return paymentMethod === 'existing-card';
}, [paymentMethod]);
const onSubmit = async data => {
console.log('Form Data:', data);
if (!window.recurly){
console.error('Recurly is not loaded');
toast.error('Payment System is not loaded. Please refresh this page and try to enter your info again. Sorry for the inconvenience!');
return;
}
if (!isValidateCard(data)) {
toast.error('Please fix the errors in the form before submitting.');
console.error('Form validation failed');
return;
}
const params = {
number: data.number.replace(/\s+/g, ''),
cvc: data.verification_value,
exp_month: data.month,
exp_year: data.year,
}
window.Stripe.card.createToken(params, (status, response) => {
if (response.error) {
// Handle error
console.error('Stripe Error:', response.error.message);
switch (response.error.code) {
case 'invalid_number':
setError('number', { type: 'manual', message: response.error.message }, { shouldFocus: false });
break;
case 'invalid_cvc':
setError('cvc', { type: 'manual', message: response.error.message }, { shouldFocus: false });
break;
case 'invalid_expiry_year':
setError('exp_year', { type: 'manual', message: response.error.message }, { shouldFocus: false });
break;
case 'invalid_expiry_month':
setError('exp_month', { type: 'manual', message: response.error.message }, { shouldFocus: false });
break;
default:
break;
}
} else {
// Handle success
// console.log('Stripe Token:', response.id);
// data.stripeToken = response.id;
// data.stripeTokenType = response.type;
// // Proceed with creating Recurly account or placing order
// constructRecurlyAccount(data);
const stripData = {
token: response.id,
zip: billingInfo.zip,
test_drive: false,
normal: false,
}
}
submitStripe(params).then((response) => {
if (response.ok) {
return response.json();
} else {
throw new Error('Failed to submit Stripe data');
}
}).then((data) => {
console.log('Stripe Response:', data);
toast.success('Payment information submitted successfully');
}).catch((error) => {
console.error('Error submitting Stripe data:', error);
setPaymentErrorMessage('Failed to submit payment information. Please try again.');
});
});
}
const isValidateCard = data => {
let _isValid = true;
if (!isValid(cardNumber)) {
_isValid = false;
//console.log('Invalid Card Number');
setError('number', { type: 'manual', message: 'Invalid Card Number' }, { shouldFocus: false });
}
if (!isExpirationDateValid(data.month, data.year)) {
_isValid = false;
//console.log('Invalid Expiration Date');
setError('month', { type: 'manual', message: 'Invalid Expiration Date' }, { shouldFocus: false });
setError('year', { type: 'manual', message: 'Invalid Expiration Date' }, { shouldFocus: false });
}
// if (!isSecurityCodeValid(data.verification_value)) {
// _isValid = false;
// console.log('Invalid Security Code');
// setError('verification_value', { type: 'manual', message: 'Invalid Security Code' }, { shouldFocus: false });
// }
return _isValid;
};
function formatCardNumber(value) {
const v = value.replace(/\s+/g, '').replace(/[^0-9]/gi, '');
const matches = v.match(/\d{4,16}/g);
const match = (matches && matches[0]) || '';
const parts = [];
for (let i = 0; i < match.length; i += 4) {
parts.push(match.substring(i, i + 4));
}
if (parts.length) {
return parts.join(' ');
} else {
return value;
}
}
const handleOnCardNumberChange = e => {
const cardNumber = e.target.value;
//console.log('Formatted Card Number:', formatCardNumber(cardNumber));
setCardNumber(formatCardNumber(cardNumber));
};
const handoverToPaypal = () => {
// Handover to Paypal
setSubmitting(true);
window.location = `${process.env.REACT_APP_CLIENT_BASE_URL}/paypal/checkout/start`;
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="mb-3">
<Card className="mb-3">
<FalconCardHeader title="Payment Method" titleTag="h5" />
<CardBody>
{paymentErrorMessage && (
<div className="alert alert-danger" role="alert">
{paymentErrorMessage}
</div>
)}
{reuseExistingCard && (
<>
<Row className="mt-3">
<Col xs={12}>
<CustomInput
label={
<>
<Flex align="center" className="mb-2">
<div className="fs-1">Reuse Existing Card</div>
</Flex>
<div>Use card ending with {billingInfo.last_four}</div>
</>
}
id="existing-card"
value="existing-card"
checked={paymentMethod === 'existing-card'}
onChange={({ target }) => setPaymentMethod(target.value)}
type="radio"
/>
</Col>
</Row>
<hr />
</>
)}
<Row>
<Col xs={12}>
<CustomInput
label={
<Flex align="center" className="mb-2 fs-1">
Credit Card
</Flex>
}
id="credit-card"
value="credit-card"
checked={paymentMethod === 'credit-card'}
onChange={({ target }) => setPaymentMethod(target.value)}
type="radio"
/>
</Col>
<Col xs={12} className="pl-4">
<Row>
<Col sm={8}>
<Row className="align-items-center">
<Col>
<FormGroup>
<input
type="text"
value={cardNumber}
className={errors.number ? 'form-control form-control-is-invalid' : 'form-control'}
placeholder="•••• •••• •••• ••••"
onChange={handleOnCardNumberChange}
disabled={disableCardFields}
/>
{/* {errors.number && (
<div className="text-danger">
<small>{errors.number.message}</small>
</div>
)} */}
</FormGroup>
</Col>
</Row>
<Row className="align-items-center">
<Col xs={4}>
<FormGroup>
<Label>Month</Label>
<input
type="text"
{...register('month')}
className={errors.month ? 'form-control form-control-is-invalid' : 'form-control'}
placeholder="MM"
maxLength={2}
disabled={disableCardFields}
/>
</FormGroup>
</Col>
<Col xs={4}>
<FormGroup>
<Label>Year</Label>
<input
type="text"
{...register('year')}
className={errors.year ? 'form-control form-control-is-invalid' : 'form-control'}
placeholder="YYYY"
maxLength={4}
disabled={disableCardFields}
/>
</FormGroup>
</Col>
<Col xs={4}>
<FormGroup>
<Label>CVV</Label>
<input
type="text"
{...register('verification_value')}
className={
errors.verification_value ? 'form-control form-control-is-invalid' : 'form-control'
}
placeholder="123"
maxLength={3}
disabled={disableCardFields}
/>
</FormGroup>
</Col>
</Row>
</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">We Accept</div>
<img src={iconPaymentMethodsGrid} alt="" width="120" />
</div>
</div>
</Row>
</Col>
</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>
{/* <hr className="border-dashed my-5" /> */}
<Row>
<Col className="pl-lg-4 pl-xl-2 pl-xxl-5 text-center">
<Button type="submit" color="primary" className="mt-3 px-5" disabled={submitting}>
Save Payment Information
</Button>
<p className="fs--1 mt-3 mb-0">
By clicking <strong>Save Payment Information</strong>, you agree to JamKazam's{' '}
<a href="https://www.jamkazam.com/corp/terms" target='_blank'>Terms Of Service</a>.
</p>
</Col>
</Row>
</CardBody>
</Card>
</form>
)
}
export default JKPaymentOptions

View File

@ -728,3 +728,25 @@ export const submitStripe = (options = {}) => {
.catch(error => reject(error));
})
}
// function updatePayment(options) {
// options = options || {}
// return $.ajax({
// type: "POST",
// url: '/api/recurly/update_payment',
// dataType: "json",
// contentType: 'application/json',
// data: JSON.stringify(options)
// })
// }
export const updatePayment = (options = {}) => {
return new Promise((resolve, reject) => {
apiFetch(`/recurly/update_payment`, {
method: 'POST',
body: JSON.stringify(options)
})
.then(response => resolve(response))
.catch(error => reject(error));
});
};

View File

@ -109,5 +109,68 @@
"no_payments": "No payments found.",
"load_more": "Load More",
"loading": "Loading..."
},
"payment_method": {
"page_title": "Payment Method",
"header": "Address and Payment Method",
"help_text_no_card": "You do not currently have a payment method on file.",
"help_text_has_card": "You currently have a payment method on file.",
"help_text": "To update your payment method, first enter your billing address and click the save button. If credit card, enter your card information and click the Save button. If PayPal, click the save button and follow PayPal's on-screen instructions to sign into your account and authorize payment to JamKazam.",
"credit_card_number": "Credit Card Number",
"expiration_date": "Expiration Date (MM/YY)",
"cvv": "CVV",
"submit": "Save Payment Method",
"first_name": "First Name",
"last_name": "Last Name",
"address1": "Address 1",
"address2": "Address 2",
"city": "City",
"state": "State or Region",
"zip_code": "Zip/Postal Code",
"country": "Country",
"credit_card": "Credit Card",
"paypal": "PayPal",
"we_accept": "We accept",
"submitting": "Submitting...",
"save_payment_info": "Save Payment Information",
"validations": {
"first_name": {
"required": "First Name is required"
},
"last_name": {
"required": "Last Name is required"
},
"address1": {
"required": "Address Line 1 is required"
},
"city": {
"required": "City is required"
},
"state": {
"required": "State or Region is required"
},
"zip_code": {
"required": "Zip/Postal Code is required"
},
"country": {
"required": "Country is required"
},
"card": {
"invalid": "Credit card details are invalid"
}
},
"aggreement": {
"text1": "By clicking",
"text2": "Save Payment Information",
"text3": "you agree to JamKazam's",
"terms_of_service": "Terms of Service"
},
"alerts": {
"try_again": "Please try again.",
"payment_method_updated": "Your payment method has been successfully updated.",
"card_update_error": "Failed to update payment method. Please try again later.",
"card_processing_error": "There was an error processing your card. Please check your details and try again.",
"billing_update_error": "There was an error processing your billing information. Please check your details and try again."
}
}
}

View File

@ -619,7 +619,7 @@ profileUtils = context.JK.ProfileUtils
<a className={classNames(submitClassNames)} onClick={this.onSubmitForm}>SUBMIT CARD INFORMATION</a>
</div>`
else
header = 'You have have a payment method on file already.'
header = 'You have a payment method on file already.'
updateCardAction = `<a className={classNames(updateCardClassNames)} onClick={this.onUnlockPaymentInfo}>I'D LIKE TO UPDATE MY PAYMENT INFO</a>`
managedSubscriptionAction = `<a href="/client#/account/subscription" className="button-orange">MANAGE MY SUBSCRIPTION</a>`
actions = `<div className="actions">