affiliate program and payee pages

This commit is contained in:
Nuwan 2024-05-09 18:42:55 +05:30
parent 97e0a8d36a
commit 8f63547f34
14 changed files with 559 additions and 9 deletions

View File

@ -0,0 +1,99 @@
import React, { useEffect, useCallback, useState } from 'react';
import { Link } from 'react-router-dom';
import { Card, CardBody, Row, Col } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import JKAffiliatePayeeAddress from './JKAffiliatePayeeAddress';
import JKAffiliatePayeePaypal from './JKAffiliatePayeePaypal';
import JKAffiliatePayeeTax from './JKAffiliatePayeeTax';
import { useAuth } from '../../context/UserAuth';
import { getAffiliatePartnerData } from '../../helpers/rest';
import { useIsMounted } from '../../hooks/useIsMounted';
import { postAffiliatePartnerData } from '../../helpers/rest';
import { toast } from 'react-toastify';
const JKAffiliatePayee = () => {
const { t } = useTranslation('affiliate');
const { currentUser } = useAuth();
const [affiliateUser, setAffiliateUser] = useState(null);
const [notAffiliate, setNotAffiliate] = useState(false);
const [loading, setLoading] = useState(false);
const isMounted = useIsMounted();
const [submitting, setSubmitting] = useState(false);
const fetchAffiliatePartnerData = useCallback(async userId => {
try {
setLoading(true);
const response = await getAffiliatePartnerData(userId);
const affiliate = await response.json();
setAffiliateUser(affiliate.account);
} catch (error) {
console.error('Error fetching affiliate partner data:', error);
if (error && error.status === 400) {
// If the affiliate partner data is not found, set the affiliate user to null
setAffiliateUser(null);
setNotAffiliate(true);
}
}finally {
setLoading(false);
}
});
const onSubmit = data => {
//post
setSubmitting(true);
const params = { ...affiliateUser, ...data};
setAffiliateUser(params);
postAffiliatePartnerData(currentUser.id, params).then(response => {
if(response.ok) {
// Show success message
console.log('Affiliate partner data updated successfully');
toast.success(t('payee.save_success'));
}
}).catch(error => {
console.error('Error updating affiliate partner data:', error);
toast.error(t('payee.save_error'));
}).finally(() => {
setSubmitting(false);
});
};
useEffect(() => {
// Fetch affiliate payee data
if (currentUser) {
fetchAffiliatePartnerData(currentUser.id);
}
}, [currentUser]);
return (
<Card>
<FalconCardHeader title={t('payee.page_title')} titleClass="font-weight-bold" />
<CardBody className="pt-3" style={{ backgroundColor: '#edf2f9' }}>
{!isMounted || loading ? <p>Loading...</p> : notAffiliate ? (
<Row>
<Col>
<p>{t('payee.not_affiliate')}</p>
<Link to="/affiliate/program">Learn how you can earn cash simply by telling your friends and followers about JamKazam.</Link>
</Col>
</Row>
) : (
<Row>
<Col className='mb-2 col-12 col-md-6 col-lg-4'>
<JKAffiliatePayeeAddress affiliateUser={affiliateUser} onSubmit={onSubmit} submitting={submitting} />
</Col>
<Col>
<JKAffiliatePayeePaypal affiliateUser={affiliateUser} onSubmit={onSubmit} submitting={submitting} />
<div className='mb-2' />
<JKAffiliatePayeeTax affiliateUser={affiliateUser} onSubmit={onSubmit} submitting={submitting} />
</Col>
<Col className='d-none d-lg-block' />
</Row>
)}
</CardBody>
</Card>
);
};
export default JKAffiliatePayee;

View File

