Android app v1.3.0: fully self-contained app (drawer, upload, tools, contributors, links, PDF viewer, native Google sign-in) + FCM push - #14
Merged
Conversation
Self-contained app (no browser/website fallback for normal features): - Side drawer (hamburger) wiring every feature to in-app screens: Upload paper, Study tools, Contributors, Links, About (+ existing tabs) - In-app Upload: same validation/throttle, gofile storage, pendingUploads Firestore queue and 10-point reward as the website; Android file picker; on-device images-to-PDF assembly (hand-rolled minimal PDF writer) - Study tools (CGPA/attendance/planner) fully on-device; request-a-tool keeps its legit t.me external intent - Contributors via ONE cached /api/contributors call (24h SWR); join card routes to in-app upload - Links: the 14 portals rendered statically in-app; only the tapped portal opens externally - PDF: "Open PDF" launches the new native PdfViewerActivity (PdfRenderer, lazy pages, pinch zoom/pan, progress/error states, temp-cache only, never the Worker); failure falls back to the system with the SAME direct URL; Drive/mediafire landing pages stay external intents - Google sign-in now native (Credential Manager -> accounts:signInWithIdp, same dsmnru-data project, nonce-protected); unconfigured builds get an in-app explainer + email/password, never a website hand-off - Audit: removed every website redirect (home shortcuts, profile website item, paper report -> in-app feedback sheet, unresolvable slugs -> in-app prefilled search); about.js documents the remaining external list - docs/GOOGLE_SIGNIN_SETUP.md for the out-of-repo console config Native FCM push (same dsmnru-data project): - FcmService: token rotation handling, version-gated `all_users` topic subscription (one FCM call per install/update, none per launch), token kept device-local only (no Firestore), foreground rendering behind the permission check, tap intent = ACTION_VIEW data URL on MainActivity - Manifest: POST_NOTIFICATIONS, FCM service + MESSAGING_EVENT filter, default channel/icon/color meta-data; Gradle: firebase-messaging dep, google-services stays conditional (builds without the json degrade silently); MainActivity: channel creation + REAL system permission dialog once per install ~9s into the first session (13+ only, never re-asked, skipped when Firebase is unconfigured) - Sender side (admin -> Worker -> FCM) does not exist yet in the repo; docs/PUSH_NOTIFICATIONS.md specifies it exactly (no invented backend) - .gitignore: google-services.json + keystores are never committed Tests: npm test = 46 tests / 44 pass / 0 fail / 2 skip (jsdom smoke runs in CI). New: features.test.mjs (17 static+unit), app-native-bridge.test.mjs (jsdom with a fake Capacitor bridge; skipped in this sandbox), fcm.test.mjs (manifest/Gradle/service/permission/payload-contract audits). Existing tests untouched and green. versionCode 4, versionName 1.3.0. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The android-apk workflow now also runs on pull_request events (checks job: Worker suite + app npm test with jsdom reused from worker/node_modules via 'npm ci --prefix worker', then the debug-apk artifact build). Correct the stale 'npm i jsdom' note in the smoke-test header and state the PR trigger in the README build section. No code changes. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…edupe, home async guards, test realm/fixtures, action bumps Root causes and fixes per CI failure: A) DsmnruAppPlugin compile errors (missing androidx.credentials.*/googleid.*): the Credential Manager + Google ID token dependencies were missing from android/app/build.gradle. Added androidx.credentials:credentials:1.3.0, credentials-play-services-auth:1.3.0 and com.google.android.libraries.identity.googleid:googleid:1.1.1 — compatible with AGP 8.13.0 / Gradle 8.14.3 / Java 21 / Capacitor 8.5.1. Native architecture unchanged: Credential Manager → Google ID token → Firebase accounts:signInWithIdp (no browser/website auth). B) MainActivity: onResume/onPause overrode BridgeActivity's PUBLIC lifecycle methods with weaker protected visibility (verified against Capacitor 8.5.1 BridgeActivity source). Overrides are now public; behavior kept. C) PdfViewerActivity PageImageView used a nonexistent ImageView.getPageBitmap(). Replaced with an explicit `displayed` field tracking the attached bitmap — the intended dedupe (LruCache hit ⇒ skip redundant setImageBitmap/matrix reset) is preserved, as are lazy rendering, zoom/pan and the temp-cache contract. D) app-frontend-smoke: the upload validation step ran as a signed-in user, for whom the form legitimately prefills "your name" — so the title error surfaced instead of the name validation state. The fixture now clears the (by-design) prefilled fields before the empty-form submit, reaching the name-validation state the assertion expects; assertion unchanged. Also fixed the real Home crash behind "renderStats → Cannot set properties of null": the section renderers invoked from late async callbacks (SWR revalidate / pull-refresh) crashed after navigating away — all Home renderers now no-op when their host nodes are gone. E) app-native-bridge searched with a typed query while signed out, but the app gates server search behind a verified session (website-parity rule) — the gate correctly fired instead of fetching. The test now follows the real policy: assert the gate, sign in via the mocked password path, then search (debounce/abort/stale-protection paths unchanged), and moved the Google not-configured/success scenarios after an explicit sign-out. All original assertions preserved; none weakened. F) jsdom suites now execute in-repo (worker/node_modules): the harness bridged window FormData/File/Blob/FileReader into the host realm so `new FormData()` in upload code shares the realm of the jsdom File objects (Node's undici FormData rejected jsdom Blobs — realm mismatch, not an app bug). Local: 46/46 app tests + all 7 Worker suites pass. G) actions/checkout@v4→v7, setup-node@v4→v7, setup-java@v4→v6 (currently supported majors, clearing the node20 deprecation warnings). Triggers and job behavior unchanged. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…ble compile diagnostics (failure behavior unchanged via pipefail) Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…lManager factory getClient() -> create() Per the androidx.credentials 1.3.0 source (CredentialManager.kt), the companion factory is now @JvmStatic fun create(context: Context); getClient(context) only existed up to 1.2.x. The plugin called the old name, so :app:compileDebugJavaWithJavac failed with 'cannot find symbol: method getClient(Activity)'. The remainder of the credential surface is unchanged and version-correct: GetGoogleIdOption.Builder setters, GoogleIdTokenCredential.createFrom (@JvmStatic), CustomCredential.getType/getData and the getCredentialAsync entry point (still located reflectively, arity-shape tolerant). Native Google architecture untouched: Credential Manager -> Google ID token -> Firebase accounts:signInWithIdp. Also reverts the temporary step-summary log capture (its purpose, surfacing the compile error, is served; failure behavior identical). Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…tion warning; behavior unchanged) Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…RL-free UI Debug APK update strategy (#1): all debug APKs (local + CI) now sign with a shared COMMITTED debug.keystore holding the standard PUBLIC Android debug credentials (store 'android', alias 'androiddebugkey') — one stable debug signature, so newly downloaded debug APKs UPDATE the installed debug build instead of failing with "package conflicts with an existing package". applicationId stays com.dsmnru.pyq everywhere (verified against the Capacitor config, namespace and manifest — one app identity); versionCode 5 / versionName 1.3.1, version now shown on Profile/About ("Version 1.3.1"). Release signing stays out-of-repo; no production credentials invented. UI decongestion (#2): global [hidden]{display:none!important} correctness rule, calmer section headers (quiet uppercase labels), softer card borders, borderless stat pills, accent-line notices instead of boxed gold panels, more vertical rhythm (stack 22px, card-pad 18px, form fields 16px), empty Home rails now hide instead of showing filler text, trimmed notice copy, drawer/tab/sheet spacing polish. All functionality and test-asserted content preserved. No technical endpoints in the UI (#3): About no longer renders the Worker hostname or the Firebase project id; the paper report POST moved to feedback.js (views stay endpoint-free); search/browse/paper/contributors/ home/upload errors are human text with details in console logs; friendly() scrubs URLs; Google fallback copy de-jargoned. Enforced by a new audit test. Back arrow (#4): root cause was CSS — .icon-btn display outranked the UA [hidden] rule, so the back arrow (and every hidden-toggled element) stayed visible. Fixed by the global [hidden] rule plus an explicit state machine: back is enabled ONLY on pushed screens; Home/tab roots show only the hamburger; tab switches clear back state; drawer never mutates the stack. Covered by a new jsdom navigation-state test. Create account (#5): the silent failure was wireSubmit looking for [data-err] INSIDE the signup/reset forms while the single error div sat outside the login form — errors were written to null. Every form now owns its error target; busy labels ('Creating account…'); duplicate-submit guard; chosen name now lands in the session AND users/{uid} profile (nameOverride through adoptTokenSession); friendly mappings added (INVALID_LOGIN_CREDENTIALS, MISSING_PASSWORD) and URL-scrubbed fallbacks. Covered by jsdom tests for EMAIL_EXISTS feedback + successful signup. Google sign-in (#6): no website hand-off existed or remains (audited); native Credential Manager → ID token → accounts:signInWithIdp preserved; not-configured/explicit fallbacks are in-app only; assertions updated to the human copy. Profile & rewards (#7/#8): photo avatar (Firebase/Google picture) with initials fallback, editable display name (accounts:update + users/{uid} name patch — the SAME website profile), lazy reward summary reading the SAME email-keyed reward_accounts/{email-key}.points + point_transactions the website's points card uses (two reads, 5-min session cache, sign-out invalidation), zero-state with upload CTA, human error + retry. No fake delete button (no existing secure flow). Sign-out clears session + caches. Auth lifecycle (#9/#10): untouched architecture; rewards load only after authentication; every async op has loading → success/readable error. Tests (#12): 48/48 pass locally (jsdom runs in-repo): new coverage for back-arrow state, signup success + Firebase error, profile rendering (name/email/version/rewards), profile update (updateMask=name), rewards rendering, no-endpoint audit, [hidden] rule, per-form signup errors. Worker suites: 398/398 pass. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…ssion-dialog path fixed (v1.3.2)
Google Sign-In verification against the committed google-services.json
(android-app branch):
- project_id dsmnru-data (same project as the website), package_name
com.dsmnru.pyq (matches applicationId, namespace and the Capacitor appId),
oauth_client client_type:3 (WEB) present -> the Google Services plugin
generates default_web_client_id at build time.
- DsmnruAppPlugin now resolves serverClientId from the GENERATED
default_web_client_id resource FIRST and keeps google_web_client_id only
as a fallback — the WEB OAuth client is always the token audience; the
ANDROID OAuth client is never used as serverClientId. Flow unchanged:
Credential Manager -> Google ID token -> Identity Toolkit
accounts:signInWithIdp (same Firebase accounts, no website hand-off,
users/{uid} profile schema reused; new Google users get accounts via the
same IdP exchange).
FCM verification + fix:
- Root cause of the missing Android 13+ permission dialog on fresh
installs: every previous CI APK had no google-services.json, and
MainActivity intentionally gates the ask on Firebase being configured.
With the config committed the gate opens; additionally:
* the gate now also accepts the generated google_app_id resource
(hasFirebaseConfigResources) so build-time config always enables the
dialog regardless of Firebase runtime init order;
* the asked-flag is persisted only AFTER requestPermissions was accepted
by the OS (a rare OEM throw can no longer silence the dialog forever);
* the ask posts only from a live activity (isFinishing/isDestroyed guard)
and remains scheduled from onResume (valid resumed state, ~9s, once per
install, never on <13, independent of login/localStorage/website).
- Verified unchanged: token rotation -> force re-subscribe, all_users
subscription independent of the notification permission, token kept in
device-local prefs only (never Firestore/UI), dsmnru_general channel,
POST_NOTIFICATIONS manifest entry, no polling.
Version: versionCode 6 / versionName 1.3.2 (Profile/About show it).
Tests: 51/51 android (new: token/subscription-permission independence,
first-session dialog execution semantics, web-client wiring audit) and all
worker suites green.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…scussion, in-app password reset states
- Branding: repo logo.png drives www/img/logo.png, all launcher densities
(legacy + adaptive foregrounds), round icon and splash (navy #0B245B
background sampled from the logo; transparency preserved; adaptive-safe
padding). CSS brand surfaces (app bar, drawer) use the same asset; the
generated emblem is fully retired.
- Home: ONE brand area (the app bar). Hero is now kicker → title → compact
search entry that hands its query to the dedicated Search screen
(router.tab('search', { q }) — Search already seeds and runs it).
- Discussion: paper comments are fully IN-APP via www/js/discussion.js —
same Firestore comments collection, fields and pyqs/{id}/comments
fallback as the website; lazy-loaded only when the section is opened;
composer with human validation; new comment appears immediately; the
'Discussion on website' hand-off is removed.
- Password reset: readable unknown-email error; busy state 'Sending reset
link…'; success note ('...check your inbox and spam folder.') shown
in-form only after the Firebase call resolves.
- Google/email auth copy: explicit human states; no raw errors anywhere.
- Paper detail: lighter chrome (title hierarchy first, tighter meta grid).
- Tests: branding/logo audit, single home brand area, home search-entry
handoff, in-app comments (lazy/post/no-website), Google + reset copy.
54/54 android (jsdom smoke included), 139/139 worker. v1.3.3 / code 7.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…hange, trimmed logo everywhere, hero spacing
- Create Account page now offers 'Continue with Google' through the SAME
startGoogleSignIn native Credential Manager flow (one shared handler —
no second implementation, no browser, no website hand-off). Existing and
new Google users both land in the same Firebase + users/{uid} state.
- Email sign-up copy pinned to spec: 'Creating your account…' /
'Account created successfully.'; friendly network error wording.
- Password reset: email format validated first, busy 'Sending reset
email…', success 'Password reset email sent. Check your inbox and spam
folder.' — shown only after the Firebase call resolves (unchanged
semantics, hardened copy).
- Profile restructured: avatar/name/email header (email read-only with an
explicit 'cannot be edited here' note), Contribution card (points +
approved uploads from the SAME reward ledger), Personal information rows
(lazy users/{uid} read — name/course/phone, the website's exact editable
schema), Account group (Edit Profile sheet with saving/success/error
states; Change Password flow), App version.
- Change Password (email/password accounts): current password re-verified
via a fresh signInWithPassword (Firebase recent-auth), then
accounts:update rotates the password and the session tokens stay fresh
(no forced sign-out). Google-only accounts get a honest explainer instead
of a fake current-password field. Passwords never stored or logged.
- Profile edits persist to the SAME users/{uid} document the website edits
(updateMask limited to name/course/phone), cache invalidated on save,
UI updates immediately without restart.
- Logo: transparent padding cropped from the repository PNG (content
re-centered) and ALL brand assets regenerated from it — web logo, five
launcher densities (legacy/round/adaptive), splash icon. No redraw.
Old emblem.png deleted (favicon now logo.png) so stale branding cannot
resurface; app-bar logo 30→32px.
- Home hero: comfortable vertical rhythm (greeting margin, 1.3 line-height,
larger paddings, search field and stats separated) — no font shrinking.
- Tests: 55/55 android (Google-on-signup interaction, shared-handler audit,
password-change re-auth ordering + no-logged-passwords audit, reset copy,
profile edit/save/read-only-email, splash chain audit, per-session
profile-read budget), 139/139 worker. v1.3.4 / versionCode 8.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- Home now starts directly with Quick access (course cards) → Recently added papers → Trending → Pick up where you left off → Shortcuts. No greeting/date line, no 'Find any PYQ in seconds' hero, no Home search field, no stat pills — no empty container left behind. - Search UX: nothing replaces the removed box. The bottom-navigation Search tab and its dedicated full search screen remain the ONE search experience (smoke-tested: opens idle, filters intact, typed queries still execute). Home no longer renders any search UI. - Cleanup of hero-only code: greeting/date rendering, home search form + focus/submit listeners, renderStats + #home-stats (the archive counts remain in the /api/homepage payload and on course cards — only the duplicate hero display was removed), and dead CSS (.hero card, .hero-kicker, .hero-top, .hero-title em, .stat-pill(s), .search-entry and the unused pre-v1.3.3 .search-launch block). .hero-title/.hero-emblem stay (Profile numbers / About emblem still use them). - Tests updated to the new structure (no test weakened): Home asserts NO hero + NO search field + quick-access-first order; Search tab still opens the full idle search. 55/55 android, 139/139 worker. v1.3.5 / versionCode 9. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…rors, robust parsing
Root cause of 'posts work, reads fail with a connection error': the read
chain only fell back on EMPTY results. The primary query (paperId ==
… orderBy createdAt DESC) needs the (paperId, createdAt) composite index;
when Firestore rejects it (FAILED_PRECONDITION) the whole load aborted and
every failure was mislabeled 'Check your connection'. Writes kept working
because document POSTs need no index. (The website survives the same
condition with its own on-error → unordered fallback.)
- discussion.js rewritten:
· fallbacks now trigger on ERROR or EMPTY: ordered query → unordered
equality-only query (still served by the automatic single-field paperId
index — NOT a collection scan; newest-first client-side, exactly the
website's fallback) → legacy pyqs/{id}/comments subcollection read,
parent-scoped without the paperId filter (legacy rows may not carry it).
· errors CLASSIFIED: network → 'Unable to load discussion. Check your
connection and try again.'; permission → 'Unable to load this
discussion right now.'; query/data → 'Unable to load this discussion.
Please try again.' Technical detail (HTTP status, server message) is
logged only, never rendered.
· one malformed document is skipped with a log line instead of breaking
the list; pending/null/missing server timestamps and legacy rows parse
safely; lists dedupe by the real Firestore document id.
· post path: same top-level collection + exact website field shape, with
the website's subcollection write fallback; 403 keeps the distinct
verify-email message; network vs other failures worded distinctly.
- paper.js discussion UI: explicit 'Loading discussion…' state, exact empty
copy 'No comments yet. Start the discussion.', classified error + Retry
that re-runs the actual fetch, comment rows carry data-comment-id and the
optimistic insert dedupes against already-shown docs (never twice).
- Still one-time fetches (no listeners), still lazy (opened only from the
paper detail), resolved paper id logged in dev logs only, no website in
either path. Firestore rules untouched; no second collection; no index
conversion into full scans.
- Tests: new discussion contract suite (scripted REST: index-failure →
fallback query shape, network/permission/query classification + exact
UI copy, malformed-row survival, doc-id dedupe, write field shape, real
doc id resolution); jsdom smoke reproduces the production failure
(ordered query 400 → classified read error → retry works → empty → post
→ reopen shows the comment exactly once). 56/56 android, 139/139 worker.
v1.3.6 / versionCode 10.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…apk CI artifact, Firebase config injection Release readiness (applicationId/namespace com.dsmnru.pyq unchanged and verified across build.gradle, Capacitor config and google-services wiring; debug + release share the same application id): - build.gradle: release signing is now injected at CI time through the ANDROID_KEYSTORE_* environment variables (GitHub Secrets) — the release buildType signs ONLY when the signing environment is present and stays UNSIGNED otherwise, so a production APK is never falsely labelled. No keystore, password or key is (or ever was) committed; the audit test now proves that. - CI workflow: new gated release-apk job — decodes the keystore from ANDROID_KEYSTORE_B64 into $RUNNER_TEMP (never echoed, never committed), runs assembleRelease, verifies signature/package/version with apksigner + aapt (certificate fingerprints are public and printed for Firebase registration), renames the APK to exactly dsmnru-pyq.apk and uploads it as that artifact. Without signing secrets the job posts a clear notice and builds nothing. GOOGLE_SERVICES_JSON_B64 can now inject the Firebase config at build time in BOTH jobs (the file is gitignored by the Android template's policy; without it, Google sign-in uses the committed web client fallback and FCM stays inactive — unchanged behaviour). - Version: 1.4.0 / versionCode 11 (continues the 1.3.x scheme, greater than every distributed build); pins updated in profile/about/tests. - Production APK STATUS: no permanent release keystore is configured in this repository (release signing was deliberately absent; no .jks / release.keystore exists; secrets are not visible to this workspace), so per the release rules NO production APK is produced or claimed by this commit. The owner must create the keystore securely and add the ANDROID_KEYSTORE_* secrets (steps in the report); CI then produces the signed dsmnru-pyq.apk automatically on the next run. - Tests: 57/57 android (new release-path audit: env-only signing, no committed keystores/credentials, dsmnru-pyq.apk artifact wiring, secrets never echoed), 139/139 worker. Nothing weakened. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes the Android app fully self-contained — every product feature now works inside the app (the browser/website is never a fallback for normal functionality) — and adds native FCM push notifications on the existing
dsmnru-dataFirebase project. Single commit, v1.3.0 (versionCode 4).What's now in-app (was: website redirects)
servers→uploadFile→ FirestorependingUploads(identical doc shape, points/review fields absent), same validation + local throttle (dsmnruUploadThrottle), 10-point reward, Android file picker, on-device images→PDF (hand-rolled minimal PDF writer, no new deps), real progress/success/error statest.meintentGET /api/contributorscall, 24 h persisted SWR cache, no per-contributor fetches; "Join" card routes to in-app Uploadaccounts:signInWithIdp(same project, nonce-protected) → same session +users/{uid}sync as password sign-in. Unconfigured builds get an in-app explainer + email/password — never a website hand-offfeedbackcollection), unresolvable deep-link slugs (now in-app pre-filled search)PDF viewing
"Open PDF" launches the new native
PdfViewerActivity(platformPdfRenderer, lazy per-page rendering into a heap-bounded LruCache, pinch-zoom/pan/double-tap, page indicator, progress/error/retry). It streams the paper's direct host URL — never through the Worker — keeps the file in the temporary cache only (deleted on close, 24 h purge) and falls back to the system (same direct URL, never the website) if rendering fails. Drive/mediafire landing pages remain genuine external intents.FCM push (new)
POST_NOTIFICATIONSrequested through the actual system dialog once per install, ~9 s into the first session; grant/deny respected forever, no fake toggle, no re-asking; skipped on < 13 and on builds without Firebase config.FcmService: token rotation handling (device-local stash — no Firestore tokens), version-gatedall_userstopic subscription (one FCM call per install/update + on token rotation; zero per-launch work),dsmnru_generalchannel, foreground rendering behind a permission check, background tray branding via manifest meta-data.data.path = /pyq/<slug>opens the in-app paper screen, cold or warm.docs/PUSH_NOTIFICATIONS.md§5 (POST /api/notifyreusing the existingverifyFirebaseAdminTokenguard, FCM v1 HTTP, service-account secret, admin-panel form stays on the website).Backend / quota impact
Worker traffic pattern unchanged (one cached contributors call added). Firebase: unchanged auth paths + the existing
pendingUploads/feedbackwrites. FCM adds zero Firestore/Worker load. Website untouched. No second backend/database anywhere.Tests
npm test: 46 tests / 44 pass / 0 fail / 2 skip (the 2 jsdom integration suites skip when jsdom isn't installed locally; they run in CI). Existing tests untouched and green. New:features.test.mjs(upload/tools/links/Google IdP contract + strict website-redirect audit),app-native-bridge.test.mjs(jsdom with a fake Capacitor bridge: in-app viewer call, same-URL fallback, Google flow →signInWithIdp),fcm.test.mjs(manifest/Gradle/service/permission/payload-contract audits). Device-only checks (12 scenarios) documented indocs/PUSH_NOTIFICATIONS.md§6.Manual config needed (documented, nothing secret committed)
google-services.jsonintoandroid-app/android/app/(now gitignored; Gradle auto-applies the plugin when present — builds without it run normally with push disabled).strings.xml+ SHA-1 registration →docs/GOOGLE_SIGNIN_SETUP.md./api/notify+ admin form perdocs/PUSH_NOTIFICATIONS.md§5.Remaining external destinations (all deliberate)
Links portals · Drive/mediafire landing pages · DownloadManager downloads · share sheet · t.me tool requests · admin panel · Firebase email-verification links · explicit "Open full website" choice on About · paper discussion (comments exist only on the site).
Notes
mainchanges; base isandroid-app(same flow as PR Android app: dedicated mobile UI (replaces website WebView wrapper) #13).