Skip to content

Android app v1.3.0: fully self-contained app (drawer, upload, tools, contributors, links, PDF viewer, native Google sign-in) + FCM push - #14

Merged
Lav-developer merged 13 commits into
android-appfrom
arena/01a066c8-pyq
Sep 3, 2026
Merged

Android app v1.3.0: fully self-contained app (drawer, upload, tools, contributors, links, PDF viewer, native Google sign-in) + FCM push#14
Lav-developer merged 13 commits into
android-appfrom
arena/01a066c8-pyq

Conversation

@arena-ai-coding-agent

Copy link
Copy Markdown
Contributor

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-data Firebase project. Single commit, v1.3.0 (versionCode 4).

What's now in-app (was: website redirects)

Feature Implementation
Side drawer Hamburger in the app bar → all 5 tabs + Upload Paper · Study Tools · Contributors · Links · About. Back-button precedence: drawer → sheet → in-app stack → exit
Upload Paper The website's flow verbatim: gofile serversuploadFile → Firestore pendingUploads (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 states
Study Tools CGPA calculator, attendance tracker (75% line, month filter), planner — 100 % on-device, same localStorage keys as the site, zero network; request-a-tool keeps its legit t.me intent
Contributors ONE GET /api/contributors call, 24 h persisted SWR cache, no per-contributor fetches; "Join" card routes to in-app Upload
Links The 14 university/government portals rendered statically in-app; only the tapped portal itself opens externally
Google Sign-In Native: Credential Manager account chooser → Google ID token → Identity Toolkit accounts: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-off
Navigation audit Removed every website redirect: home shortcuts, profile "Open DSMNRU website", paper "Open on website" (report is now an in-app feedback sheet to the same feedback collection), unresolvable deep-link slugs (now in-app pre-filled search)

PDF viewing

"Open PDF" launches the new native PdfViewerActivity (platform PdfRenderer, 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)

  • Real Android 13+ permission: POST_NOTIFICATIONS requested 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-gated all_users topic subscription (one FCM call per install/update + on token rotation; zero per-launch work), dsmnru_general channel, foreground rendering behind a permission check, background tray branding via manifest meta-data.
  • Taps ride the existing deep-link pipeline (ACTION_VIEW data URL on MainActivity): data.path = /pyq/<slug> opens the in-app paper screen, cold or warm.
  • Quota discipline: no polling, no listeners, no per-notification writes, no Worker traffic, no token sync on launch.
  • Sender side does not exist yet (Worker has no push endpoint) — it is specified, not invented, in docs/PUSH_NOTIFICATIONS.md §5 (POST /api/notify reusing the existing verifyFirebaseAdminToken guard, 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/feedback writes. 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 in docs/PUSH_NOTIFICATIONS.md §6.

Manual config needed (documented, nothing secret committed)

  1. google-services.json into android-app/android/app/ (now gitignored; Gradle auto-applies the plugin when present — builds without it run normally with push disabled).
  2. Google Web client ID in strings.xml + SHA-1 registration → docs/GOOGLE_SIGNIN_SETUP.md.
  3. Sender side (when wanted): service account + Worker /api/notify + admin form per docs/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

Lav-developer and others added 13 commits September 3, 2026 10:54
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>
@Lav-developer
Lav-developer merged commit a18d6f3 into android-app Sep 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant