Skip to content
Open
Show file tree
Hide file tree
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
83 changes: 83 additions & 0 deletions locksmith/__tests__/operations/eventCollectionOperations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import {
addManagerAddressOperation,
createEventCollectionOperation,
createEventCollectionSlug,
getEventCollectionOperation,
PUBLIC_EVENTS_COLLECTION_SLUG,
removeManagerAddressOperation,
updateEventCollectionOperation,
} from '../../src/operations/eventCollectionOperations'
import { EventStatus } from '@unlock-protocol/types'
import { Op } from 'sequelize'

// interface for link types
interface Link {
Expand Down Expand Up @@ -61,17 +65,40 @@ vi.mock('../../src/utils/createSlug', () => ({
.replace(/(^-|-$)/g, ''),
}))

vi.mock('../../src/operations/wedlocksOperations', () => ({
sendEmail: vi.fn(),
}))

vi.mock('../../src/operations/privyUserOperations', () => ({
getPrivyUserByAddress: vi.fn().mockResolvedValue({ success: false }),
}))

vi.mock('../../src/config/config', () => ({
default: {
unlockApp: 'https://app.unlock-protocol.com',
},
}))

vi.mock('../../src/logger', () => ({
default: {
error: vi.fn(),
},
}))

describe('eventCollectionOperations', () => {
let mockFindOne: ReturnType<typeof vi.fn>
let mockFindAll: ReturnType<typeof vi.fn>
let scopedEventData: any

beforeEach(() => {
vi.resetAllMocks() // Reset mocks before each test

// Mock the scoped EventData
mockFindOne = vi.fn()
mockFindAll = vi.fn()
scopedEventData = {
findOne: mockFindOne,
findAll: mockFindAll,
}
;(EventData.scope as any).mockReturnValue(scopedEventData)
})
Expand Down Expand Up @@ -198,6 +225,62 @@ describe('eventCollectionOperations', () => {
'test-collection-with-special-characters'
)
})

it('reserves the public events collection slug', async () => {
;(EventCollection.findByPk as any).mockResolvedValueOnce(null)

const slug = await createEventCollectionSlug('All Events')

expect(slug).toBe('all-events-1')
expect(EventCollection.findByPk).toHaveBeenCalledOnce()
expect(EventCollection.findByPk).toHaveBeenCalledWith('all-events-1')
})
})

describe('getEventCollectionOperation', () => {
it('returns a read-only virtual collection of deployed Unlock events', async () => {
const event = {
slug: 'community-meetup',
data: {
name: 'Community meetup',
replyTo: 'private@example.com',
attributes: [],
},
}
mockFindAll.mockResolvedValue([event])

const result = await getEventCollectionOperation(
PUBLIC_EVENTS_COLLECTION_SLUG
)

expect(EventCollection.findByPk).not.toHaveBeenCalled()
expect(mockFindAll).toHaveBeenCalledWith({
where: {
status: EventStatus.DEPLOYED,
eventType: 'unlock',
checkoutConfigId: {
[Op.ne]: null,
},
},
order: [['createdAt', 'DESC']],
})
expect(result).toMatchObject({
slug: PUBLIC_EVENTS_COLLECTION_SLUG,
title: 'Explore Unlock Events',
managerAddresses: [],
isVirtual: true,
events: [
{
slug: 'community-meetup',
data: {
name: 'Community meetup',
attributes: [],
},
},
],
})
expect(result.events[0].data).not.toHaveProperty('replyTo')
})
})

