From e3de9a2fc0df03c27d392ba88c88ea4d29d576d4 Mon Sep 17 00:00:00 2001 From: Hadrian Burkhardt Date: Wed, 14 Oct 2026 17:40:29 +0200 Subject: [PATCH] M9: monetisation decision - one-time paid, no billing code, verified in release APK --- CHANGELOG.md | 10 ++++ README.md | 22 +++++++-- docs/MONETISATION.md | 85 ++++++++++++++++++++++++++++++++++ docs/PLAY_RELEASE_CHECKLIST.md | 16 +++++-- docs/ROADMAP.md | 4 +- 5 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 docs/MONETISATION.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bbe16e..d4c1c86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,16 @@ upload needs a **higher `versionCode`** than every previous upload. ### Added +- **M9 — monetisation.** Decided: a **one-time paid app at €4.49, with a 14-day launch price + of €2.29**, both set in the Play Console. No ads, no subscription and no in-app purchases — + which means no Billing client, no `com.android.vending.BILLING` permission and no purchase + code in the APK at all; the price lives on the listing, not in the build. That is what + keeps the "no network permission, nothing leaves your device" claim true of the *shipped + artefact* rather than just the source. The rationale, the market anchors and the rejected + alternatives are in [docs/MONETISATION.md](docs/MONETISATION.md). Verified against the + rebuilt release APK: zero billing references, zero `INTERNET`, and the debug-only + pseudo-locale correctly absent. + - **M8 — localisation groundwork.** Every user-visible string lives in `res/values/strings.xml` with real ``, and an audit of the Compose sources found exactly one genuine miss (above). The debug build type enables diff --git a/README.md b/README.md index aae7801..4d2c7ed 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ no advertising or tracking SDKs. ## Status -**M8 — localisation groundwork: done.** Every user-visible string is in resources with real -plurals, the audit's one genuine miss (the details sheet showing an enum name) is fixed, and -the debug build's pseudo-locale makes the check mechanical. See the README's *Localisation* -section. +**M9 — monetisation: done.** One-time paid at €4.49 (€2.29 for the first 14 days), set in +the Play Console — so the APK carries no Billing client and no `BILLING` permission, which +keeps the offline claim true of the shipped artefact. See +[docs/MONETISATION.md](docs/MONETISATION.md). ## Tech stack @@ -153,6 +153,20 @@ UI.** Translated UI text has to be supplied in `values-/`. [docs/EDGE_CASE_TESTING.md](docs/EDGE_CASE_TESTING.md)). - The display name is still the working title — decided before the first upload (M10). +## Monetisation + +**One-time paid: €4.49 base, with a €2.29 launch price for the first 14 days.** No ads, no +subscription, no in-app purchases, no free tier. + +The price lives on the Play listing, not in the build, which is why there is no Billing +client in the dependencies and no `com.android.vending.BILLING` permission in the APK — +that is what keeps "no network permission, nothing leaves your device" true of the +*shipped artefact* rather than just the source. Rationale, market anchors, the rejected +alternatives and the Console steps: [docs/MONETISATION.md](docs/MONETISATION.md). + +⚠️ **The price must be set before the first upload** — Play allows paid → free but never +free → paid. + ## Licence Proprietary — all rights reserved. diff --git a/docs/MONETISATION.md b/docs/MONETISATION.md new file mode 100644 index 0000000..d62b924 --- /dev/null +++ b/docs/MONETISATION.md @@ -0,0 +1,85 @@ +# Monetisation + +## The decision + +**File Manager is a one-time paid app.** No ads, no subscription, no in-app purchases, no +free tier and no trial. The price lives on the Play listing, not in the build — so there is +no Billing client, no `com.android.vending.BILLING` permission, and no purchase code in the +APK. + +That is not a detail: it is what keeps the "no network permission, nothing leaves your +device" claim true in the *shipped artefact* rather than merely in the source. + +## Price + +| | Value | +|---|---| +| Base price | **€4.49** | +| Launch price | **€2.29** (≈49% off) for the first **14 days** | +| Model | One-time purchase, all features, no unlocks | + +Both are set in the Play Console under *Products › App pricing*. Nothing about them is in +this repository, and nothing about them needs to be: changing the number is a Console edit, +not a release. + +### Why €4.49 + +- **Anchors.** The only comparable *pure-paid* file manager on Play is MiXplorer Silver at + **€6.99**. Everything else is free with ads or free with a €3–5 one-time unlock (Solid + Explorer, FX+, X-plore, Cx). €4.49 sits below the premium paid rival and above the + free tier's unlock price, which is where "no ads, no account, no network" earns its keep. +- **Scope.** A file manager is a daily-use tool across every app on the device, not a + single-purpose utility. A one-shot photo-deduplication tool is reasonably priced at + €2.99; a file manager does more, more often. +- **Headroom.** It is much easier to discount a €4.49 base than to raise a €2.49 one. The + base price anchors future sales; the sale cannot anchor the base. + +### Why a launch price + +A temporary lower price gives early adopters a reason to commit and — more importantly — +produces the first reviews, which a paid app needs far more than a free one does. Once the +window closes, the price returns to €4.49. The same discount can be repeated for any later +milestone; the base price never has to move. + +**Keep it honest:** the launch price is a genuine time-boxed reduction, not a permanent +"discount" — a base price that never applies is just a two-tier price with extra steps. + +### ⚠️ This decision is irreversible in one direction + +Play's own wording: *"You can change your app from paid to free. Once your app has been +offered for free, the app can't be changed to paid."* So: + +- The price **must be set before the first upload**. +- Paid → free remains possible later if the strategy changes. +- Free → paid never does. + +## Why not the alternatives + +| Option | Rejected because | +|---|---| +| Free with ads | The product's whole pitch is "no clutter". Ads in a file manager are also a privacy problem — every ad SDK wants `INTERNET` and an advertising ID. | +| Free with a one-time unlock (IAP) | Would add the Billing client and the `BILLING` permission, require purchase-state plumbing and a restore path, and make the app's own "works offline forever" promise contingent on a Play check. | +| Subscription | Nothing here is a recurring service. Users are paying for a tool they already own a copy of. | +| Free | M9 exists to fund the work. See the irreversibility note: once free, never paid. | + +## What is in the build — verified, not assumed + +```bash +# None of these may return anything: +grep -rniE 'billing|vending' app/src gradle/ app/build.gradle.kts +$ANDROID_HOME/build-tools/36.0.0/aapt2 dump permissions \ + app/build/outputs/apk/release/app-release.apk | grep -i billing +``` + +Checked 2026-10-14 against the release APK: **no billing reference in the source, no Billing +dependency in the version catalog, no `com.android.vending.BILLING` permission, and no +`INTERNET`**. See [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) step 7. + +## Console steps + +1. Create a **payments profile** (identity, tax, bank details) for the developer account. +2. In the Play Console, on the app's *App pricing* page, set the base price to **€4.49** + (Play fills in local equivalents; review the suggested prices rather than accepting + them blindly). +3. Set the **launch price of €2.29**, and diarise changing it back after 14 days. +4. Do all of the above **before the first upload**. diff --git a/docs/PLAY_RELEASE_CHECKLIST.md b/docs/PLAY_RELEASE_CHECKLIST.md index e75a6d5..af33df7 100644 --- a/docs/PLAY_RELEASE_CHECKLIST.md +++ b/docs/PLAY_RELEASE_CHECKLIST.md @@ -122,8 +122,14 @@ factor too). - [ ] Create the app, complete *App content*, then roll out **internal → closed → production**, checking the pre-launch report on the way. -- [ ] ⚠️ **Set the price before the first upload** (M9). Once an app has been offered for - free it can never be made paid, and this app is a **one-time paid** app: no ads, no - subscription, no in-app purchases — and therefore no Billing code and no - `com.android.vending.BILLING` permission in the APK. The price lives on the listing, - not in the build. +- [ ] ⚠️ **Set the price before the first upload.** Play's own wording: *"You can change your + app from paid to free. Once your app has been offered for free, the app can't be + changed to paid."* So: + 1. Create a **payments profile** (identity, tax, bank details). + 2. *Products › App pricing*: base price **€4.49**. + 3. Launch price **€2.29** for the first **14 days**, then diarise changing it back. + 4. Do every step above **before uploading**. +- [x] **[code] no Billing client and no `BILLING` permission** — the price lives on the + listing, not in the build, so the APK contains no purchase code at all. Verified + 2026-10-14 against the release APK. Full rationale and the rejected alternatives: + [MONETISATION](MONETISATION.md). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 71b3f2f..0c36f1a 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -18,7 +18,7 @@ Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (propo | M6 | Release config & Play readiness | ✅ | Upload keystore generated (4096-bit RSA, `CN=Hadrian Burkhardt`) and wired through the git-ignored `keystore.properties`; `bundleRelease` + `assembleRelease` verified — **4 682 814-byte `.aab`** (`sha256 9014627a…`), 2 081 955-byte APK signed by the upload key, R8 mapping kept. [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) (incl. the **Permissions Declaration Form** for "All files access", the Data safety answers and the shipped-permission audit) and [PRIVACY.md](../PRIVACY.md) written for a restricted-permission app. Shipped permission set audited: **no `INTERNET`** | | M7 | On-device validation & hardening | ✅ | Run on a real **Xiaomi M2101K7AG** (Android 12 / MIUI V140) — see [EDGE_CASE_TESTING](EDGE_CASE_TESTING.md). Verified on hardware: the first-run permission prompt, granting "All files access" while the app is alive (picked up on resume), browsing real storage, the text editor's read **and** write path (independently checked with `adb shell cat`), the storage-usage screen, landscape layout, and **graceful degradation when access is revoked mid-use**. Found and fixed two real defects: misleading first-run copy ("Full access was declined" on a device where nothing had been declined), and a usage screen that stayed blank for **~45–60 s** on a 55 GB volume — now one listing per directory instead of a `stat` per node, with rows streamed out as they are measured. **142 JVM tests** | | M8 | Localisation groundwork | ✅ | Every user-visible string is in `res/values/strings.xml` with real ``, and `text/TextRes.kt` maps **both** `OperationKind` and `FileKind` onto resources so the screen, the notification and the details sheet cannot drift. The audit found a genuine miss: the details sheet rendered `FileKind.name`, i.e. untranslatable English — now a resource. `isPseudoLocalesEnabled = true` on the debug build makes "did every string come from resources?" mechanically checkable; verified `en-rXA` is present in the debug APK and the flag is debug-only. Byte units and the timestamp pattern are deliberately unlocalised, and the README says why. **142 JVM tests** | -| M9 | Monetisation | ⏳ | **one-time paid** — price set only in the Play Console before the first upload; no Billing code and no `BILLING` permission | +| M9 | Monetisation | ✅ | **Decided: one-time paid — €4.49 base with a 14-day launch price of €2.29**, both set in the Play Console (*Products › App pricing*), so there is **no Billing client, no `com.android.vending.BILLING` permission and no purchase code**. Rationale, market anchors and Console steps: [MONETISATION](MONETISATION.md). Verified against the release APK: zero billing and zero `INTERNET` references. **The price must be set before the first upload** — Play allows paid → free but never free → paid | | M10 | Store assets | ⏳ | icon/feature-graphic generators, per-form-factor screenshots, [STORE_LISTING](STORE_LISTING.md); display name confirmed | ## Key decisions @@ -28,7 +28,7 @@ Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (propo | Language / UI | Kotlin + Jetpack Compose | Mirrors the sibling app's proven, installed toolchain; no NDK | | Storage access | `MANAGE_EXTERNAL_STORAGE` first, SAF fallback | Google Play explicitly permits "All files access" for file managers; SAF keeps the app usable if the user declines | | Connectivity | Local-first (no `INTERNET` in v1) | Preserves the "nothing leaves the device" claim and keeps the Data Safety form trivial | -| Monetisation | One-time paid, price in Console | No Billing code; free → paid is impossible after the first upload, so the price must be set beforehand | +| Monetisation | One-time paid (€4.49; €2.29 launch price), set in the Console | No Billing client, no `BILLING` permission, no purchase code — which is what keeps "nothing leaves the device" true in the shipped APK. Free → paid is impossible after the first upload, so the price must be set beforehand. See [MONETISATION](MONETISATION.md) | ## Deliberately deferred (post-1.0)