From efc013f498573f35ddedf2ed161508e793c3d2fe Mon Sep 17 00:00:00 2001 From: Dwindi Ramadhana Date: Sat, 21 Feb 2026 21:28:40 +0700 Subject: [PATCH] Add APK release flow with R2 redirects and updater support --- apk-direct-release-guide.md | 176 ++++------ .../Http/Controllers/Web/SiteController.php | 51 ++- app/config/dewemoji.php | 13 + app/resources/views/site/download.blade.php | 45 ++- app/routes/web.php | 2 + app/tests/Feature/SitePagesTest.php | 15 + deployment-live-walkthrough.md | 30 +- dewemoji-capacitor/.gitignore | 1 + .../android/app/src/main/AndroidManifest.xml | 1 + .../java/com/dewemoji/app/MainActivity.java | 322 ++++++++++++++++++ scripts/apk/build-release.sh | 69 ++++ scripts/apk/make-version-json.sh | 78 +++++ scripts/apk/publish-r2.sh | 115 +++++++ scripts/apk/verify-release.sh | 67 ++++ 14 files changed, 865 insertions(+), 120 deletions(-) create mode 100755 scripts/apk/build-release.sh create mode 100755 scripts/apk/make-version-json.sh create mode 100755 scripts/apk/publish-r2.sh create mode 100755 scripts/apk/verify-release.sh diff --git a/apk-direct-release-guide.md b/apk-direct-release-guide.md index 1c25ed6..ae80b5e 100644 --- a/apk-direct-release-guide.md +++ b/apk-direct-release-guide.md @@ -1,138 +1,106 @@ -# Direct APK Release Guide (No Play Store) +# APK Direct Release Guide (Local Build + Cloudflare R2) -This guide is for shipping Dewemoji Android builds as downloadable `.apk` files from your own site. +This is the Dewemoji direct APK release flow. -## 1) One-time prerequisites +## 1) One-time setup -1. Decide and keep a stable Android package id (example: `com.dewemoji.app`). -2. Create and securely store a release keystore. -3. Keep the same keystore for all future updates. -4. Keep `versionCode` strictly increasing for each release. - -If keystore or package id changes, users will not receive in-place updates. - ---- - -## 2) Build release APK - -Use your Android build command (NativePHP/Capacitor/Gradle), and ensure output is a **release APK**. - -Typical Gradle command: +### Required tools (local machine) ```bash -./gradlew assembleRelease +brew install awscli +brew install --cask android-platform-tools ``` -Expected output path (common): +### Required environment variables ```bash -android/app/build/outputs/apk/release/app-release.apk +export R2_ACCOUNT_ID="..." +export R2_ACCESS_KEY_ID="..." +export R2_SECRET_ACCESS_KEY="..." +export R2_BUCKET="dewemoji-downloads" +export R2_PUBLIC_BASE_URL="https://downloads.dewemoji.com" ``` ---- - -## 3) Sign and verify APK - -If your build pipeline does not auto-sign, sign manually. - -### A) Sign +Optional: ```bash -apksigner sign \ - --ks /path/to/keystore.jks \ - --ks-key-alias your_alias \ - --out dewemoji-vX.Y.Z.apk \ - android/app/build/outputs/apk/release/app-release.apk +export DEWEMOJI_APK_URL="https://dewemoji.com/downloads/dewemoji-latest.apk" ``` -### B) Verify signature +### Optional signing environment (recommended) ```bash -apksigner verify --verbose --print-certs dewemoji-vX.Y.Z.apk +export ANDROID_KEYSTORE_PATH="/absolute/path/release.jks" +export ANDROID_KEYSTORE_PASSWORD="..." +export ANDROID_KEY_ALIAS="..." +export ANDROID_KEY_PASSWORD="..." ``` --- -## 4) Generate checksum +## 2) Canonical URLs used by app updater -Publish SHA-256 so users can verify file integrity. +- `https://dewemoji.com/downloads/version.json` +- `https://dewemoji.com/downloads/dewemoji-latest.apk` + +These endpoints redirect to R2 objects. + +--- + +## 3) Release steps + +Run from repo root. + +### A. Build APK ```bash -shasum -a 256 dewemoji-vX.Y.Z.apk +./scripts/apk/build-release.sh ``` -Record output in release notes. +Output APK: ---- +- `dewemoji-capacitor/dist/apk/dewemoji-v{versionName}-{versionCode}.apk` -## 5) Upload APK to your server +### B. Publish APK + metadata to R2 -Recommended path: - -```text -https://dewemoji.com/downloads/dewemoji-vX.Y.Z.apk +```bash +./scripts/apk/publish-r2.sh \ + --apk dewemoji-capacitor/dist/apk/dewemoji-v1.1.2-112.apk \ + --version-name 1.1.2 \ + --version-code 112 \ + --min-supported-version-code 100 \ + --notes "Bug fixes and update UX improvements" \ + --force false ``` -Recommended server headers: +### C. Verify published release -1. `Content-Type: application/vnd.android.package-archive` -2. `Content-Disposition: attachment; filename="dewemoji-vX.Y.Z.apk"` -3. Serve over HTTPS only - ---- - -## 6) Update Download page content - -On your `/download` page, show: - -1. Version (`vX.Y.Z`) -2. Build date -3. File size -4. Minimum Android version -5. SHA-256 checksum -6. Install instructions -7. Changelog - -Recommended install instructions for users: - -1. Download APK from official Dewemoji URL. -2. Open file on Android. -3. Allow installation from browser/files app if prompted. -4. Install/update. - ---- - -## 7) Release checklist - -Before publishing: - -1. Login works -2. Search works -3. Copy/insert flow works on device -4. Theme/tone UI works -5. Billing links/webviews (if used) open correctly -6. No crash on cold start -7. Version updated and visible in app - ---- - -## 8) Quick rollback - -If latest APK is bad: - -1. Re-point Download button to previous APK URL. -2. Keep bad APK file archived (do not overwrite silently). -3. Publish rollback notice/changelog update. - ---- - -## 9) Recommended file naming - -Use immutable names: - -```text -dewemoji-v1.1.1.apk -dewemoji-v1.1.2.apk +```bash +./scripts/apk/verify-release.sh --base-url https://dewemoji.com/downloads ``` -Avoid re-uploading different binaries under the same filename. +--- + +## 4) Versioning rules + +1. Site-only deploy: do not bump APK version and do not publish new `version.json`. +2. Runtime/app-shell change: bump `versionCode` + `versionName`, then publish. +3. `versionCode` must always increase. +4. App update prompt appears only when remote `versionCode` is higher. + +--- + +## 5) Rollback + +1. Keep all versioned APK objects immutable (never overwrite). +2. Re-upload previous good APK to `apk/dewemoji-latest.apk`. +3. Re-publish `apk/version.json` with matching checksum/version fields. +4. Re-run verify script. + +--- + +## 6) Notes + +- Direct APK update is user-confirmed install (Android policy), not silent. +- Never embed R2 credentials in app. +- Keep app update payload over HTTPS only. diff --git a/app/app/Http/Controllers/Web/SiteController.php b/app/app/Http/Controllers/Web/SiteController.php index bf6736d..722491d 100644 --- a/app/app/Http/Controllers/Web/SiteController.php +++ b/app/app/Http/Controllers/Web/SiteController.php @@ -9,6 +9,7 @@ use App\Models\Subscription; use App\Models\UserKeyword; use App\Services\System\SettingsService; use Illuminate\Contracts\View\View; +use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -264,7 +265,40 @@ class SiteController extends Controller public function download(): View { - return view('site.download'); + $downloadBaseUrl = rtrim((string) config('dewemoji.apk_release.public_base_url', ''), '/'); + $androidEnabled = (bool) config('dewemoji.apk_release.enabled', false) && $downloadBaseUrl !== ''; + + return view('site.download', [ + 'androidEnabled' => $androidEnabled, + 'androidVersionJsonUrl' => $androidEnabled ? $downloadBaseUrl.'/version.json' : '', + 'androidLatestApkUrl' => $androidEnabled ? $downloadBaseUrl.'/dewemoji-latest.apk' : '', + ]); + } + + public function downloadVersionJson(Request $request): RedirectResponse|JsonResponse + { + $target = $this->apkReleaseTargetUrl('version_json'); + if ($target === '') { + return response()->json(['ok' => false, 'error' => 'apk_release_not_configured'], 404); + } + + return redirect()->away($target, 302, [ + 'Cache-Control' => 'no-store, no-cache, must-revalidate', + 'Pragma' => 'no-cache', + ]); + } + + public function downloadLatestApk(Request $request): RedirectResponse|JsonResponse + { + $target = $this->apkReleaseTargetUrl('latest_apk'); + if ($target === '') { + return response()->json(['ok' => false, 'error' => 'apk_release_not_configured'], 404); + } + + return redirect()->away($target, 302, [ + 'Cache-Control' => 'no-store, no-cache, must-revalidate', + 'Pragma' => 'no-cache', + ]); } public function privacy(): View @@ -465,6 +499,21 @@ class SiteController extends Controller return (string) config('dewemoji.data_path'); } + private function apkReleaseTargetUrl(string $key): string + { + if (!(bool) config('dewemoji.apk_release.enabled', false)) { + return ''; + } + + $base = trim((string) config('dewemoji.apk_release.r2_public_base_url', '')); + $objectKey = trim((string) config("dewemoji.apk_release.r2_keys.{$key}", '')); + if ($base === '' || $objectKey === '') { + return ''; + } + + return rtrim($base, '/').'/'.ltrim($objectKey, '/'); + } + /** * @param array $emoji */ diff --git a/app/config/dewemoji.php b/app/config/dewemoji.php index 913e63f..b95e5cd 100644 --- a/app/config/dewemoji.php +++ b/app/config/dewemoji.php @@ -123,4 +123,17 @@ return [ 'token' => (string) env('DEWEMOJI_METRICS_TOKEN', ''), 'allow_ips' => array_values(array_filter(array_map('trim', explode(',', (string) env('DEWEMOJI_METRICS_ALLOW_IPS', '127.0.0.1,::1'))))), ], + + 'apk_release' => [ + 'enabled' => filter_var(env('DEWEMOJI_APK_RELEASE_ENABLED', false), FILTER_VALIDATE_BOOL), + 'app_id' => (string) env('DEWEMOJI_APK_APP_ID', 'com.dewemoji.app'), + 'channel' => (string) env('DEWEMOJI_APK_CHANNEL', 'stable'), + 'min_supported_version_code' => (int) env('DEWEMOJI_APK_MIN_SUPPORTED_VERSION_CODE', 1), + 'public_base_url' => (string) env('DEWEMOJI_APK_PUBLIC_BASE_URL', 'https://dewemoji.com/downloads'), + 'r2_public_base_url' => (string) env('DEWEMOJI_R2_PUBLIC_BASE_URL', ''), + 'r2_keys' => [ + 'latest_apk' => (string) env('DEWEMOJI_R2_APK_LATEST_KEY', 'apk/dewemoji-latest.apk'), + 'version_json' => (string) env('DEWEMOJI_R2_APK_VERSION_KEY', 'apk/version.json'), + ], + ], ]; diff --git a/app/resources/views/site/download.blade.php b/app/resources/views/site/download.blade.php index 7f2df26..3837c5f 100644 --- a/app/resources/views/site/download.blade.php +++ b/app/resources/views/site/download.blade.php @@ -1,7 +1,7 @@ @extends('site.layout') @section('title', 'Download - Dewemoji') -@section('meta_description', 'Download Dewemoji for Chrome and get notified when Android app is available.') +@section('meta_description', 'Download Dewemoji for Chrome and Android.') @push('jsonld')