Set up referral system!
This generates unique referral codes for people, and keeps track (by email ID) of who referred whom.
This commit is contained in:
parent
8820f58eeb
commit
d9785d601e
3 changed files with 200 additions and 13 deletions
192
server/index.js
192
server/index.js
|
@ -188,7 +188,9 @@ router.use(bodyParser.urlencoded({
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// main views
|
// main views
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
|
// the home view is used in a couple of places, so we've moved it out
|
||||||
|
let homeView = async (req, res) => {
|
||||||
if (DEBUG) console.debug('Returning home page')
|
if (DEBUG) console.debug('Returning home page')
|
||||||
|
|
||||||
// count people
|
// count people
|
||||||
|
@ -206,6 +208,25 @@ router.get('/', async (req, res) => {
|
||||||
).models
|
).models
|
||||||
|
|
||||||
if (DEBUG) console.log(`Listing ${recentPledges.length} pledges`)
|
if (DEBUG) console.log(`Listing ${recentPledges.length} pledges`)
|
||||||
|
if (DEBUG && req.params.referralCode) console.log(`Referral code: ${req.params.referralCode}`)
|
||||||
|
|
||||||
|
let referrer
|
||||||
|
if (req.params.referralCode) {
|
||||||
|
try {
|
||||||
|
let pledge = await (Pledge
|
||||||
|
.query({
|
||||||
|
where: {
|
||||||
|
referral_code: req.params.referralCode,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fetch())
|
||||||
|
|
||||||
|
referrer = pledge.get('anonymous') ? 'Anonymous' : pledge.get('name')
|
||||||
|
} catch(e) {
|
||||||
|
if (DEBUG) console.debug(`Skipping referral ${req.params.referralCode} because not found: ${e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
twing.render('index.htm.twig', {
|
twing.render('index.htm.twig', {
|
||||||
'goal_rupees': Number(goalRupees).toLocaleString('en-IN'),
|
'goal_rupees': Number(goalRupees).toLocaleString('en-IN'),
|
||||||
|
@ -215,13 +236,117 @@ router.get('/', async (req, res) => {
|
||||||
'percent_rupees': `style="width: ${total_rupees/goalRupees*100}%"`,
|
'percent_rupees': `style="width: ${total_rupees/goalRupees*100}%"`,
|
||||||
'percent_people': `style="width: ${total_people/goalPeople*100}%"`,
|
'percent_people': `style="width: ${total_people/goalPeople*100}%"`,
|
||||||
'recent_pledges': recentPledges,
|
'recent_pledges': recentPledges,
|
||||||
|
'referral_code': req.params.referralCode || null, // if it's there
|
||||||
|
'referrer': referrer,
|
||||||
}).then((output) => {
|
}).then((output) => {
|
||||||
res.end(output)
|
res.end(output)
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
|
router.get('/', homeView)
|
||||||
|
router.get('/r/:referralCode', homeView)
|
||||||
|
|
||||||
|
// helpers to generate and decode referrals
|
||||||
|
function randomCode() {
|
||||||
|
let chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||||
|
|
||||||
|
let code = ''
|
||||||
|
|
||||||
|
for (let i=0; i<10; i++) {
|
||||||
|
code += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||||
|
}
|
||||||
|
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
async function findReferralCode(email) {
|
||||||
|
let referral_code
|
||||||
|
|
||||||
|
// fetch existing referral code, if exists
|
||||||
|
try {
|
||||||
|
let pledge = await (Pledge
|
||||||
|
.query({
|
||||||
|
where: {
|
||||||
|
email: email,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fetch())
|
||||||
|
|
||||||
|
referral_code = pledge.get('referral_code')
|
||||||
|
if (DEBUG) console.log(`Using existing referral code: ${referral_code}`)
|
||||||
|
} catch(e) {
|
||||||
|
if (e.message == 'EmptyResponse' && DEBUG) {
|
||||||
|
console.log('No matching referrals found')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!referral_code) {
|
||||||
|
// no code found; create a new one and ensure uniqueness
|
||||||
|
console.log(`Generating new referral code for: ${email}`)
|
||||||
|
async function checkUniqueness(referral_code) {
|
||||||
|
let a = await UnverifiedPledge.query({where: {referral_code: referral_code}}).count()
|
||||||
|
let b = await Pledge.query({where: {referral_code: referral_code}}).count()
|
||||||
|
|
||||||
|
return a == 0 && b == 0
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
referral_code = randomCode()
|
||||||
|
} while (!checkUniqueness(referral_code))
|
||||||
|
}
|
||||||
|
|
||||||
|
return referral_code
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findReferrer(code) {
|
||||||
|
let email
|
||||||
|
|
||||||
|
// first, check for an entry with this referral code
|
||||||
|
try {
|
||||||
|
let pledge = await (Pledge
|
||||||
|
.query({
|
||||||
|
where: {
|
||||||
|
referral_code: code,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fetch())
|
||||||
|
|
||||||
|
email = pledge.get('email')
|
||||||
|
console.log(`Found: ${email}`)
|
||||||
|
} catch(e) {
|
||||||
|
if (e.message == 'EmptyResponse' && DEBUG) {
|
||||||
|
console.log('No matching referrals found')
|
||||||
|
} else {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// next, check for an entry with this email
|
||||||
|
// (maybe it's already been converted from code to email)
|
||||||
|
try {
|
||||||
|
let pledge = await (Pledge
|
||||||
|
.query({
|
||||||
|
where: {
|
||||||
|
email: code,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fetch())
|
||||||
|
|
||||||
|
email = pledge.get('email')
|
||||||
|
console.log('it seems to be')
|
||||||
|
} catch(e) {
|
||||||
|
if (e.message == 'EmptyResponse' && DEBUG) {
|
||||||
|
console.debug('No matching emails found either')
|
||||||
|
} else {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return email
|
||||||
|
}
|
||||||
|
|
||||||
// function to validate pledges before saving
|
// function to validate pledges before saving
|
||||||
function validatePledge(body, PledgeModel = Pledge) {
|
async function validatePledge(body, PledgeModel = Pledge) {
|
||||||
// errors get saved here
|
// errors get saved here
|
||||||
let errors = []
|
let errors = []
|
||||||
|
|
||||||
|
@ -268,6 +393,19 @@ function validatePledge(body, PledgeModel = Pledge) {
|
||||||
let overseas = body.overseas == 'yes' ? true : false
|
let overseas = body.overseas == 'yes' ? true : false
|
||||||
let other_message = body.other_message
|
let other_message = body.other_message
|
||||||
|
|
||||||
|
// manage referrals
|
||||||
|
let referral_code = await findReferralCode(email)
|
||||||
|
let referrer = null
|
||||||
|
|
||||||
|
console.log('ref co', body.referral_code)
|
||||||
|
if (body.referral_code) {
|
||||||
|
try {
|
||||||
|
referrer = await findReferrer(body.referral_code)
|
||||||
|
} catch (e) {
|
||||||
|
console.debug(`Error loading referrer: ${e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// enter the info
|
// enter the info
|
||||||
let pledge = new PledgeModel() // may be Pledge or UnverifiedPledge
|
let pledge = new PledgeModel() // may be Pledge or UnverifiedPledge
|
||||||
pledge.set('was_robot', was_robot)
|
pledge.set('was_robot', was_robot)
|
||||||
|
@ -281,6 +419,9 @@ function validatePledge(body, PledgeModel = Pledge) {
|
||||||
pledge.set('get_newsletter', get_newsletter)
|
pledge.set('get_newsletter', get_newsletter)
|
||||||
pledge.set('other_message', other_message)
|
pledge.set('other_message', other_message)
|
||||||
|
|
||||||
|
pledge.set('referral_code', referral_code)
|
||||||
|
pledge.set('referrer', referrer)
|
||||||
|
|
||||||
// return it all!
|
// return it all!
|
||||||
return {
|
return {
|
||||||
pledge: pledge,
|
pledge: pledge,
|
||||||
|
@ -298,7 +439,7 @@ router.post('/pledge', async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// process the pledge with our handy function
|
// process the pledge with our handy function
|
||||||
let {pledge, errors} = validatePledge(req.body, UnverifiedPledge)
|
let {pledge, errors} = await validatePledge(req.body, UnverifiedPledge)
|
||||||
|
|
||||||
// fail if there were errors
|
// fail if there were errors
|
||||||
if (!!errors.length) {
|
if (!!errors.length) {
|
||||||
|
@ -454,6 +595,32 @@ router.get('/verify', async (req, res) => {
|
||||||
newPledge.set('get_newsletter', pledge.get('get_newsletter'))
|
newPledge.set('get_newsletter', pledge.get('get_newsletter'))
|
||||||
newPledge.set('other_message', pledge.get('other_message'))
|
newPledge.set('other_message', pledge.get('other_message'))
|
||||||
|
|
||||||
|
newPledge.set('referral_code', await findReferralCode(pledge.get('email')))
|
||||||
|
|
||||||
|
// set referrer in priority: old pledge > new pledge
|
||||||
|
try {
|
||||||
|
let oldPledge = await (Pledge
|
||||||
|
.query({
|
||||||
|
where: {
|
||||||
|
email: req.query.email,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fetch())
|
||||||
|
|
||||||
|
newPledge.set('referrer',
|
||||||
|
oldPledge.get('referrer') || pledge.get('referrer') || null)
|
||||||
|
} catch(e) {
|
||||||
|
if (e.message == 'EmptyResponse' && DEBUG) {
|
||||||
|
console.log('No matching referrals found')
|
||||||
|
} else {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
newPledge.set('referrer', pledge.get('referrer') || null)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Saved referrer: ${newPledge.get('referrer')}`)
|
||||||
|
|
||||||
// destroy previous pledges by that user
|
// destroy previous pledges by that user
|
||||||
try {
|
try {
|
||||||
await (Pledge
|
await (Pledge
|
||||||
|
@ -471,6 +638,7 @@ router.get('/verify', async (req, res) => {
|
||||||
await newPledge.save()
|
await newPledge.save()
|
||||||
|
|
||||||
// send the confirmation email
|
// send the confirmation email
|
||||||
|
let referralLink = `${req.protocol}://${req.hostname}/?ref=${newPledge.get('referral_code')}`
|
||||||
let text = `Hi ${pledge.get('name')},
|
let text = `Hi ${pledge.get('name')},
|
||||||
Your pledge of ₹${Number(pledge.get('amount')).toLocaleString('en-IN')} to Snipette Analog has been saved!
|
Your pledge of ₹${Number(pledge.get('amount')).toLocaleString('en-IN')} to Snipette Analog has been saved!
|
||||||
|
|
||||||
|
@ -494,6 +662,16 @@ a pledge with the same email address as you used before.
|
||||||
If you have any questions, don't hesitate to reach out: you can drop a line
|
If you have any questions, don't hesitate to reach out: you can drop a line
|
||||||
anytime to editors@snipettemag.com.
|
anytime to editors@snipettemag.com.
|
||||||
|
|
||||||
|
### Share for Stickers!
|
||||||
|
|
||||||
|
Money helps, but we need people too—and that's where you can help! Here's your
|
||||||
|
unique referral link:
|
||||||
|
|
||||||
|
${referralLink}
|
||||||
|
|
||||||
|
Get 5 or more of your friends to sign up using that link, and, once we collect
|
||||||
|
the money and start printing, we'll send you a free pack of Snipette stickers!
|
||||||
|
|
||||||
Thanks,
|
Thanks,
|
||||||
The Snipette Team`
|
The Snipette Team`
|
||||||
|
|
||||||
|
@ -505,11 +683,13 @@ The Snipette Team`
|
||||||
})
|
})
|
||||||
|
|
||||||
// Send confirmation message
|
// Send confirmation message
|
||||||
res.redirect('/thanks')
|
res.redirect(`/thanks?ref=${newPledge.get('referral_code')}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/thanks', async (req, res) => {
|
router.get('/thanks', async (req, res) => {
|
||||||
let output = await twing.render('thanks.htm.twig')
|
let output = await twing.render('thanks.htm.twig', {
|
||||||
|
referral_code: req.query.ref,
|
||||||
|
})
|
||||||
|
|
||||||
res.send(output)
|
res.send(output)
|
||||||
return
|
return
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-orange-200 font-serif">
|
<body class="bg-orange-200 font-serif">
|
||||||
<div class="bg-orange-600 uppercase text-lg text-orange-800 p-5 font-sans">
|
<div class="bg-orange-600 uppercase text-lg text-orange-800 p-5 font-sans">
|
||||||
Snipette Analog Fundraiser
|
Snipette Analog Fundraiser{% if referrer %} (referred by: {{referrer}}){% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-orange-600 text-center p-10">
|
<div class="bg-orange-600 text-center p-10">
|
||||||
<img alt="Snipette" class="text-5xl uppercase font-sans w-full max-w-3xl mx-auto" src="/img/header.png"/>
|
<img alt="Snipette" class="text-5xl uppercase font-sans w-full max-w-3xl mx-auto" src="/img/header.png"/>
|
||||||
|
@ -385,6 +385,7 @@
|
||||||
<input name="was_robot" value="no" id="input-robo-no" type="radio" class="text-xl font-sans"/>
|
<input name="was_robot" value="no" id="input-robo-no" type="radio" class="text-xl font-sans"/>
|
||||||
<label for="input-robo-no" class="text-xl font-sans">No, of course not</label>
|
<label for="input-robo-no" class="text-xl font-sans">No, of course not</label>
|
||||||
</div>
|
</div>
|
||||||
|
{% if referral_code %}<input type="hidden" name="referral_code" value="{{referral_code}}"/>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
{% extends "base.htm.twig" %}
|
{% extends "base.htm.twig" %}
|
||||||
|
|
||||||
{% block title %}Snipette Crowdfunding: Error{% endblock %}
|
{% block title %}Snipette Crowdfunding: Error{% endblock %}
|
||||||
{% block heading %}Thank you!{% endblock %}
|
{% block heading %}{% if referral_code %}Wait, there's stickers!{% else %}Thank you!{% endif %}{% endblock %}
|
||||||
{% block message %}
|
{% block message %}
|
||||||
<p class="text-lg mb-2">Your pledge has been recorded. Thank you so much! Please check your inbox for a receipt.</p>
|
<p class="text-lg mb-2">Your pledge has been recorded. Thank you so much! Please check your inbox for a receipt.</p>
|
||||||
<p class="text-md mb-8">Meanwhile, to help us reach our subscriber goal, do consider sharing our campaign with your friends and family. We can't print if we don't have enough subscribers to send copies too, so who knows: this could be even more helpful than the pledge itself!</p>
|
<p class="text-md mb-8">
|
||||||
|
{% if referral_code %}
|
||||||
|
Meanwhile to help us reach our goal, we're offering free stickers! All you have to do is share your unique referral link with friends and family. If 5 or more of them pledge, you'll get a set of Snipette-themed puppy stickers along with your first issue! Find your referral link below:
|
||||||
|
{% else %}
|
||||||
|
Meanwhile, to help us reach our subscriber goal, do consider sharing our campaign with your friends and family. We can't print if we don't have enough subscribers to send copies too, so who knows: this could be even more helpful than the pledge itself!
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
<p class="flex space-x-1 items-center justify-center mb-8">
|
<p class="flex space-x-1 items-center justify-center mb-8">
|
||||||
<a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-size="large" data-text="Just added my #micropledge to help Snipette's print version! Help them reach their subscriber goals to bring down the price for everyone 💗" data-url="https://fund.snipettemag.com/" data-show-count="false">Tweet</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
<a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-size="large" data-text="Just added my #micropledge to help Snipette's print version! Help them reach their subscriber goals to bring down the price for everyone 💗" data-url="https://fund.snipettemag.com{% if referral_code %}/r/{{referral_code}}{% endif %}" data-show-count="false">Tweet</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
|
||||||
<iframe src="https://www.facebook.com/plugins/share_button.php?href=https%3A%2F%2Ffund.snipettemag.com&layout=button&size=large&appId=878224506053413&width=77&height=28" width="77" height="28" style="border:none;overflow:hidden" scrolling="no" frameborder="0" allowfullscreen="true" allow="autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"></iframe>
|
<iframe src="https://www.facebook.com/plugins/share_button.php?href=https%3A%2F%2Ffund.snipettemag.com{% if referral_code %}%2Fr%2F{{referral_code}}{% endif %}&layout=button&size=large&appId=878224506053413&width=77&height=28" width="77" height="28" style="border:none;overflow:hidden" scrolling="no" frameborder="0" allowfullscreen="true" allow="autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"></iframe>
|
||||||
<a class="bg-green-500 text-white no-underline text-sm bold p-2 rounded-lg" href="whatsapp://send?text=Just added my #micropledge to help Snipette's print version! Help them reach their subscriber goals to bring down the price for everyone 💗" data-action="share/whatsapp/share" target="_blank">WhatsApp</a>
|
<a class="bg-green-500 text-white no-underline text-sm bold p-2 rounded-lg" href="whatsapp://send?text=Just added my #micropledge to help Snipette's print version! Help them reach their subscriber goals to bring down the price for everyone, at https://fund.snipettemag.com{% if referral_code %}/r/{{referral_code}}{% endif %} 💗" data-action="share/whatsapp/share" target="_blank">WhatsApp</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="mb-8 flex justify-center">
|
<p class="mb-8 flex justify-center">
|
||||||
<input type="text" disabled id="share_link" name="share_link" value="https://fund.snipettemag.com" class="p-2 inline-block h-auto grow sm:grow-0"/>
|
<input type="text" disabled id="share_link" name="share_link" value="https://fund.snipettemag.com{% if referral_code %}/r/{{referral_code}}{% endif %}" class="p-2 inline-block h-auto grow sm:grow-0"/>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
function copyShareURL() {
|
function copyShareURL() {
|
||||||
let shareLink = document.getElementById('share_link')
|
let shareLink = document.getElementById('share_link')
|
||||||
|
|
Loading…
Reference in a new issue