Add APK release flow with R2 redirects and updater support

This commit is contained in:
Dwindi Ramadhana
2026-02-21 21:28:40 +07:00
parent 3d4a753be7
commit efc013f498
14 changed files with 865 additions and 120 deletions

View File

@@ -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<string,mixed> $emoji
*/

View File

@@ -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'),
],
],
];

View File

@@ -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')
<script type="application/ld+json">
@@ -78,16 +78,33 @@
</section>
<section class="glass-card rounded-2xl p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Coming soon</div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">
{{ $androidEnabled ? 'Available now' : 'Coming soon' }}
</div>
<h2 class="mt-2 text-2xl font-semibold">Android App</h2>
<p class="mt-2 text-sm text-gray-300">Native app release is in progress. We will launch internal testing first, then public release.</p>
<div class="mt-5 inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-xs text-gray-300 bg-white/5">
<i data-lucide="smartphone" class="w-4 h-4"></i>
Android release in preparation
</div>
<div class="mt-4 text-xs text-gray-400">
Recommended for now: use web dashboard + Chrome extension.
</div>
@if($androidEnabled)
<p class="mt-2 text-sm text-gray-300">Direct APK distribution from Dewemoji download channel.</p>
<a
href="{{ $androidLatestApkUrl }}"
rel="noopener"
class="mt-5 inline-flex items-center gap-2 rounded-full bg-brand-sun text-black px-5 py-2.5 text-sm font-semibold hover:brightness-95 transition-colors"
>
<i data-lucide="smartphone" class="w-4 h-4"></i>
Download APK
</a>
<div class="mt-4 text-xs text-gray-400">
Update metadata: <a href="{{ $androidVersionJsonUrl }}" class="underline hover:text-gray-200">{{ $androidVersionJsonUrl }}</a>
</div>
@else
<p class="mt-2 text-sm text-gray-300">Native app release is in progress. We will launch internal testing first, then public release.</p>
<div class="mt-5 inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-xs text-gray-300 bg-white/5">
<i data-lucide="smartphone" class="w-4 h-4"></i>
Android release in preparation
</div>
<div class="mt-4 text-xs text-gray-400">
Recommended for now: use web dashboard + Chrome extension.
</div>
@endif
</section>
<section class="glass-card rounded-2xl p-6 lg:col-span-2">
@@ -100,12 +117,14 @@
</div>
<div class="mt-1 text-sm text-emerald-100">Available</div>
</div>
<div class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4">
<div class="flex items-center gap-2 text-xs uppercase tracking-[0.2em] text-amber-300">
<div class="rounded-xl {{ $androidEnabled ? 'border border-emerald-500/30 bg-emerald-500/10' : 'border border-amber-500/30 bg-amber-500/10' }} p-4">
<div class="flex items-center gap-2 text-xs uppercase tracking-[0.2em] {{ $androidEnabled ? 'text-emerald-300' : 'text-amber-300' }}">
<i data-lucide="bot" class="w-4 h-4"></i>
<span>Android</span>
</div>
<div class="mt-1 text-sm text-amber-100">In progress</div>
<div class="mt-1 text-sm {{ $androidEnabled ? 'text-emerald-100' : 'text-amber-100' }}">
{{ $androidEnabled ? 'Available' : 'In progress' }}
</div>
</div>
<div class="rounded-xl border border-sky-500/30 bg-sky-500/10 p-4">
<div class="flex items-center gap-2 text-xs uppercase tracking-[0.2em] text-sky-300">

View File

@@ -17,6 +17,8 @@ Route::get('/emoji/{slug}', [SiteController::class, 'emojiDetail'])->name('emoji
Route::get('/pricing', [SiteController::class, 'pricing'])->name('pricing');
Route::post('/pricing/currency', [SiteController::class, 'setPricingCurrency'])->name('pricing.currency');
Route::get('/download', [SiteController::class, 'download'])->name('download');
Route::get('/downloads/version.json', [SiteController::class, 'downloadVersionJson'])->name('downloads.version');
Route::get('/downloads/dewemoji-latest.apk', [SiteController::class, 'downloadLatestApk'])->name('downloads.latest-apk');
Route::get('/support', [SiteController::class, 'support'])->name('support');
Route::get('/privacy', [SiteController::class, 'privacy'])->name('privacy');
Route::get('/terms', [SiteController::class, 'terms'])->name('terms');

View File

@@ -11,6 +11,10 @@ class SitePagesTest extends TestCase
parent::setUp();
config()->set('dewemoji.data_path', base_path('tests/Fixtures/emojis.fixture.json'));
config()->set('dewemoji.apk_release.enabled', true);
config()->set('dewemoji.apk_release.r2_public_base_url', 'https://downloads.example.com');
config()->set('dewemoji.apk_release.r2_keys.latest_apk', 'apk/dewemoji-latest.apk');
config()->set('dewemoji.apk_release.r2_keys.version_json', 'apk/version.json');
}
public function test_core_pages_are_available(): void
@@ -39,4 +43,15 @@ class SitePagesTest extends TestCase
{
$this->get('/emoji/unknown-slug')->assertNotFound();
}
public function test_download_redirect_endpoints_are_available(): void
{
$this->get('/downloads/version.json')
->assertStatus(302)
->assertRedirect('https://downloads.example.com/apk/version.json');
$this->get('/downloads/dewemoji-latest.apk')
->assertStatus(302)
->assertRedirect('https://downloads.example.com/apk/dewemoji-latest.apk');
}
}