volleyballsimtypes 0.0.478 → 0.0.479
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/src/service/index.d.ts +1 -0
- package/dist/cjs/src/service/index.js +1 -0
- package/dist/cjs/src/service/notification/catalog/en.json +41 -0
- package/dist/cjs/src/service/notification/catalog/es.json +41 -0
- package/dist/cjs/src/service/notification/catalog/fr.json +41 -0
- package/dist/cjs/src/service/notification/catalog/index.d.ts +3 -0
- package/dist/cjs/src/service/notification/catalog/index.js +22 -0
- package/dist/cjs/src/service/notification/catalog/it.json +41 -0
- package/dist/cjs/src/service/notification/catalog/pl.json +41 -0
- package/dist/cjs/src/service/notification/catalog/pt-BR.json +41 -0
- package/dist/cjs/src/service/notification/catalog/tr.json +41 -0
- package/dist/cjs/src/service/notification/index.d.ts +5 -0
- package/dist/cjs/src/service/notification/index.js +23 -0
- package/dist/cjs/src/service/notification/notification-type.d.ts +6 -0
- package/dist/cjs/src/service/notification/notification-type.js +30 -0
- package/dist/cjs/src/service/notification/notification.test.d.ts +1 -0
- package/dist/cjs/src/service/notification/notification.test.js +128 -0
- package/dist/cjs/src/service/notification/payloads.d.ts +34 -0
- package/dist/cjs/src/service/notification/payloads.js +5 -0
- package/dist/cjs/src/service/notification/registry.d.ts +13 -0
- package/dist/cjs/src/service/notification/registry.js +112 -0
- package/dist/cjs/src/service/notification/render.d.ts +10 -0
- package/dist/cjs/src/service/notification/render.js +64 -0
- package/dist/esm/src/service/index.d.ts +1 -0
- package/dist/esm/src/service/index.js +1 -0
- package/dist/esm/src/service/notification/catalog/en.json +41 -0
- package/dist/esm/src/service/notification/catalog/es.json +41 -0
- package/dist/esm/src/service/notification/catalog/fr.json +41 -0
- package/dist/esm/src/service/notification/catalog/index.d.ts +3 -0
- package/dist/esm/src/service/notification/catalog/index.js +16 -0
- package/dist/esm/src/service/notification/catalog/it.json +41 -0
- package/dist/esm/src/service/notification/catalog/pl.json +41 -0
- package/dist/esm/src/service/notification/catalog/pt-BR.json +41 -0
- package/dist/esm/src/service/notification/catalog/tr.json +41 -0
- package/dist/esm/src/service/notification/index.d.ts +5 -0
- package/dist/esm/src/service/notification/index.js +5 -0
- package/dist/esm/src/service/notification/notification-type.d.ts +6 -0
- package/dist/esm/src/service/notification/notification-type.js +26 -0
- package/dist/esm/src/service/notification/notification.test.d.ts +1 -0
- package/dist/esm/src/service/notification/notification.test.js +126 -0
- package/dist/esm/src/service/notification/payloads.d.ts +34 -0
- package/dist/esm/src/service/notification/payloads.js +4 -0
- package/dist/esm/src/service/notification/registry.d.ts +13 -0
- package/dist/esm/src/service/notification/registry.js +109 -0
- package/dist/esm/src/service/notification/render.d.ts +10 -0
- package/dist/esm/src/service/notification/render.js +58 -0
- package/package.json +6 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { NOTIFICATION_TYPES, NOTIFICATION_LOCALES } from './notification-type';
|
|
3
|
+
import { NOTIFICATION_REGISTRY } from './registry';
|
|
4
|
+
import { NOTIFICATION_CATALOG } from './catalog';
|
|
5
|
+
import { renderPush, notificationPath, interpolate, translateNotification } from './render';
|
|
6
|
+
function readNested(node, dotKey) {
|
|
7
|
+
let current = node;
|
|
8
|
+
for (const part of dotKey.split('.')) {
|
|
9
|
+
if (typeof current !== 'object' || current == null)
|
|
10
|
+
return undefined;
|
|
11
|
+
current = current[part];
|
|
12
|
+
}
|
|
13
|
+
return typeof current === 'string' ? current : undefined;
|
|
14
|
+
}
|
|
15
|
+
function placeholders(value) {
|
|
16
|
+
return (value.match(/\{\{(\w+)\}\}/g) ?? []).sort((a, b) => a.localeCompare(b));
|
|
17
|
+
}
|
|
18
|
+
function flattenStrings(node, prefix = '') {
|
|
19
|
+
const out = [];
|
|
20
|
+
if (typeof node !== 'object' || node == null)
|
|
21
|
+
return out;
|
|
22
|
+
for (const [key, value] of Object.entries(node)) {
|
|
23
|
+
if (typeof value === 'string')
|
|
24
|
+
out.push([prefix + key, value]);
|
|
25
|
+
else if (typeof value === 'object' && value != null)
|
|
26
|
+
out.push(...flattenStrings(value, `${prefix}${key}.`));
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
describe('notification registry + catalog completeness', () => {
|
|
31
|
+
it('has exactly one descriptor per declared type', () => {
|
|
32
|
+
expect(Object.keys(NOTIFICATION_REGISTRY).sort((a, b) => a.localeCompare(b)))
|
|
33
|
+
.toEqual([...NOTIFICATION_TYPES].sort((a, b) => a.localeCompare(b)));
|
|
34
|
+
});
|
|
35
|
+
it('resolves every descriptor catalog key to a non-empty string in all 7 locales', () => {
|
|
36
|
+
for (const type of NOTIFICATION_TYPES) {
|
|
37
|
+
for (const key of NOTIFICATION_REGISTRY[type].catalogKeys) {
|
|
38
|
+
for (const locale of NOTIFICATION_LOCALES) {
|
|
39
|
+
const value = readNested(NOTIFICATION_CATALOG[locale], key);
|
|
40
|
+
expect([locale, key, (value ?? '').length > 0]).toEqual([locale, key, true]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
it('ships an identical key set across all 7 locales (no drift)', () => {
|
|
46
|
+
const enKeys = flattenStrings(NOTIFICATION_CATALOG.en).map(([key]) => key).sort((a, b) => a.localeCompare(b));
|
|
47
|
+
for (const locale of NOTIFICATION_LOCALES) {
|
|
48
|
+
const keys = flattenStrings(NOTIFICATION_CATALOG[locale]).map(([key]) => key).sort((a, b) => a.localeCompare(b));
|
|
49
|
+
expect([locale, keys]).toEqual([locale, enKeys]);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
it('keeps placeholders identical across locales', () => {
|
|
53
|
+
for (const [key, enValue] of flattenStrings(NOTIFICATION_CATALOG.en)) {
|
|
54
|
+
const expected = placeholders(enValue);
|
|
55
|
+
for (const locale of NOTIFICATION_LOCALES) {
|
|
56
|
+
const value = readNested(NOTIFICATION_CATALOG[locale], key);
|
|
57
|
+
if (value == null)
|
|
58
|
+
continue;
|
|
59
|
+
expect([locale, key, placeholders(value)]).toEqual([locale, key, expected]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
it('contains no em or en dashes', () => {
|
|
64
|
+
for (const locale of NOTIFICATION_LOCALES) {
|
|
65
|
+
for (const [key, value] of flattenStrings(NOTIFICATION_CATALOG[locale])) {
|
|
66
|
+
expect([locale, key, value.includes('—') || value.includes('–')]).toEqual([locale, key, false]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('renderPush', () => {
|
|
72
|
+
it('renders a localized MATCH_RESULT win with a named opponent', () => {
|
|
73
|
+
const payload = { matchId: 'm1', result: 'WIN', side: 'HOME', awayTeam: { name: 'Rivals' } };
|
|
74
|
+
const en = renderPush('MATCH_RESULT', payload, 'en');
|
|
75
|
+
expect(en?.title).toBe('Match result');
|
|
76
|
+
expect(en?.body).toBe('You won against Rivals');
|
|
77
|
+
expect(en?.data.path).toBe('/match/m1');
|
|
78
|
+
expect(renderPush('MATCH_RESULT', payload, 'es')?.body).toBe('Ganaste contra Rivals');
|
|
79
|
+
});
|
|
80
|
+
it('uses the unnamed variant and root path when the opponent is unknown', () => {
|
|
81
|
+
const en = renderPush('MATCH_RESULT', { result: 'LOSS' }, 'en');
|
|
82
|
+
expect(en?.body).toBe('You lost your match');
|
|
83
|
+
expect(en?.data.path).toBe('/');
|
|
84
|
+
});
|
|
85
|
+
it('interpolates the injured player name and links to the infirmary', () => {
|
|
86
|
+
const en = renderPush('INJURY', { playerNickname: 'Ace', teamId: 't1' }, 'en');
|
|
87
|
+
expect(en?.title).toBe('Player injured');
|
|
88
|
+
expect(en?.body).toContain('Ace');
|
|
89
|
+
expect(en?.data.path).toBe('/infirmary');
|
|
90
|
+
});
|
|
91
|
+
it('uses a generic subject when the injury payload has no player name', () => {
|
|
92
|
+
const en = renderPush('INJURY', { teamId: 't1' }, 'en');
|
|
93
|
+
expect(en?.body.startsWith('A player')).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
it('returns undefined for in-app-only types (COMPENSATION)', () => {
|
|
96
|
+
expect(renderPush('COMPENSATION', { currency: 100 }, 'en')).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
it('falls back to generic copy and root path for an unknown type', () => {
|
|
99
|
+
const en = renderPush('SOMETHING_NEW', {}, 'en');
|
|
100
|
+
expect(en?.title).toBe('World Aces');
|
|
101
|
+
expect(en?.body).toBe('You have a new notification.');
|
|
102
|
+
expect(en?.data.path).toBe('/');
|
|
103
|
+
});
|
|
104
|
+
it('serializes the payload into the FCM data block', () => {
|
|
105
|
+
const en = renderPush('GRANT_RECEIVED', { pullType: 'MYTHIC' }, 'en');
|
|
106
|
+
expect(en?.data.type).toBe('GRANT_RECEIVED');
|
|
107
|
+
expect(JSON.parse(en?.data.payload ?? '{}')).toEqual({ pullType: 'MYTHIC' });
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('translateNotification + interpolate + notificationPath', () => {
|
|
111
|
+
it('falls back to English then to the raw key', () => {
|
|
112
|
+
expect(translateNotification('fr', 'promotion.title')).toBe('Promotion');
|
|
113
|
+
expect(translateNotification('en', 'does.not.exist')).toBe('does.not.exist');
|
|
114
|
+
});
|
|
115
|
+
it('replaces placeholders and drops unknown ones', () => {
|
|
116
|
+
expect(interpolate('Hi {{name}}!', { name: 'X' })).toBe('Hi X!');
|
|
117
|
+
expect(interpolate('Hi {{name}}!', {})).toBe('Hi !');
|
|
118
|
+
});
|
|
119
|
+
it('derives the deep-link path per type', () => {
|
|
120
|
+
expect(notificationPath('SEASON_END', { leagueId: 'l1' })).toBe('/league/l1');
|
|
121
|
+
expect(notificationPath('SEASON_END', {})).toBe('/league');
|
|
122
|
+
expect(notificationPath('PROMOTION', { teamId: 't1' })).toBe('/team/t1');
|
|
123
|
+
expect(notificationPath('GRANT_RECEIVED', {})).toBe('/recruit');
|
|
124
|
+
expect(notificationPath('COMPENSATION', {})).toBe('/');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface MatchResultTeam {
|
|
2
|
+
id?: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface MatchResultPayload {
|
|
6
|
+
matchId?: string;
|
|
7
|
+
homeTeam?: MatchResultTeam;
|
|
8
|
+
awayTeam?: MatchResultTeam;
|
|
9
|
+
result?: 'WIN' | 'LOSS';
|
|
10
|
+
side?: 'HOME' | 'AWAY';
|
|
11
|
+
}
|
|
12
|
+
export interface InjuryPayload {
|
|
13
|
+
playerId?: string;
|
|
14
|
+
playerName?: string;
|
|
15
|
+
playerNickname?: string;
|
|
16
|
+
teamId?: string;
|
|
17
|
+
matchId?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface InjuryRecoveredPayload {
|
|
20
|
+
playerId?: string;
|
|
21
|
+
playerName?: string;
|
|
22
|
+
playerNickname?: string;
|
|
23
|
+
teamId?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface SeasonEndPayload {
|
|
26
|
+
competitionId?: string;
|
|
27
|
+
leagueId?: string;
|
|
28
|
+
position?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface PromotionPayload {
|
|
31
|
+
teamId?: string;
|
|
32
|
+
amount?: number;
|
|
33
|
+
}
|
|
34
|
+
export type NotificationPayload = MatchResultPayload | InjuryPayload | InjuryRecoveredPayload | SeasonEndPayload | PromotionPayload | Record<string, unknown>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Typed payload shapes per notification type, for call-site safety in the Sim/API. The DB `Notification.payload`
|
|
2
|
+
// column is jsonb (free-form), so the registry reads payloads defensively (Record<string, unknown>); these
|
|
3
|
+
// interfaces document the intended shape and can be used by emitters when constructing the payload.
|
|
4
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type NotificationType } from './notification-type';
|
|
2
|
+
export type CatalogKeyResolver = string | ((payload: Record<string, unknown>) => string);
|
|
3
|
+
export interface NotificationDescriptor {
|
|
4
|
+
emitsPush: boolean;
|
|
5
|
+
deepLink?: (payload: Record<string, unknown>) => string;
|
|
6
|
+
pushTitleKey?: CatalogKeyResolver;
|
|
7
|
+
pushBodyKey?: CatalogKeyResolver;
|
|
8
|
+
inAppMessageKey?: CatalogKeyResolver;
|
|
9
|
+
richRender?: NotificationType;
|
|
10
|
+
params?: (payload: Record<string, unknown>) => Record<string, string>;
|
|
11
|
+
catalogKeys: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare const NOTIFICATION_REGISTRY: Record<NotificationType, NotificationDescriptor>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
function readString(value) {
|
|
2
|
+
return typeof value === 'string' && value !== '' ? value : null;
|
|
3
|
+
}
|
|
4
|
+
function teamPath(payload) {
|
|
5
|
+
const teamId = readString(payload.teamId);
|
|
6
|
+
return teamId != null ? `/team/${encodeURIComponent(teamId)}` : '/';
|
|
7
|
+
}
|
|
8
|
+
// MATCH_RESULT copy is recipient-relative: `result` is WIN/LOSS from the recipient's view and `side` marks
|
|
9
|
+
// which team is theirs, so the OTHER team is the opponent. When side/names are missing, do not name a winner.
|
|
10
|
+
function matchOpponent(payload) {
|
|
11
|
+
const homeTeam = typeof payload.homeTeam === 'object' && payload.homeTeam != null ? payload.homeTeam : {};
|
|
12
|
+
const awayTeam = typeof payload.awayTeam === 'object' && payload.awayTeam != null ? payload.awayTeam : {};
|
|
13
|
+
const homeName = readString(homeTeam.name);
|
|
14
|
+
const awayName = readString(awayTeam.name);
|
|
15
|
+
if (payload.side === 'HOME')
|
|
16
|
+
return awayName;
|
|
17
|
+
if (payload.side === 'AWAY')
|
|
18
|
+
return homeName;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
function injuredName(payload) {
|
|
22
|
+
// Matches the old Sim injuredPlayerName fallback so a name-less payload never renders a subjectless sentence.
|
|
23
|
+
return readString(payload.playerNickname) ?? readString(payload.playerName) ?? 'A player';
|
|
24
|
+
}
|
|
25
|
+
export const NOTIFICATION_REGISTRY = {
|
|
26
|
+
MATCH_RESULT: {
|
|
27
|
+
emitsPush: true,
|
|
28
|
+
deepLink: (p) => {
|
|
29
|
+
const matchId = readString(p.matchId);
|
|
30
|
+
return matchId != null ? `/match/${encodeURIComponent(matchId)}` : '/';
|
|
31
|
+
},
|
|
32
|
+
pushTitleKey: 'matchResult.title',
|
|
33
|
+
pushBodyKey: (p) => {
|
|
34
|
+
const outcome = p.result === 'WIN' ? 'won' : 'lost';
|
|
35
|
+
const named = matchOpponent(p) != null ? 'Named' : 'Unnamed';
|
|
36
|
+
return `matchResult.${outcome}${named}`;
|
|
37
|
+
},
|
|
38
|
+
params: (p) => ({ opponent: matchOpponent(p) ?? '' }),
|
|
39
|
+
richRender: 'MATCH_RESULT',
|
|
40
|
+
catalogKeys: ['matchResult.title', 'matchResult.wonNamed', 'matchResult.wonUnnamed', 'matchResult.lostNamed', 'matchResult.lostUnnamed']
|
|
41
|
+
},
|
|
42
|
+
INJURY: {
|
|
43
|
+
emitsPush: true,
|
|
44
|
+
deepLink: () => '/infirmary',
|
|
45
|
+
pushTitleKey: 'injury.title',
|
|
46
|
+
pushBodyKey: 'injury.body',
|
|
47
|
+
inAppMessageKey: 'injury.body',
|
|
48
|
+
params: (p) => ({ name: injuredName(p) }),
|
|
49
|
+
catalogKeys: ['injury.title', 'injury.body']
|
|
50
|
+
},
|
|
51
|
+
INJURY_RECOVERED: {
|
|
52
|
+
emitsPush: true,
|
|
53
|
+
deepLink: teamPath,
|
|
54
|
+
pushTitleKey: 'injuryRecovered.title',
|
|
55
|
+
pushBodyKey: 'injuryRecovered.body',
|
|
56
|
+
inAppMessageKey: 'injuryRecovered.body',
|
|
57
|
+
params: (p) => ({ name: injuredName(p) }),
|
|
58
|
+
catalogKeys: ['injuryRecovered.title', 'injuryRecovered.body']
|
|
59
|
+
},
|
|
60
|
+
SEASON_END: {
|
|
61
|
+
emitsPush: true,
|
|
62
|
+
deepLink: (p) => {
|
|
63
|
+
const leagueId = readString(p.leagueId);
|
|
64
|
+
return leagueId != null ? `/league/${encodeURIComponent(leagueId)}` : '/league';
|
|
65
|
+
},
|
|
66
|
+
pushTitleKey: 'seasonEnd.title',
|
|
67
|
+
pushBodyKey: 'seasonEnd.body',
|
|
68
|
+
inAppMessageKey: 'seasonEnd.body',
|
|
69
|
+
catalogKeys: ['seasonEnd.title', 'seasonEnd.body']
|
|
70
|
+
},
|
|
71
|
+
GRANT_RECEIVED: {
|
|
72
|
+
emitsPush: true,
|
|
73
|
+
deepLink: () => '/recruit',
|
|
74
|
+
pushTitleKey: 'grant.title',
|
|
75
|
+
pushBodyKey: 'grant.body',
|
|
76
|
+
inAppMessageKey: 'grant.body',
|
|
77
|
+
catalogKeys: ['grant.title', 'grant.body']
|
|
78
|
+
},
|
|
79
|
+
PROMOTION: {
|
|
80
|
+
emitsPush: true,
|
|
81
|
+
deepLink: teamPath,
|
|
82
|
+
pushTitleKey: 'promotion.title',
|
|
83
|
+
pushBodyKey: 'promotion.body',
|
|
84
|
+
inAppMessageKey: 'promotion.body',
|
|
85
|
+
catalogKeys: ['promotion.title', 'promotion.body']
|
|
86
|
+
},
|
|
87
|
+
DAILY_LOGIN: {
|
|
88
|
+
emitsPush: true,
|
|
89
|
+
deepLink: () => '/recruit',
|
|
90
|
+
pushTitleKey: 'dailyLogin.title',
|
|
91
|
+
pushBodyKey: 'dailyLogin.body',
|
|
92
|
+
inAppMessageKey: 'dailyLogin.body',
|
|
93
|
+
catalogKeys: ['dailyLogin.title', 'dailyLogin.body']
|
|
94
|
+
},
|
|
95
|
+
WELCOME: {
|
|
96
|
+
emitsPush: true,
|
|
97
|
+
deepLink: () => '/recruit',
|
|
98
|
+
pushTitleKey: 'welcome.title',
|
|
99
|
+
pushBodyKey: 'welcome.body',
|
|
100
|
+
richRender: 'WELCOME',
|
|
101
|
+
catalogKeys: ['welcome.title', 'welcome.body']
|
|
102
|
+
},
|
|
103
|
+
// In-app only (no push); the UI keeps a bespoke renderer (amount formatting) and its own `compensation` label.
|
|
104
|
+
COMPENSATION: {
|
|
105
|
+
emitsPush: false,
|
|
106
|
+
richRender: 'COMPENSATION',
|
|
107
|
+
catalogKeys: []
|
|
108
|
+
}
|
|
109
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type NotificationLocale } from './notification-type';
|
|
2
|
+
export interface RenderedPush {
|
|
3
|
+
title: string;
|
|
4
|
+
body: string;
|
|
5
|
+
data: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export declare function interpolate(template: string, params: Record<string, string>): string;
|
|
8
|
+
export declare function translateNotification(locale: NotificationLocale, key: string, params?: Record<string, string>): string;
|
|
9
|
+
export declare function renderPush(type: string, payload: Record<string, unknown>, locale: NotificationLocale): RenderedPush | undefined;
|
|
10
|
+
export declare function notificationPath(type: string, payload: Record<string, unknown>): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { DEFAULT_NOTIFICATION_LOCALE } from './notification-type';
|
|
2
|
+
import { NOTIFICATION_REGISTRY } from './registry';
|
|
3
|
+
import { NOTIFICATION_CATALOG } from './catalog';
|
|
4
|
+
function readNested(node, dotKey) {
|
|
5
|
+
let current = node;
|
|
6
|
+
for (const part of dotKey.split('.')) {
|
|
7
|
+
if (typeof current !== 'object' || current == null)
|
|
8
|
+
return undefined;
|
|
9
|
+
current = current[part];
|
|
10
|
+
}
|
|
11
|
+
return typeof current === 'string' ? current : undefined;
|
|
12
|
+
}
|
|
13
|
+
function lookupCatalog(locale, dotKey) {
|
|
14
|
+
return readNested(NOTIFICATION_CATALOG[locale], dotKey) ??
|
|
15
|
+
readNested(NOTIFICATION_CATALOG[DEFAULT_NOTIFICATION_LOCALE], dotKey) ??
|
|
16
|
+
dotKey;
|
|
17
|
+
}
|
|
18
|
+
function resolveKey(resolver, payload) {
|
|
19
|
+
if (resolver == null)
|
|
20
|
+
return undefined;
|
|
21
|
+
return typeof resolver === 'function' ? resolver(payload) : resolver;
|
|
22
|
+
}
|
|
23
|
+
export function interpolate(template, params) {
|
|
24
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => params[key] ?? '');
|
|
25
|
+
}
|
|
26
|
+
// Resolve one catalog key to a localized, interpolated string. Missing key/locale falls back to English,
|
|
27
|
+
// then to the raw key. Exposed so consumers can render notification copy off the shared catalog too.
|
|
28
|
+
export function translateNotification(locale, key, params = {}) {
|
|
29
|
+
return interpolate(lookupCatalog(locale, key), params);
|
|
30
|
+
}
|
|
31
|
+
// Build the localized push payload for a notification. Returns undefined for in-app-only types (emitsPush
|
|
32
|
+
// false, e.g. COMPENSATION). Unknown/future types fall back to the generic copy and the root path, matching
|
|
33
|
+
// the previous hardcoded fallback. Callers pass the recipient's locale (normalizeNotificationLocale first).
|
|
34
|
+
export function renderPush(type, payload, locale) {
|
|
35
|
+
const buildData = (path) => ({ type, payload: JSON.stringify(payload), path });
|
|
36
|
+
const descriptor = NOTIFICATION_REGISTRY[type];
|
|
37
|
+
if (descriptor == null) {
|
|
38
|
+
return {
|
|
39
|
+
title: translateNotification(locale, 'generic.title'),
|
|
40
|
+
body: translateNotification(locale, 'generic.body'),
|
|
41
|
+
data: buildData('/')
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (!descriptor.emitsPush)
|
|
45
|
+
return undefined;
|
|
46
|
+
const params = descriptor.params?.(payload) ?? {};
|
|
47
|
+
const titleKey = resolveKey(descriptor.pushTitleKey, payload) ?? 'generic.title';
|
|
48
|
+
const bodyKey = resolveKey(descriptor.pushBodyKey, payload) ?? 'generic.body';
|
|
49
|
+
return {
|
|
50
|
+
title: translateNotification(locale, titleKey, params),
|
|
51
|
+
body: translateNotification(locale, bodyKey, params),
|
|
52
|
+
data: buildData(descriptor.deepLink?.(payload) ?? '/')
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Deep-link path for a type/payload, used for both the push data.path and the in-app click target.
|
|
56
|
+
export function notificationPath(type, payload) {
|
|
57
|
+
return NOTIFICATION_REGISTRY[type]?.deepLink?.(payload) ?? '/';
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "volleyballsimtypes",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.479",
|
|
4
4
|
"description": "vbsim types",
|
|
5
5
|
"main": "./dist/cjs/src/index.js",
|
|
6
6
|
"module": "./dist/esm/src/index.js",
|
|
@@ -37,6 +37,11 @@
|
|
|
37
37
|
"import": "./dist/esm/src/data/common/index.js",
|
|
38
38
|
"require": "./dist/cjs/src/data/common/index.js"
|
|
39
39
|
},
|
|
40
|
+
"./notification": {
|
|
41
|
+
"types": "./dist/esm/src/service/notification/index.d.ts",
|
|
42
|
+
"import": "./dist/esm/src/service/notification/index.js",
|
|
43
|
+
"require": "./dist/cjs/src/service/notification/index.js"
|
|
44
|
+
},
|
|
40
45
|
"./stat-config": {
|
|
41
46
|
"types": "./dist/esm/src/stat-config/index.d.ts",
|
|
42
47
|
"import": "./dist/esm/src/stat-config/index.js",
|