Spaces:
Sleeping
Sleeping
| import semver from 'semver'; | |
| import { isAddonEnabled } from '../services/adminService.js'; | |
| import type { NoticeCondition, SystemNotice } from './types.js'; | |
| interface ConditionContext { | |
| user: { login_count: number; first_seen_version: string; role: string; noTrips: number }; | |
| currentAppVersion: string; | |
| now: Date; | |
| } | |
| // Custom predicate registry — extensible without modifying this file | |
| const customPredicates = new Map<string, (ctx: ConditionContext) => boolean>(); | |
| export function registerPredicate(id: string, fn: (ctx: ConditionContext) => boolean): void { | |
| customPredicates.set(id, fn); | |
| } | |
| function evaluateOne(condition: NoticeCondition, ctx: ConditionContext): boolean { | |
| switch (condition.kind) { | |
| case 'always': | |
| return true; | |
| case 'firstLogin': | |
| // login_count is incremented during login, so on the FIRST post-login fetch it's 1. | |
| return ctx.user.login_count <= 1; | |
| case 'noTrips': | |
| return ctx.user.noTrips === 0; | |
| case 'existingUserBeforeVersion': { | |
| // Show to users who existed BEFORE this version was released. | |
| // Backfilled users have first_seen_version='0.0.0', so all pass semver.lt. | |
| const userVersion = semver.valid(ctx.user.first_seen_version) ?? '0.0.0'; | |
| const noticeVersion = semver.valid(condition.version); | |
| if (!noticeVersion) return false; | |
| // Strip prerelease/build metadata so '3.0.0-pre.42' is treated as '3.0.0'. | |
| const appVersion = semver.coerce(ctx.currentAppVersion)?.version ?? '0.0.0'; | |
| return ( | |
| semver.lt(userVersion, noticeVersion) && | |
| semver.gte(appVersion, noticeVersion) | |
| ); | |
| } | |
| case 'dateWindow': { | |
| const start = new Date(condition.startsAt); | |
| const end = condition.endsAt ? new Date(condition.endsAt) : null; | |
| return ctx.now >= start && (end === null || ctx.now <= end); | |
| } | |
| case 'role': | |
| return condition.roles.includes(ctx.user.role as 'admin' | 'user'); | |
| case 'addonEnabled': | |
| return isAddonEnabled(condition.addonId); | |
| case 'custom': { | |
| const fn = customPredicates.get(condition.id); | |
| if (!fn) { | |
| console.warn(`[systemNotices] unknown custom predicate: "${condition.id}"`); | |
| return false; | |
| } | |
| return fn(ctx); | |
| } | |
| default: | |
| return false; | |
| } | |
| } | |
| /** Returns true only if ALL conditions pass (AND logic). */ | |
| export function evaluate(notice: SystemNotice, ctx: ConditionContext): boolean { | |
| return notice.conditions.every(c => evaluateOne(c, ctx)); | |
| } | |
| export type { ConditionContext }; | |