@ -0,0 +1,162 @@
import React, { useEffect } from 'react';
import { Card, CardBody, CardHeader, Form, FormGroup, Label, Input } from 'reactstrap';
import { useTranslation } from 'react-i18next';
import { useForm, Controller } from 'react-hook-form';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
const JKAffiliatePayeeAddress = ({ affiliateUser, onSubmit, submitting }) => {
const { t } = useTranslation('affiliate');
const {
handleSubmit,
control,
formState: { errors },
setError,
setValue
} = useForm({
defaultValues: {
address1: '',
address2: '',
city: '',
state: '',
postal_code: '',
country: ''
}
});
useEffect(() => {
setValue('address1', affiliateUser?.address?.address1 || '');
setValue('address2', affiliateUser?.address?.address2 || '');
setValue('city', affiliateUser?.address?.city || '');
setValue('state', affiliateUser?.address?.state || '');
setValue('postal_code', affiliateUser?.address?.postal_code || '');
setValue('country', affiliateUser?.address?.country || '');
}, [affiliateUser]);
const onSubmitAddress = data => {
const params = { "address": data };
onSubmit(params);
}
return (
<Card>
<CardHeader>
<h5>{t('payee.address.title')}</h5>
</CardHeader>
<CardBody className="bg-light" style={{ minHeight: 300 }}>
<small>
{t('payee.address.help_text')}
</small>
<Form noValidate className="mt-3" onSubmit={handleSubmit(onSubmitAddress)}>
<FormGroup>
<Label for="address1">{t('payee.address.form.address1')}</Label>
<Controller
name="address1"
control={control}
render={({ field }) => (
<Input
{...field}
type="text"
className="form-control"
id="address1"
/>
)}
/>
{errors.address1 && (
<div className="text-danger">
<small>{errors.address1.message}</small>
</div>
)}
</FormGroup>
<FormGroup>
<Label for="address2">{t('payee.address.form.address2')}</Label>
<Controller
name="address2"
control={control}
render={({ field }) => (
<Input
{...field}
type="text"
className="form-control"
id="address2"
/>
)}
/>
{errors.address2 && (
<div className="text-danger">
<small>{errors.address2.message}</small>
</div>
)}
</FormGroup>
<FormGroup>
<Label for="city">{t('payee.address.form.city')}</Label>
<Controller
name="city"
control={control}
render={({ field }) => (
<Input
{...field}
type="text"
className="form-control"
id="city"
/>
)}
/>
{errors.city && (
<div className="text-danger">
<small>{errors.city.message}</small>
</div>
)}
</FormGroup>
<FormGroup>
<Label for="state">{t('payee.address.form.state')}</Label>
<Controller
name="state"
control={control}
render={({ field }) => (
<Input
{...field}
type="text"
className="form-control"
id="state"
/>
)}
/>
{errors.state && (
<div className="text-danger">
<small>{errors.state.message}</small>
</div>
)}
</FormGroup>
<FormGroup>
<Label for="postal_code">{t('payee.address.form.zip')}</Label>
<Controller
name="postal_code"
control={control}
render={({ field }) => (
<Input
{...field}
type="text"
className="form-control"
id="postal_code"
/>
)}
/>
{errors.state && (
<div className="text-danger">
<small>{errors.postal_code.message}</small>
</div>
)}
</FormGroup>
<div className='d-flex align-content-center justify-content-end'>
<input type="submit" formNoValidate className="btn btn-primary" value={t('payee.address.form.submit')} disabled={submitting} data-testid="address_submit" />
<span className='ml-2'>
{ submitting && <FontAwesomeIcon icon="spinner" />}
</span>
</div>
</Form>
</CardBody>
</Card>
);
};
export default JKAffiliatePayeeAddress;

View File

