diff --git a/libs/inngest/src/client.ts b/libs/inngest/src/client.ts index 8f9a3d8..c82489c 100644 --- a/libs/inngest/src/client.ts +++ b/libs/inngest/src/client.ts @@ -6,6 +6,7 @@ import { schema as googleSchema } from './integrations/google/index.js'; import { schema as sanitySchema } from './integrations/sanity/index.js'; import { schema as stripeSchema } from './integrations/stripe/index.js'; import { schema as websiteSchema } from './integrations/website/index.js'; +import { schema as kitSchema } from './integrations/kit/index.js'; // TODO pin Zod to 3 or figure out other workaround for // https://github.com/inngest/inngest-js/issues/1014 @@ -16,7 +17,8 @@ export const schemas = new EventSchemas() .fromZod(googleSchema) .fromZod(sanitySchema) .fromZod(stripeSchema) - .fromZod(websiteSchema); + .fromZod(websiteSchema) + .fromZod(kitSchema); if (!process.env.INNGEST_EVENT_KEY) { console.error('missing INNGEST_EVENT_KEY. Workflows will not run.'); diff --git a/libs/inngest/src/index.ts b/libs/inngest/src/index.ts index 54f6059..0d4510c 100644 --- a/libs/inngest/src/index.ts +++ b/libs/inngest/src/index.ts @@ -43,6 +43,7 @@ import { handleUpdateUserProfile, handleWDCIntakeSubmit, } from './integrations/website/steps.js'; +import { tagSubscriber } from './integrations/kit/steps.js'; export { inngest } from './client.js'; @@ -94,4 +95,5 @@ export const functions: any[] = [ handleLWJIntake, handleUpdateUserProfile, handleWDCIntakeSubmit, + tagSubscriber, ]; diff --git a/libs/inngest/src/integrations/kit/index.ts b/libs/inngest/src/integrations/kit/index.ts new file mode 100644 index 0000000..4132733 --- /dev/null +++ b/libs/inngest/src/integrations/kit/index.ts @@ -0,0 +1 @@ +export { schema } from './types.js'; diff --git a/libs/inngest/src/integrations/kit/steps.ts b/libs/inngest/src/integrations/kit/steps.ts new file mode 100644 index 0000000..45692b5 --- /dev/null +++ b/libs/inngest/src/integrations/kit/steps.ts @@ -0,0 +1,18 @@ +import { inngest } from '../../client.js'; +import { tagSubscriber as tagSubscriberKit } from '@codetv/kit'; + +export const tagSubscriber = inngest.createFunction( + { id: 'kit/subscriber.tag.add' }, + { event: 'kit/subscriber.tag.add' }, + async function ({ + event, + step, + }: { + event: any; + step: any; + }): Promise<{ subscription: any }> { + return await step.run('tag-subscriber', async () => { + return await tagSubscriberKit(event.data.email, event.data.tagName); + }); + }, +); diff --git a/libs/inngest/src/integrations/kit/types.ts b/libs/inngest/src/integrations/kit/types.ts new file mode 100644 index 0000000..ccec006 --- /dev/null +++ b/libs/inngest/src/integrations/kit/types.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const schema = { + 'kit/subscriber.tag.add': { + data: z.object({ + email: z.string(), + tag: z.string(), + }), + }, +}; \ No newline at end of file diff --git a/libs/inngest/src/integrations/sanity/steps.ts b/libs/inngest/src/integrations/sanity/steps.ts index d8d4649..b9c1531 100644 --- a/libs/inngest/src/integrations/sanity/steps.ts +++ b/libs/inngest/src/integrations/sanity/steps.ts @@ -15,7 +15,7 @@ import { inngest } from '../../client.js'; export const getCurrentActiveHackathon = inngest.createFunction( { id: 'sanity/hackathon.get-current-active' }, { event: 'sanity/hackathon.get-current-active' }, - async function ({ event }): Promise<{ _id: string } | null> { + async function ({ event }): Promise<{ _id: string; slug: string } | null> { return await getActiveHackathon(); }, ); diff --git a/libs/inngest/src/integrations/website/steps.ts b/libs/inngest/src/integrations/website/steps.ts index 85f506c..8fd7940 100644 --- a/libs/inngest/src/integrations/website/steps.ts +++ b/libs/inngest/src/integrations/website/steps.ts @@ -21,6 +21,7 @@ import { updateUserRole, } from '../discord/steps.ts'; import { userGetById } from '../clerk/steps.ts'; +import { tagSubscriber } from '../kit/steps.ts'; export const handleUpdateUserProfile = inngest.createFunction( { id: 'codetv/user.profile.update' }, @@ -246,6 +247,14 @@ export const handleHackathonSubmission = inngest.createFunction( optOutSponsorship: event.data.optOutSponsorship, }, }), + // Tag subscriber in Kit + step.invoke('tag-subscriber', { + function: tagSubscriber, + data: { + email: event.data.email, + tagName: `wdc-hackathon-${hackathon?.slug}`, + }, + }), ]); return submission; diff --git a/libs/kit/src/index.ts b/libs/kit/src/index.ts index 90c557b..6e44a25 100644 --- a/libs/kit/src/index.ts +++ b/libs/kit/src/index.ts @@ -1,25 +1,29 @@ const ck_api = new URL('https://api.convertkit.com'); const api_key = process.env.KIT_SECRET_KEY; -if (api_key) { - ck_api.searchParams.set('api_secret', api_key); -} else { +if (!api_key) { console.error( 'KIT_SECRET_KEY is not set in env. Newsletter activities will not work.', ); } +function createApiUrl(pathname: string) { + const url = new URL(pathname, CK_BASE_URL); + url.searchParams.set('api_secret', api_key!); + return url; +} + export async function addSubscriber(first_name: string, email: string) { /** @see https://app.convertkit.com/forms/designers/1269192/edit */ - ck_api.pathname = '/v3/forms/1269192/subscribe'; + const url = createApiUrl('/v3/forms/1269192/subscribe'); - const response = await fetch(ck_api, { + const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ - api_key, + api_secret: api_key, first_name, email, }), @@ -44,10 +48,10 @@ export async function addSubscriber(first_name: string, email: string) { } export async function getSubscriberByEmail(email: string) { - ck_api.pathname = '/v3/subscribers'; - ck_api.searchParams.set('email_address', email); + const url = createApiUrl('/v3/subscribers'); + url.searchParams.set('email_address', email); - const res = await fetch(ck_api); + const res = await fetch(url); if (!res.ok) { console.error(res.statusText); @@ -59,3 +63,56 @@ export async function getSubscriberByEmail(email: string) { return data.subscribers.at(0); } + +export async function getTags() { + const url = createApiUrl('/v3/tags'); + + const res = await fetch(url); + + if (!res.ok) { + console.error(res.statusText); + throw new Error('Error fetching tags'); + } + + const data = (await res.json()) as any; + + return data.tags as Array<{ id: number; name: string }>; +} + +export async function tagSubscriber(email: string, tagName: string) { + if (!tagName) { + throw new Error('tagName is required to tag a subscriber'); + } + + // Look up the tag ID by name + const tags = await getTags(); + const tag = tags.find((t) => t.name?.toLowerCase() === tagName.toLowerCase()); + + if (!tag) { + throw new Error(`Tag "${tagName}" not found in Kit`); + } + + const url = createApiUrl(`/v3/tags/${tag.id}/subscribe`); + + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify({ + api_secret: api_key, + email, + }), + }); + + if (!res.ok) { + const errorText = await res.text(); + console.error(`Error tagging subscriber: ${res.status} ${res.statusText}`); + console.error(errorText); + throw new Error(`Error tagging subscriber: ${res.statusText}`); + } + + const data = (await res.json()) as any; + + return data.subscription; +} \ No newline at end of file diff --git a/libs/kit/tsconfig.lib.json b/libs/kit/tsconfig.lib.json index 911ba65..83847af 100644 --- a/libs/kit/tsconfig.lib.json +++ b/libs/kit/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "allowImportingTsExtensions": true, "baseUrl": ".", "rootDir": "src", "outDir": "dist", diff --git a/libs/sanity/src/index.ts b/libs/sanity/src/index.ts index 20808f3..b804d42 100644 --- a/libs/sanity/src/index.ts +++ b/libs/sanity/src/index.ts @@ -619,7 +619,8 @@ const hackathonBySlugQuery = groq` const activeHackathonQuery = groq` *[_type == "hackathon" && hidden != "hidden" && dateTime(pubDate) <= dateTime(now()) && dateTime(deadline) > dateTime(now())] | order(pubDate desc)[0] { - _id + _id, + 'slug': slug.current, } `; @@ -813,11 +814,10 @@ export async function getHackathonBySlug(params: { slug: string }) { } export async function getActiveHackathon() { - const hackathon = await client.fetch<{ _id: string } | null>( - activeHackathonQuery, - {}, - { useCdn: true }, - ); + const hackathon = await client.fetch<{ + _id: string; + slug: string; + } | null>(activeHackathonQuery, {}, { useCdn: true }); if (!hackathon) { return null;