Skip to content

Commit

Permalink
feat: prototype of referral system in console (#142)
Browse files Browse the repository at this point in the history
### Summary

This PR spikes out a referral system that stores data in Cloudflare's
D1. It allows anonymous or logged-in users to create referral codes
associated with their email addresses, and records the referral code a
user used to sign up for an account. It does not implement the discounts
and other rewards associated with the referral system - these are
expected to be handled manually in the first released version and
implemented as automated systems later, so are a non-goal of this PR.

### Referrals service

The referrals service has been extracted to its own worker:

storacha/referrals-service#1

### User Experience

The UX is just about dialed in - need to do a pass on the copy and some
minor layout stuff, but if you have any thoughts on the big picture now
is a great time to chime in!

<img width="1626" alt="Screenshot 2024-11-05 at 4 01 18 PM"
src="https://github.com/user-attachments/assets/cf1fb92d-94fa-4a27-a751-310dd9088cd7">
<img width="1223" alt="Screenshot 2024-11-05 at 4 00 10 PM"
src="https://github.com/user-attachments/assets/5d0ddc1c-53bc-4c7c-9c7a-8f15d65ee7c1">
<img width="1222" alt="Screenshot 2024-11-05 at 4 00 20 PM"
src="https://github.com/user-attachments/assets/a637bb4f-09d5-4959-af92-21a2eea2affc">

<img width="1414" alt="Screenshot 2024-11-05 at 5 57 42 PM"
src="https://github.com/user-attachments/assets/bbe765f4-ef09-4d54-a1f1-588e50d7524c">

---------

Co-authored-by: Vicente Olmedo <[email protected]>
  • Loading branch information
travis and volmedo authored Dec 3, 2024
1 parent 5af26ab commit 6f760fe
Show file tree
Hide file tree
Showing 12 changed files with 371 additions and 60 deletions.
4 changes: 4 additions & 0 deletions .env.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL_LINK=https://billing.stripe.com/p/login/test_

# set this to skip forcing users to pick a Stripe plan
NEXT_PUBLIC_DISABLE_PLAN_GATE=false

# point these at the marketing website and referrals service
NEXT_PUBLIC_REFERRAL_URL=http://localhost:3001/referred
NEXT_PUBLIC_REFERRALS_SERVICE_URL=http://localhost:4001
8 changes: 8 additions & 0 deletions .github/workflows/deploy-storacha.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,13 @@ jobs:
echo "NEXT_PUBLIC_W3UP_RECEIPTS_URL=https://staging.up.storacha.network/receipt/" >> .env
echo "NEXT_PUBLIC_W3UP_PROVIDER=did:web:staging.web3.storage" >> .env
echo "NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID=prctbl_1NzhdvF6A5ufQX5vKNZuRhie" >> .env
echo "NEXT_PUBLIC_STRIPE_TRIAL_PRICING_TABLE_ID=prctbl_1QIDHGF6A5ufQX5vOK9Xl8Up" >> .env
echo "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51LO87hF6A5ufQX5viNsPTbuErzfavdrEFoBuaJJPfoIhzQXdOUdefwL70YewaXA32ZrSRbK4U4fqebC7SVtyeNcz00qmgNgueC" >> .env
echo "NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL_LINK=https://billing.stripe.com/p/login/test_6oE29Gff99KO6mk8ww" >> .env
# use example.com in preview because we can't predict the preview URL of the storacha.network site
echo "NEXT_PUBLIC_REFERRAL_URL=http://example.com/referred" >> .env
echo "NEXT_PUBLIC_REFERRALS_SERVICE_URL=https://staging-referrals.storacha.network" >> .env
# as long as this uses https://github.com/cloudflare/next-on-pages/blob/dc529d7efa8f8568ea8f71b5cdcf78df89be6c12/packages/next-on-pages/bin/index.js,
# env vars won't get passed through to wrangler, so if wrangler will need them, write them to .env like the previous step
- run: pnpm pages:build
Expand Down Expand Up @@ -131,8 +136,11 @@ jobs:
echo "NEXT_PUBLIC_W3UP_RECEIPTS_URL=https://up.storacha.network/receipt/" >> .env
echo "NEXT_PUBLIC_W3UP_PROVIDER=did:web:web3.storage" >> .env
echo "NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID=prctbl_1OCJ1qF6A5ufQX5vM5DWg4rA" >> .env
echo "NEXT_PUBLIC_STRIPE_TRIAL_PRICING_TABLE_ID=prctbl_1QPYsuF6A5ufQX5vdIGAe54g" >> .env
echo "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_51LO87hF6A5ufQX5vQTO5BHyz8y9ybJp4kg1GsBjYuqwluuwtQTkbeZzkoQweFQDlv7JaGjuIdUWAyuwXp3tmCfsM005lJK9aS8" >> .env
echo "NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL_LINK=https://billing.stripe.com/p/login/cN22aA62U6bO1sA9AA" >> .env
echo "NEXT_PUBLIC_REFERRAL_URL=http://storacha.network/referred" >> .env
echo "NEXT_PUBLIC_REFERRALS_SERVICE_URL=https://referrals.storacha.network" >> .env
- run: pnpm pages:build
# as long as this uses https://github.com/cloudflare/next-on-pages/blob/dc529d7efa8f8568ea8f71b5cdcf78df89be6c12/packages/next-on-pages/bin/index.js,
# env vars won't get passed through to wrangler, so if wrangler will need them, write them to .env like the previous step
Expand Down
42 changes: 0 additions & 42 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,48 +29,6 @@
background-position: bottom;
background-repeat: no-repeat;
}
.bg-hot-red,
.hover\:bg-hot-red:hover {
background-color: var(--hot-red);
}
.bg-hot-red-light,
.hover\:bg-hot-red-light:hover {
background-color: var(--hot-red-light);
}
.bg-hot-blue-light {
background-color: var(--hot-blue-light);
}
.bg-hot-yellow {
background-color: var(--hot-yellow);
}
.hover\:bg-hot-yellow:hover {
background-color: var(--hot-yellow);
}
.bg-hot-yellow-light,
.hover\:bg-hot-yellow-light:hover {
background-color: var(--hot-yellow-light);
}
.border-hot-red,
.hover\:border-hot-red:hover {
border-color: var(--hot-red);
}
.border-hot-yellow {
border-color: var(--hot-yellow);
}
.border-hot-yellow-light {
border-color: var(--hot-yellow-light);
}
.text-hot-red,
.hover\:text-hot-red:hover {
color: var(--hot-red);
}
.text-hot-yellow {
color: var(--hot-yellow);
}
.text-hot-blue {
color: var(--hot-blue);
}

