From 8f63547f34df9e469a906e31af1eb29dcd56ed93 Mon Sep 17 00:00:00 2001 From: Nuwan Date: Thu, 9 May 2024 18:42:55 +0530 Subject: [PATCH] affiliate program and payee pages --- .../components/affiliate/JKAffiliatePayee.js | 99 +++++++++++ .../affiliate/JKAffiliatePayeeAddress.js | 162 ++++++++++++++++++ .../affiliate/JKAffiliatePayeePaypal.js | 66 +++++++ .../affiliate/JKAffiliatePayeeTax.js | 71 ++++++++ .../affiliate/JKAffiliateProgram.js | 29 ++++ .../components/dashboard/JKDashboardMain.js | 5 + .../sessions/JKSessionsHistoryList.js | 2 - .../components/sessions/JKUseSessionHelper.js | 15 +- jam-ui/src/helpers/rest.js | 20 +++ jam-ui/src/i18n/config.js | 10 +- jam-ui/src/i18n/locales/en/affiliate.json | 59 +++++++ jam-ui/src/i18n/locales/es/affiliate.json | 11 ++ jam-ui/src/routes.js | 17 +- ruby/lib/jam_ruby/models/music_session.rb | 2 +- 14 files changed, 559 insertions(+), 9 deletions(-) create mode 100644 jam-ui/src/components/affiliate/JKAffiliatePayee.js create mode 100644 jam-ui/src/components/affiliate/JKAffiliatePayeeAddress.js create mode 100644 jam-ui/src/components/affiliate/JKAffiliatePayeePaypal.js create mode 100644 jam-ui/src/components/affiliate/JKAffiliatePayeeTax.js create mode 100644 jam-ui/src/components/affiliate/JKAffiliateProgram.js create mode 100644 jam-ui/src/i18n/locales/en/affiliate.json create mode 100644 jam-ui/src/i18n/locales/es/affiliate.json diff --git a/jam-ui/src/components/affiliate/JKAffiliatePayee.js b/jam-ui/src/components/affiliate/JKAffiliatePayee.js new file mode 100644 index 000000000..0c6380e0e --- /dev/null +++ b/jam-ui/src/components/affiliate/JKAffiliatePayee.js @@ -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 ( + + + + {!isMounted || loading ?

Loading...

: notAffiliate ? ( + + +

{t('payee.not_affiliate')}

+ Learn how you can earn cash simply by telling your friends and followers about JamKazam. + +
+ ) : ( + + + + + + +
+ + + + + )} + + + ); +}; + +export default JKAffiliatePayee; diff --git a/jam-ui/src/components/affiliate/JKAffiliatePayeeAddress.js b/jam-ui/src/components/affiliate/JKAffiliatePayeeAddress.js new file mode 100644 index 000000000..90dce5abb --- /dev/null +++ b/jam-ui/src/components/affiliate/JKAffiliatePayeeAddress.js @@ -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 ( + + +
{t('payee.address.title')}
+
+ + + {t('payee.address.help_text')} + +
+ + + ( + + )} + /> + {errors.address1 && ( +
+ {errors.address1.message} +
+ )} +
+ + + ( + + )} + /> + {errors.address2 && ( +
+ {errors.address2.message} +
+ )} +
+ + + ( + + )} + /> + {errors.city && ( +
+ {errors.city.message} +
+ )} +
+ + + ( + + )} + /> + {errors.state && ( +
+ {errors.state.message} +
+ )} +
+ + + ( + + )} + /> + {errors.state && ( +
+ {errors.postal_code.message} +
+ )} +
+
+ + + { submitting && } + +
+
+
+
+ ); +}; + +export default JKAffiliatePayeeAddress; diff --git a/jam-ui/src/components/affiliate/JKAffiliatePayeePaypal.js b/jam-ui/src/components/affiliate/JKAffiliatePayeePaypal.js new file mode 100644 index 000000000..8bba3d2df --- /dev/null +++ b/jam-ui/src/components/affiliate/JKAffiliatePayeePaypal.js @@ -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 ( + + +
{t('payee.paypal.title')}
+
+ + {t('payee.paypal.help_text')} +
+ + + } + /> + {errors.paypal_id && ( +
+ {errors.paypal_id.message} +
+ )} +
+
+ + + { submitting && } + +
+
+
+
+ ); +}; + +export default JKAffiliatePayeePaypal; diff --git a/jam-ui/src/components/affiliate/JKAffiliatePayeeTax.js b/jam-ui/src/components/affiliate/JKAffiliatePayeeTax.js new file mode 100644 index 000000000..74b9176d0 --- /dev/null +++ b/jam-ui/src/components/affiliate/JKAffiliatePayeeTax.js @@ -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 ( + + +
{t('payee.tax.title')}
+
+ + {t('payee.tax.help_text')} +
+ + + } + /> + {errors.tax_identifier && ( +
+ {errors.tax_identifier.message} +
+ )} +
+
+ + {submitting && } +
+
+
+
+ ); +}; + +export default JKAffiliatePayeeTax; diff --git a/jam-ui/src/components/affiliate/JKAffiliateProgram.js b/jam-ui/src/components/affiliate/JKAffiliateProgram.js new file mode 100644 index 000000000..1d99d1a06 --- /dev/null +++ b/jam-ui/src/components/affiliate/JKAffiliateProgram.js @@ -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 ( + + + +
+

{t('program.paragraph1')}

+

{t('program.paragraph2')}

+

{t('program.paragraph3')}

+

+ {t('program.paragraph4-1')} + {t('program.click-here')} + {t('program.paragraph4-2')} + {t('program.click-here')} + {t('program.paragraph4-3')} +

+
+
+
+ ); +}; + +export default JKAffiliateProgram; diff --git a/jam-ui/src/components/dashboard/JKDashboardMain.js b/jam-ui/src/components/dashboard/JKDashboardMain.js index 8293dbd93..dce14d428 100644 --- a/jam-ui/src/components/dashboard/JKDashboardMain.js +++ b/jam-ui/src/components/dashboard/JKDashboardMain.js @@ -41,6 +41,9 @@ import JKEditAccount from '../page/JKEditAccount'; import JKAccountSubscription from '../page/JKAccountSubscription'; import JKPaymentHistory from '../page/JKPaymentHistory'; +import JKAffiliateProgram from '../affiliate/JKAffiliateProgram'; +import JKAffiliatePayee from '../affiliate/JKAffiliatePayee'; + //import loadable from '@loadable/component'; //const DashboardRoutes = loadable(() => import('../../layouts/JKDashboardRoutes')); @@ -269,6 +272,8 @@ function JKDashboardMain() { + + {/*Redirect*/} diff --git a/jam-ui/src/components/sessions/JKSessionsHistoryList.js b/jam-ui/src/components/sessions/JKSessionsHistoryList.js index 7e8d18a8e..847c93b36 100644 --- a/jam-ui/src/components/sessions/JKSessionsHistoryList.js +++ b/jam-ui/src/components/sessions/JKSessionsHistoryList.js @@ -1,12 +1,10 @@ import React from 'react'; import { groupByKey } from '../../helpers/utils'; import { Table } from 'reactstrap'; -import { useResponsive } from '@farfetch/react-context-responsive'; import JKSessionsHistoryItem from './JKSessionsHistoryItem'; import { useTranslation } from 'react-i18next'; const JKSessionsHistoryList = ({ sessions }) => { - const { greaterThan } = useResponsive(); const sessionsById = groupByKey(sessions, 'session_id'); const { t } = useTranslation(); diff --git a/jam-ui/src/components/sessions/JKUseSessionHelper.js b/jam-ui/src/components/sessions/JKUseSessionHelper.js index 63a7103f1..d78beaa07 100644 --- a/jam-ui/src/components/sessions/JKUseSessionHelper.js +++ b/jam-ui/src/components/sessions/JKUseSessionHelper.js @@ -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 { - sessionDescription: sessionDescription(session) + sessionDescription: sessionDescription(session), + sessionDateTime: sessionDateTime(session) }; }; diff --git a/jam-ui/src/helpers/rest.js b/jam-ui/src/helpers/rest.js index 8b2d718ce..65b80dbdb 100644 --- a/jam-ui/src/helpers/rest.js +++ b/jam-ui/src/helpers/rest.js @@ -327,4 +327,24 @@ export const getInvoiceHistory = (options = {}) => { .then(response => resolve(response)) .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)) + }); } \ No newline at end of file diff --git a/jam-ui/src/i18n/config.js b/jam-ui/src/i18n/config.js index 52c9a7c26..fbd0ddf05 100644 --- a/jam-ui/src/i18n/config.js +++ b/jam-ui/src/i18n/config.js @@ -10,6 +10,7 @@ import sessTranslationsEN from './locales/en/sessions.json' import unsubscribeTranslationsEN from './locales/en/unsubscribe.json' import profileEN from './locales/en/profile.json' import accountEN from './locales/en/account.json' +import affiliateEN from './locales/en/affiliate.json' import commonTranslationsES from './locales/es/common.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 profileES from './locales/es/profile.json' import accountES from './locales/es/account.json' +import affiliateES from './locales/es/affiliate.json' i18n.use(initReactI18next).init({ fallbackLng: 'en', lng: 'en', resources: { en: { - //translations: require('./locales/en/translations.json') common: commonTranslationsEN, home: homeTranslationsEN, people: peopleTranslationsEN, @@ -35,10 +36,10 @@ i18n.use(initReactI18next).init({ unsubscribe: unsubscribeTranslationsEN, profile: profileEN, account: accountEN, - friends: friendsTranslationsEN + friends: friendsTranslationsEN, + affiliate: affiliateEN }, es: { - //translations: require('./locales/es/translations.json') common: commonTranslationsES, home: homeTranslationsES, people: peopleTranslationsES, @@ -47,7 +48,8 @@ i18n.use(initReactI18next).init({ unsubscribe: unsubscribeTranslationsES, profile: profileES, account: accountES, - friends: friendsTranslationsES + friends: friendsTranslationsES, + affiliate: affiliateES } }, //ns: ['translations'], diff --git a/jam-ui/src/i18n/locales/en/affiliate.json b/jam-ui/src/i18n/locales/en/affiliate.json new file mode 100644 index 000000000..93510ebcb --- /dev/null +++ b/jam-ui/src/i18n/locales/en/affiliate.json @@ -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 don’t know JamKazam exists. Now you can let your audience know about JamKazam – something they’ll 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" + } +} \ No newline at end of file diff --git a/jam-ui/src/i18n/locales/es/affiliate.json b/jam-ui/src/i18n/locales/es/affiliate.json new file mode 100644 index 000000000..05b3a091d --- /dev/null +++ b/jam-ui/src/i18n/locales/es/affiliate.json @@ -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 don’t know JamKazam exists. Now you can let your audience know about JamKazam – something they’ll 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." + } +} \ No newline at end of file diff --git a/jam-ui/src/routes.js b/jam-ui/src/routes.js index 0c1404d20..841c59f7a 100644 --- a/jam-ui/src/routes.js +++ b/jam-ui/src/routes.js @@ -59,7 +59,21 @@ export const accountRoutes = { { to: '/account/subscription', name: 'Subscription'}, { to: '/account/payments', name: 'Payment History'}, { 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, profileRoute, accountRoutes, + affiliateRoutes, helpRoutes, //legacyRoute, //homeRoutes, diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 9baf5175f..96b8b0c82 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -582,7 +582,7 @@ module JamRuby ON 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 def self.scheduled user, only_public = false