From 14a6dfa79657e575a30ac48cda1f6d57c35da2ac Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Wed, 15 Apr 2026 13:38:52 +0200 Subject: [PATCH] fix: marketing cta types --- __tests__/boot.ts | 1 + src/routes/boot.ts | 11 ++++-- src/schema/users.ts | 88 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/__tests__/boot.ts b/__tests__/boot.ts index e7b533d732..f1bc66c2f3 100644 --- a/__tests__/boot.ts +++ b/__tests__/boot.ts @@ -177,6 +177,7 @@ const LOGGED_IN_BODY = { }, }, marketingCta: null, + marketingCtaVariants: [], feeds: [], }; diff --git a/src/routes/boot.ts b/src/routes/boot.ts index 4e21333773..d3a329f12b 100644 --- a/src/routes/boot.ts +++ b/src/routes/boot.ts @@ -10,7 +10,11 @@ import { DataSource, Not, QueryRunner } from 'typeorm'; import { getBetterAuth } from '../betterAuth'; import { generateUUID } from '../ids'; import { generateSessionId, setTrackingId } from '../tracking'; -import { GQLUser, getMarketingCta } from '../schema/users'; +import { + GQLUser, + getMarketingCta, + getMarketingCtaVariants, +} from '../schema/users'; import { extractHandleFromUrl } from '../common/schema/socials'; import { Alerts, @@ -179,6 +183,7 @@ export type LoggedInBoot = BaseBoot & { }; accessToken?: AccessToken; marketingCta: MarketingCta | null; + marketingCtaVariants: string[]; }; export type FunnelLoggedInUser = GQLUser & { @@ -650,7 +655,7 @@ const loggedInBoot = async ({ visit, roles, extra, - [alerts, settings, marketingCta, user], + [alerts, settings, marketingCta, user, marketingCtaVariants], [ squads, lastBanner, @@ -673,6 +678,7 @@ const loggedInBoot = async ({ getSettings(queryRunner, userId), getMarketingCta(queryRunner, log, userId), getUser(queryRunner, userId), + getMarketingCtaVariants(queryRunner, userId), ]); }), queryReadReplica(con, async ({ queryRunner }) => { @@ -800,6 +806,7 @@ const loggedInBoot = async ({ accessToken, exp, marketingCta, + marketingCtaVariants, feeds, geo, ...extra, diff --git a/src/schema/users.ts b/src/schema/users.ts index 31d20b72e1..5843edf778 100644 --- a/src/schema/users.ts +++ b/src/schema/users.ts @@ -19,6 +19,7 @@ import { getAuthorPostStats, Invite, MarketingCta, + MarketingCtaStatus, Post, PostStats, Settings, @@ -1264,6 +1265,40 @@ export const typeDefs = /* GraphQL */ ` impressionsAds: Int! } + """ + Marketing CTA flags content + """ + type MarketingCtaFlags { + title: String! + description: String + image: String + tagText: String + tagColor: String + ctaUrl: String! + ctaText: String! + } + + """ + Marketing CTA platform targets + """ + type MarketingCtaTargets { + webapp: Boolean! + extension: Boolean! + ios: Boolean! + } + + """ + Marketing CTA shown to a user + """ + type MarketingCta { + campaignId: String! + createdAt: DateTime! + variant: String! + status: String! + flags: MarketingCtaFlags! + targets: MarketingCtaTargets! + } + extend type Query { """ Get user based on logged in session @@ -1490,6 +1525,11 @@ export const typeDefs = /* GraphQL */ ` Get daily impressions history for all posts authored by the authenticated user (last 45 days) """ userPostsAnalyticsHistory: [UserPostsAnalyticsHistoryNode!]! @auth + + """ + Get all active marketing CTAs the user qualifies for and hasn't dismissed, filtered by variant + """ + marketingCtasByVariant(variant: String!): [MarketingCta!]! @auth } ${toGQLEnum(UploadPreset, 'UploadPreset')} @@ -1903,6 +1943,27 @@ export const getMarketingCta = async ( return marketingCta || cachePrefillMarketingCta(con, userId); }; +export const getMarketingCtaVariants = async ( + con: DataSource | QueryRunner, + userId: string, +): Promise => { + if (!userId || systemUserIds.includes(userId)) { + return []; + } + + const rows = await con.manager + .getRepository(UserMarketingCta) + .createQueryBuilder('umc') + .innerJoin('umc.marketingCta', 'mc') + .select('DISTINCT mc.variant', 'variant') + .where('umc."userId" = :userId', { userId }) + .andWhere('umc."readAt" IS NULL') + .andWhere('mc.status = :status', { status: MarketingCtaStatus.Active }) + .getRawMany<{ variant: string }>(); + + return rows.map((row) => row.variant); +}; + const getUserStreakQuery = async ( id: string, ctx: Context, @@ -3024,6 +3085,33 @@ export const resolvers: IResolvers = { }), }); }, + marketingCtasByVariant: async ( + _, + { variant }: { variant: string }, + ctx: AuthContext, + ): Promise => { + const userMarketingCtas = await ctx.con + .getRepository(UserMarketingCta) + .createQueryBuilder('umc') + .innerJoinAndSelect('umc.marketingCta', 'mc') + .where('umc."userId" = :userId', { userId: ctx.userId }) + .andWhere('umc."readAt" IS NULL') + .andWhere('mc.status = :status', { + status: MarketingCtaStatus.Active, + }) + .andWhere('mc.variant = :variant', { variant }) + .getMany(); + + return userMarketingCtas + .map((umc) => umc.marketingCta) + .filter((mc): mc is MarketingCta => !!mc) + .map((mc) => { + if (mc.flags?.image) { + mc.flags.image = mapCloudinaryUrl(mc.flags.image); + } + return mc; + }); + }, }, Mutation: { clearImage: async (