mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-03 05:10:25 +03:00
fix(api): correct apns clear payload and badge handling (#2092)
This commit is contained in:
@@ -143,7 +143,7 @@ function buildApnsHeaders(params: {
|
||||
'apns-topic': params.topic,
|
||||
'apns-push-type': isClear ? APNS_PUSH_TYPE_BACKGROUND : APNS_PUSH_TYPE_ALERT,
|
||||
'apns-priority': isClear ? '5' : '10',
|
||||
'apns-expiration': String(Math.floor(Date.now() / 1000) + (isClear ? 300 : 86400)),
|
||||
'apns-expiration': String(Math.floor(Date.now() / 1000) + (isClear ? 3600 : 86400)),
|
||||
'content-type': 'application/json',
|
||||
};
|
||||
if (collapseId && Buffer.byteLength(collapseId) <= 64) {
|
||||
@@ -155,18 +155,11 @@ function buildApnsHeaders(params: {
|
||||
function buildApnsPayload(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
if (isClearNotificationPayload(payload)) {
|
||||
const data = isRecord(payload.data) ? payload.data : {};
|
||||
const badge = normalizeBadgeCount(data.badge_count);
|
||||
const aps: Record<string, unknown> = {
|
||||
'content-available': 1,
|
||||
};
|
||||
if (badge !== undefined) {
|
||||
aps.badge = badge;
|
||||
}
|
||||
return {
|
||||
...data,
|
||||
type: 'notification_clear',
|
||||
action: 'clear_channel',
|
||||
aps,
|
||||
aps: {'content-available': 1},
|
||||
};
|
||||
}
|
||||
const data = isRecord(payload.data) ? payload.data : {};
|
||||
@@ -177,16 +170,18 @@ function buildApnsPayload(payload: Record<string, unknown>): Record<string, unkn
|
||||
const channelId = optionalString(data.channel_id);
|
||||
const threadId =
|
||||
optionalString(data.notification_tag) ?? (channelId ? `channel:${channelId}` : undefined) ?? 'fluxer-message';
|
||||
const imageUrl = firstString([payload.image_url, notification.image, notification.icon, payload.icon]);
|
||||
const imageUrl = firstString([payload.image_url, notification.image]);
|
||||
const aps: Record<string, unknown> = {
|
||||
alert: {title, body},
|
||||
sound: APNS_DEFAULT_SOUND,
|
||||
badge,
|
||||
'thread-id': threadId,
|
||||
category: APNS_CATEGORY_MESSAGE,
|
||||
'interruption-level': 'active',
|
||||
'relevance-score': 0.5,
|
||||
};
|
||||
if (badge !== undefined) {
|
||||
aps.badge = badge;
|
||||
}
|
||||
if (imageUrl) {
|
||||
aps['mutable-content'] = 1;
|
||||
}
|
||||
@@ -292,13 +287,13 @@ function isPermanentApnsFailure(statusCode: number, reason: string): boolean {
|
||||
return reason === 'Unregistered';
|
||||
}
|
||||
|
||||
function normalizeBadgeCount(value: unknown): number {
|
||||
function normalizeBadgeCount(value: unknown): number | undefined {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return Math.max(0, Math.floor(value));
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isFinite(parsed) ? Math.max(0, parsed) : 0;
|
||||
return Number.isFinite(parsed) ? Math.max(0, parsed) : undefined;
|
||||
}
|
||||
return 0;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function firstString(values: Array<unknown>): string | undefined {
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('ApnsPushService', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
it('builds silent APNs clear payloads with badge when badge_count is present', () => {
|
||||
it('builds silent APNs clear payloads as pure background pushes', () => {
|
||||
const payload = ApnsPushServiceTestHooks.buildApnsPayload({
|
||||
type: 'notification_clear',
|
||||
action: 'clear_channel',
|
||||
@@ -56,11 +56,11 @@ describe('ApnsPushService', () => {
|
||||
badge_count: 0,
|
||||
aps: {
|
||||
'content-available': 1,
|
||||
badge: 0,
|
||||
},
|
||||
});
|
||||
expect(payload.aps).not.toHaveProperty('alert');
|
||||
expect(payload.aps).not.toHaveProperty('sound');
|
||||
expect(payload.aps).not.toHaveProperty('badge');
|
||||
});
|
||||
it('uses APNs push-type and priority headers that match alert versus background delivery', () => {
|
||||
const alertHeaders = ApnsPushServiceTestHooks.buildApnsHeaders({
|
||||
@@ -94,6 +94,43 @@ describe('ApnsPushService', () => {
|
||||
'apns-priority': '5',
|
||||
'apns-collapse-id': 'channel:123',
|
||||
});
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
expect(Number(clearHeaders['apns-expiration']) - now).toBeGreaterThan(1800);
|
||||
});
|
||||
|
||||
it('omits the badge entirely when no usable badge count is supplied', () => {
|
||||
const payload = ApnsPushServiceTestHooks.buildApnsPayload({
|
||||
tag: 'channel:123:456',
|
||||
data: {channel_id: '123', message_id: '456'},
|
||||
notification: {title: 'Alice', body: 'Hello'},
|
||||
});
|
||||
expect(payload.aps).not.toHaveProperty('badge');
|
||||
const unparseable = ApnsPushServiceTestHooks.buildApnsPayload({
|
||||
tag: 'channel:123:456',
|
||||
data: {channel_id: '123', message_id: '456', badge_count: 'not-a-number'},
|
||||
notification: {title: 'Alice', body: 'Hello'},
|
||||
});
|
||||
expect(unparseable.aps).not.toHaveProperty('badge');
|
||||
});
|
||||
|
||||
it('keeps a genuine zero badge so reading the last message clears the app icon', () => {
|
||||
const payload = ApnsPushServiceTestHooks.buildApnsPayload({
|
||||
tag: 'channel:123:456',
|
||||
data: {channel_id: '123', message_id: '456', badge_count: 0},
|
||||
notification: {title: 'Alice', body: 'Hello'},
|
||||
});
|
||||
expect((payload.aps as Record<string, unknown>).badge).toBe(0);
|
||||
});
|
||||
|
||||
it('does not use the sender avatar as the notification media image', () => {
|
||||
const payload = ApnsPushServiceTestHooks.buildApnsPayload({
|
||||
tag: 'channel:123:456',
|
||||
data: {channel_id: '123', message_id: '456', author_avatar_url: 'https://cdn.example/avatar.png'},
|
||||
notification: {title: 'Alice', body: 'Hello', icon: 'https://cdn.example/avatar.png'},
|
||||
});
|
||||
expect(payload.image_url).toBeUndefined();
|
||||
expect(payload.aps).not.toHaveProperty('mutable-content');
|
||||
expect(payload.author_avatar_url).toBe('https://cdn.example/avatar.png');
|
||||
});
|
||||
it('marks only permanent APNs token failures as subscription deletion signals', () => {
|
||||
expect(ApnsPushServiceTestHooks.isPermanentApnsFailure(410, 'Unregistered')).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user