.w3ui-button-colors {
@apply text-white bg-slate-800 hover:bg-blue-800 transition-colors ease-in;
}
Expand Down
148 changes: 148 additions & 0 deletions src/app/referrals/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
'use client'

import CopyButton from '@/components/CopyButton'
import DefaultLoader from '@/components/Loader'
import { H1, H3 } from '@/components/Text'
import { RefcodeResult, useReferrals } from '@/lib/referrals/hooks'
import { useEffect } from 'react'
import { KeyedMutator } from 'swr'

export const runtime = "edge"

export function RefcodeCreator ({
accountEmail,
urlQueryEmail,
createRefcode,
mutateRefcode,
setReferrerEmail
}: {
accountEmail: string
urlQueryEmail: string | null
createRefcode: (form: FormData) => Promise<Response>
mutateRefcode: KeyedMutator<RefcodeResult>
setReferrerEmail: (email: string) => void
}
) {
const prefilledEmail = urlQueryEmail || accountEmail
useEffect(function () {
if (prefilledEmail) {
(async () => {
const form = new FormData()
form.append('email', prefilledEmail)
await createRefcode(form)
await mutateRefcode()
})()
}
}, [prefilledEmail])
return (
<>
{
prefilledEmail ? (
<DefaultLoader className="w-6 h-6 color-hot-red" />
) : (
<form onSubmit={async (e) => {
e.preventDefault()
try {
const form = new FormData(e.currentTarget)
const email = form.get('email')
if (email){
setReferrerEmail(email.toString())
await createRefcode(form)
} else {
console.log("email was undefined, this is strange!")
}
} finally {
// mutate here to pick up any changes from either create or set
mutateRefcode()
}
}} className=''>
<label className='block mb-2 uppercase text-xs text-hot-red font-epilogue m-1' htmlFor='email'>Your Email</label>
<input
id='email'
name='email'
type='email'
className='text-black py-2 px-2 rounded-xl block mb-4 border border-hot-red w-80'
placeholder='Email'
defaultValue={urlQueryEmail || ''}
required={true}
/>
<button type='submit' className={`inline-block bg-hot-red border border-hot-red hover:bg-white hover:text-hot-red font-epilogue text-white uppercase text-sm px-6 py-2 rounded-full whitespace-nowrap`}>
Create
</button>
</form>
)
}
</>
)
}

export function RefcodeLink ({ referralLink }: { referralLink: string }) {
return (
<div className="border border-hot-red rounded-full px-4 py-2 flex flex-row justify-between items-center">
<div>{referralLink}</div>
<CopyButton text={referralLink} />
</div>
)
}

export function ReferralsList () {
const { referrals } = useReferrals()
return (
(referrals && referrals.length > 0) ? (
<>
<H3>Referrals</H3>
<div className="divide-solid divide-hot-red py-4">
{
/**
* TODO: once we can determine when a user signed up and what plan they signed up for, update
* this UI to differentiate between them with different names and give users a countdown timer
* in the lozenge.
*/
referrals.map((referral, i) =>
<div key={i} className="flex flex-row justify-between items-center py-4">
<div>Referred Racha</div>
<div className="rounded-full bg-hot-red-light text-hot-red px-4 py-2 font-mono text-sm">In Progress</div>
</div>
)
}
</div>
</>
) : (
<>
<H3>Earn Free Storage and Racha Points!</H3>
<p className='text-hot-red mb-4 max-w-lg'>
Turn your friends into Lite or Business Rachas and receive up to 16 months of Lite or
3 months of Business for free! You can also earn Racha Points.
</p>
</>
)
)
}

