account subscription

let user to change subscription
This commit is contained in:
Nuwan 2024-04-05 10:52:06 +05:30
parent ee95d07dfc
commit dbb5c4a520
12 changed files with 603 additions and 4 deletions

View File

@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
const JKModalDialog = (args) => {
const { show, title, children, onToggle, showFooter, ...rest } = args;
const { show, title, children, onToggle, showFooter, modalFooterButtons, ...rest } = args;
const [modal, setModal] = useState(show);
const toggle = () => {
@ -24,7 +24,16 @@ const JKModalDialog = (args) => {
<ModalBody>{children}</ModalBody>
{showFooter && (
<ModalFooter>
<Button onClick={toggle}>{t('close', { ns: 'common' })}</Button>
{modalFooterButtons ? (
<>
{modalFooterButtons.map((button, index) => (
<Button key={index} onClick={button.onClick} color={button.color} disabled={button.disabled}>
{button.text}
</Button>
))}
</>
) : (<Button onClick={toggle}>{t('close', { ns: 'common' })}</Button>) }
</ModalFooter>
)}
</Modal>

View File

@ -36,6 +36,7 @@ import JKMusicSessionsLobby from '../page/JKMusicSessionsLobby';
import JKEditProfile from '../page/JKEditProfile';
import JKEditAccount from '../page/JKEditAccount';
import JKAccountSubscription from '../page/JKAccountSubscription';
//import loadable from '@loadable/component';
@ -261,6 +262,7 @@ function JKDashboardMain() {
<PrivateRoute path="/notifications" component={JKNotifications} />
<PrivateRoute path="/profile" component={JKEditProfile} />
<PrivateRoute path="/account/identity" component={JKEditAccount} />
<PrivateRoute path="/account/subscription" component={JKAccountSubscription} />
{/*Redirect*/}
<Redirect to="/errors/404" />
</Switch>

View File

@ -0,0 +1,57 @@
import React, { useState, useEffect } from 'react';
import { Row, Col, Card, CardBody } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import JKSubscriptionPlan from '../profile/JKSubscriptionPlan';
import JKSubscriptionPlaytime from '../profile/JKSubscriptionPlaytime';
import { getSubscription } from '../../helpers/rest';
const JKAccountSubscription = () => {
const { t } = useTranslation('account');
const [userPlan, setUserPlan] = useState(null);
useEffect(() => {
getSubscription()
.then(resp => resp.json())
.then(data => {
setUserPlan(data);
})
.catch(error => {
console.log('subscriptionError', error);
});
}, []);
const getDisplayName = planCode => {
if (planCode == '') {
planCode = null;
}
const plan = window.gon.global.subscription_codes.find(plan => plan.id === planCode);
if (plan) {
return plan.name;
}
return `Unknown plan code=${planCode}`;
};
return (
<Card>
<FalconCardHeader title={t('subscription.page_title')} titleClass="font-weight-bold" />
<CardBody className="pt-3" style={{ backgroundColor: '#edf2f9' }}>
{userPlan ? (
<Row>
<Col className="mb-2">
<JKSubscriptionPlan userPlan={userPlan} setUserPlan={setUserPlan} getDisplayName={getDisplayName} />
</Col>
<Col>
<JKSubscriptionPlaytime userPlan={userPlan} getDisplayName={getDisplayName} />
</Col>
<Col />
</Row>
) : 'Loading...' }
</CardBody>
</Card>
);
};
export default JKAccountSubscription;

View File

@ -20,7 +20,7 @@ const JKEditAccount = () => {
<FalconCardHeader title={t('identity.page_title')} titleClass="font-weight-bold" />
<CardBody className="pt-3" style={{ backgroundColor: '#edf2f9' }}>
<Row>
<Col>
<Col className='mb-2'>
<JKEditEmail setAlert={setAlertText} toggleAlert={toggleAlert} />
</Col>
<Col>

View File

@ -93,7 +93,7 @@ const JKEditPassword = ({ setAlert, toggleAlert }) => {
<CardHeader>
<h5>Password</h5>
</CardHeader>
<CardBody className="bg-light" style={{ minHeight: 300 }}>
<CardBody className="bg-light" style={{ minHeight: 300, minWidth: 280 }}>
<small>
{t('identity.password_form.help_text')}
</small>

View File

@ -0,0 +1,230 @@
import React, { useState, useEffect } from 'react';
import { Card, CardHeader, CardBody, Form, FormGroup, Label } from 'reactstrap';
import Select from 'react-select';
import { useTranslation } from 'react-i18next';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useForm, Controller, set } from 'react-hook-form';
import { toast } from 'react-toastify';
import JKModalDialog from '../common/JKModalDialog';
import { getSubscription, changeSubscription } from '../../helpers/rest';
function JKSubscriptionPlan({ userPlan, setUserPlan, getDisplayName }) {
const { t } = useTranslation('account');
const [plans, setPlans] = useState([]);
const [submitting, setSubmitting] = useState(false);
const [alertText, setAlertText] = useState('');
const [showAlert, setShowAlert] = useState(false);
const toggleAlert = () => setShowAlert(!showAlert);
const {
handleSubmit,
control,
formState: { errors },
setError,
setValue,
getValues
} = useForm({
defaultValues: {
plan_code: ''
}
});
useEffect(() => {
if (userPlan) {
setValue('plan_code', userPlan.desired_plan_code);
}
}, [userPlan]);
useEffect(() => {
if (window.gon) {
const monthly = [{ value: 'monthly', isDisabled: true, label: '-------- MONTHLY PLANS --------' }];
const yearly = [{ value: 'yearly', isDisabled: true, label: '-------- YEARLY PLANS --------' }];
window.gon.global.subscription_codes.forEach(plan => {
if (plan.cycle === 'month') {
monthly.push({ value: plan.id || '', label: `${plan.name} (${plan.price.toFixed(2)}/${plan.cycle})` });
}
if (plan.cycle === 'year') {
yearly.push({ value: plan.id || '', label: `${plan.name} (${plan.price.toFixed(2)}/${plan.cycle})` });
}
});
const all = [...monthly, ...yearly];
setPlans(all);
}
}, [window.gon]);
const handleChange = selectedOption => {
console.log('selectedOption', selectedOption);
setError('plan_code', { type: 'manual', message: '' });
setValue('plan_code', selectedOption.value);
};
const getDisplayNamePrice = planCode => {
if (planCode == '') {
planCode = null;
}
const plan = window.gon.global.subscription_codes.find(plan => plan.id === planCode);
if (plan) {
return plan.price;
}
return `Unknown plan code=${planCode}`;
};
const getDisplayCycle = planCode => {
if (planCode == '') {
planCode = null;
}
for (const subscriptionCode of window.gon.global.subscription_codes) {
if (planCode === subscriptionCode.id) {
if (subscriptionCode.cycle === 'year') {
return 'annual';
} else {
return subscriptionCode.cycle + 'ly';
}
}
}
return `Unknown plan code=${planCode}`;
};
const onSubmit = () => {
const planCode = getValues('plan_code');
if (planCode === null) {
setError('plan_code', {
type: 'manual',
message: t('subscription.current_plan.validations.subscription_plan.required')
});
setSubmitting(false);
return;
} else if (planCode === '') {
setAlertText(t('subscription.alerts.changed_to_free_plan'));
toggleAlert();
} else {
setAlertText(
`You have selected the ${getDisplayName(planCode).toUpperCase()} ${getDisplayCycle(
planCode
).toUpperCase()} PLAN and will be charged US$${getDisplayNamePrice(planCode)}.`
);
toggleAlert();
}
};
const updateSubscription = () => {
toggleAlert();
setSubmitting(true);
const planCode = getValues('plan_code');
changeSubscription(planCode)
.then(resp => {
console.log('changeSubscription', resp);
if (resp.ok) {
getSubscription()
.then(resp => resp.json())
.then(data => {
console.log('subscriptionData', data);
setUserPlan(data);
setValue('plan_code', data.desired_plan_code);
toast.success(t('subscription.alerts.changed_plan_successfully'));
})
.catch(error => {
console.log('subscriptionError', error);
});
}
})
.catch(error => {
setAlertText(`${t('subscription.alerts.failed_to_change_plan')}: ${error}`);
console.log('changeSubscriptionError', error);
setValue('plan_code', userPlan.desired_plan_code);
})
.finally(() => {
setSubmitting(false);
});
};
const cancelUpdateSubscription = () => {
setValue('plan_code', userPlan.desired_plan_code);
setSubmitting(false);
toggleAlert();
};
return (
<>
<Card>
<CardHeader>
<h5>{t('subscription.current_plan.title')}</h5>
</CardHeader>
<CardBody className="bg-light" style={{ minHeight: 300, minWidth: 280 }}>
<small>{t('subscription.current_plan.help_text')}</small>
<Form
className="mt-2"
onSubmit={handleSubmit(onSubmit)}
data-testid="edit_email_form"
>
{plans && userPlan && userPlan.plan_code && (
<>
<FormGroup>
<Label for="plan_code">{t('subscription.current_plan.subscription_plan')}</Label>
<Controller
name="plan_code"
control={control}
render={({ field: { onChange, value } }) => {
const plan = plans.find(plan => plan.value === value);
if (!plan) {
return <Select data-testid="countrySelect" onChange={handleChange} options={plans} />;
}
return (
<Select data-testid="countrySelect" value={plan} onChange={handleChange} options={plans} />
);
}}
/>
{errors.plan_code && <div className="text-danger">{errors.plan_code.message}</div>}
</FormGroup>
<div className="d-flex justify-content-end">
<input
type="submit"
className="btn btn-primary"
value={t('subscription.current_plan.submit')}
disabled={submitting || [userPlan.desired_plan_code].includes(getValues('plan_code'))}
data-testid="email_submit"
/>
<span className="ml-2">{submitting && <FontAwesomeIcon icon="spinner" />}</span>
</div>
<div className='mt-3'>
<small>
{ t('subscription.current_plan.notice.part1') }<a target='_blank' href="https://jamkazam.freshdesk.com/support/solutions/articles/66000122535-what-are-">{ t('subscription.current_plan.notice.click_here') }</a>{ t('subscription.current_plan.notice.part2') }
</small>
</div>
</>
)}
</Form>
</CardBody>
</Card>
<JKModalDialog
show={showAlert}
onToggle={toggleAlert}
title={t('subscription.alerts.title')}
data-testid="subscription-plan-update-notification"
size="md"
modalFooterButtons={[
{
text: t('cancel', { ns: 'common' }),
color: 'secondary',
onClick: cancelUpdateSubscription,
disabled: submitting
},
{
text: t('ok', { ns: 'common' }),
color: 'primary',
onClick: updateSubscription,
disabled: submitting
}
]}
>
{alertText}
</JKModalDialog>
</>
);
}
export default JKSubscriptionPlan;

View File

@ -0,0 +1,204 @@
import React, { useEffect, useState, useRef } from 'react';
import { Card, CardHeader, CardBody, Label } from 'reactstrap';
import { useTranslation } from 'react-i18next';
import { formatDateShort } from '../../helpers/utils';
import { get } from 'react-hook-form';
function JKSubscriptionPlaytime({ userPlan, getDisplayName }) {
const { t } = useTranslation('account');
const [explanation, setExplanation] = useState('');
const [warning, setWarning] = useState('');
const [billingAddendum, setBillingAddendum] = useState(''); // [TODO: addendum to the warning message about billing, if needed]
const showPaymentInfoRef = useRef(false);
const displayTime = until_time => {
if (until_time < 0) return 'no time';
const untilTime = getTimeRemaining(until_time * 1000);
let timeString = '';
if (untilTime.days !== 0) timeString += `${untilTime.days} days, `;
if (untilTime.hours !== 0 || timeString.length > 0) timeString += `${untilTime.hours} hours, `;
if (untilTime.minutes !== 0 || timeString.length > 0) timeString += `${untilTime.minutes} minutes `;
if (timeString === '') timeString = 'now!';
return timeString;
};
const getTimeRemaining = t => {
if (t < 0) t = -t;
const seconds = Math.floor((t / 1000) % 60);
const minutes = Math.floor((t / 1000 / 60) % 60);
const hours = Math.floor((t / (1000 * 60 * 60)) % 24);
const days = Math.floor(t / (1000 * 60 * 60 * 24));
return {
total: t,
days: days,
hours: hours,
minutes: minutes,
seconds: seconds
};
};
const getDisplayNamePrice = planCode => {
if (planCode == '') {
planCode = null;
}
const plan = window.gon.global.subscription_codes.find(plan => plan.id === planCode);
if (plan) {
return plan.price;
}
return `Unknown plan code=${planCode}`;
};
const getDisplayCycle = planCode => {
if (planCode == '') {
planCode = null;
}
for (const subscriptionCode of window.gon.global.subscription_codes) {
if (planCode === subscriptionCode.id) {
if (subscriptionCode.cycle === 'year') {
return 'annual';
} else {
return subscriptionCode.cycle + 'ly';
}
}
}
return `Unknown plan code=${planCode}`;
};
const planNameWithCycle = planCode => {
return getDisplayName(planCode) + ' (' + getDisplayCycle(planCode) + ')';
};
useEffect(() => {
if (userPlan) {
let expl,
note,
warning,
billingAddendumTxt,
playtimeTxt = '';
const adminOverride = userPlan.admin_override_plan_code;
const inTrail = userPlan.in_trail;
const hasBillingInfo = userPlan.has_billing_info;
const effectivePlanName = planNameWithCycle(userPlan.plan_code);
const desiredPlanCode = userPlan.desired_plan_code;
const desiredPlanName = planNameWithCycle(desiredPlanCode);
const hasPendingSubscription = userPlan.subscription.pending_subscription;
const canceledSubscription = userPlan.subscription.remaining_billing_cycles === 0;
if (adminOverride) {
expl = `You have a <strong>${effectivePlanName}</strong> account until your gifted plan ends ${formatDateShort(
userPlan.admin_override_ends_at
)}.`;
} else if (inTrail) {
if (desiredPlanCode) {
if (hasBillingInfo) {
note = `Billing starts for the <strong>${desiredPlanName}</strong> plan after the trial ends.`;
} else {
warning = `You will drop to the <strong>free plan</strong> after the trial ends because you have not yet entered payment info.`;
showPaymentInfoRef.current = true;
}
} else {
if (hasBillingInfo) {
warning = `You will drop to the <strong>free plan</strong> after the trial ends because you have not selected a plan.`;
} else {
warning = `You will drop to the <strong>free plan</strong> after the trial ends because you have not yet entered payment info or selected a plan.`;
showPaymentInfoRef.current = true;
}
}
expl = `You have a <strong>${effectivePlanName}</strong> account until your trial ends <strong>${formatDateShort(
userPlan.trial_ends_at
)}</strong>. ${note}`;
} else {
// NOT admin override and NOT in trial
if (desiredPlanCode && !userPlan.plan_code && !hasBillingInfo) {
expl = `You have successfully upgraded your plan to the <strong>${desiredPlanName}</strong> level, thank you!`;
warning = `For this plan to take effect, you must provide a payment method (e.g. a credit card), for the monthly subscription charge. Please click the Update Payment Method button to do this now.`;
//show_payment_info = true
showPaymentInfoRef.current = true;
} else {
if (desiredPlanCode) {
if (!hasBillingInfo) {
//show_payment_info = true
showPaymentInfoRef.current = true;
expl = `You have successfully upgraded your plan to the ${desiredPlanName} level, thank you`;
warning = `However, you must provide a payment method (e.g. a credit card), for the monthly subscription charge. Please click the Update Payment Method button to do this now.`;
} else {
expl = `You are currently on the <strong>${effectivePlanName}</strong> level, thank you!`;
}
} else {
//free plan situation - not much to go on about
expl = `You are currently on the <strong>${effectivePlanName}</strong> plan.`;
}
}
}
setExplanation(expl);
setWarning(warning);
//billingAddendum
if (hasPendingSubscription) {
if (userPlan.subscription.plan.plan_code !== userPlan.plan_code) {
billingAddendumTxt = ` You have paid only for the <strong>${planNameWithCycle(
userPlan.subscription.plan.plan_code
)}</strong> level for the current billing cycle, so there will be a change to the <strong>${this.planNameWithCycle(
this.props.subscription.subscription.pending_subscription.plan.plan_code
)}</strong> level on the next billing cycle.`;
} else {
billingAddendumTxt = ` And your plan and billing will switch to the <strong>${planNameWithCycle(
userPlan.subscription.pending_subscription.plan.plan_code
)}</strong> level on the next billing cycle.`;
}
} else if (canceledSubscription && userPlan.desired_plan_code === null && userPlan.plan_code !== null) {
billingAddendumTxt = ` However, your cancelled <strong>${effectivePlanName}</strong> plan is still active until the end of the billing cycle. You will be billed a final time at the <strong>${planNameWithCycle(
userPlan.subscription.plan.plan_code
)}</strong> at end of this billing cycle.`;
} else {
billingAddendumTxt = '';
}
setBillingAddendum(billingAddendumTxt);
}
}, [userPlan]);
return (
<Card>
<CardHeader>
<h5>{t('subscription.play_time.title')}</h5>
</CardHeader>
<CardBody className="bg-light" style={{ minHeight: 300, minWidth: 280 }}>
<div>
{userPlan && userPlan.subscription_rules.remaining_month_play_time ? (
<div className="play-time">
<p>
You have <strong>{planNameWithCycle(userPlan.subscription_rules.remaining_month_play_time)}</strong> remaining
this month. Only the time you spend in a session with 2 or more people uses your session play time.
</p>
</div>
) : (
<div className="play-time">
<p>You have unlimited play time.</p>
</div>
)}
</div>
<div className="mt-3">
<small>
{explanation && (
<div className="alert alert-info">
<span dangerouslySetInnerHTML={{ __html: explanation }} />
<span dangerouslySetInnerHTML={{ __html: billingAddendum }} />
</div>
)}
{warning && <div className="alert alert-warning" dangerouslySetInnerHTML={{ __html: warning }} />}
</small>
</div>
</CardBody>
</Card>
);
}
export default JKSubscriptionPlaytime;

View File

@ -1,3 +1,4 @@
import { error } from 'is_js';
import apiFetch from './apiFetch';
export const getMusicians = page => {
@ -277,4 +278,26 @@ export const postUserAppInteraction = (userId, options) => {
.then(response => resolve(response))
.catch(error => reject(error));
});
}
export const getSubscription = () => {
return new Promise((resolve, reject) => {
apiFetch('/recurly/get_subscription')
.then(response => resolve(response))
.catch(error => reject(error))
});
}
export const changeSubscription = (plan_code) => {
const options = {plan_code}
return new Promise((resolve, reject) => {
apiFetch('/recurly/change_subscription', {
method: 'POST',
body: JSON.stringify(options)
})
.then(response => resolve(response))
.catch(error => reject(error));
})
}

View File

@ -234,4 +234,16 @@ export const currencyFormat = (num) => {
return new Intl.NumberFormat('en-US',
{ style: 'currency', currency: 'USD' }
).format(num);
}
const days = new Array("Sun", "Mon", "Tue",
"Wed", "Thu", "Fri", "Sat");
const months = new Array("January", "February", "March",
"April", "May", "June", "July", "August", "September",
"October", "November", "December");
export const formatDateShort = (dateString) => {
const date = dateString instanceof Date ? dateString : new Date(dateString);
return months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
}

View File

@ -54,5 +54,35 @@
}
},
"subscription": {
"page_title": "Subscription",
"current_plan": {
"title": "Current Plan",
"help_text": "Your JamKazam subscription plan is currently set to the plan displayed below. To change your plan, click the subscription plan box below, select a new plan from the list, and then click the Save Plan button.",
"subscription_plan": "Subscription Plan",
"validations": {
"subscription_plan": {
"required": "Subscription plan is required"
}
},
"submit": "Save Plan",
"notice": {
"part1": "To compare the features available for different subscription plans ",
"click_here": "click here",
"part2": " to view a help article on our available plans."
}
},
"play_time": {
"title": "Play Time",
"description": ""
},
"alerts": {
"title": "Subscription Plan Update",
"changed_to_free_plan": "You have chosen to go back down to the FREE PLAN. Your subscription will be canceled, and you will keep your plan until the end of the current billing cycle.",
"failed_to_change_plan": "Failed to update subscription plan. Please try again later. Please contact support@jamkazam.com if you continue to have problems.",
"changed_plan_successfully": "You have successfully updated your subscription plan."
}
}
}

View File

@ -6,6 +6,8 @@
"actions": "Actions",
"no_records": "No Records!",
"close": "Close",
"cancel": "Cancel",
"ok": "OK",
"navigation": {
"home": "Home",
"friends": "Friends",

View File

@ -54,5 +54,35 @@
}
},
"subscription": {
"page_title": "Suscripción",
"current_plan": {
"title": "plan actual",
"help_text": "Your JamKazam subscription plan is currently set to the plan displayed below. To change your plan, click the subscription plan box below, select a new plan from the list, and then click the Save Plan button.",
"subscription_plan": "Subscription Plan",
"validations": {
"subscription_plan": {
"required": "Subscription plan is required"
}
},
"submit": "Save Plan",
"notice": {
"part1": "To compare the features available for different subscription plans ",
"click_here": "click here",
"part2": " to view a help article on our available plans."
}
},
"play_time": {
"title": "Tiempo de juego",
"description": ""
},
"alerts": {
"title": "Subscription Plan Update",
"changed_to_free_plan": "You have chosen to go back down to the FREE PLAN. Your subscription will be canceled, and you will keep your plan until the end of the current billing cycle.",
"failed_to_change_plan": "Failed to update subscription plan. Please try again later. Please contact support@jamkazam.com if you continue to have problems.",
"changed_plan_successfully": "You have successfully updated your subscription plan."
}
}
}