UNPKG

@coursebuilder/core

Version:

Core package for Course Builder

411 lines (375 loc) 9.45 kB
import { z } from 'zod' import { find, isEmpty } from '@coursebuilder/nodash' import { Cookie } from '../lib/utils/cookie' import { CookieOption } from '../types' import { EmailListConfig, EmailListConsumerConfig, EmailListSubscribeOptions, } from './index' export default function ConvertkitProvider( options: EmailListConsumerConfig, ): EmailListConfig { return { id: 'convertkit', name: 'Convertkit', type: 'email-list', defaultListType: 'form', options, apiKey: options.apiKey, apiSecret: options.apiSecret, tagSubscriber: async ({ tag, subscriberId, }: { tag: string subscriberId: string }) => { const subscriber = await fetchSubscriber({ convertkitId: subscriberId, convertkitApiSecret: options.apiSecret, convertkitApiKey: options.apiKey, }) }, getSubscriberByEmail: async (email: string) => { console.log({ email }) if (!email) return null return await fetchSubscriber({ subscriberEmail: email, convertkitApiSecret: options.apiSecret, convertkitApiKey: options.apiKey, }) }, getSubscriber: async (subscriberId: string | null | CookieOption) => { if (!subscriberId) return null return await fetchSubscriber({ convertkitId: subscriberId, convertkitApiSecret: options.apiSecret, convertkitApiKey: options.apiKey, }) }, subscribeToList: async (subscribeOptions: EmailListSubscribeOptions) => { const { listId, user, listType, fields } = subscribeOptions if (!listId) { throw new Error('No listId provided') } const getEndpoint = () => { switch (listType) { case 'form': return `/forms/${listId}/subscribe` case 'sequence': return `/sequences/${listId}/subscribe` case 'tag': return `/tags/${listId}/subscribe` } } const subscriber = await subscribeToEndpoint({ endPoint: getEndpoint(), params: { email: user.email, first_name: user.name, fields: subscribeOptions.fields, }, convertkitApiKey: options.apiKey, }) const fullSubscriber = await fetchSubscriber({ convertkitId: subscriber.id.toString(), convertkitApiKey: options.apiKey, convertkitApiSecret: options.apiSecret, }) const fullSubscriberWithoutEmptyFields = filterNullFields(fullSubscriber) if (fields) { await setConvertkitSubscriberFields({ fields, subscriber: fullSubscriber, convertkitApiSecret: options.apiSecret, convertkitApiKey: options.apiKey, }) } return await fetchSubscriber({ convertkitId: subscriber.id.toString(), convertkitApiKey: options.apiKey, convertkitApiSecret: options.apiSecret, }) }, async updateSubscriberFields({ subscriberId, subscriberEmail, fields, }: { subscriberId?: string subscriberEmail?: string fields: Record<string, any> }) { const subscriber = await fetchSubscriber({ convertkitId: subscriberId, subscriberEmail, convertkitApiKey: options.apiKey, convertkitApiSecret: options.apiSecret, }) await setConvertkitSubscriberFields({ subscriber, fields, convertkitApiKey: options.apiKey, convertkitApiSecret: options.apiSecret, }) return await fetchSubscriber({ convertkitId: subscriberId, subscriberEmail, convertkitApiKey: options.apiKey, convertkitApiSecret: options.apiSecret, }) }, } } const hour = 3600000 export const oneYear = 365 * 24 * hour const TagSubscriberResponseSchema = z.object({ subscription: z.object({ subscriber: z.object({ id: z.string(), fields: z.record(z.string().nullable()).optional(), }), }), }) async function createConvertkitTag({ name, convertkitApiSecret, }: { name: string convertkitApiSecret: string }) { try { const response = await fetch(`${convertkitBaseUrl}/tags`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ api_secret: convertkitApiSecret, name, }), }) if (!response.ok) { console.error(`Failed to create tag: ${response.statusText}`) return null } const data = await response.json() console.log('Tag created successfully') return data } catch (error) { console.error('Error creating tag:', error) return null } } async function tagSubscriber({ email, tagId, convertkitApiKey, }: { email: string tagId: string convertkitApiKey: string }) { const url = `${convertkitBaseUrl}/tags/${tagId}/subscribe` return await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ email, api_key: convertkitApiKey, }), }) .then((res) => res.json()) .then((jsonRes: any) => { const result = TagSubscriberResponseSchema.safeParse(jsonRes) if (!result.success) { return undefined } return result.data.subscription.subscriber }) } export function getConvertkitSubscriberCookie(subscriber: any): Cookie[] { return subscriber ? [ { name: 'ck_subscriber', value: JSON.stringify(subscriber), options: { secure: true, httpOnly: true, path: '/', maxAge: oneYear, sameSite: 'lax', }, }, { name: process.env.NEXT_PUBLIC_CONVERTKIT_SUBSCRIBER_KEY || 'ck_subscriber_id', value: subscriber.id, options: { secure: true, httpOnly: true, path: '/', maxAge: 31556952, sameSite: 'lax', }, }, ] : [] } type Subscriber = Record<string, any> export function filterNullFields(obj: Subscriber): Subscriber { const filteredObj: Subscriber = {} for (const key in obj) { if (obj[key] !== null) { if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) { filteredObj[key] = filterNullFields(obj[key] as Subscriber) // Recursively filter nested objects } else { filteredObj[key] = obj[key] } } } return filteredObj } const convertkitBaseUrl = 'https://api.convertkit.com/v3/' export async function subscribeToEndpoint({ endPoint, params, convertkitApiKey, }: { endPoint?: string params: Record<string, any> convertkitApiKey: string }) { if (!endPoint) { throw new Error('No endPoint provided') } return await fetch(`${convertkitBaseUrl}${endPoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ ...params, api_key: convertkitApiKey, }), }) .then((res) => res.json()) .then( ({ subscription }: { subscription: { subscriber: { id: number } } }) => { return subscription.subscriber }, ) .catch((error) => { console.error(error) throw error }) } async function fetchSubscriber({ convertkitId, convertkitApiSecret, convertkitApiKey, subscriberEmail, }: { convertkitId?: string | number | CookieOption subscriberEmail?: string convertkitApiSecret: string convertkitApiKey: string }) { let subscriber if (convertkitId) { const subscriberUrl = `${convertkitBaseUrl}/subscribers/${convertkitId}?api_secret=${convertkitApiSecret}` subscriber = await fetch(subscriberUrl) .then((res) => res.json()) .then(({ subscriber }: any) => { return subscriber }) } if (!subscriber && subscriberEmail) { const tagsApiUrl = `${convertkitBaseUrl}subscribers?api_secret=${convertkitApiSecret}&email_address=${subscriberEmail.trim().toLowerCase()}` console.log({ tagsApiUrl }) subscriber = await fetch(tagsApiUrl) .then((res) => res.json()) .then((res: any) => { const subscribers = res.subscribers return subscribers[0] }) } if (isEmpty(subscriber)) return const tagsApiUrl = `${convertkitBaseUrl}/subscribers/${subscriber.id}/tags?api_key=${convertkitApiKey}` const tags = await fetch(tagsApiUrl).then((res) => res.json()) return { ...subscriber, tags } } export async function setConvertkitSubscriberFields({ fields, subscriber, convertkitApiSecret, convertkitApiKey, }: { subscriber: { id: string | number; fields: Record<string, string | null> } fields: Record<string, string> convertkitApiSecret: string convertkitApiKey: string }) { for (const field in fields) { await createConvertkitCustomField({ customField: field, subscriberId: subscriber.id.toString(), convertkitApiSecret, convertkitApiKey, }) } return await fetch(`${convertkitBaseUrl}/subscribers/${subscriber.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ api_secret: process.env.CONVERTKIT_API_SECRET, fields, }), }) } export async function createConvertkitCustomField({ customField, subscriberId, convertkitApiSecret, convertkitApiKey, }: { convertkitApiSecret: string convertkitApiKey: string customField: string subscriberId: string }) { try { const subscriber = await fetchSubscriber({ convertkitId: subscriberId, convertkitApiSecret, convertkitApiKey, }) const fieldExists = subscriber?.fields && !isEmpty( find(Object.keys(subscriber.fields), (field) => field === customField), ) if (!fieldExists) { await fetch(`${convertkitBaseUrl}/custom_fields`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8', }, body: JSON.stringify({ api_secret: convertkitApiSecret, label: customField, }), }) } } catch (e) { console.log({ e }) console.debug(`convertkit field not created: ${customField}`) } }