describe('updateEventCollectionOperation', () => {
Expand Down
42 changes: 42 additions & 0 deletions locksmith/src/operations/eventCollectionOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ import config from '../config/config'
import logger from '../logger'
import { getPrivyUserByAddress } from './privyUserOperations'
import { Op } from 'sequelize'
import { EventStatus } from '@unlock-protocol/types'
import { removeProtectedAttributesFromObject } from '../utils/protectedAttributes'

export const PUBLIC_EVENTS_COLLECTION_SLUG = 'all-events'

const PUBLIC_EVENTS_COLLECTION = {
slug: PUBLIC_EVENTS_COLLECTION_SLUG,
title: 'Explore Unlock Events',
description:
'Discover public events created with Unlock Protocol. New deployed events appear here automatically.',
coverImage: '/images/illustrations/events/farcon-hero.png',
banner: '/images/illustrations/events/party.svg',
links: [],
managerAddresses: [],
isVirtual: true,
} as const

// event collection body schema
const EventCollectionBody = z.object({
Expand Down Expand Up @@ -45,6 +61,10 @@ export async function createEventCollectionSlug(
const baseSlug = kebabCase(cleanTitle)
const slug = index ? `${baseSlug}-${index}` : baseSlug

if (slug === PUBLIC_EVENTS_COLLECTION_SLUG) {
return createEventCollectionSlug(title, 1)
}

// Check if the slug already exists
const existingCollection = await EventCollection.findByPk(slug)
if (existingCollection) {
Expand Down Expand Up @@ -111,6 +131,28 @@ export const createEventCollectionOperation = async (
* @throws An error if the event collection is not found.
*/
export const getEventCollectionOperation = async (slug: string) => {
if (slug === PUBLIC_EVENTS_COLLECTION_SLUG) {
const events = await EventData.scope('withoutId').findAll({
where: {
status: EventStatus.DEPLOYED,
eventType: 'unlock',
checkoutConfigId: {
[Op.ne]: null,
},
},
order: [['createdAt', 'DESC']],
})

events.forEach((event) => {
event.data = removeProtectedAttributesFromObject(event.data)
})

return {
...PUBLIC_EVENTS_COLLECTION,
events,
}
}

const eventCollection = await EventCollection.findByPk(slug, {
include: [
{
Expand Down
2 changes: 2 additions & 0 deletions packages/unlock-js/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,8 @@ components:
type: array
items:
type: string
isVirtual:
type: boolean
createdAt:
type: string
format: date-time
Expand Down
12 changes: 9 additions & 3 deletions unlock-app/src/components/content/event/EventLandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Button } from '@unlock-protocol/ui'
import Link from 'next/link'
import { LockTypeLandingPage } from '~/components/interface/LockTypeLandingPage'
import Image from 'next/image'
import { TbCalendarEvent } from 'react-icons/tb'

const customers = [
{
Expand Down Expand Up @@ -136,9 +137,14 @@ export const EventLandingPageCallToAction = ({
handleCreateEvent,
}: EventLandingPageCallToActionProps) => {
return (
<Button onClick={handleCreateEvent} className="my-8">
Get started for free
</Button>
<div className="flex flex-col sm:flex-row gap-3 my-8">
<Button onClick={handleCreateEvent}>Get started for free</Button>
<Link href="/events/all-events">
<Button variant="outlined-primary" iconLeft={<TbCalendarEvent />}>
Explore events
</Button>
</Link>
</div>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ export default function EventsCollectionDetailContent({
const [isEventDetailDrawerOpen, setIsEventDetailDrawerOpen] = useState(false)
const [selectedEvent, setSelectedEvent] = useState<any | null>(null)

const isVirtualCollection = Boolean(
(eventCollection as { isVirtual?: boolean } | undefined)?.isVirtual
)

const hasValidEvents = useMemo(() => {
return (
eventCollection?.events?.some(
Expand Down Expand Up @@ -284,12 +288,14 @@ export default function EventsCollectionDetailContent({
<div className="flex flex-col gap-6 lg:col-span-10">
<div className="flex flex-col sm:flex-row items-center space-y-2 justify-between my-5">
<h2 className="text-3xl font-bold">Events</h2>
<Button onClick={handleAddEvent} className="w-full sm:w-auto">
<div className="flex items-center gap-2">
<Icon icon={TbPlus} size={20} />
{isManager ? 'Add Event' : 'Submit Event'}
</div>
</Button>
{!isVirtualCollection && (
<Button onClick={handleAddEvent} className="w-full sm:w-auto">
<div className="flex items-center gap-2">
<Icon icon={TbPlus} size={20} />
{isManager ? 'Add Event' : 'Submit Event'}
</div>
</Button>
)}
</div>
{hasValidEvents ? (
<>
Expand Down Expand Up @@ -333,15 +339,19 @@ export default function EventsCollectionDetailContent({
<ImageBar
src="/images/illustrations/no-locks.svg"
description={
<div>
No events have been added yet.{' '}
<span
onClick={handleAddEvent}
className="text-brand-ui-primary cursor-pointer"
>
{isManager ? 'Add an event' : 'Submit an event'}
</span>
</div>
isVirtualCollection ? (
'No public events are available yet.'
) : (
<div>
No events have been added yet.{' '}
<span
onClick={handleAddEvent}
className="text-brand-ui-primary cursor-pointer"
>
{isManager ? 'Add an event' : 'Submit an event'}
</span>
</div>
)
}
/>
)}
Expand All @@ -352,13 +362,15 @@ export default function EventsCollectionDetailContent({
</div>

{/* Add Event Drawer */}
<AddEventsToCollectionDrawer
collectionSlug={eventCollection?.slug}
isOpen={isAddEventDrawerOpen}
setIsOpen={setIsAddEventDrawerOpen}
isManager={isManager!}
existingEventSlugs={existingEventSlugs}
/>
{!isVirtualCollection && (
<AddEventsToCollectionDrawer
collectionSlug={eventCollection?.slug}
isOpen={isAddEventDrawerOpen}
setIsOpen={setIsAddEventDrawerOpen}
isManager={isManager!}
existingEventSlugs={existingEventSlugs}
/>
)}
{/* Event Detail Drawer */}
{eventCollection?.slug && (
<EventDetailDrawer
Expand Down