Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

clients/onboarding: update creator onboarding layout #2452

Merged
merged 1 commit into from
Feb 5, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,39 +1,31 @@
import { AnimatedIconButton } from '@/components/Feed/Posts/Post'
import SubscriptionGroupIcon from '@/components/Subscriptions/SubscriptionGroupIcon'
import { useAuth, useCurrentOrgAndRepoFromURL } from '@/hooks'
import { useCurrentOrgAndRepoFromURL } from '@/hooks'
import {
ArrowForward,
AttachMoneyOutlined,
DiamondOutlined,
CloseOutlined,
ViewDayOutlined,
WifiTetheringOutlined,
} from '@mui/icons-material'
import { Platforms } from '@polar-sh/sdk'
import Link from 'next/link'
import { LogoIcon } from 'polarkit/components/brand'
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from 'polarkit/components/ui/atoms'
import {
useOrganizationArticles,
useSubscriptionBenefits,
useSubscriptionTiers,
} from 'polarkit/hooks'
import { useRef } from 'react'
import { useHoverDirty } from 'react-use'

import Icon from '@/components/Icons/Icon'
import { Status } from '@polar-sh/sdk'
import { ACCOUNT_TYPE_DISPLAY_NAMES, ACCOUNT_TYPE_ICON } from 'polarkit/account'
import { useAccount } from 'polarkit/hooks'