export default function ReferralsPage () {
const { refcodeIsLoading, referralLink, setReferrerEmail, accountEmail, urlQueryEmail, createRefcode, mutateRefcode, } = useReferrals()
return (
<div className='p-10 bg-racha-fire/50 w-full h-screen'>
<H1>Generate a Referral Code</H1>
<div className='border border-hot-red rounded-2xl bg-white p-5'>
{refcodeIsLoading ? (
<DefaultLoader className="text-hot-red h-6 w-6" />
) : (
<>
<ReferralsList />
{referralLink ? (
<RefcodeLink referralLink={referralLink} />
) : (
<RefcodeCreator
accountEmail={accountEmail}
urlQueryEmail={urlQueryEmail}
createRefcode={createRefcode}
mutateRefcode={mutateRefcode}
setReferrerEmail={setReferrerEmail} />
)}
</>
)}
</div>
</div >
)
}
48 changes: 47 additions & 1 deletion src/app/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import useSWR from 'swr'
import Link from 'next/link'
import { usePlan } from '@/hooks'
import { SettingsNav } from './layout'
import { H1, H2 } from '@/components/Text'
import { H1, H2, H3 } from '@/components/Text'
import { GB, TB, filesize } from '@/lib'
import DefaultLoader from '@/components/Loader'
import { RefcodeLink, ReferralsList, RefcodeCreator } from '../referrals/page'
import { useReferrals } from '@/lib/referrals/hooks'
import { useSearchParams } from 'next/navigation'

const Plans: Record<`did:${string}`, { name: string, limit: number }> = {
'did:web:starter.web3.storage': { name: 'Starter', limit: 5 * GB },
Expand All @@ -16,6 +19,9 @@ const Plans: Record<`did:${string}`, { name: string, limit: number }> = {
'did:web:free.web3.storage': { name: 'Free', limit: Infinity },
}

const MAX_REFERRALS = 11
const MAX_CREDITS = 460

export default function SettingsPage (): JSX.Element {
const [{ client, accounts }] = useW3()
// TODO: introduce account switcher
Expand Down Expand Up @@ -62,10 +68,50 @@ export default function SettingsPage (): JSX.Element {
const allocated = Object.values(usage ?? {}).reduce((total, n) => total + n, 0)
const limit = plan?.product ? Plans[plan.product]?.limit : 0

const { referrals, referralLink, setReferrerEmail, accountEmail, urlQueryEmail, createRefcode, mutateRefcode, } = useReferrals()

const referred = referrals?.length || 0

// TODO: need to calculate these from the referral information that gets added during the TBD cronjob
const credits = 0
const points = 0
const params = useSearchParams()
const referralsEnabled = (params.get('referrals') === 'enabled')
return (
<>
<SettingsNav />
<H1>Settings</H1>
{referralsEnabled && (
<>
<H2>Rewards</H2>
<div className='flex flex-row space-x-2 justify-between max-w-4xl mb-4'>
<div className='border border-hot-red rounded-2xl bg-white p-5 flex-grow'>
<H3>Referred</H3>
<span className='text-4xl'>{referred}</span> / {MAX_REFERRALS}
</div>
<div className='border border-hot-red rounded-2xl bg-white p-5 flex-grow'>
<H3>USD Credits</H3>
<span className='text-4xl'>{credits}</span> / {MAX_CREDITS}
</div>
<div className='border border-hot-red rounded-2xl bg-white p-5 flex-grow'>
<H3>Racha Points</H3>
<span className='text-4xl'>{points}</span>
</div>
</div>
<div className='border border-hot-red rounded-2xl bg-white p-5 max-w-4xl mb-4'>
<ReferralsList />
{referralLink ? (
<RefcodeLink referralLink={referralLink} />
) : (
<RefcodeCreator
accountEmail={accountEmail}
urlQueryEmail={urlQueryEmail}
createRefcode={createRefcode}
mutateRefcode={mutateRefcode}
setReferrerEmail={setReferrerEmail} />)}
</div>
</>
)}
<div className='border border-hot-red rounded-2xl bg-white p-5 max-w-4xl'>
<H2>Plan</H2>
<p className='font-epilogue mb-4'>
Expand Down
8 changes: 8 additions & 0 deletions src/components/Authenticator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
import { Logo } from '../brand'
import { TopLevelLoader } from './Loader'

import { useRecordRefcode } from '@/lib/referrals/hooks'

export function AuthenticationForm (): JSX.Element {
const [{ submitted }] = useAuthenticator()
return (
Expand Down Expand Up @@ -39,6 +41,12 @@ export function AuthenticationForm (): JSX.Element {
export function AuthenticationSubmitted (): JSX.Element {
const [{ email }] = useAuthenticator()

// ensure the referral of this user is tracked if necessary.
// we might use the result of this hook in the future to tell
// people that they get special pricing on the next page after
// they verify their email.
useRecordRefcode()

return (
<div className='authenticator'>
<div className='text-hot-red bg-white border border-hot-red rounded-2xl shadow-md px-10 pt-8 pb-8'>
Expand Down
Loading

0 comments on commit 6f760fe

Please sign in to comment.