Add APK release flow with R2 redirects and updater support
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user