Skip to content

Commit

Permalink
feat(unlock-app): Event Referrals (#13512)
Browse files Browse the repository at this point in the history
* feat(unlock-app): adding a UI to setup referrer fees and copy referral URLs for events

* Apply suggestions from code review
  • Loading branch information
julien51 authored Mar 24, 2024
1 parent fe037f2 commit 1186f39
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 12 deletions.
6 changes: 3 additions & 3 deletions unlock-app/src/components/content/event/CopyUrlButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { ToastHelper } from '~/components/helpers/toast.helper'
import useClipboard from 'react-use-clipboard'

interface CopyUrlButtonProps {
eventUrl: string
url: string
}

export const CopyUrlButton = ({ eventUrl }: CopyUrlButtonProps) => {
const [_, setCopied] = useClipboard(eventUrl, {
export const CopyUrlButton = ({ url }: CopyUrlButtonProps) => {
const [_, setCopied] = useClipboard(url, {
successDuration: 1000,
})

Expand Down
7 changes: 2 additions & 5 deletions unlock-app/src/components/content/event/EventDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -276,13 +276,10 @@ Powered by Unlock Protocol`}
<TweetItButton event={event} eventUrl={eventUrl} />
</li>
<li>
<CastItButton
event={event}
eventUrl={`https://events-frame.unlock-protocol.com/events/s/${eventProp.slug}`}
/>
<CastItButton event={event} eventUrl={eventUrl} />
</li>
<li>
<CopyUrlButton eventUrl={eventUrl} />
<CopyUrlButton url={eventUrl} />
</li>
</ul>
</section>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import { Button, Modal } from '@unlock-protocol/ui'
import { Checkout } from '~/components/interface/checkout/main'
import { useState } from 'react'
import { useMemo, useState } from 'react'
import { selectProvider } from '~/hooks/useAuthenticate'
import { useConfig } from '~/utils/withConfig'
import { useRouter } from 'next/router'

export const EmbeddedCheckout = ({ checkoutConfig, refresh }: any) => {
const [isCheckoutOpen, setCheckoutOpen] = useState(false)
const config = useConfig()
const { query } = useRouter()
const injectedProvider = selectProvider(config)
const paywallConfig = useMemo(() => {
if (query.referrer) {
return {
...checkoutConfig.config,
referrer: query.referrer,
}
}
return checkoutConfig.config
}, [checkoutConfig, query])

return (
<>
<Modal isOpen={isCheckoutOpen} setIsOpen={setCheckoutOpen} empty={true}>
<Checkout
injectedProvider={injectedProvider as any}
paywallConfig={checkoutConfig.config}
paywallConfig={paywallConfig}
handleClose={() => {
refresh()
setCheckoutOpen(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ReactNode, useState } from 'react'
import { SettingTab } from '~/pages/locks/settings'
import { PaywallConfigType, Event } from '@unlock-protocol/core'
import { General } from './General'
import { Referrals } from './Referrals'
import Link from 'next/link'

interface EventSettingsProps {
Expand Down Expand Up @@ -36,6 +37,12 @@ export const EventSettings = ({
description: `Update your event's public information such as its location, date and more!`,
children: <General event={event} checkoutConfig={checkoutConfig} />,
},
{
id: 'referrals',
label: 'Referrals',
description: `Create referral links to share with your community and reward them.`,
children: <Referrals event={event} checkoutConfig={checkoutConfig} />,
},
// {
// id: 'checkout',
// label: 'Checkout',
Expand Down
244 changes: 244 additions & 0 deletions unlock-app/src/components/content/event/Settings/Referrals.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import { FaTrash as TrashIcon } from 'react-icons/fa'
import {
Button,
Input,
Detail,
AddressInput,
Placeholder,
} from '@unlock-protocol/ui'
import { PaywallConfigType, Event } from '@unlock-protocol/core'
import { SettingCard } from '~/components/interface/locks/Settings/elements/SettingCard'
import { useLockData } from '~/hooks/useLockData'
import { onResolveName } from '~/utils/resolvers'
import { Controller, useForm } from 'react-hook-form'
import { useReferrerFee } from '~/hooks/useReferrerFee'
import { ToastHelper } from '~/components/helpers/toast.helper'
import useEns from '~/hooks/useEns'
import { addressMinify } from '~/utils/strings'
import CopyUrlButton from '../CopyUrlButton'
import { getEventUrl } from '../utils'

interface ReferralsProps {
event: Event
checkoutConfig: {
id?: string
config: PaywallConfigType
}
}

interface FormProps {
referralFeePercentage: number
referralAddress: string
}

export const Referral = ({
eventUrl,
onDelete,
referral: { referrer, fee },
}: {
eventUrl: string
onDelete: (referrer: string) => void
referral: { referrer: string; fee: number }
}) => {
const addressToEns = useEns(referrer)

const resolvedAddress =
addressToEns === referrer ? addressMinify(referrer) : addressToEns

const eventUrlWithReferrer = new URL(eventUrl)
eventUrlWithReferrer.searchParams.append('referrer', referrer)

return (
<div className="flex gap-4 mb-2">
<Detail valueSize="medium" label="Fee:">
{fee / 100}%
</Detail>
<Detail valueSize="medium" label="Address:">
{resolvedAddress}
</Detail>
<div className="grow flex justify-end place-items-center flex-row ">
<CopyUrlButton url={eventUrlWithReferrer.toString()} />
<div>
<Button
size="tiny"
iconLeft={<TrashIcon />}
onClick={() => onDelete(referrer)}
>
Delete
</Button>
</div>
</div>
</div>
)
}

export const ReferralsForLock = ({
eventUrl,
lockAddress,
network,
}: {
eventUrl: string
lockAddress: string
network: number
}) => {
const { lock, isLockLoading } = useLockData({ lockAddress, network })

const {
register,
handleSubmit,
reset,
control,
formState: { errors, isValid },
} = useForm<FormProps>({})

const {
data: referralFees,
isLoading,
setReferrerFee,
refetch,
} = useReferrerFee({
lockAddress,
network,
})

const onSubmit = async (data: FormProps) => {
const setReferrerFeePromise = setReferrerFee.mutateAsync({
...data,
})

await ToastHelper.promise(setReferrerFeePromise, {
loading: 'Setting a new referral fee!',
error: 'Failed to set a new referrer, please try again.',
success: 'Referral fee set!',
})

reset()
refetch()
}

const deleteReferrer = async (referralAddress: string) => {
const setReferrerFeePromise = setReferrerFee.mutateAsync({
referralAddress,
referralFeePercentage: 0,
})

await ToastHelper.promise(setReferrerFeePromise, {
loading: 'Deleting referral',
error: 'Failed to delete the referrer, please try again.',
success: 'Referral fee deleted!',
})

reset()
refetch()
}

if (isLockLoading || !lock) {
return (
<SettingCard label="">
<Placeholder.Root>
<Placeholder.Line size="sm" />
<Placeholder.Line size="sm" />
</Placeholder.Root>
</SettingCard>
)
}
return (
<SettingCard label={lock.name} description={``}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col md:flex-row gap-2">
<span className="grow">
<Controller
{...register('referralAddress', {
required: true,
})}
control={control}
render={({ field }) => {
return (
<AddressInput
placeholder="Address or ENS"
onResolveName={onResolveName}
{...field}
/>
)
}}
/>
</span>
<span>
<Input
type="number"
{...register('referralFeePercentage', {
valueAsNumber: true,
min: 1,
max: 100,
required: true,
})}
placeholder="Referrer fee (%)"
error={
errors?.referralFeePercentage &&
'This field accept percentage value between 1 and 100.'
}
/>
</span>
<span className="flex items-start ">
<Button
type="submit"
className="w-24 md:mt-1"
disabled={!isValid}
size="small"
loading={false}
>
Add
</Button>
</span>
</div>

{isLoading && (
<Placeholder.Root>
<Placeholder.Line size="sm" />
<Placeholder.Line size="sm" />
</Placeholder.Root>
)}
{referralFees?.length > 0 && (
<h2 className="text-lg font-bold mt-4 mb-2">Referrals:</h2>
)}
<div className="flex flex-col gap-2">
{referralFees?.map((referral) => {
return (
<Referral
eventUrl={eventUrl}
onDelete={deleteReferrer}
key={referral.id}
referral={referral}
/>
)
})}
</div>
</form>
</SettingCard>
)
}

export const Referrals = ({ event, checkoutConfig }: ReferralsProps) => {
const eventUrl = getEventUrl({
event,
})

const locks = Object.keys(checkoutConfig.config.locks)
return (
<>
{locks.map((lock) => {
const network =
checkoutConfig.config.locks[lock].network ||
checkoutConfig.config.network
return (
<ReferralsForLock
eventUrl={eventUrl}
lockAddress={lock}
key={lock}
network={network!}
/>
)
})}
</>
)
}
6 changes: 4 additions & 2 deletions unlock-app/src/hooks/useReferrerFee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const useReferrerFee = ({ lockAddress, network }: ReferrerFeeProps) => {
return service.lock(
{
where: {
id: lockAddress,
address: lockAddress,
},
},
{
Expand All @@ -52,7 +52,9 @@ export const useReferrerFee = ({ lockAddress, network }: ReferrerFeeProps) => {
async () => getLock()
)

const data = referralFeesData?.referrerFees || []
const data = (referralFeesData?.referrerFees || []).filter(
(referralFeesData) => referralFeesData.fee > 0
)

return {
isLoading,
Expand Down
1 change: 1 addition & 0 deletions unlock-app/src/pages/locks/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type SettingTab =
| 'emails'
| 'verifiers'
| 'checkout'
| 'referrals'

const Settings: NextPage = () => {
const { query } = useRouter()
Expand Down

0 comments on commit 1186f39

Please sign in to comment.