@ -0,0 +1,66 @@
import React, { useEffect } from 'react';
import { Card, CardBody, CardHeader, Form, FormGroup, Label, Input } from 'reactstrap';
import { useTranslation } from 'react-i18next';
import { useForm, Controller } from 'react-hook-form';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
const JKAffiliatePayeePaypal = ({ affiliateUser, onSubmit, submitting }) => {
const { t } = useTranslation('affiliate');
const {
handleSubmit,
control,
formState: { errors },
setError,
setValue
} = useForm({
defaultValues: {
paypal_id: ''
}
});
useEffect(() => {
setValue('paypal_id', affiliateUser?.paypal_id || '');
}, [affiliateUser]);
const onSubmitPaypal = data => {
const params = { "paypal_id": data.paypal_id };
onSubmit(params);
}
return (
<Card>
<CardHeader>
<h5>{t('payee.paypal.title')}</h5>
</CardHeader>
<CardBody className="bg-light" style={{ minHeight: 200 }}>
<small>{t('payee.paypal.help_text')}</small>
<Form noValidate className="mt-3" onSubmit={handleSubmit(onSubmitPaypal)}>
<FormGroup>
<Label for="paypal_id">{t('payee.paypal.form.paypal_id')}</Label>
<Controller
name="paypal_id"
control={control}
rules={{
required: t('payee.paypal.form.validations.paypal_id.required')
}}
render={({ field }) => <Input {...field} type="text" className="form-control" id="paypal_id" />}
/>
{errors.paypal_id && (
<div className="text-danger">
<small>{errors.paypal_id.message}</small>
</div>
)}
</FormGroup>
<div className='d-flex align-content-center justify-content-end'>
<input type="submit" formNoValidate className="btn btn-primary" value={t('payee.paypal.form.submit')} disabled={submitting} data-testid="paypal_submit" />
<span className='ml-2'>
{ submitting && <FontAwesomeIcon icon="spinner" />}
</span>
</div>
</Form>
</CardBody>
</Card>
);
};
export default JKAffiliatePayeePaypal;

View File

@ -0,0 +1,71 @@
import React, { useEffect } from 'react';
import { Card, CardBody, CardHeader, Form, FormGroup, Label, Input } from 'reactstrap';
import { useTranslation } from 'react-i18next';
import { useForm, Controller } from 'react-hook-form';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
const JKAffiliatePayeeTax = ({ affiliateUser, onSubmit, submitting }) => {
const { t } = useTranslation('affiliate');
const {
handleSubmit,
control,
formState: { errors },
setError,
setValue
} = useForm({
defaultValues: {
tax_identifier: ''
}
});
useEffect(() => {
setValue('tax_identifier', affiliateUser?.tax_identifier || '');
}, [affiliateUser]);
const onSubmitTaxId = data => {
const params = { "tax_identifier": data.tax_identifier };
onSubmit(params);
};
return (
<Card>
<CardHeader>
<h5>{t('payee.tax.title')}</h5>
</CardHeader>
<CardBody className="bg-light" style={{ minHeight: 200 }}>
<small>{t('payee.tax.help_text')}</small>
<Form noValidate className="mt-3" onSubmit={handleSubmit(onSubmitTaxId)}>
<FormGroup>
<Label for="tax_identifier">{t('payee.tax.form.tax_id')}</Label>
<Controller
name="tax_identifier"
control={control}
rules={{
required: t('payee.tax.form.validations.tax_id.required')
}}
render={({ field }) => <Input {...field} type="text" className="form-control" id="tax_identifier" />}
/>
{errors.tax_identifier && (
<div className="text-danger">
<small>{errors.tax_identifier.message}</small>
</div>
)}
</FormGroup>
<div className="d-flex align-content-center justify-content-end">
<input
type="submit"
formNoValidate
className="btn btn-primary"
value={t('payee.tax.form.submit')}
disabled={submitting}
data-testid="tax_submit"
/>
<span className="ml-2">{submitting && <FontAwesomeIcon icon="spinner" />}</span>
</div>
</Form>
</CardBody>
</Card>
);
};
export default JKAffiliatePayeeTax;

View File

@ -0,0 +1,29 @@
import React from 'react';
import { Card, CardBody } from 'reactstrap';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
const JKAffiliateProgram = () => {
const { t } = useTranslation('affiliate');
return (
<Card style={{ width: "75%"}} className='mx-auto'>
<FalconCardHeader title={t('program.page_title')} titleClass="font-weight-semi-bold" />
<CardBody className="pt-3">
<div>
<p>{t('program.paragraph1')}</p>
<p>{t('program.paragraph2')}</p>
<p>{t('program.paragraph3')}</p>
<p>
{t('program.paragraph4-1')}
<a href=""><strong>{t('program.click-here')}</strong></a>
{t('program.paragraph4-2')}
<a href=""><strong>{t('program.click-here')}</strong></a>
{t('program.paragraph4-3')}
</p>
</div>
</CardBody>
</Card>
);
};
export default JKAffiliateProgram;

View File

