diff --git a/functions/dist/billing/revenueCatWebhook.js b/functions/dist/billing/revenueCatWebhook.js index e39b7c20..a8d1cbae 100644 --- a/functions/dist/billing/revenueCatWebhook.js +++ b/functions/dist/billing/revenueCatWebhook.js @@ -35,8 +35,10 @@ var __importStar = (this && this.__importStar) || (function () { Object.defineProperty(exports, "__esModule", { value: true }); exports.revenueCatWebhook = void 0; const crypto = __importStar(require("crypto")); -const functions = __importStar(require("firebase-functions")); +const https_1 = require("firebase-functions/v2/https"); +const params_1 = require("firebase-functions/params"); const entitlementLogic_1 = require("./entitlementLogic"); +const log_1 = require("../log"); /** * RevenueCat webhook handler. * @@ -46,14 +48,16 @@ const entitlementLogic_1 = require("./entitlementLogic"); * Authentication: * - Verifies the Ed25519 signature in the X-Signature request header. * - REVENUECAT_SIGNING_KEY must be set to the base64-encoded DER/SPKI Ed25519 - * public key from your RevenueCat project dashboard. + * public key from your RevenueCat project dashboard. It is bound as a Secret + * Manager secret (below) and injected into process.env at runtime. * - Missing key → 500 (our config error). Invalid/absent signature → 401. * * Processing: * - Parses the RevenueCat event payload. * - Writes entitlement state to Firestore at users/{userId}/entitlements. */ -exports.revenueCatWebhook = functions.https.onRequest(async (req, res) => { +const revenueCatSigningKey = (0, params_1.defineSecret)('REVENUECAT_SIGNING_KEY'); +exports.revenueCatWebhook = (0, https_1.onRequest)({ secrets: [revenueCatSigningKey] }, async (req, res) => { var _a; if (req.method !== 'POST') { res.status(405).json({ error: 'method_not_allowed' }); @@ -64,7 +68,7 @@ exports.revenueCatWebhook = functions.https.onRequest(async (req, res) => { } catch (err) { if (err instanceof ConfigError) { - console.error('[revenueCatWebhook] configuration error:', err.message); + log_1.logger.error('[revenueCatWebhook] configuration error', { error: err.message }); res.status(500).json({ error: 'internal_error' }); return; } @@ -84,7 +88,7 @@ exports.revenueCatWebhook = functions.https.onRequest(async (req, res) => { await (0, entitlementLogic_1.applyEntitlementEvent)(event); } catch (err) { - console.error('[revenueCatWebhook] entitlement sync failed:', err); + log_1.logger.error('[revenueCatWebhook] entitlement sync failed', { error: String(err) }); res.status(500).json({ error: 'processing_failed' }); return; } diff --git a/functions/dist/billing/revenueCatWebhook.js.map b/functions/dist/billing/revenueCatWebhook.js.map index 49e7567e..ce1ffff0 100644 --- a/functions/dist/billing/revenueCatWebhook.js.map +++ b/functions/dist/billing/revenueCatWebhook.js.map @@ -1 +1 @@ -{"version":3,"file":"revenueCatWebhook.js","sourceRoot":"","sources":["../../src/billing/revenueCatWebhook.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"revenueCatWebhook.js","sourceRoot":"","sources":["../../src/billing/revenueCatWebhook.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/users/onUserDelete.js b/functions/dist/users/onUserDelete.js index 89c31b45..ad98fa95 100644 --- a/functions/dist/users/onUserDelete.js +++ b/functions/dist/users/onUserDelete.js @@ -34,12 +34,19 @@ var __importStar = (this && this.__importStar) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.onUserDelete = void 0; -const functions = __importStar(require("firebase-functions")); const admin = __importStar(require("firebase-admin")); -const pruneTokens_1 = require("../notifications/pruneTokens"); +const v1_1 = require("firebase-functions/v1"); +const push_1 = require("../notifications/push"); +const log_1 = require("../log"); /** * Auth deletion trigger — cascades account deletion server-side. * + * This is the ONE function kept on the v1 (1st-gen) API: 2nd gen has no + * `auth.user().onDelete` equivalent, so it is imported explicitly from + * firebase-functions/v1 and coexists with the v2 functions in this codebase. + * Global v2 options (setGlobalOptions) do not apply to it, so the timeout/memory + * for its dual recursiveDelete + Storage sweep are set here via runWith(). + * * Fires when a Firebase Auth user is deleted (via client deleteAccount() or * Admin SDK). The Admin SDK bypasses Firestore rules, so this function can * reach everything the client cannot. @@ -53,7 +60,9 @@ const pruneTokens_1 = require("../notifications/pruneTokens"); * 3. Recursively delete the user doc + all subcollections * (entitlements, fcmTokens, notification_queue). */ -exports.onUserDelete = functions.auth.user().onDelete(async (user) => { +exports.onUserDelete = (0, v1_1.runWith)({ timeoutSeconds: 300, memory: '512MB' }) + .auth.user() + .onDelete(async (user) => { var _a, _b, _c, _d; const uid = user.uid; const db = admin.firestore(); @@ -79,7 +88,7 @@ exports.onUserDelete = functions.auth.user().onDelete(async (user) => { await partnerRef.update({ coupleId: null }); // Send push notification to the partner. await notifyPartner(partnerId, partnerDoc, db).catch((err) => { - console.warn(`[onUserDelete] FCM notification to partner ${partnerId} failed:`, err); + log_1.logger.warn(`[onUserDelete] FCM notification to partner ${partnerId} failed`, { error: String(err) }); }); } } @@ -92,29 +101,17 @@ exports.onUserDelete = functions.auth.user().onDelete(async (user) => { await admin.storage().bucket().deleteFiles({ prefix: `users/${uid}/` }); } catch (err) { - console.warn(`[onUserDelete] Storage cleanup failed for ${uid}:`, err); + log_1.logger.warn(`[onUserDelete] Storage cleanup failed for ${uid}`, { error: String(err) }); } // ── 3. User doc + subcollections ────────────────────────────────────────── await db.recursiveDelete(userDocRef); - console.log(`[onUserDelete] completed cleanup for user ${uid}`); + log_1.logger.log(`[onUserDelete] completed cleanup for user ${uid}`); }); async function notifyPartner(partnerId, partnerDoc, db) { - var _a; - const messaging = admin.messaging(); - // Collect FCM tokens (legacy field + fcmTokens subcollection). - const tokens = []; - const legacyToken = (_a = partnerDoc.data()) === null || _a === void 0 ? void 0 : _a.fcmToken; - if (typeof legacyToken === 'string' && legacyToken.length > 0) { - tokens.push(legacyToken); - } - const tokenSnap = await db.collection('users').doc(partnerId).collection('fcmTokens').get(); - tokenSnap.docs.forEach((doc) => { - var _a; - const t = (_a = doc.data()) === null || _a === void 0 ? void 0 : _a.token; - if (typeof t === 'string' && t.length > 0 && !tokens.includes(t)) { - tokens.push(t); - } - }); + const partnerData = partnerDoc.data(); + // Preserve the original behavior: only write the in-app record + push if the partner has a + // live device token (nothing to deliver otherwise). + const tokens = await (0, push_1.getUserTokens)(db, partnerId, partnerData); if (tokens.length === 0) return; // Write an in-app notification record. @@ -129,20 +126,13 @@ async function notifyPartner(partnerId, partnerDoc, db) { read: false, createdAt: admin.firestore.FieldValue.serverTimestamp(), }); - const sendResults = await Promise.allSettled(tokens.map((token) => messaging.send({ - token, + await (0, push_1.sendPushToUser)(db, admin.messaging(), partnerId, { notification: { title: 'Your partner deleted their account', body: 'You are no longer paired. Tap to create a new invite.', }, android: { notification: { channelId: 'partner_activity' } }, // E-OBS data: { type: 'partner_deleted_account' }, - }))); - sendResults.forEach((result, i) => { - if (result.status === 'rejected') { - console.warn(`[onUserDelete] FCM send to token ${tokens[i]} failed:`, result.reason); - } - }); - await (0, pruneTokens_1.pruneDeadTokens)(db, partnerId, tokens, sendResults); + }, partnerData); } //# sourceMappingURL=onUserDelete.js.map \ No newline at end of file diff --git a/functions/dist/users/onUserDelete.js.map b/functions/dist/users/onUserDelete.js.map index 27161420..d3fe2e18 100644 --- a/functions/dist/users/onUserDelete.js.map +++ b/functions/dist/users/onUserDelete.js.map @@ -1 +1 @@ -{"version":3,"file":"onUserDelete.js","sourceRoot":"","sources":["../../src/users/onUserDelete.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"onUserDelete.js","sourceRoot":"","sources":["../../src/users/onUserDelete.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,sDAAuC;AACvC,8CAAqD;AACrD,gDAAqE;AACrE,gCAA+B;AAE/B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACU,QAAA,YAAY,GAAG,IAAA,YAAO,EAAC,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;KAC1E,IAAI,CAAC,IAAI,EAAE;KACX,QAAQ,CAAC,KAAK,EAAE,IAAqB,EAAE,EAAE;;IACxC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAA;IACpB,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAE5B,6EAA6E;IAE7E,MAAM,UAAU,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAClD,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,GAAG,EAAE,CAAA;IACtC,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM;QAC7B,CAAC,CAAE,MAAA,OAAO,CAAC,IAAI,EAAE,0CAAE,QAA+B;QAClD,CAAC,CAAC,SAAS,CAAA;IAEb,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,SAAS,GAAG,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACxD,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,CAAA;QAEvC,IAAI,SAAS,CAAC,MAAM,EAAE,CAAC;YACrB,MAAM,OAAO,GAAG,CAAC,MAAA,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,OAAO,mCAAI,EAAE,CAAa,CAAA;YAC7D,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,GAAG,CAAC,CAAA;YAElD,IAAI,SAAS,EAAE,CAAC;gBACd,MAAM,UAAU,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;gBACxD,MAAM,UAAU,GAAG,MAAM,UAAU,CAAC,GAAG,EAAE,CAAA;gBAEzC,4DAA4D;gBAC5D,IAAI,UAAU,CAAC,MAAM,IAAI,CAAA,MAAA,UAAU,CAAC,IAAI,EAAE,0CAAE,QAAQ,MAAK,QAAQ,EAAE,CAAC;oBAClE,mEAAmE;oBACnE,oEAAoE;oBACpE,MAAM,UAAU,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;oBAE3C,yCAAyC;oBACzC,MAAM,aAAa,CAAC,SAAS,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;wBAC3D,YAAM,CAAC,IAAI,CAAC,8CAA8C,SAAS,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;oBACvG,CAAC,CAAC,CAAA;gBACJ,CAAC;YACH,CAAC;YAED,0DAA0D;YAC1D,MAAM,EAAE,CAAC,eAAe,CAAC,SAAS,CAAC,CAAA;QACrC,CAAC;IACH,CAAC;IAED,6EAA6E;IAE7E,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,SAAS,GAAG,GAAG,EAAE,CAAC,CAAA;IACzE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,YAAM,CAAC,IAAI,CAAC,6CAA6C,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACzF,CAAC;IAED,6EAA6E;IAE7E,MAAM,EAAE,CAAC,eAAe,CAAC,UAAU,CAAC,CAAA;IAEpC,YAAM,CAAC,GAAG,CAAC,6CAA6C,GAAG,EAAE,CAAC,CAAA;AAChE,CAAC,CAAC,CAAA;AAEJ,KAAK,UAAU,aAAa,CAC1B,SAAiB,EACjB,UAA4C,EAC5C,EAA6B;IAE7B,MAAM,WAAW,GAAG,UAAU,CAAC,IAAI,EAAE,CAAA;IAErC,2FAA2F;IAC3F,oDAAoD;IACpD,MAAM,MAAM,GAAG,MAAM,IAAA,oBAAa,EAAC,EAAE,EAAE,SAAS,EAAE,WAAW,CAAC,CAAA;IAC9D,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAM;IAE/B,uCAAuC;IACvC,MAAM,EAAE;SACL,UAAU,CAAC,OAAO,CAAC;SACnB,GAAG,CAAC,SAAS,CAAC;SACd,UAAU,CAAC,oBAAoB,CAAC;SAChC,GAAG,CAAC;QACH,IAAI,EAAE,yBAAyB;QAC/B,KAAK,EAAE,oCAAoC;QAC3C,IAAI,EAAE,uDAAuD;QAC7D,IAAI,EAAE,KAAK;QACX,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE;KACxD,CAAC,CAAA;IAEJ,MAAM,IAAA,qBAAc,EAClB,EAAE,EACF,KAAK,CAAC,SAAS,EAAE,EACjB,SAAS,EACT;QACE,YAAY,EAAE;YACZ,KAAK,EAAE,oCAAoC;YAC3C,IAAI,EAAE,uDAAuD;SAC9D;QACD,OAAO,EAAE,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,EAAE,EAAE,QAAQ;QACtE,IAAI,EAAE,EAAE,IAAI,EAAE,yBAAyB,EAAE;KAC1C,EACD,WAAW,CACZ,CAAA;AACH,CAAC"} \ No newline at end of file diff --git a/functions/src/billing/revenueCatWebhook.ts b/functions/src/billing/revenueCatWebhook.ts index eb5c138d..017db079 100644 --- a/functions/src/billing/revenueCatWebhook.ts +++ b/functions/src/billing/revenueCatWebhook.ts @@ -1,6 +1,8 @@ import * as crypto from 'crypto' -import * as functions from 'firebase-functions' +import { onRequest, Request } from 'firebase-functions/v2/https' +import { defineSecret } from 'firebase-functions/params' import { applyEntitlementEvent, EntitlementEvent } from './entitlementLogic' +import { logger } from '../log' /** * RevenueCat webhook handler. @@ -11,51 +13,57 @@ import { applyEntitlementEvent, EntitlementEvent } from './entitlementLogic' * Authentication: * - Verifies the Ed25519 signature in the X-Signature request header. * - REVENUECAT_SIGNING_KEY must be set to the base64-encoded DER/SPKI Ed25519 - * public key from your RevenueCat project dashboard. + * public key from your RevenueCat project dashboard. It is bound as a Secret + * Manager secret (below) and injected into process.env at runtime. * - Missing key → 500 (our config error). Invalid/absent signature → 401. * * Processing: * - Parses the RevenueCat event payload. * - Writes entitlement state to Firestore at users/{userId}/entitlements. */ -export const revenueCatWebhook = functions.https.onRequest(async (req, res) => { - if (req.method !== 'POST') { - res.status(405).json({ error: 'method_not_allowed' }) - return - } +const revenueCatSigningKey = defineSecret('REVENUECAT_SIGNING_KEY') - try { - verifySignature(req) - } catch (err) { - if (err instanceof ConfigError) { - console.error('[revenueCatWebhook] configuration error:', err.message) - res.status(500).json({ error: 'internal_error' }) +export const revenueCatWebhook = onRequest( + { secrets: [revenueCatSigningKey] }, + async (req, res) => { + if (req.method !== 'POST') { + res.status(405).json({ error: 'method_not_allowed' }) return } - res.status(401).json({ error: 'unauthorized' }) - return - } - const event = req.body?.event as EntitlementEvent | undefined - if (!event || !event.type || !event.app_user_id) { - res.status(400).json({ error: 'malformed_payload' }) - return - } + try { + verifySignature(req) + } catch (err) { + if (err instanceof ConfigError) { + logger.error('[revenueCatWebhook] configuration error', { error: err.message }) + res.status(500).json({ error: 'internal_error' }) + return + } + res.status(401).json({ error: 'unauthorized' }) + return + } - // Security review Batch 2: process BEFORE acking. Previously we returned 200 up front - // and only logged failures, so a failed entitlement sync was silently dropped (no retry). - // RevenueCat retries on non-2xx, and applyEntitlementEvent is idempotent (it sets state), - // so returning 500 on failure recovers the event safely. - try { - await applyEntitlementEvent(event) - } catch (err) { - console.error('[revenueCatWebhook] entitlement sync failed:', err) - res.status(500).json({ error: 'processing_failed' }) - return - } + const event = req.body?.event as EntitlementEvent | undefined + if (!event || !event.type || !event.app_user_id) { + res.status(400).json({ error: 'malformed_payload' }) + return + } - res.status(200).json({ received: true }) -}) + // Security review Batch 2: process BEFORE acking. Previously we returned 200 up front + // and only logged failures, so a failed entitlement sync was silently dropped (no retry). + // RevenueCat retries on non-2xx, and applyEntitlementEvent is idempotent (it sets state), + // so returning 500 on failure recovers the event safely. + try { + await applyEntitlementEvent(event) + } catch (err) { + logger.error('[revenueCatWebhook] entitlement sync failed', { error: String(err) }) + res.status(500).json({ error: 'processing_failed' }) + return + } + + res.status(200).json({ received: true }) + } +) class ConfigError extends Error {} class AuthError extends Error {} @@ -67,7 +75,7 @@ class AuthError extends Error {} * these are deployment problems, not auth failures. * Throws AuthError for any missing or invalid signature — these are 401s. */ -function verifySignature(req: functions.https.Request): void { +function verifySignature(req: Request): void { const signingKey = process.env.REVENUECAT_SIGNING_KEY if (!signingKey) { throw new ConfigError('REVENUECAT_SIGNING_KEY environment variable is not set') diff --git a/functions/src/users/onUserDelete.ts b/functions/src/users/onUserDelete.ts index cf028d0c..3d63a8bb 100644 --- a/functions/src/users/onUserDelete.ts +++ b/functions/src/users/onUserDelete.ts @@ -1,10 +1,17 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { pruneDeadTokens } from '../notifications/pruneTokens' +import { auth, runWith } from 'firebase-functions/v1' +import { getUserTokens, sendPushToUser } from '../notifications/push' +import { logger } from '../log' /** * Auth deletion trigger — cascades account deletion server-side. * + * This is the ONE function kept on the v1 (1st-gen) API: 2nd gen has no + * `auth.user().onDelete` equivalent, so it is imported explicitly from + * firebase-functions/v1 and coexists with the v2 functions in this codebase. + * Global v2 options (setGlobalOptions) do not apply to it, so the timeout/memory + * for its dual recursiveDelete + Storage sweep are set here via runWith(). + * * Fires when a Firebase Auth user is deleted (via client deleteAccount() or * Admin SDK). The Admin SDK bypasses Firestore rules, so this function can * reach everything the client cannot. @@ -18,84 +25,75 @@ import { pruneDeadTokens } from '../notifications/pruneTokens' * 3. Recursively delete the user doc + all subcollections * (entitlements, fcmTokens, notification_queue). */ -export const onUserDelete = functions.auth.user().onDelete(async (user) => { - const uid = user.uid - const db = admin.firestore() +export const onUserDelete = runWith({ timeoutSeconds: 300, memory: '512MB' }) + .auth.user() + .onDelete(async (user: auth.UserRecord) => { + const uid = user.uid + const db = admin.firestore() - // ── 1. Couple unpair ────────────────────────────────────────────────────── + // ── 1. Couple unpair ────────────────────────────────────────────────────── - const userDocRef = db.collection('users').doc(uid) - const userDoc = await userDocRef.get() - const coupleId = userDoc.exists - ? (userDoc.data()?.coupleId as string | undefined) - : undefined + const userDocRef = db.collection('users').doc(uid) + const userDoc = await userDocRef.get() + const coupleId = userDoc.exists + ? (userDoc.data()?.coupleId as string | undefined) + : undefined - if (coupleId) { - const coupleRef = db.collection('couples').doc(coupleId) - const coupleDoc = await coupleRef.get() + if (coupleId) { + const coupleRef = db.collection('couples').doc(coupleId) + const coupleDoc = await coupleRef.get() - if (coupleDoc.exists) { - const userIds = (coupleDoc.data()?.userIds ?? []) as string[] - const partnerId = userIds.find((id) => id !== uid) + if (coupleDoc.exists) { + const userIds = (coupleDoc.data()?.userIds ?? []) as string[] + const partnerId = userIds.find((id) => id !== uid) - if (partnerId) { - const partnerRef = db.collection('users').doc(partnerId) - const partnerDoc = await partnerRef.get() + if (partnerId) { + const partnerRef = db.collection('users').doc(partnerId) + const partnerDoc = await partnerRef.get() - // Only act if the partner is still recorded in this couple. - if (partnerDoc.exists && partnerDoc.data()?.coupleId === coupleId) { - // Clear partner's coupleId first so the onCoupleLeave trigger does - // not fire a second time (it would find no partner left to notify). - await partnerRef.update({ coupleId: null }) + // Only act if the partner is still recorded in this couple. + if (partnerDoc.exists && partnerDoc.data()?.coupleId === coupleId) { + // Clear partner's coupleId first so the onCoupleLeave trigger does + // not fire a second time (it would find no partner left to notify). + await partnerRef.update({ coupleId: null }) - // Send push notification to the partner. - await notifyPartner(partnerId, partnerDoc, db).catch((err) => { - console.warn(`[onUserDelete] FCM notification to partner ${partnerId} failed:`, err) - }) + // Send push notification to the partner. + await notifyPartner(partnerId, partnerDoc, db).catch((err) => { + logger.warn(`[onUserDelete] FCM notification to partner ${partnerId} failed`, { error: String(err) }) + }) + } } + + // Recursively delete the couple doc + all subcollections. + await db.recursiveDelete(coupleRef) } - - // Recursively delete the couple doc + all subcollections. - await db.recursiveDelete(coupleRef) } - } - // ── 2. Storage cleanup ──────────────────────────────────────────────────── + // ── 2. Storage cleanup ──────────────────────────────────────────────────── - try { - await admin.storage().bucket().deleteFiles({ prefix: `users/${uid}/` }) - } catch (err) { - console.warn(`[onUserDelete] Storage cleanup failed for ${uid}:`, err) - } + try { + await admin.storage().bucket().deleteFiles({ prefix: `users/${uid}/` }) + } catch (err) { + logger.warn(`[onUserDelete] Storage cleanup failed for ${uid}`, { error: String(err) }) + } - // ── 3. User doc + subcollections ────────────────────────────────────────── + // ── 3. User doc + subcollections ────────────────────────────────────────── - await db.recursiveDelete(userDocRef) + await db.recursiveDelete(userDocRef) - console.log(`[onUserDelete] completed cleanup for user ${uid}`) -}) + logger.log(`[onUserDelete] completed cleanup for user ${uid}`) + }) async function notifyPartner( partnerId: string, partnerDoc: admin.firestore.DocumentSnapshot, db: admin.firestore.Firestore ): Promise { - const messaging = admin.messaging() - - // Collect FCM tokens (legacy field + fcmTokens subcollection). - const tokens: string[] = [] - const legacyToken = partnerDoc.data()?.fcmToken - if (typeof legacyToken === 'string' && legacyToken.length > 0) { - tokens.push(legacyToken) - } - const tokenSnap = await db.collection('users').doc(partnerId).collection('fcmTokens').get() - tokenSnap.docs.forEach((doc) => { - const t = doc.data()?.token - if (typeof t === 'string' && t.length > 0 && !tokens.includes(t)) { - tokens.push(t) - } - }) + const partnerData = partnerDoc.data() + // Preserve the original behavior: only write the in-app record + push if the partner has a + // live device token (nothing to deliver otherwise). + const tokens = await getUserTokens(db, partnerId, partnerData) if (tokens.length === 0) return // Write an in-app notification record. @@ -111,25 +109,18 @@ async function notifyPartner( createdAt: admin.firestore.FieldValue.serverTimestamp(), }) - const sendResults = await Promise.allSettled( - tokens.map((token) => - messaging.send({ - token, - notification: { - title: 'Your partner deleted their account', - body: 'You are no longer paired. Tap to create a new invite.', - }, - android: { notification: { channelId: 'partner_activity' } }, // E-OBS - data: { type: 'partner_deleted_account' }, - }) - ) + await sendPushToUser( + db, + admin.messaging(), + partnerId, + { + notification: { + title: 'Your partner deleted their account', + body: 'You are no longer paired. Tap to create a new invite.', + }, + android: { notification: { channelId: 'partner_activity' } }, // E-OBS + data: { type: 'partner_deleted_account' }, + }, + partnerData, ) - - sendResults.forEach((result, i) => { - if (result.status === 'rejected') { - console.warn(`[onUserDelete] FCM send to token ${tokens[i]} failed:`, result.reason) - } - }) - - await pruneDeadTokens(db, partnerId, tokens, sendResults) }