const useUpsellCards = () => {
const { org: currentOrg } = useCurrentOrgAndRepoFromURL()
const { currentUser } = useAuth()
import { ShadowBoxOnMd } from 'polarkit/components/ui/atoms'
import { useOrganizationArticles, useSubscriptionTiers } from 'polarkit/hooks'
import { useCallback, useEffect, useRef, useState } from 'react'

const ONBOARDING_MAP_KEY = 'creator_onboarding'

const isPersonal = currentOrg?.name === currentUser?.username
interface OnboardingMap {
postCreated: boolean
subscriptionTierCreated: boolean
polarPageShared: boolean
}

const useUpsellSteps = () => {
const { org: currentOrg } = useCurrentOrgAndRepoFromURL()
const [upsellSteps, setUpsellSteps] = useState<UpsellStepProps[]>([])
const [onboardingCompletedMap, setOnboardingCompletedMap] = useState<
Partial<OnboardingMap>
>(JSON.parse(localStorage.getItem(ONBOARDING_MAP_KEY) ?? '{}'))

const { data: tiers, isPending: tiersPending } = useSubscriptionTiers(
currentOrg?.name ?? '',
Expand All @@ -43,113 +35,78 @@ const useUpsellCards = () => {
platform: Platforms.GITHUB,
showUnpublished: false,
})
const { data: benefits, isPending: benefitsPending } =
useSubscriptionBenefits(currentOrg?.name ?? '')

const { data: organizationAccount, isPending: organizationAccountPending } =
useAccount(currentOrg?.account_id)
const { data: personalAccount, isPending: personalAccountPending } =
useAccount(currentUser?.account_id)

const setupLink = isPersonal
? '/finance/account'
: `/maintainer/${currentOrg?.name}/finance/account`

const currentAccount = isPersonal
? organizationAccount || personalAccount
: organizationAccount
const isAccountActive =
currentAccount?.status === Status.UNREVIEWED ||
currentAccount?.status === Status.ACTIVE
const isAccountUnderReview = currentAccount?.status === Status.UNDER_REVIEW

const upsellCards: UpsellCardProps[] = []

if (tiersPending || articlesPending || benefitsPending) {
return upsellCards
}

if (!currentAccount) {
upsellCards.push({
icon: (
<AttachMoneyOutlined className="text-blue-500 dark:text-blue-400" />
),
title: 'Setup your payout account',
description:
'Setup your Stripe or Open Collective account to receive payments',
href: setupLink,
})
}

if (currentAccount && !isAccountActive && !isAccountUnderReview) {
const AccountTypeIcon = ACCOUNT_TYPE_ICON[currentAccount.account_type]
upsellCards.push({
icon: <Icon classes="bg-blue-500 p-1" icon={<AccountTypeIcon />} />,
title: `Continue setting up your ${
ACCOUNT_TYPE_DISPLAY_NAMES[currentAccount.account_type]
} account`,
description: `Continue the setup of your ${
ACCOUNT_TYPE_DISPLAY_NAMES[currentAccount.account_type]
} account to receive transfers`,
href: setupLink,
})
}

if (posts?.items?.length === 0) {
upsellCards.push({
icon: <ViewDayOutlined className="text-blue-500 dark:text-blue-400" />,
title: 'Write your first post',
description: 'Start engaging with your community by writing a post',
href: `/maintainer/${currentOrg?.name}/posts/new`,
})
}

const individualTiers =
tiers?.items?.filter((tier) => tier.type === 'individual') ?? []
const businessTiers =
tiers?.items?.filter((tier) => tier.type === 'business') ?? []

if (individualTiers.length === 0) {
upsellCards.push({
icon: <SubscriptionGroupIcon type="individual" className="text-2xl" />,
title: 'Setup an Individual Subscription',
description:
'Allow individuals to obtain a subscription, and give them benefits in return',
href: `/maintainer/${currentOrg?.name}/subscriptions/tiers/new?type=individual`,
})
}

if (businessTiers.length === 0) {
upsellCards.push({
icon: <SubscriptionGroupIcon type="business" className="text-2xl" />,
title: 'Offer a Business Subscription',
description:
'Make it possible for companies to obtain a subscription, and offer benefits in return',
href: `/maintainer/${currentOrg?.name}/subscriptions/tiers/new?type=business`,
})
}

const nonBuiltInBenefits = benefits?.items?.filter(
(benefit) => benefit.deletable,
const handleDismiss = useCallback(
(onboardingKey: keyof OnboardingMap) => {
setOnboardingCompletedMap((prev) => ({
...prev,
[onboardingKey]: true,
}))
},
[setOnboardingCompletedMap],
)

if (nonBuiltInBenefits?.length === 0) {
upsellCards.push({
icon: <DiamondOutlined className="text-blue-500 dark:text-blue-400" />,
title: 'Create a custom Benefit',
description:
'Create a custom benefit like Discord invites, consulting or private access to your repositories',
href: `/maintainer/${currentOrg?.name}/subscriptions/benefits`,
})
useEffect(() => {
const steps: UpsellStepProps[] = []

if (posts?.items?.length === 0 && !onboardingCompletedMap.postCreated) {
steps.push({
icon: <ViewDayOutlined className="text-blue-500 dark:text-blue-400" />,
title: 'Write your first post',
description:
'Start building a community & newsletter by writing your first post – your hello world on Polar',
href: `/maintainer/${currentOrg?.name}/posts/new`,
onboardingKey: 'postCreated',
onDismiss: handleDismiss,
})
}

const nonFreeTiers =
tiers?.items?.filter((tier) => tier.type !== 'free') ?? []

if (
nonFreeTiers.length === 0 &&
!onboardingCompletedMap.subscriptionTierCreated
) {
steps.push({
icon: <SubscriptionGroupIcon type="individual" className="text-2xl" />,
title: 'Setup paid subscriptions & membership benefits',
description:
'Offer built-in benefits like premium posts, Discord invites, sponsor ads & private GitHub repository access',
href: `/maintainer/${currentOrg?.name}/subscriptions/tiers`,
onboardingKey: 'subscriptionTierCreated',
onDismiss: handleDismiss,
})
}

if (!onboardingCompletedMap.polarPageShared) {
steps.push({
icon: (
<WifiTetheringOutlined className="text-2xl text-blue-500 dark:text-blue-400" />
),
title: 'Review & Share your Polar page',
description:
'Promote it on social media & GitHub to build free- and paid subscribers',
href: `/${currentOrg?.name}`,
onboardingKey: 'polarPageShared',
onDismiss: handleDismiss,
})
}

setUpsellSteps(steps)
}, [currentOrg, onboardingCompletedMap, posts, tiers, handleDismiss])

if (tiersPending || articlesPending) {
return []
} else {
return upsellSteps
}

return upsellCards.slice(0, 3)
}

export const CreatorUpsell = () => {
const cards = useUpsellCards()
const steps = useUpsellSteps()

if (cards.length < 1) {
if (steps.length < 1) {
return null
}

Expand All @@ -167,44 +124,80 @@ export const CreatorUpsell = () => {
Polar.
</p>
</div>
<div className="col-span-2 flex grid-cols-3 flex-col gap-6 md:grid">
{cards.map((card) => (
<UpsellCard key={card.title} {...card} />
<div className="col-span-2 flex flex-col gap-y-4">
{steps.map((card) => (
<UpsellStep key={card.title} {...card} />
))}
</div>
</div>
</div>
)
}

export interface UpsellCardProps {
export interface UpsellStepProps {
icon: React.ReactNode
title: string
description: string
href: string
onboardingKey: keyof OnboardingMap
onDismiss: (onboardingKey: keyof OnboardingMap) => void
}

const UpsellCard = ({ icon, title, description, href }: UpsellCardProps) => {
const UpsellStep = ({
icon,
title,
description,
href,
onboardingKey,
onDismiss,
}: UpsellStepProps) => {
const ref = useRef<HTMLAnchorElement>(null)
const isHovered = useHoverDirty(ref)
const [dismissed, setDismissed] = useState(false)

const handleDismiss = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()

const onboardingCompletedMap: Partial<OnboardingMap> = JSON.parse(
localStorage.getItem(ONBOARDING_MAP_KEY) ?? '{}',
)
onboardingCompletedMap[onboardingKey] = true
localStorage.setItem(
ONBOARDING_MAP_KEY,
JSON.stringify(onboardingCompletedMap),
)

setDismissed(true)

onDismiss(onboardingKey)
}

if (dismissed) {
return null
}

return (
<Link ref={ref} href={href} className="h-full">
<Card className="dark:hover:bg-polar-800 relative flex h-full flex-col transition-colors hover:bg-blue-50">
<CardHeader>{icon}</CardHeader>
<CardContent className="flex flex-grow flex-col gap-y-6">
<h3 className="text-lg font-medium [text-wrap:balance]">{title}</h3>
<p className="dark:text-polar-500 text-gray-500">{description}</p>
</CardContent>
<CardFooter>
<AnimatedIconButton
active={isHovered}
variant="secondary"
href={href}
>
<ArrowForward fontSize="inherit" />
</AnimatedIconButton>
</CardFooter>
</Card>
<Link ref={ref} href={href} className="relative">
<ShadowBoxOnMd className="dark:hover:bg-polar-800 relative flex h-full flex-row items-end justify-between transition-colors hover:bg-blue-50">
<div className="flex w-3/4 flex-row gap-x-6">
<div>{icon}</div>
<div className="flex flex-col gap-y-2">
<h3 className="mt-0 text-lg font-medium [text-wrap:balance]">
{title}
</h3>
<p className="dark:text-polar-500 text-gray-500 [text-wrap:pretty]">
{description}
</p>
</div>
</div>
</ShadowBoxOnMd>

<div
className="dark:text-polar-500 dark:hover:text-polar-300 absolute right-4 top-4 cursor-pointer p-2 text-gray-300 hover:text-gray-500"
onClick={handleDismiss}
>
<CloseOutlined fontSize="inherit" />
</div>
</Link>
)
}
Loading