diff --git a/.env.tpl b/.env.tpl index ef94963..7a6a5a6 100644 --- a/.env.tpl +++ b/.env.tpl @@ -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 \ No newline at end of file diff --git a/.github/workflows/deploy-storacha.yml b/.github/workflows/deploy-storacha.yml index d4ae079..5d3a993 100644 --- a/.github/workflows/deploy-storacha.yml +++ b/.github/workflows/deploy-storacha.yml @@ -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 @@ -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 diff --git a/src/app/globals.css b/src/app/globals.css index 116fdb7..466fb30 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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; } diff --git a/src/app/referrals/page.tsx b/src/app/referrals/page.tsx new file mode 100644 index 0000000..939a769 --- /dev/null +++ b/src/app/referrals/page.tsx @@ -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 + mutateRefcode: KeyedMutator + 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 ? ( + + ) : ( +
{ + 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=''> + + + +
+ ) + } + + ) +} + +export function RefcodeLink ({ referralLink }: { referralLink: string }) { + return ( +
+
{referralLink}
+ +
+ ) +} + +export function ReferralsList () { + const { referrals } = useReferrals() + return ( + (referrals && referrals.length > 0) ? ( + <> +

Referrals

+
+ { + /** + * 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) => +
+
Referred Racha
+
In Progress
+
+ ) + } +
+ + ) : ( + <> +

Earn Free Storage and Racha Points!

+

+ 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. +

+ + ) + ) +} + +export default function ReferralsPage () { + const { refcodeIsLoading, referralLink, setReferrerEmail, accountEmail, urlQueryEmail, createRefcode, mutateRefcode, } = useReferrals() + return ( +
+

Generate a Referral Code

+
+ {refcodeIsLoading ? ( + + ) : ( + <> + + {referralLink ? ( + + ) : ( + + )} + + )} +
+
+ ) +} \ No newline at end of file diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index dec7d11..5f16edf 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -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 }, @@ -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 @@ -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 ( <>

Settings

+ {referralsEnabled && ( + <> +

Rewards

+
+
+

Referred

+ {referred} / {MAX_REFERRALS} +
+
+

USD Credits

+ {credits} / {MAX_CREDITS} +
+
+

Racha Points

+ {points} +
+
+
+ + {referralLink ? ( + + ) : ( + )} +
+ + )}

Plan

diff --git a/src/components/Authenticator.tsx b/src/components/Authenticator.tsx index ba503dd..4eee63c 100644 --- a/src/components/Authenticator.tsx +++ b/src/components/Authenticator.tsx @@ -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 ( @@ -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 (

diff --git a/src/components/PlanGate.tsx b/src/components/PlanGate.tsx index c26d33e..837aff0 100644 --- a/src/components/PlanGate.tsx +++ b/src/components/PlanGate.tsx @@ -2,37 +2,56 @@ import { ReactNode, useState } from 'react' import { useW3 } from '@w3ui/react' -import StripePricingTable from './PricingTable'; +import StripePricingTable, { StripeTrialPricingTable } from './PricingTable'; import { TopLevelLoader } from './Loader'; import { Logo } from '@/brand'; import { usePlan } from '@/hooks'; +import { useRecordRefcode, useReferredBy } from '@/lib/referrals/hooks'; export function PlanGate ({ children }: { children: ReactNode }): ReactNode { const [{ accounts }] = useW3() + const email = accounts[0]?.toEmail() const { data: plan, error } = usePlan(accounts[0]) + const { referredBy } = useRecordRefcode() if (!plan && !error) { return } - if (!plan?.product) { return (
-
-

Welcome, {accounts[0]?.toEmail()}!

-

- To get started you'll need to sign up for a subscription. If you choose - the starter plan we won't charge your credit card, but we do need a card on file - before we will store your bits. -

-

- Pick a plan below and complete the Stripe signup flow to get started! -

-
- -
-
+ {referredBy ? ( + <> +
+

Welcome, {email}!

+

+ Congratulations! You are eligible for a free trial of our Lite or Business subscriptions. That means + we won't charge you anything today. We do need you to provide a valid credit card before we can start your + trial - pick a plan below and complete the checkout flow to get started! +

+
+ + + ) : ( + <> +
+

Welcome, {email}!

+

+ To get started you'll need to sign up for a subscription. If you choose + the starter plan we won't charge your credit card, but we do need a card on file + before we will store your bits. +

+

+ Pick a plan below and complete the Stripe checkout flow to get started! +

+
+ + + ) + } +
+
) } diff --git a/src/components/PricingTable.tsx b/src/components/PricingTable.tsx index 2696bf4..ab58886 100644 --- a/src/components/PricingTable.tsx +++ b/src/components/PricingTable.tsx @@ -16,3 +16,18 @@ export default function StripePricingTable ({ className = '' }) { ) } + +export function StripeTrialPricingTable ({ className = '' }) { + const [{ accounts }] = useW3() + return ( + <> +