@ -41,6 +41,9 @@ import JKEditAccount from '../page/JKEditAccount';
import JKAccountSubscription from '../page/JKAccountSubscription'; import JKAccountSubscription from '../page/JKAccountSubscription';
import JKPaymentHistory from '../page/JKPaymentHistory'; import JKPaymentHistory from '../page/JKPaymentHistory';
import JKAffiliateProgram from '../affiliate/JKAffiliateProgram';
import JKAffiliatePayee from '../affiliate/JKAffiliatePayee';
//import loadable from '@loadable/component'; //import loadable from '@loadable/component';
//const DashboardRoutes = loadable(() => import('../../layouts/JKDashboardRoutes')); //const DashboardRoutes = loadable(() => import('../../layouts/JKDashboardRoutes'));
@ -269,6 +272,8 @@ function JKDashboardMain() {
<PrivateRoute path="/account/identity" component={JKEditAccount} /> <PrivateRoute path="/account/identity" component={JKEditAccount} />
<PrivateRoute path="/account/subscription" component={JKAccountSubscription} /> <PrivateRoute path="/account/subscription" component={JKAccountSubscription} />
<PrivateRoute path="/account/payments" component={JKPaymentHistory} /> <PrivateRoute path="/account/payments" component={JKPaymentHistory} />
<PrivateRoute path="/affiliate/program" component={JKAffiliateProgram} />
<PrivateRoute path="/affiliate/payee" component={JKAffiliatePayee} />
{/*Redirect*/} {/*Redirect*/}
<Redirect to="/errors/404" /> <Redirect to="/errors/404" />
</Switch> </Switch>

View File

@ -1,12 +1,10 @@
import React from 'react'; import React from 'react';
import { groupByKey } from '../../helpers/utils'; import { groupByKey } from '../../helpers/utils';
import { Table } from 'reactstrap'; import { Table } from 'reactstrap';
import { useResponsive } from '@farfetch/react-context-responsive';
import JKSessionsHistoryItem from './JKSessionsHistoryItem'; import JKSessionsHistoryItem from './JKSessionsHistoryItem';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const JKSessionsHistoryList = ({ sessions }) => { const JKSessionsHistoryList = ({ sessions }) => {
const { greaterThan } = useResponsive();
const sessionsById = groupByKey(sessions, 'session_id'); const sessionsById = groupByKey(sessions, 'session_id');
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -15,8 +15,21 @@ const useSessionHelper = (session) => {
} }
}; };
const sessionDateTime = session => {
const date = new Date(session.created_at);
const d = new Date(date);
return d.toLocaleDateString('en-us', {
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
timeZoneName: 'short'
});
};
return { return {
sessionDescription: sessionDescription(session) sessionDescription: sessionDescription(session),
sessionDateTime: sessionDateTime(session)
}; };
}; };

View File

@ -328,3 +328,23 @@ export const getInvoiceHistory = (options = {}) => {
.catch(error => reject(error)) .catch(error => reject(error))
}); });
} }
export const getAffiliatePartnerData = (userId) => {
return new Promise((resolve, reject) => {
apiFetch(`/users/${userId}/affiliate_partner`)
.then(response => resolve(response))
.catch(error => reject(error))
});
}
export const postAffiliatePartnerData = (userId, params) => {
return new Promise((resolve, reject) => {
apiFetch(`/users/${userId}/affiliate_partner`, {
method: 'POST',
body: JSON.stringify(params)
})
.then(response => resolve(response))
.catch(error => reject(error))
});
}

View File

