feat: cloud function for date match cleanup, ViewModel and repo improvements
This commit is contained in:
parent
fb87fe149a
commit
c816033e74
|
|
@ -9,7 +9,6 @@ import app.closer.domain.model.SwipeAction
|
||||||
import app.closer.domain.repository.DateMatchRepository
|
import app.closer.domain.repository.DateMatchRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import java.util.UUID
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
@ -20,9 +19,12 @@ import javax.inject.Singleton
|
||||||
* shipped as a Cloud Function seed. Firestore is the source of truth for
|
* shipped as a Cloud Function seed. Firestore is the source of truth for
|
||||||
* per-partner swipes and revealed matches.
|
* per-partner swipes and revealed matches.
|
||||||
*
|
*
|
||||||
* Mutual match detection happens client-side after recording a swipe: if the
|
* Mutual match detection is performed server-side by the
|
||||||
* current user swiped [SwipeAction.LOVE] and the partner also has a love swipe
|
* `createDateMatchOnMutualLove` Cloud Function: the `date_matches` collection is
|
||||||
* on the same date idea, a revealed match is created.
|
* server-write-only (Firestore rules deny client writes), so when both partners
|
||||||
|
* swipe [SwipeAction.LOVE] on an idea the function creates the match and it
|
||||||
|
* arrives on the client via [observeMatches]. [recordSwipe] therefore only
|
||||||
|
* records the swipe and always resolves with a null match.
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class DateMatchRepositoryImpl @Inject constructor(
|
class DateMatchRepositoryImpl @Inject constructor(
|
||||||
|
|
@ -45,25 +47,8 @@ class DateMatchRepositoryImpl @Inject constructor(
|
||||||
swipedAt = System.currentTimeMillis()
|
swipedAt = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
swipeDataSource.recordSwipe(coupleId, swipe)
|
swipeDataSource.recordSwipe(coupleId, swipe)
|
||||||
|
// Match creation is server-side (see class docs); surfaced via observeMatches.
|
||||||
if (action != SwipeAction.LOVE) return@runCatching null
|
null
|
||||||
|
|
||||||
val existing = matchDataSource.findMatchByDateIdeaId(coupleId, dateIdeaId)
|
|
||||||
if (existing != null) return@runCatching existing
|
|
||||||
|
|
||||||
val allSwipes = swipeDataSource.getAllSwipesForDate(coupleId, dateIdeaId)
|
|
||||||
val partnerSwipe = allSwipes.firstOrNull { it.userId != userId && it.action == SwipeAction.LOVE }
|
|
||||||
?: return@runCatching null
|
|
||||||
|
|
||||||
val matchedBy = listOf(userId, partnerSwipe.userId).distinct().sorted()
|
|
||||||
val matchId = matchDataSource.createMatch(coupleId, dateIdeaId, matchedBy)
|
|
||||||
DateMatch(
|
|
||||||
id = matchId,
|
|
||||||
coupleId = coupleId,
|
|
||||||
dateIdeaId = dateIdeaId,
|
|
||||||
revealedAt = System.currentTimeMillis(),
|
|
||||||
matchedBy = matchedBy
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getOwnSwipe(coupleId: String, userId: String, dateIdeaId: String): DateSwipe? {
|
override suspend fun getOwnSwipe(coupleId: String, userId: String, dateIdeaId: String): DateSwipe? {
|
||||||
|
|
|
||||||
|
|
@ -90,12 +90,25 @@ class DateMatchViewModel @Inject constructor(
|
||||||
val matchesFlow: Flow<List<DateMatch>> = repository.observeMatches(coupleId)
|
val matchesFlow: Flow<List<DateMatch>> = repository.observeMatches(coupleId)
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
combine(swipesFlow, matchesFlow) { swipes, matches ->
|
// Matches are created server-side and stream in via observeMatches.
|
||||||
_uiState.value.copy(
|
// Track ids already seen so the "It's a match!" celebration only fires
|
||||||
ownSwipes = swipes.associate { it.dateIdeaId to it.action },
|
// for matches created after this screen started observing — not for
|
||||||
matches = matches
|
// matches that already existed on load.
|
||||||
)
|
var knownMatchIds: Set<String>? = null
|
||||||
}.collect { _uiState.value = it }
|
combine(swipesFlow, matchesFlow) { swipes, matches -> swipes to matches }
|
||||||
|
.collect { (swipes, matches) ->
|
||||||
|
val newMatch = knownMatchIds?.let { known ->
|
||||||
|
matches.firstOrNull { it.id !in known }
|
||||||
|
}
|
||||||
|
knownMatchIds = matches.mapTo(mutableSetOf()) { it.id }
|
||||||
|
_uiState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
ownSwipes = swipes.associate { it.dateIdeaId to it.action },
|
||||||
|
matches = matches,
|
||||||
|
justMatched = newMatch ?: state.justMatched
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,13 +121,11 @@ class DateMatchViewModel @Inject constructor(
|
||||||
_uiState.update { it.copy(justMatched = null) }
|
_uiState.update { it.copy(justMatched = null) }
|
||||||
val result = repository.recordSwipe(coupleId, userId, current.id, action)
|
val result = repository.recordSwipe(coupleId, userId, current.id, action)
|
||||||
result.fold(
|
result.fold(
|
||||||
onSuccess = { match ->
|
onSuccess = {
|
||||||
_uiState.update {
|
// Advance to the next idea. A resulting match (if both partners
|
||||||
it.copy(
|
// loved this idea) is created server-side and surfaces via the
|
||||||
currentIndex = it.currentIndex + 1,
|
// observeMatches flow, which sets justMatched.
|
||||||
justMatched = match
|
_uiState.update { it.copy(currentIndex = it.currentIndex + 1) }
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onFailure = { err ->
|
onFailure = { err ->
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || (function () {
|
||||||
|
var ownKeys = function(o) {
|
||||||
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||||
|
var ar = [];
|
||||||
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||||
|
return ar;
|
||||||
|
};
|
||||||
|
return ownKeys(o);
|
||||||
|
};
|
||||||
|
return function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.createDateMatchOnMutualLove = void 0;
|
||||||
|
const functions = __importStar(require("firebase-functions"));
|
||||||
|
const admin = __importStar(require("firebase-admin"));
|
||||||
|
const LOVE = 'love';
|
||||||
|
/**
|
||||||
|
* Creates a revealed date match when both partners have swiped LOVE on the
|
||||||
|
* same date idea.
|
||||||
|
*
|
||||||
|
* Trigger: couples/{coupleId}/date_swipes/{dateIdeaId} (onWrite)
|
||||||
|
*
|
||||||
|
* The `date_matches` collection is server-write-only — Firestore rules deny all
|
||||||
|
* client writes (`allow create, update, delete: if false`). This trigger is
|
||||||
|
* therefore the single source of truth for match creation. The client only
|
||||||
|
* records swipes and observes `date_matches` for the result.
|
||||||
|
*
|
||||||
|
* Idempotency: the match document id is the date idea id and creation runs in a
|
||||||
|
* transaction, so repeated swipes on the same idea and concurrent invocations
|
||||||
|
* never produce a duplicate match.
|
||||||
|
*/
|
||||||
|
exports.createDateMatchOnMutualLove = functions.firestore
|
||||||
|
.document('couples/{coupleId}/date_swipes/{dateIdeaId}')
|
||||||
|
.onWrite(async (change, context) => {
|
||||||
|
var _a;
|
||||||
|
const after = change.after.data();
|
||||||
|
if (!after)
|
||||||
|
return; // swipe document was deleted
|
||||||
|
const actions = ((_a = after.actions) !== null && _a !== void 0 ? _a : {});
|
||||||
|
const lovedBy = Object.entries(actions)
|
||||||
|
.filter(([, entry]) => (entry === null || entry === void 0 ? void 0 : entry.action) === LOVE)
|
||||||
|
.map(([uid]) => uid)
|
||||||
|
.sort();
|
||||||
|
// A match needs both partners to have loved the same idea.
|
||||||
|
if (lovedBy.length < 2)
|
||||||
|
return;
|
||||||
|
const { coupleId, dateIdeaId } = context.params;
|
||||||
|
const db = admin.firestore();
|
||||||
|
const matchRef = db
|
||||||
|
.collection('couples')
|
||||||
|
.doc(coupleId)
|
||||||
|
.collection('date_matches')
|
||||||
|
.doc(dateIdeaId);
|
||||||
|
await db.runTransaction(async (tx) => {
|
||||||
|
const existing = await tx.get(matchRef);
|
||||||
|
if (existing.exists)
|
||||||
|
return; // already matched — no-op
|
||||||
|
tx.set(matchRef, {
|
||||||
|
dateIdeaId,
|
||||||
|
matchedBy: lovedBy,
|
||||||
|
revealedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
//# sourceMappingURL=createDateMatch.js.map
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"createDateMatch.js","sourceRoot":"","sources":["../../src/dates/createDateMatch.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,sDAAuC;AAEvC,MAAM,IAAI,GAAG,MAAM,CAAA;AAOnB;;;;;;;;;;;;;;GAcG;AACU,QAAA,2BAA2B,GAAG,SAAS,CAAC,SAAS;KAC3D,QAAQ,CAAC,6CAA6C,CAAC;KACvD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;;IACjC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;IACjC,IAAI,CAAC,KAAK;QAAE,OAAM,CAAC,6BAA6B;IAEhD,MAAM,OAAO,GAAG,CAAC,MAAA,KAAK,CAAC,OAAO,mCAAI,EAAE,CAA+B,CAAA;IACnE,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;SACpC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAA,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,MAAM,MAAK,IAAI,CAAC;SAC7C,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC;SACnB,IAAI,EAAE,CAAA;IAET,2DAA2D;IAC3D,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAM;IAE9B,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,MAGxC,CAAA;IAED,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAC5B,MAAM,QAAQ,GAAG,EAAE;SAChB,UAAU,CAAC,SAAS,CAAC;SACrB,GAAG,CAAC,QAAQ,CAAC;SACb,UAAU,CAAC,cAAc,CAAC;SAC1B,GAAG,CAAC,UAAU,CAAC,CAAA;IAElB,MAAM,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACnC,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACvC,IAAI,QAAQ,CAAC,MAAM;YAAE,OAAM,CAAC,0BAA0B;QACtD,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE;YACf,UAAU;YACV,SAAS,EAAE,OAAO;YAClB,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,eAAe,EAAE;SACzD,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
||||||
|
|
@ -33,8 +33,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.health = exports.checkDeviceIntegrity = exports.sendPartnerAnsweredNotification = exports.sendDailyQuestionReminder = exports.syncEntitlement = exports.revenueCatWebhook = void 0;
|
exports.health = exports.createDateMatchOnMutualLove = exports.checkDeviceIntegrity = exports.sendPartnerAnsweredNotification = exports.sendDailyQuestionReminder = exports.syncEntitlement = exports.revenueCatWebhook = void 0;
|
||||||
const functions = __importStar(require("firebase-functions"));
|
const functions = __importStar(require("firebase-functions"));
|
||||||
|
const admin = __importStar(require("firebase-admin"));
|
||||||
|
// Initialize the Admin SDK once for every function in this codebase.
|
||||||
|
// Handlers call admin.firestore()/messaging() lazily at invocation time, so a
|
||||||
|
// single idempotent init here is sufficient and avoids "already exists" errors.
|
||||||
|
if (admin.apps.length === 0) {
|
||||||
|
admin.initializeApp();
|
||||||
|
}
|
||||||
var revenueCatWebhook_1 = require("./billing/revenueCatWebhook");
|
var revenueCatWebhook_1 = require("./billing/revenueCatWebhook");
|
||||||
Object.defineProperty(exports, "revenueCatWebhook", { enumerable: true, get: function () { return revenueCatWebhook_1.revenueCatWebhook; } });
|
Object.defineProperty(exports, "revenueCatWebhook", { enumerable: true, get: function () { return revenueCatWebhook_1.revenueCatWebhook; } });
|
||||||
var syncEntitlement_1 = require("./billing/syncEntitlement");
|
var syncEntitlement_1 = require("./billing/syncEntitlement");
|
||||||
|
|
@ -44,6 +51,8 @@ Object.defineProperty(exports, "sendDailyQuestionReminder", { enumerable: true,
|
||||||
Object.defineProperty(exports, "sendPartnerAnsweredNotification", { enumerable: true, get: function () { return reminders_1.sendPartnerAnsweredNotification; } });
|
Object.defineProperty(exports, "sendPartnerAnsweredNotification", { enumerable: true, get: function () { return reminders_1.sendPartnerAnsweredNotification; } });
|
||||||
var checkDeviceIntegrity_1 = require("./security/checkDeviceIntegrity");
|
var checkDeviceIntegrity_1 = require("./security/checkDeviceIntegrity");
|
||||||
Object.defineProperty(exports, "checkDeviceIntegrity", { enumerable: true, get: function () { return checkDeviceIntegrity_1.checkDeviceIntegrity; } });
|
Object.defineProperty(exports, "checkDeviceIntegrity", { enumerable: true, get: function () { return checkDeviceIntegrity_1.checkDeviceIntegrity; } });
|
||||||
|
var createDateMatch_1 = require("./dates/createDateMatch");
|
||||||
|
Object.defineProperty(exports, "createDateMatchOnMutualLove", { enumerable: true, get: function () { return createDateMatch_1.createDateMatchOnMutualLove; } });
|
||||||
/**
|
/**
|
||||||
* Basic health check callable.
|
* Basic health check callable.
|
||||||
* Useful for verifying function deployment and firebase-tools wiring.
|
* Useful for verifying function deployment and firebase-tools wiring.
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAE/C,iEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAC1B,6DAA2D;AAAlD,kHAAA,eAAe,OAAA;AACxB,uDAGkC;AAFhC,sHAAA,yBAAyB,OAAA;AACzB,4HAAA,+BAA+B,OAAA;AAEjC,wEAAsE;AAA7D,4HAAA,oBAAoB,OAAA;AAE7B;;;GAGG;AACU,QAAA,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC3D,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;AACxC,CAAC,CAAC,CAAA"}
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,sDAAuC;AAEvC,qEAAqE;AACrE,8EAA8E;AAC9E,gFAAgF;AAChF,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IAC5B,KAAK,CAAC,aAAa,EAAE,CAAA;AACvB,CAAC;AAED,iEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAC1B,6DAA2D;AAAlD,kHAAA,eAAe,OAAA;AACxB,uDAGkC;AAFhC,sHAAA,yBAAyB,OAAA;AACzB,4HAAA,+BAA+B,OAAA;AAEjC,wEAAsE;AAA7D,4HAAA,oBAAoB,OAAA;AAC7B,2DAAqE;AAA5D,8HAAA,2BAA2B,OAAA;AAEpC;;;GAGG;AACU,QAAA,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC3D,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;AACxC,CAAC,CAAC,CAAA"}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
|
const LOVE = 'love'
|
||||||
|
|
||||||
|
interface SwipeEntry {
|
||||||
|
action?: string
|
||||||
|
swipedAt?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a revealed date match when both partners have swiped LOVE on the
|
||||||
|
* same date idea.
|
||||||
|
*
|
||||||
|
* Trigger: couples/{coupleId}/date_swipes/{dateIdeaId} (onWrite)
|
||||||
|
*
|
||||||
|
* The `date_matches` collection is server-write-only — Firestore rules deny all
|
||||||
|
* client writes (`allow create, update, delete: if false`). This trigger is
|
||||||
|
* therefore the single source of truth for match creation. The client only
|
||||||
|
* records swipes and observes `date_matches` for the result.
|
||||||
|
*
|
||||||
|
* Idempotency: the match document id is the date idea id and creation runs in a
|
||||||
|
* transaction, so repeated swipes on the same idea and concurrent invocations
|
||||||
|
* never produce a duplicate match.
|
||||||
|
*/
|
||||||
|
export const createDateMatchOnMutualLove = functions.firestore
|
||||||
|
.document('couples/{coupleId}/date_swipes/{dateIdeaId}')
|
||||||
|
.onWrite(async (change, context) => {
|
||||||
|
const after = change.after.data()
|
||||||
|
if (!after) return // swipe document was deleted
|
||||||
|
|
||||||
|
const actions = (after.actions ?? {}) as Record<string, SwipeEntry>
|
||||||
|
const lovedBy = Object.entries(actions)
|
||||||
|
.filter(([, entry]) => entry?.action === LOVE)
|
||||||
|
.map(([uid]) => uid)
|
||||||
|
.sort()
|
||||||
|
|
||||||
|
// A match needs both partners to have loved the same idea.
|
||||||
|
if (lovedBy.length < 2) return
|
||||||
|
|
||||||
|
const { coupleId, dateIdeaId } = context.params as {
|
||||||
|
coupleId: string
|
||||||
|
dateIdeaId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = admin.firestore()
|
||||||
|
const matchRef = db
|
||||||
|
.collection('couples')
|
||||||
|
.doc(coupleId)
|
||||||
|
.collection('date_matches')
|
||||||
|
.doc(dateIdeaId)
|
||||||
|
|
||||||
|
await db.runTransaction(async (tx) => {
|
||||||
|
const existing = await tx.get(matchRef)
|
||||||
|
if (existing.exists) return // already matched — no-op
|
||||||
|
tx.set(matchRef, {
|
||||||
|
dateIdeaId,
|
||||||
|
matchedBy: lovedBy,
|
||||||
|
revealedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -1,4 +1,12 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
|
// Initialize the Admin SDK once for every function in this codebase.
|
||||||
|
// Handlers call admin.firestore()/messaging() lazily at invocation time, so a
|
||||||
|
// single idempotent init here is sufficient and avoids "already exists" errors.
|
||||||
|
if (admin.apps.length === 0) {
|
||||||
|
admin.initializeApp()
|
||||||
|
}
|
||||||
|
|
||||||
export { revenueCatWebhook } from './billing/revenueCatWebhook'
|
export { revenueCatWebhook } from './billing/revenueCatWebhook'
|
||||||
export { syncEntitlement } from './billing/syncEntitlement'
|
export { syncEntitlement } from './billing/syncEntitlement'
|
||||||
|
|
@ -7,6 +15,7 @@ export {
|
||||||
sendPartnerAnsweredNotification,
|
sendPartnerAnsweredNotification,
|
||||||
} from './notifications/reminders'
|
} from './notifications/reminders'
|
||||||
export { checkDeviceIntegrity } from './security/checkDeviceIntegrity'
|
export { checkDeviceIntegrity } from './security/checkDeviceIntegrity'
|
||||||
|
export { createDateMatchOnMutualLove } from './dates/createDateMatch'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic health check callable.
|
* Basic health check callable.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue