at the end of the day, it was inevitable

This commit is contained in:
Mo Elzubeir
2022-12-09 08:36:26 -06:00
commit 1218570914
1768 changed files with 887087 additions and 0 deletions
@@ -0,0 +1,200 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Col, FormGroup, Label, Row } from 'reactstrap';
import { getData } from 'country-list';
import { CardElement } from '@stripe/react-stripe-js';
import { Input } from '../../../common/FormControls';
import { Trans, translate } from 'react-i18next';
const countries = getData().map((v) => ({ label: v.name, value: v.code }));
const cardElementOptions = {
hidePostalCode: true,
style: {
base: {
fontSize: '16px',
color: '#424770'
},
invalid: {
color: '#d92550'
}
}
};
function BillingDetailsForm(props) {
const { form, errors, handleChange, handleValidation, t } = props;
return (
<Row>
<Col md={12}>
<Input
name="name"
title={t('plans.billingForm.fullName')}
type="text"
required
value={form.name}
error={errors.name}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="line1"
title={t('plans.billingForm.addr1')}
type="text"
required
description={t('plans.billingForm.addr1Desc')}
value={form.line1}
error={errors.line1}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="line2"
title={t('plans.billingForm.addr2')}
type="text"
description={t('plans.billingForm.addr2Desc')}
value={form.line2}
error={errors.line2}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="city"
title={t('plans.billingForm.city')}
type="text"
required
description={t('plans.billingForm.cityDesc')}
value={form.city}
error={errors.city}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="state"
title={t('plans.billingForm.state')}
type="text"
required
description={t('plans.billingForm.stateDesc')}
value={form.state}
error={errors.state}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="postal_code"
title={t('plans.billingForm.zip')}
type="text"
required
description={t('plans.billingForm.zipDesc')}
value={form.postal_code}
error={errors.postal_code}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="country"
title={t('plans.billingForm.country')}
type="select"
required
options={countries}
value={form.country}
error={errors.country}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="email"
title={t('plans.billingForm.email')}
type="email"
required
value={form.email}
error={errors.email}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="phone"
title={t('plans.billingForm.phone')}
type="tel"
required
description={t('plans.billingForm.phoneDesc')}
value={form.phone}
error={errors.phone}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col xs={12} className="mb-2">
<FormGroup>
<Label>{t('plans.billingForm.cardHeading')}</Label>
<CardElement
options={cardElementOptions}
className="border b-radius-5 p-3"
/>
</FormGroup>
</Col>
<Col md={12}>
<p className="text-muted">
<Trans i18nKey="plans.billingForm.agreement">
By submitting, you agree to our
<a
title="Privacy Policy"
target="_blank"
href="https://www.socialhose.io/en/legal/privacy"
className="footer__link"
>
Privacy Policy
</a>
<a
title="Terms and Conditions"
target="_blank"
href="https://www.socialhose.io/en/legal/terms"
className="footer__link"
>
Terms & Conditions
</a>
<a
title="Acceptable Use Policy"
target="_blank"
href="https://www.socialhose.io/en/legal/acceptable-use"
className="footer__link"
>
Acceptable Use Policy
</a>
.
</Trans>
</p>
</Col>
</Row>
);
}
BillingDetailsForm.propTypes = {
t: PropTypes.func,
form: PropTypes.object,
errors: PropTypes.object,
handleChange: PropTypes.func,
handleValidation: PropTypes.func
};
export default React.memo(
translate(['tabsContent'], { wait: true })(BillingDetailsForm)
);
@@ -0,0 +1,244 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import useForm from '../../../common/hooks/useForm';
import {
ListGroupItem,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
Row,
Form,
Button,
Col,
ListGroup,
Label
} from 'reactstrap';
import { Checkbox, Input } from '../../../common/FormControls';
import { cancelPlan, cancelPlanHubspot } from '../../../../api/plans/userPlans';
import { planRoutes } from './UserPlans';
const formParams = {
rs1: 1,
rs2: 2,
rs3: 3,
rs4: 4,
rs5: 5,
rs6: 'Other'
};
const initForm = {
[formParams.rs1]: false,
[formParams.rs2]: false,
[formParams.rs3]: false,
[formParams.rs4]: false,
[formParams.rs5]: false,
[formParams.rs6]: false,
Other: false,
content: '',
email: '',
subject: 'Cancellation',
errors: {
email: null
}
};
function CancellationFeedback({ t, actions, isOpen = false, toggle, user }) {
const {
form,
handleChange,
handleValidation,
validateSubmit,
errors
} = useForm(initForm);
const [cancelLoading, setCancelLoading] = useState(false);
const [reasonError, setReasonError] = useState('');
useEffect(() => {
handleChange('email', user.email);
}, [user.email]);
useEffect(() => {
if (Object.values(formParams).some((v) => form[v])) {
setReasonError('');
}
}, [...Object.values(form)]);
function cancelSubscription() {
const obj = validateSubmit();
if (!obj) {
return actions.addAlert({ type: 'error', transKey: 'requiredInfo' });
} else if (!Object.values(formParams).some((v) => obj[v])) {
setReasonError(t('plans.currentPlan.cancelModal.reasonSelect'));
return;
}
setCancelLoading(true);
cancelPlan().then((res) => {
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setCancelLoading(false);
return;
}
cancelPlanHubspot({ ...obj });
actions.addAlert({
type: 'notice',
transKey: 'cancelledSubscription'
});
// refresh page on success and move to active plan details
setTimeout(() => {
window.location.pathname = `/app/plans/${planRoutes.current}`;
}, 1000);
});
}
return (
<div>
<Modal size="lg" backdrop="static" isOpen={isOpen} toggle={toggle}>
<ModalHeader toggle={toggle}>
{t('plans.currentPlan.cancelModal.header')}
</ModalHeader>
<ModalBody>
<Row>
<Col md={6} className="mb-3">
<p className="mb-3">
{t('plans.currentPlan.cancelModal.line1', {
firstName: user.firstName
})}
</p>
<p className="mb-2">{t('plans.currentPlan.cancelModal.line2')}</p>
<ListGroup className="text-muted">
<ListGroupItem>
{t('plans.currentPlan.cancelModal.warn1')}
</ListGroupItem>
<ListGroupItem>
{t('plans.currentPlan.cancelModal.warn2')}
</ListGroupItem>
<ListGroupItem>
{t('plans.currentPlan.cancelModal.warn3')}
</ListGroupItem>
<ListGroupItem>
{t('plans.currentPlan.cancelModal.warn4')}
</ListGroupItem>
</ListGroup>
</Col>
<Col md={6} className="mb-3">
<Form>
<p className="mb-4">
{t('plans.currentPlan.cancelModal.feedbackPara')}
</p>
<div>
<Label className="d-inline-block mb-2">
{t('plans.currentPlan.cancelModal.reasonCancellation')}
<span className="text-danger">*</span>
</Label>
<div className="pl-3 mb-3">
<Checkbox
hideTitle
name={formParams.rs1}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.noNeeds')}
description={t('plans.currentPlan.cancelModal.noNeeds')}
value={form[formParams.rs1]}
error={errors[formParams.rs1]}
handleChange={handleChange}
/>
<Checkbox
hideTitle
name={formParams.rs2}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.tooNoisy')}
description={t('plans.currentPlan.cancelModal.tooNoisy')}
value={form[formParams.rs2]}
error={errors[formParams.rs2]}
handleChange={handleChange}
/>
<Checkbox
hideTitle
name={formParams.rs3}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.confusing')}
description={t('plans.currentPlan.cancelModal.confusing')}
value={form[formParams.rs3]}
error={errors[formParams.rs3]}
handleChange={handleChange}
/>
<Checkbox
hideTitle
name={formParams.rs4}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.expensive')}
description={t('plans.currentPlan.cancelModal.expensive')}
value={form[formParams.rs4]}
error={errors[formParams.rs4]}
handleChange={handleChange}
/>
<Checkbox
hideTitle
name={formParams.rs5}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.covid')}
description={t('plans.currentPlan.cancelModal.covid')}
value={form[formParams.rs5]}
error={errors[formParams.rs5]}
handleChange={handleChange}
/>
<Checkbox
hideTitle
name={formParams.rs6}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.other')}
description={t('plans.currentPlan.cancelModal.other')}
value={form[formParams.rs6]}
error={errors[formParams.rs6]}
handleChange={handleChange}
/>
<span className="text-danger">{reasonError}</span>
</div>
</div>
<Input
name="content"
title={t('plans.currentPlan.cancelModal.tellMore')}
type="textarea"
value={form.content}
error={errors.content}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Form>
</Col>
</Row>
</ModalBody>
<ModalFooter>
<Button color="link" onClick={toggle}>
{t('plans.currentPlan.cancelModal.undoBtn')}
</Button>
<Button
color="danger"
disabled={cancelLoading}
onClick={cancelSubscription}
>
{cancelLoading
? t('plans.currentPlan.cancelModal.loadingBtn')
: t('plans.currentPlan.cancelModal.cancelSubscriptionBtn')}
</Button>
</ModalFooter>
</Modal>
</div>
);
}
CancellationFeedback.propTypes = {
t: PropTypes.func,
actions: PropTypes.object,
isOpen: PropTypes.bool,
toggle: PropTypes.func,
user: PropTypes.object
};
export default CancellationFeedback;
@@ -0,0 +1,185 @@
import React, { Fragment, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
import { Alert, Button, Card, CardBody, CardTitle, Col, Row } from 'reactstrap';
import { reduxActions } from '../../../../redux/utils/connect';
import useForm from '../../../common/hooks/useForm';
import useIsMounted from '../../../common/hooks/useIsMounted';
import BillingDetailsForm from './BillingDetailsForm';
import { changeCardDetails } from '../../../../api/plans/userPlans';
import { planRoutes } from './UserPlans';
import { setDocumentData } from '../../../../common/helper';
import { translate } from 'react-i18next';
const initialForm = {
name: '',
line1: '',
line2: '',
city: '',
state: '',
postal_code: '',
country: '',
email: '',
phone: '',
errors: {
name: null,
line1: null,
city: null,
state: null,
postal_code: null,
country: null,
email: null,
phone: null
}
};
function ChangeCard({ actions, t }) {
const isMounted = useIsMounted();
const stripe = useStripe();
const elements = useElements();
const {
form,
errors,
handleChange,
handleValidation,
validateSubmit
} = useForm(initialForm);
const [paymentError, setPaymentError] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(() => {
setDocumentData('title', 'Change Card');
return () => setDocumentData('title'); // default
}, []);
const submitPayment = async () => {
if (!stripe || !elements) {
// Stripe.js has not loaded yet.
return;
}
setPaymentError(false);
setLoading(true);
const obj = validateSubmit();
if (!obj) {
setLoading(false);
return actions.addAlert({
type: 'error',
transKey: 'requiredInfo'
});
}
const cardElement = elements.getElement(CardElement);
const {
name,
line1,
line2,
city,
state,
postal_code,
country,
email,
phone
} = obj;
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: {
name,
email,
phone,
address: {
line1: line1,
line2: line2,
city: city,
state: state,
postal_code: postal_code,
country: country
}
}
});
if (error) {
setPaymentError(error);
setLoading(false);
return;
}
const newObj = {};
newObj.paymentID = paymentMethod.id; //stripe card element ID
const res = await changeCardDetails(newObj);
if (!isMounted.current) {
return;
}
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setLoading(false);
return;
}
actions.addAlert({ type: 'notice', transKey: 'cardUpdated' });
// refresh page on success and move to active plan details
setTimeout(() => {
window.location.pathname = `/app/plans/${planRoutes.current}`;
}, 1000);
};
return (
<Col xs="12" lg="8" xl="9">
<Card className="mb-3">
<CardBody>
<CardTitle>{t('plans.changeCard.heading')}</CardTitle>
<p className="text-muted mb-3">{t('plans.changeCard.subText')}</p>
<BillingDetailsForm
form={form}
errors={errors}
handleChange={handleChange}
handleValidation={handleValidation}
/>
<Row className="divider" />
{paymentError && (
<Alert color="danger">
<Fragment>
<p className="font-size-xs font-weight-bold text-uppercase">
{t('plans.changeCard.error')}
</p>
{paymentError.message}
</Fragment>
</Alert>
)}
<div className="text-right">
<Button
type="button"
color="primary"
onClick={submitPayment}
disabled={!stripe || !elements || loading}
className="btn-wide btn-hover-shine mb-2 mb-sm-0"
size="lg"
>
{loading
? t('plans.changeCard.loadingBtn')
: t('plans.changeCard.changeCardBtn')}
</Button>
</div>
</CardBody>
</Card>
</Col>
);
}
ChangeCard.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object
};
export default reduxActions()(
translate(['tabsContent'], { wait: true })(ChangeCard)
);
@@ -0,0 +1,286 @@
import React, { Fragment, useEffect, useState } from 'react';
import { useHistory } from 'react-router';
import PropTypes from 'prop-types';
import { Button, Card, CardBody, CardTitle, Col, Row } from 'reactstrap';
import reduxConnect from '../../../../redux/utils/connect';
import { planRoutes } from './UserPlans';
import { allMediaTypes } from '../../../../redux/modules/appState/searchByFilters';
import { capitalize } from 'lodash';
import { convertUTCtoLocal, setDocumentData } from '../../../../common/helper';
import { translate } from 'react-i18next';
import CancellationFeedback from './CancellationFeedback';
function CurrentPlan({ actions, user, t }) {
const [cancelModal, setCancelModal] = useState(false);
const { restrictions } = user;
const { push } = useHistory();
useEffect(() => {
setDocumentData('title', 'Active Plan Details');
return () => setDocumentData('title'); // default
}, []);
function changePlan() {
push(`/app/plans/${planRoutes.update}`);
}
function toggleCancelModal() {
setCancelModal((prev) => !prev);
}
const {
plans,
limits,
isPlanCancelled,
subStartDate,
subEndDate
} = restrictions;
const selectedMedias = [];
const notSelectedMedias = [];
allMediaTypes.map((v) => {
if (plans[v]) {
selectedMedias.push(t(`searchTab.sourceTypes.${v}`, capitalize(v)));
} else {
notSelectedMedias.push(t(`searchTab.sourceTypes.${v}`, capitalize(v)));
}
});
const isRTL = document.documentElement.dir === 'rtl';
return (
<Col xs="12" lg="8" xl="9">
<Row>
<Col sm="6" md="4">
<div className="card mb-3 widget-chart text-left">
<div className="widget-chart-content">
<div className="widget-subheading">
{t('plans.currentPlan.subHeading')}
</div>
<div className="widget-numbers">
{plans.price === 0
? t('plans.currentPlan.freePlan')
: `$${plans.price}`}
</div>
<div className="widget-description">
<span>
{plans.price === 0 ? (
<Fragment>&nbsp;</Fragment>
) : subStartDate && subEndDate ? (
`${convertUTCtoLocal(
subStartDate,
'MMM D, YYYY'
)} - ${convertUTCtoLocal(subEndDate, 'MMM D, YYYY')}`
) : (
t('plans.currentPlan.perMonth')
)}
</span>
</div>
</div>
</div>
</Col>
<Col sm="6">
<button
className="card mb-3 widget-chart bg-success text-white text-left"
onClick={changePlan}
>
<div className="widget-chart-content">
<div className="widget-subheading">
{t('plans.currentPlan.changePlan')}
</div>
<div className="widget-numbers font-size-xlg">
{t('plans.currentPlan.upgradeYourPlan')}
</div>
<div className="widget-description">
<span>{t('plans.currentPlan.upgradeText')}</span>
</div>
</div>
</button>
</Col>
<Col xs="12">
<Card>
<CardBody>
<CardTitle>{t('plans.currentPlan.currentPlanDetails')}</CardTitle>
<div className="mb-3">
<p className="text-muted">
{t('plans.currentPlan.selectedMediaTypes')}
</p>
<p className="font-size-xlg">
{selectedMedias.length > 0
? selectedMedias.join(', ')
: t('plans.currentPlan.none')}
{notSelectedMedias.length > 0 ? (
<span className="font-size-md opacity-6 ml-2">
({t('plans.currentPlan.upgradeToGet')}:{' '}
{notSelectedMedias.join(', ')})
</span>
) : (
''
)}
</p>
</div>
<div className="divider" />
<div className="mb-3">
<p className="text-muted mb-2">
{t('plans.currentPlan.selectedLicenses')}
</p>
<Row>
<Col xs="12" sm="6" md="3">
<div className="mb-3 card widget-chart">
{!isRTL ? (
<div className="widget-numbers">
{limits.savedFeeds.current}/{limits.savedFeeds.limit}
</div>
) : (
<div className="widget-numbers">
{limits.savedFeeds.limit}/{limits.savedFeeds.current}
</div>
)}
<div className="widget-subheading mb-3">
{t('plans.currentPlan.feedsLicenses')}
</div>
</div>
</Col>
<Col xs="12" sm="6" md="3">
<div className="mb-3 card widget-chart">
{!isRTL ? (
<div className="widget-numbers">
{limits.searchesPerDay.current}/
{limits.searchesPerDay.limit}
</div>
) : (
<div className="widget-numbers">
{limits.searchesPerDay.limit}/
{limits.searchesPerDay.current}
</div>
)}
<div className="widget-subheading mb-3">
{t('plans.currentPlan.searchLicenses')}
</div>
</div>
</Col>
<Col xs="12" sm="6" md="3">
<div className="mb-3 card widget-chart">
{!isRTL ? (
<div className="widget-numbers">
{limits.webFeeds.current}/{limits.webFeeds.limit}
</div>
) : (
<div className="widget-numbers">
{limits.webFeeds.limit}/{limits.webFeeds.current}
</div>
)}
<div className="widget-subheading mb-3">
{t('plans.currentPlan.webfeedLicenses')}
</div>
</div>
</Col>
<Col xs="12" sm="6" md="3">
<div className="mb-3 card widget-chart">
{!isRTL ? (
<div className="widget-numbers">
{limits.alerts.current}/{limits.alerts.limit}
</div>
) : (
<div className="widget-numbers">
{limits.alerts.limit}/{limits.alerts.current}
</div>
)}
<div className="widget-subheading mb-3">
{t('plans.currentPlan.alertLicenses')}
</div>
</div>
</Col>
<Col xs="12" sm="6" md="3">
<div className="mb-3 card widget-chart">
{!isRTL ? (
<div className="widget-numbers">
{limits.subscriberAccounts.current}/
{limits.subscriberAccounts.limit}
</div>
) : (
<div className="widget-numbers">
{limits.subscriberAccounts.limit}/
{limits.subscriberAccounts.current}
</div>
)}
<div className="widget-subheading mb-3">
{t('plans.currentPlan.userAccounts')}
</div>
</div>
</Col>
</Row>
</div>
<div className="divider" />
<div className="mb-3">
<p className="text-muted">{t('plans.currentPlan.features')}</p>
<p className="font-size-xlg">
{plans.analytics ? (
t('plans.currentPlan.analytics')
) : (
<Fragment>
{t('plans.currentPlan.none')}
<span className="font-size-md opacity-6 ml-2">
({t('plans.currentPlan.upgradeToGet')}:{' '}
{t('plans.currentPlan.analytics')})
</span>
</Fragment>
)}
</p>
</div>
{plans.price > 0 && (
<Fragment>
<div className="divider" />
<div className="mb-3">
{!isPlanCancelled ? (
<div className="text-muted">
<Button
color="danger"
outline
onClick={toggleCancelModal}
>
{t('plans.currentPlan.cancelSubscriptionBtn')}
</Button>
<p className="text-muted mt-2">
{t('plans.currentPlan.cancelWarning')}
</p>
</div>
) : (
<div className="text-muted">
<Button color="secondary" outline disabled>
{t('plans.currentPlan.cancelSubscriptionBtn')}
</Button>
<p className="d-block d-md-inline-block ml-md-3 mt-md-0 mt-2 ml-0 text-muted">
{t('plans.currentPlan.alreadyCancelled')}
</p>
</div>
)}
</div>
</Fragment>
)}
<CancellationFeedback
isOpen={cancelModal}
toggle={toggleCancelModal}
actions={actions}
user={user}
t={t}
/>
</CardBody>
</Card>
</Col>
</Row>
</Col>
);
}
CurrentPlan.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object,
user: PropTypes.object
};
export default reduxConnect('user', ['common', 'auth', 'user'])(
translate(['tabsContent'], { wait: true })(CurrentPlan)
);
@@ -0,0 +1,168 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import {
Button,
Col,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
Row,
Table
} from 'reactstrap';
import { convertUTCtoLocal } from '../../../../common/helper';
import moment from 'moment';
import { capitalize } from 'lodash';
import { translate } from 'react-i18next';
function ShowTransactionDetails(props) {
const { data, closeModal, t } = props;
const plan = data && data.lines && data.lines.data && data.lines.data[0];
useEffect(() => {
return () => closeModal();
}, []);
return (
<Modal isOpen={!!data && !!plan} toggle={closeModal} size="lg">
<ModalHeader toggle={closeModal}>
{t('plans.transactions.modal.heading')}
</ModalHeader>
<ModalBody>
{data && (
<Row>
<Col xs="12" lg="6" className="mb-3">
<h6 className="mb-3">
{t('plans.transactions.modal.transactionDetails')}
</h6>
<Table striped>
<tbody>
<tr>
<th>{t('plans.transactions.modal.transactionDate')}</th>
<td>
{convertUTCtoLocal(
moment.unix(
data.status_transitions &&
data.status_transitions.paid_at
),
'MM/DD/YYYY hh:mm:ss a'
)}
</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.activationDate')}</th>
<td>
{convertUTCtoLocal(
moment.unix(plan && plan.period.start),
'MM/DD/YYYY'
)}
</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.expirationDate')}</th>
<td>
{convertUTCtoLocal(
moment.unix(plan && plan.period.end),
'MM/DD/YYYY'
)}
</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.amount')}</th>
<td>${data.amount_paid / 100}</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.status')}</th>
<td>{capitalize(data.status)}</td>
</tr>
</tbody>
</Table>
</Col>
<Col xs="12" lg="6" className="mb-3">
<h6 className="mb-3">
{t('plans.transactions.modal.billingDetails')}
</h6>
<Table striped>
<tbody>
<tr>
<th>{t('plans.transactions.modal.name')}</th>
<td>{data.customer_name || '-'}</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.email')}</th>
<td>{data.customer_email || '-'}</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.phone')}</th>
<td>{data.customer_phone || '-'}</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.address')}</th>
<td>{data.customer_address || '-'}</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.invoiceNo')}</th>
<td>
{data.number} (
<a
href={data.hosted_invoice_url}
rel="noopener noreferrer"
target="_blank"
>
{t('plans.transactions.modal.showInvoiceLink')}
</a>
)
</td>
</tr>
</tbody>
</Table>
</Col>
{/* <Col xs="12" lg="6" className="mb-3">
<h6 className="mb-3">Plan Details</h6>
<Table striped>
<tbody>
<tr>
<th>Feeds Licenses</th>
<td>0</td>
</tr>
<tr>
<th>Webfeed Licenses</th>
<td>0</td>
</tr>
<tr>
<th>Newsletter Licenses</th>
<td>0</td>
</tr>
<tr>
<th>User Accounts</th>
<td>0</td>
</tr>
<tr>
<th>Analytics</th>
<td>No</td>
</tr>
</tbody>
</Table>
</Col> */}
</Row>
)}
</ModalBody>
<ModalFooter>
<Button color="link" onClick={closeModal}>
{t('plans.transactions.modal.cancelBtn')}
</Button>
</ModalFooter>
</Modal>
);
}
ShowTransactionDetails.propTypes = {
t: PropTypes.func,
closeModal: PropTypes.func,
data: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired
};
export default React.memo(
translate(['tabsContent'], { wait: true })(ShowTransactionDetails)
);
@@ -0,0 +1,749 @@
/* eslint-disable react/jsx-no-bind */
import React, { Fragment, useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Slider from 'rc-slider';
import Tooltip from 'rc-tooltip';
import {
Alert,
Button,
Card,
CardBody,
CardTitle,
Col,
Form,
FormGroup,
Label,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
Row
} from 'reactstrap';
import {
licenses,
mediaTypes,
features,
addonFeatures
} from '../../../LoginRegister/Registration/PlanConstants';
import useForm from '../../../common/hooks/useForm';
import { debounce } from 'lodash';
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
import useIsMounted from '../../../common/hooks/useIsMounted';
import reduxConnect from '../../../../redux/utils/connect';
import {
getPlans,
updatePrice
} from '../../../../api/registration/registration';
import {
updatePlanHubspot,
updatePlanPayment
} from '../../../../api/plans/userPlans';
import { planRoutes } from './UserPlans';
import BillingDetailsForm from './BillingDetailsForm';
import simpleNumberLocalizer from 'react-widgets-simple-number';
import NumberPicker from 'react-widgets/lib/NumberPicker';
import LoadersAdvanced from '../../../common/Loader/Loader';
import { IoIosWarning } from 'react-icons/io';
import { convertUTCtoLocal, setDocumentData } from '../../../../common/helper';
import { translate } from 'react-i18next';
simpleNumberLocalizer();
const Handle = Slider.Handle;
const handle = (props) => {
// eslint-disable-next-line react/prop-types
const { value, dragging, index, ...restProps } = props;
return (
<Tooltip
key={index}
prefixCls="rc-slider-tooltip"
overlay={value}
visible={dragging}
placement="top"
>
<Handle value={value} {...restProps} />
</Tooltip>
);
};
const initialForm = {
savedFeeds: 0,
searchesPerDay: 0,
webFeeds: 0,
alerts: 0,
news: 0,
blog: 0,
reddit: 0,
instagram: 0,
twitter: 0,
analytics: 0,
subscriberAccounts: 0,
masterAccounts: 0
};
const initialPaymentForm = {
name: '',
line1: '',
line2: '',
city: '',
state: '',
postal_code: '',
country: '',
email: '',
phone: '',
errors: {
name: null,
line1: null,
city: null,
state: null,
postal_code: null,
country: null,
email: null,
phone: null
}
};
function UpdatePlan({ actions, restrictions, t }) {
const stripe = useStripe();
const elements = useElements();
const isMounted = useIsMounted();
// first step
const { form, handleChange, resetForm } = useForm(initialForm);
const [updatingPrice, setUpdatingPrice] = useState(true);
const [totalCost, setTotalCost] = useState(' - ');
const [modal, setModal] = useState(false);
const [loading, setLoading] = useState(false);
const [planLoading, setPlanLoading] = useState(true);
const [planError, setPlanError] = useState(false);
const [planList, setPlanList] = useState([]);
const [disableUpdate, setDisableUpdate] = useState(true);
// second step
const [nextStep, setNextStep] = useState(false);
const {
form: paymentForm,
handleChange: handlePaymentForm,
errors: paymentFormErrors,
handleValidation: handlePaymentValidation,
validateSubmit
} = useForm(initialPaymentForm);
const [paymentError, setPaymentError] = useState(false);
const [paymentLoading, setPaymentLoading] = useState(false);
// to update price when input changes
useEffect(() => {
if (planList.length > 0) {
debouncePrice(form);
}
}, [...Object.values(form)]);
const debouncePrice = useCallback(
debounce((form) => {
setUpdatingPrice(true);
updatePrice(form).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || isNaN(res.data.totalPrice)) {
actions.addAlert(res.data);
setUpdatingPrice(false);
setTotalCost('Error');
return;
}
setTotalCost(res.data.totalPrice);
setUpdatingPrice(false);
});
}, 1000),
[isMounted.current]
);
useEffect(() => {
if (!restrictions.isPlanCancelled && !restrictions.isPlanDowngrade) {
setDisableUpdate(false);
} else {
setDisableUpdate(true);
}
}, [restrictions.isPlanCancelled, restrictions.isPlanDowngrade]);
useEffect(() => {
getBillingPlans();
setDocumentData('title', 'Update Plan');
return () => setDocumentData('title'); // default
}, []);
function getBillingPlans() {
setPlanLoading(true);
setPlanError(false);
getPlans().then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data || !res.data.length) {
setPlanError(true);
setPlanLoading(false);
res.data && res.data.length > 0 && actions.addAlert(res.data);
return;
}
setPlanLoading(false);
setPlanList(res.data);
const modified = { ...initialForm };
let selectedPlan = {};
if (restrictions.plans.price > 0) {
selectedPlan = { ...restrictions.plans };
Object.entries(restrictions.limits).map(([key, value]) => {
selectedPlan[key] = value.limit;
});
selectedPlan.blog = selectedPlan.blogs;
delete selectedPlan.blogs;
} else {
selectedPlan = res.data[0];
}
Object.keys(initialForm).map((key) => {
modified[key] =
selectedPlan[key] === undefined
? modified[key]
: selectedPlan[key] === true
? 1
: selectedPlan[key] === false
? 0
: selectedPlan[key];
});
resetForm(modified);
});
}
function changePlan(id) {
const selectedPlan = planList.find((plan) => plan.id === id);
const modified = { ...initialForm };
Object.keys(initialForm).map((key) => {
modified[key] =
selectedPlan[key] === undefined
? modified[key]
: selectedPlan[key] === true
? 1
: selectedPlan[key] === false
? 0
: selectedPlan[key];
});
resetForm(modified);
}
function handleSubmit() {
if (restrictions.isPlanCancelled || restrictions.isPlanDowngrade) {
return;
}
// move to payment page if new basic user
// instruct according to upgrade and downgrade
// if card already stored then only update the plan by showing modal or providing option to change card
setLoading(true);
if (restrictions.isPaymentId) {
setModal(true); // show details of card
} else {
setNextStep(true);
window.scrollTo(0, 0);
}
setLoading(false);
}
function toggle() {
setModal((prev) => !prev);
}
function proceedToDetails() {
toggle();
setNextStep(true);
window.scrollTo(0, 0);
}
const submitPayment = async () => {
if (!stripe || !elements) {
// Stripe.js has not loaded yet.
return;
}
if (restrictions.isPlanCancelled || restrictions.isPlanDowngrade) {
return;
}
setPaymentError(false);
setPaymentLoading(true);
const obj = validateSubmit();
if (!obj) {
setPaymentLoading(false);
return actions.addAlert({
type: 'error',
transKey: 'requiredInfo'
});
}
const cardElement = elements.getElement(CardElement);
const {
name,
line1,
line2,
city,
state,
postal_code,
country,
email,
phone
} = obj;
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: {
name,
email,
phone,
address: {
line1: line1,
line2: line2,
city: city,
state: state,
postal_code: postal_code,
country: country
}
}
});
if (error) {
setPaymentError(error);
setPaymentLoading(false);
return;
}
const newObj = { ...form };
newObj.masterAccounts = '1';
newObj.paymentID = paymentMethod.id; //stripe card element ID
const res = await updatePlanPayment(newObj);
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setPaymentLoading(false);
return;
}
window.gtag &&
window.gtag('event', 'purchase', {
currency: 'USD',
value: totalCost
});
await updatePlanHubspot({ ...obj, ...form, totalCost });
actions.addAlert({ type: 'notice', transKey: 'planUpdated' });
// refresh page on success and move to active plan details
setTimeout(() => {
window.location.pathname = `/app/plans/${planRoutes.current}`;
}, 1000);
};
const proceedPayment = async () => {
// payment with old card
setLoading(true);
const newObj = { ...form };
newObj.masterAccounts = '1';
const res = await updatePlanPayment(newObj);
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setLoading(false);
return;
}
window.gtag &&
window.gtag('event', 'purchase', {
currency: 'USD',
value: totalCost
});
await updatePlanHubspot({ ...form, totalCost });
actions.addAlert({ type: 'notice', transKey: 'planUpdated' });
// refresh page on success and move to active plan details
setTimeout(() => {
window.location.pathname = `/app/plans/${planRoutes.current}`;
}, 1000);
};
function moveBack() {
window.scrollTo(0, 0);
setNextStep(false);
}
if (planError || planLoading) {
return (
<Col xs="12" lg="8" xl="9">
<Card className="h-75 mb-3">
<CardBody>
<CardTitle>{t('plans.updatePlan.heading')}</CardTitle>
{planError && (
<div className="text-danger text-center p-4">
<IoIosWarning
className="d-block mx-auto mb-2"
fontSize="32px"
/>
{t('plans.updatePlan.planLoadingFailed')}{' '}
<Button color="link" onClick={getBillingPlans} className="p-0">
{t('plans.updatePlan.tryAgainBtn')}
</Button>
</div>
)}
</CardBody>
{planLoading && <LoadersAdvanced />}
</Card>
</Col>
);
}
const isRTL = document.documentElement.dir === 'rtl';
return (
<Col xs="12" lg="8" xl="9">
<Card className="mb-3">
{!nextStep ? (
<CardBody>
<CardTitle>{t('plans.updatePlan.heading')}</CardTitle>
<p className="text-muted">
{t('plans.updatePlan.subText')}{' '}
<a
href="https://www.socialhose.io/en/pricing"
rel="noopener noreferrer"
target="_blank"
>
{t('plans.updatePlan.learnMoreBtn')}
</a>
.
</p>
<hr />
<Form>
<Row>
<Col md={12}>
<div className="mb-3">
<h6 className="font-weight-bold mb-3">
{t('plans.updatePlan.prePlans')}
</h6>
<div className="d-flex flex-wrap justify-content-center justify-content-md-start">
{planList.map((plan) => (
<Button
outline
key={plan.id}
color="primary"
type="button"
className="btn-wide btn-lg p-sm-3 mb-2 mr-2"
onClick={() => changePlan(plan.id)}
>
{plan.name}
</Button>
))}
</div>
</div>
<hr />
<div className="mb-3">
<h6 className="font-weight-bold mb-3">
{t('plans.updatePlan.mediaTypes')}
</h6>
<div>
{mediaTypes.map((type) => (
<Button
key={type.name}
size="lg"
type="button"
title={
form[type.name]
? 'Click to deselect'
: 'Click to select'
}
outline={!form[type.name]}
className="btn-pill mb-2 mr-2"
color={form[type.name] ? 'success' : 'light'}
onClick={() =>
handleChange(type.name, !form[type.name])
}
>
{t(`searchTab.sourceTypes.${type.transKey}`)} (
{type.price})
</Button>
))}
</div>
</div>
<hr />
<div className="mb-3">
<h6 className="font-weight-bold mb-3">
{t('plans.updatePlan.licenses')}
</h6>
<Row noGutters className="justify-content-center">
{licenses.map((license) => (
<Col sm={6} key={license.name}>
<div className="p-4 m-2 border b-radius-5 shadow-sm">
<FormGroup>
<div className="d-flex justify-content-between">
<Label title={license.title}>
{t(`plans.currentPlan.${license.transKey}`)}
</Label>
<span className="font-size-lg font-weight-bold text-primary">
{form[license.name]}
</span>
</div>
<Slider
{...license.props}
reverse={isRTL}
handle={handle}
value={form[license.name]}
onChange={(val) =>
handleChange(license.name, val)
}
/>
</FormGroup>
</div>
</Col>
))}
</Row>
</div>
<hr />
<Row>
<Col md="6">
<div className="mb-3">
<h6 className="font-weight-bold mb-3">
{t('plans.updatePlan.features')}
</h6>
<div>
{features.map((type) => (
<Button
key={type.name}
size="lg"
type="button"
title={
form[type.name]
? t('plans.updatePlan.deselectTooltip')
: t('plans.updatePlan.selectTooltip')
}
outline={!form[type.name]}
className="btn-pill mb-2 mr-2"
color={form[type.name] ? 'success' : 'light'}
onClick={() =>
handleChange(type.name, !form[type.name])
}
>
{t(`plans.currentPlan.${type.transKey}`)} (
{type.price})
</Button>
))}
<div className="pl-2">
{features.map((type) =>
form[type.name] ? (
<p
key={type.name}
className="font-size-sm text-muted mb-1"
>
{type.desc}
</p>
) : null
)}
</div>
</div>
</div>
</Col>
<Col md="6">
<div className="mb-3">
<h6 className="font-weight-bold mb-3">
{t('plans.updatePlan.addOns')}
</h6>
<Row className="px-3">
{addonFeatures.map((type) => (
<Col xs="12" key={type.name}>
<FormGroup>
<Label>
{t(`plans.currentPlan.${type.transKey}`)}
</Label>
<NumberPicker
{...type.props}
value={form[type.name]}
onChange={(val) =>
handleChange(type.name, val)
}
/>
</FormGroup>
</Col>
))}
</Row>
</div>
</Col>
</Row>
<div className="widget-content total-price">
<div className="widget-content-wrapper justify-content-start justify-content-md-end mr-5">
<div className="widget-content-left">
<div className="widget-heading">
{t('plans.updatePlan.totalCost')}
</div>
<div className="widget-subheading">
{t('plans.updatePlan.monthly')}
</div>
</div>
<div className="widget-content-right position-relative ml-0 ml-5">
{/* {updatingPrice && (
<div className="widget-numbers position-absolute text-secondary px-3">
<FontAwesomeIcon icon={faSpinner} pulse />
</div>
)} */}
<div
className={`widget-numbers text-warning ${
updatingPrice ? 'opacity-3' : ''
}`}
>
${totalCost}
</div>
</div>
</div>
</div>
</Col>
</Row>
<hr />
{restrictions.isPlanCancelled || restrictions.isPlanDowngrade ? (
<p className="text-danger mb-3">
{t('plans.updatePlan.cancelledWarning', {
text: restrictions.isPlanCancelled
? 'cancelled'
: 'downgraded'
})}{' '}
{restrictions.subStartDate && restrictions.subEndDate
? `(${convertUTCtoLocal(
restrictions.subStartDate,
'MMM D, YYYY'
)} - ${convertUTCtoLocal(
restrictions.subEndDate,
'MMM D, YYYY'
)})`
: ''}
</p>
) : (
''
)}
<div className="text-right">
<Button
type="button"
disabled={updatingPrice || loading || disableUpdate}
onClick={handleSubmit}
className="btn-wide"
color="primary"
size="lg"
>
{loading
? t('plans.updatePlan.continueBtnLoading')
: t('plans.updatePlan.continueBtn')}
</Button>
</div>
</Form>
</CardBody>
) : (
<CardBody>
<CardTitle>{t('plans.updatePlan.billingHeading')}</CardTitle>
<BillingDetailsForm
form={paymentForm}
errors={paymentFormErrors}
handleChange={handlePaymentForm}
handleValidation={handlePaymentValidation}
/>
<Row className="divider" />
{paymentError && (
<Alert color="danger">
<Fragment>
<p className="font-size-xs font-weight-bold text-uppercase">
{t('plans.updatePlan.error')}
</p>
{paymentError.message}
</Fragment>
</Alert>
)}
<div className="d-flex justify-content-between flex-column-reverse flex-sm-row">
<Button
type="button"
color="secondary"
size="lg"
disabled={paymentLoading}
onClick={moveBack}
>
{t('plans.updatePlan.back')}
</Button>
<Button
type="button"
color="primary"
onClick={submitPayment}
disabled={!stripe || !elements || paymentLoading}
className="btn-wide btn-hover-shine mb-2 mb-sm-0"
size="lg"
>
{paymentLoading
? t('plans.updatePlan.payLoading')
: t('plans.updatePlan.payBtn', { totalCost })}
</Button>
</div>
</CardBody>
)}
</Card>
<Modal isOpen={modal} toggle={toggle} backdrop="static">
<ModalHeader toggle={toggle}>
{t('plans.updatePlan.confirmationHeading')}
</ModalHeader>
<ModalBody>
<div>
{restrictions.plans && restrictions.plans.price > 0 ? (
restrictions.plans.price === totalCost ? null : restrictions.plans
.price < totalCost ? (
<p className="text-muted mb-3">
{t('plans.updatePlan.upgradeNotice')}
</p>
) : (
<p className="text-muted mb-3">
{t('plans.updatePlan.downgradeNotice')}
</p>
)
) : null}
<p>{t('plans.updatePlan.alreadyStoredCard')}</p>
</div>
</ModalBody>
<ModalFooter>
<Button color="link" onClick={proceedToDetails} disabled={loading}>
{t('plans.updatePlan.payWithOtherCardBtn')}
</Button>
<Button color="primary" disabled={loading} onClick={proceedPayment}>
{loading
? t('plans.updatePlan.payLoading')
: t('plans.updatePlan.payWithStoredCardBtn')}
</Button>
</ModalFooter>
</Modal>
</Col>
);
}
UpdatePlan.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object,
restrictions: PropTypes.object
};
export default reduxConnect('restrictions', [
'common',
'auth',
'user',
'restrictions'
])(translate(['tabsContent'], { wait: true })(UpdatePlan));
@@ -0,0 +1,72 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Button, Modal, ModalBody, ModalHeader } from 'reactstrap';
import { planRoutes } from './UserPlans';
import { Trans, translate } from 'react-i18next';
function UpgradePlanModal({ isModalOpen = false, toggle, t }) {
function toggleModal() {
return toggle();
}
return (
<Modal
isOpen={isModalOpen}
toggle={toggleModal}
modalClassName="zoom-modal"
backdrop="static"
>
<ModalHeader toggle={toggleModal} />
<ModalBody className="px-4 px-sm-5 pb-5">
<div className="text-center">
<div className="display-4 mb-2">
<i className="lnr-rocket text-primary"></i>
</div>
<h3 className="mb-3">{t('plans.upgradeModal.heading')}</h3>
<div className="mb-4">
<p className="text-muted">
<Trans i18nKey="plans.upgradeModal.text">
You have to upgrade your plan to get access of these features.
Take a look at our bite-sized
<strong>à la carte menu options</strong> with monthly billing.
</Trans>{' '}
<a
href="https://www.socialhose.io/en/pricing"
rel="noopener noreferrer"
target="_blank"
>
{t('plans.upgradeModal.learnMore')}
</a>
</p>
</div>
<div>
<Button
tag={Link}
to={`/app/plans/${planRoutes.update}`}
onClick={toggleModal}
className="btn-pill btn-wide d-block mx-auto"
color="success"
size="lg"
>
{t('plans.upgradeModal.upgradeNowBtn')}
</Button>
<Button color="link" size="sm" onClick={toggleModal}>
{t('plans.upgradeModal.maybeLaterBtn')}
</Button>
</div>
</div>
</ModalBody>
</Modal>
);
}
UpgradePlanModal.propTypes = {
isModalOpen: PropTypes.bool,
t: PropTypes.func.isRequired,
toggle: PropTypes.func
};
export default React.memo(
translate(['tabsContent'], { wait: true })(UpgradePlanModal)
);
@@ -0,0 +1,128 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import {
NavLink,
Redirect,
Route,
Switch,
useRouteMatch
} from 'react-router-dom';
import reduxConnect from '../../../../redux/utils/connect';
import ChangeCard from './ChangeCard';
import CurrentPlan from './CurrentPlan';
import UpdatePlan from './UpdatePlan';
import UserTransactions from './UserTransactions';
import { Card, CardBody, Col, Row } from 'reactstrap';
import { translate } from 'react-i18next';
export const planRoutes = {
current: 'current',
changeCard: 'change-card',
txn: 'transactions',
update: 'update'
};
function UserPlans({ actions, restrictions, t }) {
const match = useRouteMatch();
useEffect(() => {
const { setEnableClosedSidebar } = actions;
actions.getRestrictions();
setEnableClosedSidebar(true);
return () => setEnableClosedSidebar(false);
}, []);
return (
<Row>
<Col xs={12} lg={4} xl={3}>
<Card className="mb-3">
<CardBody className="navigation-vertical">
<ul className="navigation-ul">
<li className="navigation-item">
<NavLink
className="navigation-link"
activeClassName="active"
to={`${match.url}/${planRoutes.current}`}
>
<em>
<i className="font-size-lg lnr-file-empty"> </i>
</em>
<span>{t('plans.sidebar.activePlanDetails')}</span>
</NavLink>
</li>
{restrictions.isPaymentId && (
<li>
<NavLink
className="navigation-link"
activeClassName="active"
to={`${match.url}/${planRoutes.changeCard}`}
>
<em>
<i className="font-size-lg lnr-license"> </i>
</em>
<span>{t('plans.sidebar.changeCard')}</span>
</NavLink>
</li>
)}
<li>
<NavLink
className="navigation-link"
activeClassName="active"
to={`${match.url}/${planRoutes.update}`}
>
<em>
<i className="font-size-lg lnr-arrow-up-circle"> </i>
</em>
<span>{t('plans.sidebar.updatePlan')}</span>
</NavLink>
</li>
<li>
<NavLink
className="navigation-link"
activeClassName="active"
to={`${match.url}/${planRoutes.txn}`}
>
<em>
<i className="font-size-lg lnr-list"> </i>
</em>
<span>{t('plans.sidebar.yourTransactions')}</span>
</NavLink>
</li>
</ul>
</CardBody>
</Card>
</Col>
<Switch>
<Route path={`${match.url}/${planRoutes.current}`}>
<CurrentPlan />
</Route>
{restrictions.isPaymentId && (
<Route path={`${match.url}/${planRoutes.changeCard}`}>
<ChangeCard />
</Route>
)}
<Route path={`${match.url}/${planRoutes.txn}`}>
<UserTransactions />
</Route>
<Route path={`${match.url}/${planRoutes.update}`}>
<UpdatePlan />
</Route>
<Redirect to={`${match.url}/current`} />
</Switch>
</Row>
);
}
UserPlans.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired,
restrictions: PropTypes.object.isRequired
};
export default reduxConnect('restrictions', [
'common',
'auth',
'user',
'restrictions'
])(translate(['tabsContent'], { wait: true })(UserPlans));
@@ -0,0 +1,132 @@
/* eslint-disable react/jsx-no-bind */
/* eslint-disable react/prop-types */
import React, { useState, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { reduxActions } from '../../../../redux/utils/connect';
import {
convertUTCtoLocal,
getQueryParams,
setDocumentData
} from '../../../../common/helper';
import { getTransactions } from '../../../../api/plans/userPlans';
import Table from '../../../common/Table/Table';
import { Button, Col } from 'reactstrap';
import ShowTransactionDetails from './ShowTransactionDetails';
import moment from 'moment';
import { capitalize } from 'lodash';
import { translate } from 'react-i18next';
function UserTransactions(props) {
const [dataSource, setDataSource] = useState({ data: [] });
const [loading, setLoading] = useState(true);
const [selectedData, setSelectedData] = useState(false);
const { actions, t } = props;
useEffect(() => {
setDocumentData('title', 'User Transactions');
return () => setDocumentData('title'); // default
}, []);
const columns = [
{
id: 'activeDate',
Header: t('plans.transactions.activationDate'),
accessor: (d) => d.lines.data[0] && d.lines.data[0].period.start,
Cell: (props) => convertUTCtoLocal(moment.unix(props.value), 'MM/DD/YYYY')
},
{
id: 'expireDate',
Header: t('plans.transactions.expirationDate'),
accessor: (d) => d.lines.data[0] && d.lines.data[0].period.end,
Cell: (props) => convertUTCtoLocal(moment.unix(props.value), 'MM/DD/YYYY')
},
{
id: 'paid_at',
Header: t('plans.transactions.transactionDate'),
accessor: (d) => d.status_transitions.paid_at,
Cell: (props) =>
convertUTCtoLocal(moment.unix(props.value), 'MM/DD/YYYY HH:mm:ss')
},
{
Header: t('plans.transactions.amount'),
accessor: 'amount_paid',
Cell: (props) => (props.value ? `$${props.value / 100}` : '-')
},
{
Header: t('plans.transactions.status'),
accessor: 'status',
Cell: (props) => capitalize(props.value)
},
{
Header: t('plans.transactions.actions'),
accessor: 'id',
Cell: (props) => (
<Button
outline
className="border-0 btn-transition"
color="primary"
size="sm"
onClick={() => setSelectedData(props.original)}
>
{t('plans.transactions.more')}
</Button>
)
}
];
function closeModal() {
setSelectedData(false);
}
const getTransactionList = useCallback((page, pageSize) => {
setLoading(true);
const params = getQueryParams({ page, pageSize });
getTransactions(params).then((res) => {
if (res.error || !res.data || !res.data.success || !res.data.data) {
setDataSource({ data: [] }); // comment this line when API is ready
setLoading(false);
return actions.addAlert({
type: 'error',
transKey: 'somethingWrong'
});
}
// setDataSource(sampleData); // comment this line when API is ready
setDataSource({
data:
res.data.data.data && res.data.data.data.length > 0
? res.data.data.data
: []
});
setLoading(false);
});
}, []);
const { data = [], totalCount = 0, limit = 100, page = 1 } = dataSource;
return (
<Col xs="12" lg="8" xl="9">
<Table
cardTitle={t('plans.transactions.heading')}
columns={columns}
data={data}
totalCount={totalCount}
showTotalCount
limit={limit}
page={page}
isLoading={loading}
onFetchData={getTransactionList}
/>
<ShowTransactionDetails data={selectedData} closeModal={closeModal} />
</Col>
);
}
UserTransactions.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object
};
export default reduxActions()(
translate(['tabsContent'], { wait: true })(UserTransactions)
);