All files / src/utils fcm.js

96.15% Statements 25/26
100% Branches 21/21
100% Functions 4/4
95.45% Lines 21/22

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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            12x 12x               6x   4x 1x 1x     4x   3x         3x 3x 8x 6x 6x           4x   2x               3x             4x 2x 1x 1x               12x  
/**
 * Shared FCM (Firebase Cloud Messaging) utilities.
 *
 * Extracted from rooms.js, conversations.js, and reports.js to eliminate duplication.
 */
 
const { messaging, db, FieldValue } = require('./firebase');
const log = require('./log');
 
/**
 * Send a data-only FCM message to multiple tokens via Firebase Admin SDK.
 * All values are stringified (FCM data messages require string values).
 * Returns a list of invalid tokens that should be cleaned up.
 */
async function sendFcmToTokens(tokens, data) {
  if (!tokens || tokens.length === 0) return [];
 
  if (process.env.NODE_ENV === 'local') {
    log.info('fcm', `[FCM-LOCAL] Would send to ${tokens.length} tokens: ${data?.title}`);
    return [];
  }
 
  const stringData = Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v)]));
 
  const result = await messaging.sendEachForMulticast({
    tokens,
    data: stringData,
  });
 
  const invalidTokens = [];
  result.responses.forEach((resp, i) => {
    if (resp.error) {
      const code = resp.error.code;
      if (
        code === 'messaging/invalid-registration-token' ||
        code === 'messaging/registration-token-not-registered' ||
        code === 'messaging/sender-id-mismatch' ||
        code === 'messaging/invalid-argument'
      ) {
        invalidTokens.push(tokens[i]);
      } else {
        log.warn('fcm', `FCM send failed for token index ${i}`, {
          code,
          message: resp.error.message,
        });
      }
    }
  });
 
  return invalidTokens;
}
 
/**
 * Remove invalid FCM tokens from a user's doc using arrayRemove.
 */
async function cleanupInvalidTokens(invalidTokens, userId) {
  if (!invalidTokens || invalidTokens.length === 0 || !userId) return;
  if (process.env.NODE_ENV === 'local') return;
  try {
    await db.doc(`users/${userId}`).update({
      fcmTokens: FieldValue.arrayRemove(...invalidTokens),
    });
  } catch (err) {
    log.error('fcm', 'Failed to clean invalid tokens', { userId, error: err.message });
  }
}
 
module.exports = { sendFcmToTokens, cleanupInvalidTokens };