Skip to content

Android beta foundation: target, sync, i18n - #46

Merged
ReinaldoAssis merged 14 commits into
mainfrom
feat/android-beta-foundation
Oct 22, 2026
Merged

ReinaldoAssis merged 14 commits into
mainfrom
feat/android-beta-foundation

Conversation

@ReinaldoAssis

@ReinaldoAssis ReinaldoAssis commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Why

First release train for the Android beta: the app builds for Android, syncs
over the LAN like the desktop, speaks English as well as Portuguese, gates the
mobile free tier in Rust and can sell the Pro subscription through Play
Billing. The Play pipeline signs the AAB and uploads it to the internal track.

What changed

  • Android target: tauri android init output committed, minSdk 29 /
    targetSdk 36, permanent application id com.reinaldoassis.atlas, version
    0.4.0 and a versionCode derived from VERSION (major*10000+minor*100+patch).
  • LAN sync on Android: the sync module is no longer compiled out. Android
    drops multicast without a MulticastLock, so a small Tauri plugin in
    gen/android takes the lock on sync_start and releases it on sync_stop.
  • i18n: pt-BR + en, runtime selector (system / Portuguese / English),
    fallback to English for any non-Portuguese system language, Intl formatting
    per locale, localized seeds, and a guard test that fails on new hardcoded
    copy. csp:check boots the app in English and back.
  • Freemium: plan.rs enforces the free limits inside the db_batch
    transaction (rollback + plan-limit:<key>:<limit>), keeps a Pro cache with a
    seven-day offline grace, and the UI blocks creation early with a paywall.
    History on the free plan stops at the last seven days.
  • Billing: tauri-plugin-iap (Play Billing 9) is a mobile-only dependency;
    the paywall shows localized store prices, purchases/acknowledges/restores and
    listens for renewals, recording everything through plan_set_pro.
  • Release: Gradle signs the AAB from the upload keystore env vars; the
    Android workflow runs on tags and uploads to the internal track. The
    signed AAB is never published as an artifact.
  • Docs: docs/I18N.md, docs/ANDROID-BETA.md, docs/PLAY-STORE.md,
    docs/STORE-LISTING.md, docs/PRIVACY.md, Android section in
    docs/RELEASES.md.

Verification

  • npm run check, npm test (264), npm run build, npm run csp:check
    (23/23), cargo test (50) — all green.
  • Android debug APK built for aarch64 (com.reinaldoassis.atlas.debug,
    versionCode 400, minSdk 29, targetSdk 36); release AAB built and signed with a
    throwaway keystore to validate the Gradle signing config.
  • com.android.vending.BILLING permission and MulticastLockPlugin present in
    the APK.

Pending manual checks

  • Install the debug APK on a device and smoke-test boot, i18n, gating and sync.
  • Create the two subscription products in Play Console and run the sandbox
    purchase/restore with license testers.
  • Recruit 12 testers and start the closed test (14 continuous days).
  • Publish the privacy policy at atlas.reinaldoassis.com/privacy and produce
    the store assets.

- Bake versionCode (major*10000 + minor*100 + patch) into `version.mjs`
  so every tagged release derives it from VERSION; 0.4.0 opens the series.
- Initialize `src-tauri/gen/android` with minSdk 29 / targetSdk 36 and the
  new permanent application id `com.reinaldoassis.atlas`.
- Trim the default category seed to 4 finance and 5 time entries so the
  free tier can start inside its own limits.
- Record the beta decisions (tracks, billing, gating, sync, i18n) in
  `docs/ANDROID-BETA.md` for the next slices.
- Android drops multicast packets unless an app holds a `MulticastLock`,
  so discovery would announce but never see peers. A small Tauri plugin
  in `gen/android` takes the lock on `sync_start` and releases it on
  `sync_stop`.
- `sync` and the `mdns-sd` dependency are no longer compiled out on
  Android; the same Rust discovery code now runs on desktop and mobile.
- Declare `CHANGE_WIFI_MULTICAST_STATE` in the Android manifest and
  document the behaviour in `docs/SYNC.md`.
- Add English messages and switch Paraglide to localStorage/preferred
  language; `locale.svelte.ts` resolves an explicit choice, falls back to
  en for any system language that is not Portuguese, and remounts the app
  through `{#key i18n.locale}` (message functions are not reactive).
- Migrate every visible string in the Svelte tree: markup, placeholders,
  aria labels, toasts, errors, empty states and the main page.
- Format dates and money through `intlLocale()` instead of a hardcoded
  pt-BR, and seed categories, graph domains and boolean labels in the
  language of the first boot.
- Add a guard test that fails when copy appears outside `m.*`, with an
  allowlist that is now empty; extend `csp:check` to boot the app in
  English and back.
- Add `plan.rs`: free limits (notes, attachments, habits, trackers,
  categories, graph nodes, history days), a Pro cache in `plan.json` and a
  seven-day grace window for offline use. Desktop is always Pro.
- Enforce the limits inside the `db_batch` transaction: an over-limit batch
  is rolled back and returns `plan-limit:<key>:<limit>`, so DevTools cannot
  bypass the gate.
- Add the optimistic UI guard before creating notes, habits, trackers,
  categories and graph nodes, plus a paywall sheet, a plan row in Settings
  and a seven-day cut in the history list for free users.
- Cover the limit rules and the `json_extract` dependency with tests.
- Add `tauri-plugin-iap` (Billing 9) as a mobile-only dependency with a
  mobile-only capability, so the free desktop build never loads it.
- Query localized prices from the store, purchase monthly/annual with the
  default offer, acknowledge immediately (Play refunds unacknowledged
  purchases after 3 days), restore and listen for renewals.
- Record the result through `plan_set_pro`, keeping the Rust entitlement
  (and its seven-day offline grace) as the single source of truth.
- Keep the fixed prices as a fallback while the Play products do not exist;
  outcomes surface as toasts (success, pending, cancelled, failure).
- Add a release signing config to the generated Gradle project, fed by the
  upload keystore env vars; local release builds without them stay
  unsigned.
- Add the `Android` workflow: tag check, SDK/NDK/JDK 17, Rust targets,
  signed AAB and Play Developer API upload to `internal`. The signed AAB is
  never exposed as an artifact.
- Document the Android release secrets and the upload-key ceremony in
  `docs/RELEASES.md`, and mark billing/pipeline done in the beta plan.
android-actions/setup-android fails while installing the obsolete `tools`
package on the new cmdline-tools; the ubuntu image already has sdkmanager,
so install only the platform, build-tools and NDK we need. Also bump
setup-java to v5.
@ReinaldoAssis
ReinaldoAssis merged commit e4860ea into main Oct 22, 2026
4 checks passed
@ReinaldoAssis
ReinaldoAssis deleted the feat/android-beta-foundation branch October 22, 2026 16:51
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