2026-06-27 13:31:09 -05:00
|
|
|
// qa/qa_push.js — send a REAL notification+data FCM to a user's token (faithful to the app's own
|
|
|
|
|
// pushes) so a KILLED app shows it in the shade and tapping it cold-starts via the real OS splash
|
|
|
|
|
// handover. This is the ONE path that reproduces the "opens-and-closes" crash class (e.g. the
|
|
|
|
|
// splash-exit iconView NPE) — `adb am start --es …` does NOT (different launch path), and
|
|
|
|
|
// `am force-stop` can't even receive FCM. See ClaudeQAPlan.md (Pass E crash-triage / reproduction
|
|
|
|
|
// fidelity) and memory/project_notifications.md.
|
|
|
|
|
//
|
|
|
|
|
// Usage: NODE_PATH=functions/node_modules node qa/qa_push.js <uid> <type> [key=value ...]
|
|
|
|
|
// node qa/qa_push.js <uid> partner_started_game game_type=this_or_that
|
|
|
|
|
// node qa/qa_push.js <uid> partner_finished_game game_type=wheel # auto-resolves a completed session
|
|
|
|
|
// node qa/qa_push.js <uid> chat_message conversation_id=main
|
|
|
|
|
// node qa/qa_push.js <uid> partner_answered
|
|
|
|
|
// Env overrides: SA_JSON, COUPLE_ID.
|
|
|
|
|
const admin = require('firebase-admin')
|
|
|
|
|
const path = require('path')
|
feat: security hardening, CI, analytics, invite deep links, solo mode, weekly recap
Roadmap phases 0-3.1 (see plan). Verified: unit tests, functions tests,
firestore rules 119/119, live emulator smokes.
Security + CI:
- Move App Check debug token out of committed build.gradle.kts to
local.properties (secret() resolver: local.properties -> -P -> env);
admin service-account key relocated outside the repo.
- Add GitHub Actions (android unit/scan/gitleaks, backend functions +
rules emulator via emulators:exec) with a CI google-services.json stub
and a reviewed .gitleaksignore baseline.
- Fix firestore rules tests to the current deployed contract (was 108/118,
now 119/119).
Analytics + consent:
- FirebaseRetentionAnalytics behind a fail-safe composite sink, single-
sourced hashing (AnalyticsHashing), Settings -> Privacy opt-out toggle
gating Firebase + the sink, and the ~11 missing event call sites.
- NoPlaintextInAnalyticsTest guards the content-free rule at build time.
Funnel:
- Invite deep links (https App Link + closer:// fallback), JoinLink parser,
PendingJoinCodeStore, prefilled/already-paired pairing, richer share text.
- Solo pre-pairing: UnpairedLockedCard on Play, SoloAnswerMigrator + consent
dialog moving pre-pairing answers into Couple Lore.
Weekly recap:
- WeeklyRecapScreen driven by the existing generator from local answer data,
Home card rewired, content-free shareable card.
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-06 20:33:38 -05:00
|
|
|
const os = require('os')
|
2026-06-27 13:31:09 -05:00
|
|
|
|
feat: security hardening, CI, analytics, invite deep links, solo mode, weekly recap
Roadmap phases 0-3.1 (see plan). Verified: unit tests, functions tests,
firestore rules 119/119, live emulator smokes.
Security + CI:
- Move App Check debug token out of committed build.gradle.kts to
local.properties (secret() resolver: local.properties -> -P -> env);
admin service-account key relocated outside the repo.
- Add GitHub Actions (android unit/scan/gitleaks, backend functions +
rules emulator via emulators:exec) with a CI google-services.json stub
and a reviewed .gitleaksignore baseline.
- Fix firestore rules tests to the current deployed contract (was 108/118,
now 119/119).
Analytics + consent:
- FirebaseRetentionAnalytics behind a fail-safe composite sink, single-
sourced hashing (AnalyticsHashing), Settings -> Privacy opt-out toggle
gating Firebase + the sink, and the ~11 missing event call sites.
- NoPlaintextInAnalyticsTest guards the content-free rule at build time.
Funnel:
- Invite deep links (https App Link + closer:// fallback), JoinLink parser,
PendingJoinCodeStore, prefilled/already-paired pairing, richer share text.
- Solo pre-pairing: UnpairedLockedCard on Play, SoloAnswerMigrator + consent
dialog moving pre-pairing answers into Couple Lore.
Weekly recap:
- WeeklyRecapScreen driven by the existing generator from local answer data,
Home card rewired, content-free shareable card.
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-06 20:33:38 -05:00
|
|
|
// Admin SA key lives OUTSIDE the repo (never keep live credentials in an agent-accessed
|
|
|
|
|
// working tree). Override with SA_JSON if yours is elsewhere.
|
2026-06-27 13:31:09 -05:00
|
|
|
const SA = process.env.SA_JSON ||
|
feat: security hardening, CI, analytics, invite deep links, solo mode, weekly recap
Roadmap phases 0-3.1 (see plan). Verified: unit tests, functions tests,
firestore rules 119/119, live emulator smokes.
Security + CI:
- Move App Check debug token out of committed build.gradle.kts to
local.properties (secret() resolver: local.properties -> -P -> env);
admin service-account key relocated outside the repo.
- Add GitHub Actions (android unit/scan/gitleaks, backend functions +
rules emulator via emulators:exec) with a CI google-services.json stub
and a reviewed .gitleaksignore baseline.
- Fix firestore rules tests to the current deployed contract (was 108/118,
now 119/119).
Analytics + consent:
- FirebaseRetentionAnalytics behind a fail-safe composite sink, single-
sourced hashing (AnalyticsHashing), Settings -> Privacy opt-out toggle
gating Firebase + the sink, and the ~11 missing event call sites.
- NoPlaintextInAnalyticsTest guards the content-free rule at build time.
Funnel:
- Invite deep links (https App Link + closer:// fallback), JoinLink parser,
PendingJoinCodeStore, prefilled/already-paired pairing, richer share text.
- Solo pre-pairing: UnpairedLockedCard on Play, SoloAnswerMigrator + consent
dialog moving pre-pairing answers into Couple Lore.
Weekly recap:
- WeeklyRecapScreen driven by the existing generator from local answer data,
Home card rewired, content-free shareable card.
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-06 20:33:38 -05:00
|
|
|
path.join(os.homedir(), '.config', 'closer', 'closer-admin-sa.json')
|
2026-06-27 13:31:09 -05:00
|
|
|
const COUPLE = process.env.COUPLE_ID || 'Xal3Kw3gjSdn0niERYKJ'
|
|
|
|
|
|
|
|
|
|
admin.initializeApp({ credential: admin.credential.cert(require(SA)) })
|
|
|
|
|
const db = admin.firestore()
|
|
|
|
|
|
|
|
|
|
const [uid, type, ...rest] = process.argv.slice(2)
|
|
|
|
|
if (!uid || !type) {
|
|
|
|
|
console.error('usage: qa_push.js <uid> <type> [key=value ...]')
|
|
|
|
|
process.exit(2)
|
|
|
|
|
}
|
|
|
|
|
const extras = {}
|
|
|
|
|
for (const kv of rest) {
|
|
|
|
|
const i = kv.indexOf('=')
|
|
|
|
|
if (i > 0) extras[kv.slice(0, i)] = kv.slice(i + 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Channel must exist in the app (NotificationChannelSetup). game_* → game_activity, else partner_activity.
|
|
|
|
|
const channelId = type.includes('game') ? 'game_activity' : 'partner_activity'
|
|
|
|
|
|
|
|
|
|
;(async () => {
|
|
|
|
|
// A results-ready push needs a completed session id; auto-resolve one if not supplied.
|
|
|
|
|
if (type === 'partner_finished_game' && !extras.game_session_id && extras.game_type) {
|
|
|
|
|
const q = await db.collection('couples').doc(COUPLE).collection(extras.game_type).get()
|
|
|
|
|
let pick = null
|
|
|
|
|
q.forEach((d) => {
|
|
|
|
|
const a = d.data().answers || {}
|
|
|
|
|
if (Object.keys(a).length >= 2 && !pick) pick = d.id
|
|
|
|
|
})
|
|
|
|
|
if (pick) extras.game_session_id = pick
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tk = await db.collection('users').doc(uid).collection('fcmTokens').get()
|
|
|
|
|
const token = tk.docs[0] && tk.docs[0].data().token
|
|
|
|
|
if (!token) {
|
|
|
|
|
console.error('NO_TOKEN for ' + uid + ' (launch the app once so it registers a token)')
|
|
|
|
|
process.exit(3)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = { type, couple_id: COUPLE, ...extras }
|
|
|
|
|
// Distinct body so the smoke can find + tap exactly this notification (not a grouped summary).
|
|
|
|
|
const marker = 'QA-SMOKE:' + type
|
|
|
|
|
const res = await admin.messaging().send({
|
|
|
|
|
token,
|
|
|
|
|
notification: { title: 'Closer', body: marker },
|
|
|
|
|
android: { priority: 'high', notification: { channelId } },
|
|
|
|
|
data,
|
|
|
|
|
})
|
|
|
|
|
console.log('SENT ' + type + ' data=' + JSON.stringify(data) + ' -> ' + res)
|
|
|
|
|
process.exit(0)
|
|
|
|
|
})().catch((e) => {
|
|
|
|
|
console.error('ERR ' + e.message)
|
|
|
|
|
process.exit(1)
|
|
|
|
|
})
|