File size: 2,529 Bytes
57a889c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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 };