chore(functions): rebuild dist for deployed daily-question + game-copy changes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
15e4579410
commit
3acba138a3
|
|
@ -157,7 +157,12 @@ exports.onGameSessionUpdate = functions.firestore
|
|||
return;
|
||||
}
|
||||
// ── Session completed (reveal ready for both) ────────────────────────
|
||||
if (status === 'completed' && !currentData.finishNotifiedAt) {
|
||||
// Only a REAL completion has results: the async flow marks each user complete at reveal, so
|
||||
// status flips to 'completed' with both uids in completedByUsers. An abandoned/quit session
|
||||
// also lands on 'completed' (the abandon write) but with an empty list — pushing
|
||||
// "finished — see your results!" for those was a false notification into an empty reveal.
|
||||
const completedBy = Array.isArray(currentData.completedByUsers) ? currentData.completedByUsers : [];
|
||||
if (status === 'completed' && !currentData.finishNotifiedAt && completedBy.length >= 2) {
|
||||
const claimed = await db.runTransaction(async (tx) => {
|
||||
const fresh = await tx.get(sessionRef);
|
||||
const d = fresh.data();
|
||||
|
|
@ -240,8 +245,20 @@ exports.onGamePartFinished = functions.firestore
|
|||
const finisher = await db.collection('users').doc(finisherUid).get();
|
||||
const finisherName = 'Your partner'; // displayName is E2EE; the app shows the real name in-app
|
||||
const finisherAvatar = (_e = finisher.data()) === null || _e === void 0 ? void 0 : _e.photoUrl;
|
||||
await notifyPartner(db, messaging, recipient, finisherName, gameType, 'partner_completed_part', `${finisherName} finished their part — your turn to play!`, coupleId, finisherAvatar, sessionId);
|
||||
await notifyPartner(db, messaging, recipient, finisherName, gameType, 'partner_completed_part', yourTurnBody(gameType), coupleId, finisherAvatar, sessionId);
|
||||
});
|
||||
/**
|
||||
* Per-game body for the "your turn" (partner_completed_part) push — mirrors the client banner's
|
||||
* YOUR_TURN copy in ui/components/GamePromptBanner.kt so foreground and background read the same.
|
||||
*/
|
||||
function yourTurnBody(gameType) {
|
||||
switch (gameType) {
|
||||
case 'how_well': return 'Your turn — guess their answers';
|
||||
case 'desire_sync': return 'Your turn — only mutual yeses ever show';
|
||||
case 'this_or_that': return 'Your turn — see where you line up';
|
||||
default: return 'Your turn — reveal how you line up';
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Send notification to partner via FCM and write to notification_queue.
|
||||
*/
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -34,9 +34,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.assignDailyQuestionCallable = exports.assignDailyQuestion = void 0;
|
||||
exports.formatCstDate = formatCstDate;
|
||||
exports.parseCstDate = parseCstDate;
|
||||
exports.timestampAt6PmCst = timestampAt6PmCst;
|
||||
const functions = __importStar(require("firebase-functions"));
|
||||
const admin = __importStar(require("firebase-admin"));
|
||||
const CST_OFFSET_HOURS = -6;
|
||||
/**
|
||||
* Scheduled function that assigns one daily question per couple every day.
|
||||
*
|
||||
|
|
@ -57,9 +59,9 @@ exports.assignDailyQuestion = functions.pubsub
|
|||
const db = admin.firestore();
|
||||
const today = cstDateString();
|
||||
const nextDay = nextCstDateString();
|
||||
const questionId = await pickRandomQuestionId();
|
||||
const questionId = await pickDailyQuestionId(today);
|
||||
if (!questionId) {
|
||||
console.error('[assignDailyQuestion] no active questions available');
|
||||
console.error('[assignDailyQuestion] questions pool not seeded — skipping assignment');
|
||||
return;
|
||||
}
|
||||
const couplesSnap = await db.collection('couples').get();
|
||||
|
|
@ -135,7 +137,7 @@ exports.assignDailyQuestionCallable = functions.https.onCall(async (data, contex
|
|||
if (date !== today) {
|
||||
throw new functions.https.HttpsError('invalid-argument', 'Daily question can only be assigned for today.');
|
||||
}
|
||||
const questionId = await pickRandomQuestionId();
|
||||
const questionId = await pickDailyQuestionId(today);
|
||||
if (!questionId) {
|
||||
throw new functions.https.HttpsError('internal', 'No active questions available.');
|
||||
}
|
||||
|
|
@ -173,20 +175,51 @@ exports.assignDailyQuestionCallable = functions.https.onCall(async (data, contex
|
|||
* so the app still functions during rollout. In production, keep the local
|
||||
* Room database and Firestore pool in sync through the existing seed flow.
|
||||
*/
|
||||
async function pickRandomQuestionId() {
|
||||
// Weekday → daily mode tag. FROZEN to mirror the client's DailyModeResolver.DOW_DEFAULTS
|
||||
// (app/.../domain/DailyModeResolver.kt); JS getUTCDay(): 0=Sun … 6=Sat.
|
||||
const WEEKDAY_MODE_TAGS = {
|
||||
0: 'mode_tiny_date_night', // Sunday — Slow Burn Sunday
|
||||
1: 'mode_soft_monday', // Monday — Mood Check Monday
|
||||
2: 'mode_snack_mission', // Tuesday — Tiny Win Tuesday
|
||||
3: 'mode_no_phone_moment', // Wednesday — Real One Wednesday
|
||||
4: 'mode_laugh_reset', // Thursday — Laugh It Off Thursday
|
||||
5: 'mode_flirty_friday', // Friday — Flirty Friday
|
||||
6: 'mode_weekend_side_quest', // Saturday — Side Quest Saturday
|
||||
};
|
||||
/** Days since epoch for a YYYY-MM-DD Chicago date — mirrors LocalDate.toEpochDay() the client
|
||||
* uses for its deterministic daily offset, so server and client land on the same question. */
|
||||
function epochDayOf(dateStr) {
|
||||
const [y, m, d] = dateStr.split('-').map(Number);
|
||||
return Math.floor(Date.UTC(y, m - 1, d) / 86400000);
|
||||
}
|
||||
/**
|
||||
* Deterministically pick the SAME daily question the free client would compute for [dateStr]:
|
||||
* today's weekday mode, ordered by id, indexed by epochDay % poolSize. Making this SERVER-side and
|
||||
* authoritative also fixes partners in different time zones getting different questions (the client
|
||||
* falls back to device-local weekday only when there is no server assignment). Returns null if the
|
||||
* questions pool is unseeded so the caller can no-op rather than write an unresolvable id.
|
||||
*/
|
||||
async function pickDailyQuestionId(dateStr) {
|
||||
const db = admin.firestore();
|
||||
// The whole free daily pool is tiny (~75 rows); fetch once and filter in memory to avoid a
|
||||
// composite index on (active, isPremium, modeTag).
|
||||
const snapshot = await db
|
||||
.collection('questions')
|
||||
.where('active', '==', true)
|
||||
.where('isPremium', '==', false)
|
||||
.get();
|
||||
if (snapshot.empty) {
|
||||
// Rollout fallback: keep the feature working until the questions pool is seeded.
|
||||
return 'q_default_daily';
|
||||
}
|
||||
const docs = snapshot.docs;
|
||||
const chosen = docs[Math.floor(Math.random() * docs.length)];
|
||||
return chosen.id;
|
||||
if (snapshot.empty)
|
||||
return null;
|
||||
const weekday = new Date(`${dateStr}T12:00:00Z`).getUTCDay();
|
||||
const modeTag = WEEKDAY_MODE_TAGS[weekday];
|
||||
const inMode = snapshot.docs
|
||||
.filter((d) => d.get('modeTag') === modeTag)
|
||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
||||
const pool = inMode.length > 0
|
||||
? inMode
|
||||
: snapshot.docs.slice().sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); // wildcard/empty-mode fallback
|
||||
const offset = ((epochDayOf(dateStr) % pool.length) + pool.length) % pool.length;
|
||||
return pool[offset].id;
|
||||
}
|
||||
/**
|
||||
* Returns today's date as YYYY-MM-DD in America/Chicago time.
|
||||
|
|
@ -203,35 +236,47 @@ function nextCstDateString(from) {
|
|||
d.setUTCDate(d.getUTCDate() + 1);
|
||||
return formatCstDate(d);
|
||||
}
|
||||
const d = new Date();
|
||||
const cst = applyCstOffset(d);
|
||||
cst.setUTCDate(cst.getUTCDate() + 1);
|
||||
return formatCstDate(cst);
|
||||
// Tomorrow in Chicago: parse today's Chicago date back to a concrete instant and add 24h.
|
||||
const todayChicago = parseCstDate(formatCstDate(new Date()));
|
||||
todayChicago.setUTCDate(todayChicago.getUTCDate() + 1);
|
||||
return formatCstDate(todayChicago);
|
||||
}
|
||||
function applyCstOffset(d) {
|
||||
const utcMs = d.getTime();
|
||||
const cstMs = utcMs + CST_OFFSET_HOURS * 60 * 60 * 1000;
|
||||
return new Date(cstMs);
|
||||
/**
|
||||
* UTC-vs-Chicago offset in hours at the given instant (5 during CDT, 6 during CST).
|
||||
* DST-safe replacement for the old hardcoded -6 (which mislabeled dates and shifted
|
||||
* reveals by an hour for the ~8 months of the year Chicago observes daylight time).
|
||||
* Pattern matches streakReminder.ts's Intl-based chicagoDateKey.
|
||||
*/
|
||||
function chicagoOffsetHours(atUtcMs) {
|
||||
const parts = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: 'America/Chicago',
|
||||
hour12: false,
|
||||
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
|
||||
}).formatToParts(new Date(atUtcMs));
|
||||
const get = (t) => { var _a; return Number((_a = parts.find((p) => p.type === t)) === null || _a === void 0 ? void 0 : _a.value); };
|
||||
const localAsUtcMs = Date.UTC(get('year'), get('month') - 1, get('day'), get('hour') % 24, get('minute'));
|
||||
return Math.round((atUtcMs - localAsUtcMs) / 3600000);
|
||||
}
|
||||
/** YYYY-MM-DD for the given instant in America/Chicago (en-CA gives ISO date order). */
|
||||
function formatCstDate(d) {
|
||||
const cst = applyCstOffset(d);
|
||||
const y = cst.getUTCFullYear();
|
||||
const m = String(cst.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(cst.getUTCDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
return new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: 'America/Chicago',
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
}).format(d);
|
||||
}
|
||||
/** The UTC instant of midnight America/Chicago on the given calendar date. */
|
||||
function parseCstDate(dateStr) {
|
||||
const [y, m, day] = dateStr.split('-').map(Number);
|
||||
// Build a UTC timestamp that represents midnight CST, then reverse the offset.
|
||||
const cstMidnightMs = Date.UTC(y, m - 1, day, 0, 0, 0, 0);
|
||||
return new Date(cstMidnightMs - CST_OFFSET_HOURS * 60 * 60 * 1000);
|
||||
// Guess with the standard offset, then correct with the real offset at that instant.
|
||||
const guessMs = Date.UTC(y, m - 1, day, 6);
|
||||
const offset = chicagoOffsetHours(guessMs);
|
||||
return new Date(Date.UTC(y, m - 1, day, offset));
|
||||
}
|
||||
/** The UTC instant of 6:00 PM America/Chicago on the given calendar date. */
|
||||
function timestampAt6PmCst(dateStr) {
|
||||
const d = parseCstDate(dateStr);
|
||||
const utcMs = d.getTime();
|
||||
// 6:00 PM CST == 18:00 CST == 00:00 UTC next day.
|
||||
// We already have midnight CST in UTC form; add 18 hours.
|
||||
const at6pmCstMs = utcMs + 18 * 60 * 60 * 1000;
|
||||
return admin.firestore.Timestamp.fromMillis(at6pmCstMs);
|
||||
const [y, m, day] = dateStr.split('-').map(Number);
|
||||
const guessMs = Date.UTC(y, m - 1, day, 18 + 6);
|
||||
const offset = chicagoOffsetHours(guessMs);
|
||||
return admin.firestore.Timestamp.fromMillis(Date.UTC(y, m - 1, day, 18 + offset));
|
||||
}
|
||||
//# sourceMappingURL=assignDailyQuestion.js.map
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,38 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const assignDailyQuestion_1 = require("./assignDailyQuestion");
|
||||
// DST regression tests for the Chicago date helpers (the old hardcoded -6 offset
|
||||
// mislabeled dates and shifted reveal times during the ~8 CDT months).
|
||||
describe('Chicago date helpers (DST-safe)', () => {
|
||||
it('labels a summer (CDT, UTC-5) evening with the correct local date', () => {
|
||||
// 2026-07-08T03:30Z == 2026-07-07 22:30 CDT — the old -6 math said "07-07" too,
|
||||
// but 2026-07-08T04:30Z == 2026-07-07 23:30 CDT is where they diverge:
|
||||
expect((0, assignDailyQuestion_1.formatCstDate)(new Date('2026-07-08T03:30:00Z'))).toBe('2026-07-07');
|
||||
// 23:30 CDT is still Jul 7 locally; hardcoded -6 called it Jul 7 22:30 → same date,
|
||||
// but 2026-07-08T05:30Z == Jul 8 00:30 CDT; old math: Jul 7 23:30 → WRONG date.
|
||||
expect((0, assignDailyQuestion_1.formatCstDate)(new Date('2026-07-08T05:30:00Z'))).toBe('2026-07-08');
|
||||
});
|
||||
it('labels a winter (CST, UTC-6) instant correctly', () => {
|
||||
// 2026-01-08T05:30Z == 2026-01-07 23:30 CST
|
||||
expect((0, assignDailyQuestion_1.formatCstDate)(new Date('2026-01-08T05:30:00Z'))).toBe('2026-01-07');
|
||||
expect((0, assignDailyQuestion_1.formatCstDate)(new Date('2026-01-08T06:30:00Z'))).toBe('2026-01-08');
|
||||
});
|
||||
it('parses midnight Chicago with the seasonal offset', () => {
|
||||
// Summer: midnight CDT == 05:00Z
|
||||
expect((0, assignDailyQuestion_1.parseCstDate)('2026-07-07').toISOString()).toBe('2026-07-07T05:00:00.000Z');
|
||||
// Winter: midnight CST == 06:00Z
|
||||
expect((0, assignDailyQuestion_1.parseCstDate)('2026-01-07').toISOString()).toBe('2026-01-07T06:00:00.000Z');
|
||||
});
|
||||
it('puts the 6 PM reveal at 6 PM local in both seasons', () => {
|
||||
// Summer: 18:00 CDT == 23:00Z
|
||||
expect((0, assignDailyQuestion_1.timestampAt6PmCst)('2026-07-07').toDate().toISOString()).toBe('2026-07-07T23:00:00.000Z');
|
||||
// Winter: 18:00 CST == 00:00Z next day
|
||||
expect((0, assignDailyQuestion_1.timestampAt6PmCst)('2026-01-07').toDate().toISOString()).toBe('2026-01-08T00:00:00.000Z');
|
||||
});
|
||||
it('round-trips format(parse(d)) across the spring-forward boundary', () => {
|
||||
for (const day of ['2026-03-07', '2026-03-08', '2026-03-09', '2026-11-01', '2026-11-02']) {
|
||||
expect((0, assignDailyQuestion_1.formatCstDate)((0, assignDailyQuestion_1.parseCstDate)(day))).toBe(day);
|
||||
}
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=assignDailyQuestion.test.js.map
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"assignDailyQuestion.test.js","sourceRoot":"","sources":["../../src/questions/assignDailyQuestion.test.ts"],"names":[],"mappings":";;AAAA,+DAAsF;AAEtF,iFAAiF;AACjF,uEAAuE;AACvE,QAAQ,CAAC,iCAAiC,EAAE,GAAG,EAAE;IAC/C,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,gFAAgF;QAChF,uEAAuE;QACvE,MAAM,CAAC,IAAA,mCAAa,EAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;QAC1E,oFAAoF;QACpF,gFAAgF;QAChF,MAAM,CAAC,IAAA,mCAAa,EAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;IAC5E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,4CAA4C;QAC5C,MAAM,CAAC,IAAA,mCAAa,EAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;QAC1E,MAAM,CAAC,IAAA,mCAAa,EAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;IAC5E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,iCAAiC;QACjC,MAAM,CAAC,IAAA,kCAAY,EAAC,YAAY,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;QACjF,iCAAiC;QACjC,MAAM,CAAC,IAAA,kCAAY,EAAC,YAAY,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;IACnF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,8BAA8B;QAC9B,MAAM,CAAC,IAAA,uCAAiB,EAAC,YAAY,CAAC,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;QAC/F,uCAAuC;QACvC,MAAM,CAAC,IAAA,uCAAiB,EAAC,YAAY,CAAC,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;IACjG,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,KAAK,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,CAAC,EAAE,CAAC;YACzF,MAAM,CAAC,IAAA,mCAAa,EAAC,IAAA,kCAAY,EAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACpD,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
||||
Loading…
Reference in New Issue