@ -10,6 +10,7 @@ import sessTranslationsEN from './locales/en/sessions.json'
import unsubscribeTranslationsEN from './locales/en/unsubscribe.json' import unsubscribeTranslationsEN from './locales/en/unsubscribe.json'
import profileEN from './locales/en/profile.json' import profileEN from './locales/en/profile.json'
import accountEN from './locales/en/account.json' import accountEN from './locales/en/account.json'
import affiliateEN from './locales/en/affiliate.json'
import commonTranslationsES from './locales/es/common.json' import commonTranslationsES from './locales/es/common.json'
import homeTranslationsES from './locales/es/home.json' import homeTranslationsES from './locales/es/home.json'
@ -20,13 +21,13 @@ import sessTranslationsES from './locales/es/sessions.json'
import unsubscribeTranslationsES from './locales/es/unsubscribe.json' import unsubscribeTranslationsES from './locales/es/unsubscribe.json'
import profileES from './locales/es/profile.json' import profileES from './locales/es/profile.json'
import accountES from './locales/es/account.json' import accountES from './locales/es/account.json'
import affiliateES from './locales/es/affiliate.json'
i18n.use(initReactI18next).init({ i18n.use(initReactI18next).init({
fallbackLng: 'en', fallbackLng: 'en',
lng: 'en', lng: 'en',
resources: { resources: {
en: { en: {
//translations: require('./locales/en/translations.json')
common: commonTranslationsEN, common: commonTranslationsEN,
home: homeTranslationsEN, home: homeTranslationsEN,
people: peopleTranslationsEN, people: peopleTranslationsEN,
@ -35,10 +36,10 @@ i18n.use(initReactI18next).init({
unsubscribe: unsubscribeTranslationsEN, unsubscribe: unsubscribeTranslationsEN,
profile: profileEN, profile: profileEN,
account: accountEN, account: accountEN,
friends: friendsTranslationsEN friends: friendsTranslationsEN,
affiliate: affiliateEN
}, },
es: { es: {
//translations: require('./locales/es/translations.json')
common: commonTranslationsES, common: commonTranslationsES,
home: homeTranslationsES, home: homeTranslationsES,
people: peopleTranslationsES, people: peopleTranslationsES,
@ -47,7 +48,8 @@ i18n.use(initReactI18next).init({
unsubscribe: unsubscribeTranslationsES, unsubscribe: unsubscribeTranslationsES,
profile: profileES, profile: profileES,
account: accountES, account: accountES,
friends: friendsTranslationsES friends: friendsTranslationsES,
affiliate: affiliateES
} }
}, },
//ns: ['translations'], //ns: ['translations'],

View File

@ -0,0 +1,59 @@
{
"program": {
"page_title": "Program Information",
"paragraph1": "Do you have an audience of musician followers you can reach via YouTube, Instagram, TikTok, Twitter, your website, or an email list?",
"paragraph2": "Most of the musicians in the world dont know JamKazam exists. Now you can let your audience know about JamKazam something theyll appreciate learning about from you while generating recurring affiliate income from JamKazam, all without selling anything.",
"paragraph3": "Musicians you refer to JamKazam can play together free on our platform. If they decide after using our free services to upgrade to a premium subscription (e.g. $10/month gold subscriptions) or purchase premium JamTracks (mostly $3/unit), JamKazam pays you 30% of each purchase for the first 3 years from signup for each user.",
"paragraph4-1": "To get started, ",
"click-here": "click here",
"paragraph4-2": " to review details and agree to the JamKazam Affiliate Agreement. Then ",
"paragraph4-3": " to get instructions on how to share links to JamKazam with your affiliate code. Anyone that signs up using a link you shared will have their account tagged with your affiliate code, and JamKazam will pay you for purchases on all your tagged accounts (see Affiliate Agreement for details). There are reporting features where you can see visits, signups, purchases, and your earnings by month."
},
"payee": {
"page_title": "Payee",
"not_affiliate": "You are not currently a JamKazam affiliate.",
"learn_to_earn": "Learn how you can earn cash simply by telling your friends and followers about JamKazam.",
"address": {
"title": "Address",
"help_text": "If you are a US resident, you must provide your home mailing address for JamKazam to process affiliate payments, per US tax regulations. Please enter your address information below.",
"form": {
"address1": "Street Address 1",
"address2": "Street Address 2",
"city": "City",
"state": "State",
"zip": "Zip Code",
"submit": "Save",
"validations": {
"address1": "Street Address 1 is required",
"city": "City is required",
"state": "State is required",
"zip_code": "Zip Code is required"
}
}
},
"paypal": {
"title": "PayPal",
"help_text": "JamKazam makes affiliate payments via PayPal. Please enter your PayPal.Me account information below - e.g. the yourname text in the following PayPal URL: paypal.me/yourname",
"form": {
"paypal_id": "PayPal Account",
"submit": "Save",
"validations": {
"paypal": "PayPal.Me Account is required"
}
}
},
"tax": {
"title": "Tax",
"help_text": "If you are a US resident, you must provide your tax ID for JamKazam to process affiliate payments, per US tax regulations. Please enter your tax ID information below. If you are not a US resident, you may leave this field blank.",
"form": {
"tax_id": "Tax ID",
"submit": "Save",
"validations": {
"paypal": "Tax ID is required"
}
}
},
"save_success": "Payee account was saved successfully",
"save_error": "Error saving payee account details"
}
}

View File

@ -0,0 +1,11 @@
{
"program": {
"page_title": "Program Information",
"paragraph1": "Do you have an audience of musician followers you can reach via YouTube, Instagram, TikTok, Twitter, your website, or an email list?",
"paragraph2": "Most of the musicians in the world dont know JamKazam exists. Now you can let your audience know about JamKazam something theyll appreciate learning about from you while generating recurring affiliate income from JamKazam, all without selling anything.",
"paragraph3": "Musicians you refer to JamKazam can play together free on our platform. If they decide after using our free services to upgrade to a premium subscription (e.g. $10/month gold subscriptions) or purchase premium JamTracks (mostly $3/unit), JamKazam pays you 30% of each purchase for the first 3 years from signup for each user.",
"paragraph4-1": "To get started,",
"click-here": "click here",
"paragraph4-2": "to review details and agree to the JamKazam Affiliate Agreement. Then click here to get instructions on how to share links to JamKazam with your affiliate code. Anyone that signs up using a link you shared will have their account tagged with your affiliate code, and JamKazam will pay you for purchases on all your tagged accounts (see Affiliate Agreement for details). There are reporting features where you can see visits, signups, purchases, and your earnings by month."
}
}

View File

@ -59,7 +59,21 @@ export const accountRoutes = {
{ to: '/account/subscription', name: 'Subscription'}, { to: '/account/subscription', name: 'Subscription'},
{ to: '/account/payments', name: 'Payment History'}, { to: '/account/payments', name: 'Payment History'},
{ to: '/account/payment-method', name: 'Payment Method'}, { to: '/account/payment-method', name: 'Payment Method'},
{ to: '/account/affiliate', name: 'Affiliate Program'}, ]
}
export const affiliateRoutes = {
name: 'Affiliate',
to: '/affiliate',
exact: true,
icon: 'dollar-sign',
children: [
{ to: '/affiliate/program', name: 'Program'},
{ to: '/affiliate/payee', name: 'Payee'},
{ to: '/affiliate/links', name: 'Links'},
{ to: '/affiliate/signups', name: 'Signups'},
{ to: '/affiliate/earnings', name: 'Earnings'},
{ to: '/affiliate/agreement', name: 'Agreement'}
] ]
} }
@ -406,6 +420,7 @@ export default [
jamTrackRoutes, jamTrackRoutes,
profileRoute, profileRoute,
accountRoutes, accountRoutes,
affiliateRoutes,
helpRoutes, helpRoutes,
//legacyRoute, //legacyRoute,
//homeRoutes, //homeRoutes,

View File

@ -582,7 +582,7 @@ module JamRuby
ON ON
music_sessions_user_history.user_id = users.id music_sessions_user_history.user_id = users.id
} }
).order('music_sessions.created_at DESC').select("music_sessions.id AS session_id, music_sessions_user_history.id AS session_history_id, music_sessions.created_at, music_sessions.name, music_sessions.description, music_sessions_user_history.instruments, users.first_name, users.last_name, users.photo_url, users.id AS user_id").offset(offset).limit(limit) ).order('music_sessions.created_at DESC').select("music_sessions.id AS session_id, music_sessions_user_history.id AS session_history_id, music_sessions.created_at, music_sessions.name, music_sessions.description, music_sessions.musician_access, music_sessions.approval_required, music_sessions_user_history.instruments, users.first_name, users.last_name, users.photo_url, users.id AS user_id").offset(offset).limit(limit)
end end
def self.scheduled user, only_public = false def self.scheduled user, only_public = false