feat: page publique de téléchargement des applications mobiles
CI - Intégration continue / quality-and-tests (push) Successful in 3m16s
CD - Déploiement continu / deploy (push) Successful in 3m7s

Page /telecharger avec deux cartes (Android / iOS). Pour chaque plateforme,
utilise en priorité un lien de store (MOBILE_*_URL) sinon le binaire déposé
dans public/downloads/ (.apk / .ipa), et affiche "Bientôt disponible" à défaut.

- config/mobile.php + .env.example (URLs/versions)
- DownloadController + vue telecharger + lien dans le header (public)
- public/downloads/ (README) ; binaires ignorés par git
- tests: accès public, lien store prioritaire, indisponibilité

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 22:55:15 +02:00
parent dede70f70f
commit 507d77246c
9 changed files with 229 additions and 0 deletions
+8
View File
@@ -76,4 +76,12 @@ GITEA_URL=https://gitea.sam-coffre.duckdns.org
GITEA_TOKEN=
GITEA_REPO=Groupe1/Ressources-relationnel
# --- Applications mobiles (page /telecharger) ---
# Laisser vide pour servir les fichiers de public/downloads/ (.apk / .ipa),
# ou renseigner un lien de store / TestFlight (prioritaire sur le fichier local).
MOBILE_ANDROID_URL=
MOBILE_IOS_URL=
MOBILE_ANDROID_VERSION=1.0.0
MOBILE_IOS_VERSION=1.0.0
VITE_APP_NAME="${APP_NAME}"
+2
View File
@@ -18,6 +18,8 @@
/public/build
/public/hot
/public/storage
/public/downloads/*.apk
/public/downloads/*.ipa
/storage/*.key
/storage/pail
/vendor
@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers;
class DownloadController extends Controller
{
// Page publique de téléchargement des applications mobiles
public function index()
{
return view('telecharger', [
'android' => $this->resoudre('android'),
'ios' => $this->resoudre('ios'),
]);
}
/**
* Détermine, pour une plateforme, le lien de téléchargement et sa disponibilité.
*
* @return array{version: string, url: string|null, disponible: bool}
*/
private function resoudre(string $plateforme): array
{
$config = config("mobile.{$plateforme}");
if (! is_array($config)) {
$config = [];
}
$version = is_string($config['version'] ?? null) ? $config['version'] : '1.0.0';
$url = (is_string($config['url'] ?? null) && $config['url'] !== '') ? $config['url'] : null;
$file = is_string($config['file'] ?? null) ? $config['file'] : null;
// À défaut d'URL explicite, on propose le fichier précompilé s'il existe.
if ($url === null && $file !== null && file_exists(public_path($file))) {
$url = asset($file);
}
return [
'version' => $version,
'url' => $url,
'disponible' => $url !== null,
];
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Applications mobiles (téléchargement)
|--------------------------------------------------------------------------
|
| Pour chaque plateforme, la page de téléchargement utilise en priorité
| l'URL fournie (lien de store OU URL directe). À défaut, elle propose le
| fichier précompilé déposé dans public/downloads/. Si aucun des deux n'est
| disponible, le bouton affiche « bientôt disponible ».
|
*/
'android' => [
'version' => env('MOBILE_ANDROID_VERSION', '1.0.0'),
'url' => env('MOBILE_ANDROID_URL'),
'file' => 'downloads/ressources-relationnelles.apk',
],
'ios' => [
'version' => env('MOBILE_IOS_VERSION', '1.0.0'),
'url' => env('MOBILE_IOS_URL'),
'file' => 'downloads/ressources-relationnelles.ipa',
],
];
+25
View File
@@ -0,0 +1,25 @@
# Applications mobiles précompilées
Déposez ici les binaires servis par la page **/telecharger** :
- `ressources-relationnelles.apk` — application Android
- `ressources-relationnelles.ipa` — application Apple (iOS)
La page détecte automatiquement la présence de ces fichiers.
## Alternative : liens de store
Plutôt que des fichiers locaux, vous pouvez renseigner des liens (Play Store,
App Store, TestFlight…) dans le fichier `.env` du serveur :
```
MOBILE_ANDROID_URL=https://play.google.com/store/apps/details?id=...
MOBILE_IOS_URL=https://apps.apple.com/app/id...
MOBILE_ANDROID_VERSION=1.0.0
MOBILE_IOS_VERSION=1.0.0
```
Si une URL est renseignée, elle est prioritaire sur le fichier local.
Si ni URL ni fichier ne sont disponibles, le bouton affiche « Bientôt disponible ».
> Les binaires (`*.apk`, `*.ipa`) ne sont pas versionnés dans git (voir `.gitignore`).
+3
View File
@@ -23,6 +23,9 @@
</div>
<nav class="flex items-center gap-2 whitespace-nowrap">
<a href="{{ route('telecharger') }}" class="hidden lg:inline-flex items-center px-4 py-2.5 text-sm font-bold text-gray-700 rounded-lg hover:bg-gray-100 transition-smooth duration-200">
📱 Télécharger l'app
</a>
@auth
<a href="{{ route('suggestions.create') }}" class="hidden sm:inline-flex items-center px-4 py-2.5 text-sm font-bold text-niagara border-2 border-niagara rounded-lg hover:bg-niagara hover:text-white transition-smooth duration-200">
💡 Proposer une amélioration
+78
View File
@@ -0,0 +1,78 @@
@extends('app')
@section('title', "Télécharger l'application - (RE)Sources Relationnelles")
@section('content')
<section class="container mx-auto px-4 py-12">
<div class="max-w-3xl mx-auto">
<!-- En-tête -->
<div class="text-center mb-10">
<h1 class="text-4xl font-bold text-gray-900 mb-3">📱 Télécharger l'application</h1>
<p class="text-gray-600 max-w-xl mx-auto">
Emportez (RE)Sources Relationnelles partout avec vous. Choisissez votre plateforme
et installez l'application mobile.
</p>
</div>
<div class="grid gap-6 md:grid-cols-2">
{{-- Carte Android --}}
<div class="bg-white rounded-2xl border border-gray-200 p-8 flex flex-col items-center text-center shadow-sm">
<div class="w-16 h-16 rounded-2xl bg-green-50 flex items-center justify-center mb-4">
<span class="text-4xl">🤖</span>
</div>
<h2 class="text-2xl font-bold text-gray-900 mb-1">Android</h2>
<p class="text-sm text-gray-500 mb-6">Version {{ $android['version'] }}</p>
@if ($android['disponible'])
<a href="{{ $android['url'] }}"
@if (str_starts_with($android['url'], url('/'))) download @else target="_blank" rel="noopener" @endif
class="w-full px-6 py-3 gradient-niagara text-white font-bold rounded-lg hover:shadow-lg transition-smooth duration-200">
⬇️ Télécharger (.apk)
</a>
<p class="text-xs text-gray-400 mt-3">
Sur Android, autorisez l'installation depuis « sources inconnues » si nécessaire.
</p>
@else
<button type="button" disabled
class="w-full px-6 py-3 bg-gray-200 text-gray-500 font-bold rounded-lg cursor-not-allowed">
Bientôt disponible
</button>
@endif
</div>
{{-- Carte iOS --}}
<div class="bg-white rounded-2xl border border-gray-200 p-8 flex flex-col items-center text-center shadow-sm">
<div class="w-16 h-16 rounded-2xl bg-gray-100 flex items-center justify-center mb-4">
<span class="text-4xl"></span>
</div>
<h2 class="text-2xl font-bold text-gray-900 mb-1">Apple (iOS)</h2>
<p class="text-sm text-gray-500 mb-6">Version {{ $ios['version'] }}</p>
@if ($ios['disponible'])
<a href="{{ $ios['url'] }}"
@if (str_starts_with($ios['url'], url('/'))) download @else target="_blank" rel="noopener" @endif
class="w-full px-6 py-3 bg-gray-900 text-white font-bold rounded-lg hover:bg-black transition-smooth duration-200">
⬇️ Télécharger
</a>
<p class="text-xs text-gray-400 mt-3">
L'installation d'un fichier .ipa nécessite TestFlight ou un profil adapté.
</p>
@else
<button type="button" disabled
class="w-full px-6 py-3 bg-gray-200 text-gray-500 font-bold rounded-lg cursor-not-allowed">
Bientôt disponible
</button>
@endif
</div>
</div>
<div class="mt-10 text-center">
<a href="{{ route('home') }}" class="text-sm font-semibold text-niagara hover:underline">
Retour à l'accueil
</a>
</div>
</div>
</section>
@endsection
+4
View File
@@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\DownloadController;
use App\Http\Controllers\LoginController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\SuggestionController;
@@ -35,6 +36,9 @@ Route::get('/resources', function () {
return view('resources');
})->name('resources.index');
// Téléchargement des applications mobiles (public)
Route::get('/telecharger', [DownloadController::class, 'index'])->name('telecharger');
// Routes d'authentification
Route::get('/login', [LoginController::class, 'login'])->name('login');
Route::post('/login', [LoginController::class, 'store'])->name('login.store');
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace Tests\Feature;
use Tests\TestCase;
class TelechargementTest extends TestCase
{
public function test_la_page_de_telechargement_est_accessible_publiquement(): void
{
$response = $this->get('/telecharger');
$response->assertStatus(200);
$response->assertSee('Android');
$response->assertSee('iOS');
}
public function test_un_lien_de_store_configure_est_utilise(): void
{
config(['mobile.android.url' => 'https://play.google.com/store/apps/details?id=test']);
$response = $this->get('/telecharger');
$response->assertStatus(200);
$response->assertSee('play.google.com/store/apps/details?id=test');
}
public function test_sans_binaire_ni_lien_le_bouton_est_indisponible(): void
{
config(['mobile.ios.url' => null]);
$response = $this->get('/telecharger');
$response->assertStatus(200);
$response->assertSee('Bientôt disponible');
}
}