From a730d15821a4eec4a2c48782ae674b8310e65866 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 14:55:07 +0200 Subject: [PATCH 0001/1086] fix(discovery): bind continuation to search criteria and authenticated viewer --- discovery/api/composition.py | 51 +++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/discovery/api/composition.py b/discovery/api/composition.py index 7c85109ac..1330bd1fd 100644 --- a/discovery/api/composition.py +++ b/discovery/api/composition.py @@ -1,5 +1,8 @@ from __future__ import annotations +import hashlib +import json + from dataclasses import dataclass from types import SimpleNamespace @@ -65,7 +68,40 @@ def _authenticated(profile) -> bool: return bool(getattr(profile, "is_authenticated", False)) -def parse_page_params(params) -> tuple[int, int]: +def _continuation_context(params, *, profile=None) -> str: + """Bind a cursor to the exact request criteria and authenticated viewer. + + Page and continuation are navigation controls, not exploration criteria. + No client-provided profile or authority is ever trusted. + """ + if hasattr(params, "lists"): + criteria = { + key: sorted(str(value) for value in values) + for key, values in params.lists() + if key not in {"page", "continuation"} + } + else: + criteria = { + str(key): [str(value)] + for key, value in params.items() + if key not in {"page", "continuation"} + } + viewer = ( + str(profile.pk) + if _authenticated(profile) + else None + ) + canonical = json.dumps( + {"criteria": criteria, "viewer": viewer}, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + ) + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + +def parse_page_params(params, *, profile=None) -> tuple[int, int]: + """A request without parameters is valid; a cursor must match its context.""" try: continuation = params.get("continuation") if continuation: @@ -74,6 +110,13 @@ def parse_page_params(params) -> tuple[int, int]: salt=DISCOVERY_CONTINUATION_SALT, max_age=60 * 60 * 24, ) + if ( + not isinstance(cursor, dict) + or cursor.get("context") != _continuation_context( + params, profile=profile + ) + ): + raise ValueError("Contexte de continuation incompatible.") page = max(int(cursor["page"]), 1) else: page = max(int(params.get("page") or 1), 1) @@ -82,7 +125,7 @@ def parse_page_params(params) -> tuple[int, int]: DISCOVERY_API_MAX_PAGE_SIZE, ) except (signing.BadSignature, KeyError, TypeError, ValueError) as exc: - raise ValidationError("Pagination invalide.") from exc + raise ValidationError("Pagination invalide ou continuation expiree.") from exc return page, page_size @@ -635,7 +678,7 @@ def paginated_projection( include_internal_rows=False, ) -> dict: composition = compose_discovery(params, profile=profile) - page, page_size = parse_page_params(params) + page, page_size = parse_page_params(params, profile=profile) start = (page - 1) * page_size end = start + page_size page_rows = composition.rows[start:end] @@ -702,7 +745,7 @@ def paginated_projection( "state": "more" if has_next else "end", "token": ( signing.dumps( - {"page": page + 1}, + {"page": page + 1, "context": _continuation_context(params, profile=profile)}, salt=DISCOVERY_CONTINUATION_SALT, compress=True, ) From 6bfd8c80765a35049b501091d1586ff9a5f73475 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 15:21:12 +0200 Subject: [PATCH 0002/1086] feat(mobile): focus guest welcome on search and action journey --- .../features/guest/public_home_screen.dart | 310 ++++++++++++++---- 1 file changed, 255 insertions(+), 55 deletions(-) diff --git a/mobile/lib/features/guest/public_home_screen.dart b/mobile/lib/features/guest/public_home_screen.dart index 0be3ce4b8..5d4a7317d 100644 --- a/mobile/lib/features/guest/public_home_screen.dart +++ b/mobile/lib/features/guest/public_home_screen.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import '../../design/makolo_theme.dart'; import 'public_shell.dart'; -/// A calm public threshold, not a second Discovery engine or a marketing feed. +/// Public entry point: orientation towards Discover, not a second feed. class PublicHomeScreen extends StatefulWidget { const PublicHomeScreen({super.key}); @@ -21,10 +21,12 @@ class _PublicHomeScreenState extends State { super.dispose(); } - void _explore([String? vertical]) { + void _explore({String? vertical, bool includeQuery = true}) { + FocusManager.instance.primaryFocus?.unfocus(); final query = { - if (_search.text.trim().isNotEmpty) 'q': _search.text.trim(), - 'vertical': ?vertical, + if (includeQuery && _search.text.trim().isNotEmpty) + 'q': _search.text.trim(), + if (vertical != null) 'vertical': vertical, }; final suffix = Uri(queryParameters: query).query; context.go(suffix.isEmpty ? '/discover' : '/discover?$suffix'); @@ -33,89 +35,287 @@ class _PublicHomeScreenState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final colors = theme.colorScheme; + final muted = colors.onSurfaceVariant; + final border = colors.outlineVariant; + return PublicExperienceShell( current: 0, child: ListView( key: const Key('guest-public-home'), padding: const EdgeInsets.all(MakoloSpacing.lg), children: [ - const SizedBox(height: MakoloSpacing.lg), + const SizedBox(height: MakoloSpacing.sm), Text( - 'MAKOLO', - style: theme.textTheme.labelMedium?.copyWith( - color: theme.colorScheme.primary, + 'DÉCOUVRIR · PRÉPARER · AVANCER', + style: theme.textTheme.labelSmall?.copyWith( + color: colors.primary, + fontWeight: FontWeight.w800, + letterSpacing: 1, ), ), - const SizedBox(height: MakoloSpacing.sm), + const SizedBox(height: MakoloSpacing.md), Text( - 'Faites avancer ce qui compte.', - style: theme.textTheme.headlineMedium, + 'Ce que vous voulez faire commence ici.', + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w800, + height: 1.13, + ), + ), + const SizedBox(height: MakoloSpacing.md), + Text( + 'Trouvez des activités, des services, des occasions et des ' + 'personnes ou organisations qui peuvent vous aider à avancer.', + style: theme.textTheme.bodyLarge?.copyWith( + color: muted, + height: 1.5, + ), ), - const SizedBox(height: MakoloSpacing.sm), - const Text('Trouvez ce que vous voulez vivre, faire ou obtenir.'), const SizedBox(height: MakoloSpacing.xl), - TextField( - key: const Key('guest-home-query'), - controller: _search, - textInputAction: TextInputAction.search, - onSubmitted: (_) => _explore(), - decoration: const InputDecoration( - labelText: 'Que souhaitez-vous faire ?', - hintText: 'Ex. Voyager à Kolwezi demain matin', - prefixIcon: Icon(Icons.search), - border: OutlineInputBorder(), + Text( + 'Rechercher des possibilités', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, ), ), const SizedBox(height: MakoloSpacing.sm), - FilledButton.icon( - onPressed: () => _explore(), - icon: const Icon(Icons.arrow_forward), - label: const Text('Découvrir'), + Semantics( + label: 'Rechercher des possibilités', + child: TextField( + key: const Key('guest-home-query'), + controller: _search, + textInputAction: TextInputAction.search, + onSubmitted: (_) => _explore(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + decoration: InputDecoration( + hintText: 'Voyager, participer, être accompagné…', + hintMaxLines: 2, + prefixIcon: const Icon(Icons.search_rounded), + suffixIcon: IconButton( + key: const Key('guest-home-search-submit'), + tooltip: 'Lancer la recherche', + onPressed: _explore, + icon: const Icon(Icons.arrow_forward_rounded), + ), + filled: true, + fillColor: colors.surface, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(MakoloRadii.card), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(MakoloRadii.card), + borderSide: BorderSide(color: border), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(MakoloRadii.card), + borderSide: BorderSide(color: colors.primary, width: 2), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: MakoloSpacing.md, + vertical: MakoloSpacing.md, + ), + ), + ), ), - const SizedBox(height: MakoloSpacing.xl), - Text('Explorer autrement', style: theme.textTheme.titleLarge), const SizedBox(height: MakoloSpacing.md), Wrap( spacing: MakoloSpacing.sm, runSpacing: MakoloSpacing.sm, children: [ - ActionChip( - avatar: const Icon(Icons.directions_bus_outlined), - label: const Text('Voyager'), - onPressed: () => _explore('transport'), + _ExploreChip( + label: 'Voyager', + icon: Icons.directions_bus_outlined, + onPressed: () => + _explore(vertical: 'transport', includeQuery: false), ), - ActionChip( - avatar: const Icon(Icons.event_outlined), - label: const Text('Événements'), - onPressed: () => _explore('event'), + _ExploreChip( + label: 'Participer', + icon: Icons.event_outlined, + onPressed: () => + _explore(vertical: 'event', includeQuery: false), ), - ActionChip( - avatar: const Icon(Icons.handshake_outlined), - label: const Text('Être accompagné'), - onPressed: () => _explore('service'), + _ExploreChip( + label: 'Être accompagné', + icon: Icons.handshake_outlined, + onPressed: () => + _explore(vertical: 'service', includeQuery: false), ), - ActionChip( - avatar: const Icon(Icons.explore_outlined), - label: const Text('Toutes les possibilités'), - onPressed: () => _explore(), + _ExploreChip( + label: 'Tout explorer', + icon: Icons.explore_outlined, + onPressed: () => _explore(includeQuery: false), ), ], ), - const SizedBox(height: MakoloSpacing.xl), - Card( - child: ListTile( - leading: const Icon(Icons.groups_outlined), - title: const Text('Les Espaces sur Makolo'), - subtitle: const Text( - 'Découvrez les organisations, équipes et communautés qui rendent des possibilités accessibles.', - ), - isThreeLine: true, - trailing: const Icon(Icons.arrow_forward_ios, size: 16), + const SizedBox(height: MakoloSpacing.strong), + Container( + padding: const EdgeInsets.all(MakoloSpacing.inner), + decoration: BoxDecoration( + color: colors.surface, + borderRadius: BorderRadius.circular(MakoloRadii.large), + border: Border.all(color: border), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Makolo vous accompagne jusqu’à l’action', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: MakoloSpacing.lg), + const Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _JourneyStep( + number: '01', + label: 'Découvrez', + icon: Icons.explore_outlined, + ), + ), + SizedBox(width: MakoloSpacing.sm), + Expanded( + child: _JourneyStep( + number: '02', + label: 'Préparez', + icon: Icons.fact_check_outlined, + ), + ), + SizedBox(width: MakoloSpacing.sm), + Expanded( + child: _JourneyStep( + number: '03', + label: 'Avancez', + icon: Icons.arrow_outward_rounded, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: MakoloSpacing.lg), + Material( + color: colors.surface, + borderRadius: BorderRadius.circular(MakoloRadii.large), + child: InkWell( + key: const Key('guest-home-spaces'), + borderRadius: BorderRadius.circular(MakoloRadii.large), onTap: () => context.go('/spaces'), + child: Container( + padding: const EdgeInsets.all(MakoloSpacing.inner), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(MakoloRadii.large), + border: Border.all(color: border), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.groups_outlined, + color: colors.primary, + size: 28, + ), + const SizedBox(width: MakoloSpacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Les Espaces sur Makolo', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: MakoloSpacing.xs), + Text( + 'Découvrez les organisations, équipes et ' + 'communautés présentes sur Makolo.', + style: theme.textTheme.bodyMedium?.copyWith( + color: muted, + ), + ), + ], + ), + ), + const SizedBox(width: MakoloSpacing.sm), + Icon(Icons.chevron_right_rounded, color: muted), + ], + ), + ), ), ), + const SizedBox(height: MakoloSpacing.lg), ], ), ); } } + +class _ExploreChip extends StatelessWidget { + const _ExploreChip({ + required this.label, + required this.icon, + required this.onPressed, + }); + + final String label; + final IconData icon; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).colorScheme; + return ActionChip( + backgroundColor: colors.surface, + side: BorderSide(color: colors.outlineVariant), + shape: const StadiumBorder(), + avatar: Icon(icon, size: 20, color: colors.primary), + label: Text(label), + onPressed: onPressed, + ); + } +} + +class _JourneyStep extends StatelessWidget { + const _JourneyStep({ + required this.number, + required this.label, + required this.icon, + }); + + final String number; + final String label; + final IconData icon; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = theme.colorScheme; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 26, color: colors.primary), + const SizedBox(height: MakoloSpacing.compact), + Text( + number, + style: theme.textTheme.labelMedium?.copyWith( + color: colors.primary, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: MakoloSpacing.xs), + Text( + label, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ], + ); + } +} From 252976821969eb4ea63db4c6813369cdb59d8b08 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 15:22:46 +0200 Subject: [PATCH 0003/1086] feat(web): make public entry search-first and explain the action journey --- templates/core/public_home.html | 98 +++++++++++++++++++++------------ 1 file changed, 62 insertions(+), 36 deletions(-) diff --git a/templates/core/public_home.html b/templates/core/public_home.html index cdf66d484..506ac732d 100644 --- a/templates/core/public_home.html +++ b/templates/core/public_home.html @@ -1,50 +1,76 @@ {% extends 'base/public.html' %} -{% block title %}Makolo — Vivre, faire, obtenir{% endblock %} +{% block title %}Makolo — Découvrez, préparez, avancez{% endblock %} {% block content %} -
-
+
+
Découvrir · Préparer · Avancer -

Faites avancer ce qui compte.

-

Trouvez ce que vous voulez vivre, faire ou obtenir.

-
- -
- +

Ce que vous voulez faire commence ici.

+

Trouvez des activités, des services, des occasions et des personnes ou organisations qui peuvent vous aider à avancer.

+ + + +
+
+ + +
+ +
- -
- -
-
-
-
Découvrir

Trouvez ce qui vous convient.

Explorez selon ce que vous voulez faire, vivre ou obtenir.

-
-

Voyager

Trajets, départs et possibilités de déplacement.

-

Participer

Événements, sessions et expériences à vivre.

-

Être accompagné

Services et accompagnements pour avancer.

-

Explorer

Autres possibilités à faire, obtenir ou financer.

+
-
-
-
Espaces

Présents sur Makolo

Voir tous
+
+
+
+
+ Espaces +

Les Espaces sur Makolo

+

Découvrez les organisations, équipes et communautés présentes sur Makolo.

+
+ Voir tous +
-{% endblock %} \ No newline at end of file +{% endblock %} From d07956f35d1a9ce6e60e298622d6cf476d455994 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 15:23:33 +0200 Subject: [PATCH 0004/1086] test(web): cover revised guest search and action path --- e2e/specs/public.spec.mjs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/e2e/specs/public.spec.mjs b/e2e/specs/public.spec.mjs index 44a87adb2..4ede94e4e 100644 --- a/e2e/specs/public.spec.mjs +++ b/e2e/specs/public.spec.mjs @@ -3,11 +3,11 @@ import { test, expect } from '../fixtures/makolo.mjs'; test('visitor discovers an event and its public organizer @firefox', async ({ page }) => { await page.goto('/'); - await expect(page.getByRole('heading', { name: /Faites avancer ce qui compte/i })).toBeVisible(); + await expect(page.getByRole('heading', { name: /Ce que vous voulez faire commence ici/i })).toBeVisible(); await expect(page.getByRole('banner').getByRole('link', { name: 'Connexion', exact: true })).toBeVisible(); - await page.getByLabel('Que voulez-vous faire ?').fill('Festival Makolo E2E'); - await page.getByRole('button', { name: 'Découvrir' }).click(); + await page.getByLabel('Rechercher des possibilités').fill('Festival Makolo E2E'); + await page.getByRole('button', { name: 'Rechercher' }).click(); await expect(page).toHaveURL(/\/discover\/\?q=Festival/); await expect(page.getByRole('heading', { name: 'Festival Makolo E2E' }).first()).toBeVisible(); @@ -22,6 +22,19 @@ test('visitor discovers an event and its public organizer @firefox', async ({ pa }); + +test('guest home explains the action path and exposes existing Discover filters', async ({ page }) => { + await page.goto('/'); + await expect(page.getByRole('heading', { name: 'Makolo vous accompagne jusqu’à l’action' })).toBeVisible(); + const journey = page.getByRole('complementary', { name: 'Comment Makolo vous accompagne' }); + await expect(journey.getByRole('heading', { name: 'Découvrez' })).toBeVisible(); + await expect(journey.getByRole('heading', { name: 'Préparez' })).toBeVisible(); + await expect(journey.getByRole('heading', { name: 'Avancez' })).toBeVisible(); + await page.getByRole('navigation', { name: 'Explorer les possibilités' }) + .getByRole('link', { name: 'Participer' }).click(); + await expect(page).toHaveURL(/\\/discover\\/\\?vertical=event/); +}); + test('private participant surface sends visitor to login with next preserved', async ({ page }) => { await page.goto('/tickets/'); await expect(page).toHaveURL(/\/login\/\?next=\/tickets\//); @@ -31,11 +44,11 @@ test('private participant surface sends visitor to login with next preserved', a test('mobile public home remains usable without horizontal overflow @mobile', async ({ page }) => { await page.goto('/'); - await expect(page.getByRole('heading', { name: /Faites avancer ce qui compte/i })).toBeVisible(); + await expect(page.getByRole('heading', { name: /Ce que vous voulez faire commence ici/i })).toBeVisible(); await expect(page.getByRole('banner').getByRole('link', { name: 'Connexion', exact: true })).toBeVisible(); const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth); expect(overflow).toBeLessThanOrEqual(1); - await page.getByLabel('Que voulez-vous faire ?').fill('Atelier Makolo Visuel'); - await page.getByRole('button', { name: 'Découvrir' }).click(); + await page.getByLabel('Rechercher des possibilités').fill('Atelier Makolo Visuel'); + await page.getByRole('button', { name: 'Rechercher' }).click(); await expect(page.getByText('Atelier Makolo Visuel').first()).toBeVisible(); }); From 183b7c913bea1a37014fb69171dc4d2331629583 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 15:24:05 +0200 Subject: [PATCH 0005/1086] test(mobile): validate guest welcome search, filters and Spaces --- mobile/test/guest_router_test.dart | 86 +++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/mobile/test/guest_router_test.dart b/mobile/test/guest_router_test.dart index 15fb69cb9..1d440f20d 100644 --- a/mobile/test/guest_router_test.dart +++ b/mobile/test/guest_router_test.dart @@ -98,7 +98,8 @@ void main() { expect(tester.takeException(), isNull); expect(find.byKey(const Key('guest-public-home')), findsOneWidget); - expect(find.text('Faites avancer ce qui compte.'), findsOneWidget); + expect(find.text('Ce que vous voulez faire commence ici.'), findsOneWidget); + expect(find.byKey(const Key('guest-home-search-submit')), findsOneWidget); expect(find.text('Accueil'), findsOneWidget); expect(find.text('Espaces'), findsWidgets); expect(find.text('En cours'), findsNothing); @@ -114,6 +115,89 @@ void main() { expect(find.byType(LoginScreen), findsOneWidget); }); + testWidgets('guest welcome search submits to native Discover', ( + tester, + ) async { + final seenQueries = []; + final client = MockClient((request) async { + expect(request.headers.containsKey('Authorization'), isFalse); + expect(request.url.path, '/api/v1/discovery/items/'); + seenQueries.add(request.url.queryParameters['q'] ?? ''); + return MockResponse( + jsonEncode({ + 'data': { + 'count': 0, + 'page': 1, + 'page_size': 12, + 'has_next': false, + 'results': [], + }, + }), + 200, + ); + }); + final router = _router( + _guestRuntime(SessionRecoveryController(), client: client), + ); + await _pump(tester, router); + + await tester.enterText( + find.byKey(const Key('guest-home-query')), + 'Voyager à Kolwezi', + ); + await tester.testTextInput.receiveAction(TextInputAction.search); + await tester.pumpAndSettle(); + + expect(router.routeInformationProvider.value.uri.path, '/discover'); + expect( + router.routeInformationProvider.value.uri.queryParameters['q'], + 'Voyager à Kolwezi', + ); + expect(seenQueries, contains('Voyager à Kolwezi')); + expect(find.byType(LoginScreen), findsNothing); + }); + + testWidgets('guest welcome quick filter navigates natively', ( + tester, + ) async { + final router = _router(_guestRuntime(SessionRecoveryController())); + await _pump(tester, router); + + await tester.tap(find.text('Participer')); + await tester.pumpAndSettle(); + + expect(router.routeInformationProvider.value.uri.path, '/discover'); + expect( + router.routeInformationProvider.value.uri.queryParameters['vertical'], + 'event', + ); + expect( + router.routeInformationProvider.value.uri.queryParameters['q'], + isNull, + ); + }); + + testWidgets('guest welcome links to public Spaces', (tester) async { + final router = _router(_guestRuntime(SessionRecoveryController())); + await _pump(tester, router); + + await tester.scrollUntilVisible( + find.byKey(const Key('guest-home-spaces')), + 160, + scrollable: find + .descendant( + of: find.byKey(const Key('guest-public-home')), + matching: find.byType(Scrollable), + ) + .first, + ); + await tester.tap(find.byKey(const Key('guest-home-spaces'))); + await tester.pumpAndSettle(); + + expect(router.routeInformationProvider.value.uri.path, '/spaces'); + expect(find.byType(LoginScreen), findsNothing); + }); + testWidgets( 'public Discover actually searches the server, not loaded items', (tester) async { From 8f1fb5a82c15227648f05fede920f96ac9aafe76 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 15:24:38 +0200 Subject: [PATCH 0006/1086] test(web): align action aside and category URL assertion --- e2e/specs/public.spec.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/specs/public.spec.mjs b/e2e/specs/public.spec.mjs index 4ede94e4e..afe8c6307 100644 --- a/e2e/specs/public.spec.mjs +++ b/e2e/specs/public.spec.mjs @@ -26,7 +26,7 @@ test('visitor discovers an event and its public organizer @firefox', async ({ pa test('guest home explains the action path and exposes existing Discover filters', async ({ page }) => { await page.goto('/'); await expect(page.getByRole('heading', { name: 'Makolo vous accompagne jusqu’à l’action' })).toBeVisible(); - const journey = page.getByRole('complementary', { name: 'Comment Makolo vous accompagne' }); + const journey = page.getByRole('complementary', { name: 'Makolo vous accompagne jusqu’à l’action' }); await expect(journey.getByRole('heading', { name: 'Découvrez' })).toBeVisible(); await expect(journey.getByRole('heading', { name: 'Préparez' })).toBeVisible(); await expect(journey.getByRole('heading', { name: 'Avancez' })).toBeVisible(); From 7885f293e888529750c5ca0b39bd99860f3b200c Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 15:24:59 +0200 Subject: [PATCH 0007/1086] test(web): fix public filter URL regexp --- e2e/specs/public.spec.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/specs/public.spec.mjs b/e2e/specs/public.spec.mjs index afe8c6307..68534bde5 100644 --- a/e2e/specs/public.spec.mjs +++ b/e2e/specs/public.spec.mjs @@ -32,7 +32,7 @@ test('guest home explains the action path and exposes existing Discover filters' await expect(journey.getByRole('heading', { name: 'Avancez' })).toBeVisible(); await page.getByRole('navigation', { name: 'Explorer les possibilités' }) .getByRole('link', { name: 'Participer' }).click(); - await expect(page).toHaveURL(/\\/discover\\/\\?vertical=event/); + await expect(page).toHaveURL(/\/discover\/\?vertical=event/); }); test('private participant surface sends visitor to login with next preserved', async ({ page }) => { From a6f348cf42473ff02be1ec2fafe04fe6c86647f8 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 15:26:17 +0200 Subject: [PATCH 0008/1086] style(web): reuse compiled public spacing utilities --- templates/core/public_home.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/core/public_home.html b/templates/core/public_home.html index 506ac732d..52525e678 100644 --- a/templates/core/public_home.html +++ b/templates/core/public_home.html @@ -2,20 +2,20 @@ {% block title %}Makolo — Découvrez, préparez, avancez{% endblock %} {% block content %}
-
+
Découvrir · Préparer · Avancer

Ce que vous voulez faire commence ici.

Trouvez des activités, des services, des occasions et des personnes ou organisations qui peuvent vous aider à avancer.

- From 053064b5f60f4846af0277476790e53fd413afd4 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 15:39:32 +0200 Subject: [PATCH 0009/1086] style(mobile): use null-aware Discovery query entry --- mobile/lib/features/guest/public_home_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/features/guest/public_home_screen.dart b/mobile/lib/features/guest/public_home_screen.dart index 5d4a7317d..8981690a2 100644 --- a/mobile/lib/features/guest/public_home_screen.dart +++ b/mobile/lib/features/guest/public_home_screen.dart @@ -26,7 +26,7 @@ class _PublicHomeScreenState extends State { final query = { if (includeQuery && _search.text.trim().isNotEmpty) 'q': _search.text.trim(), - if (vertical != null) 'vertical': vertical, + 'vertical': ?vertical, }; final suffix = Uri(queryParameters: query).query; context.go(suffix.isEmpty ? '/discover' : '/discover?$suffix'); From 0c4291c72722d6923a7188097406a7117f103936 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 15:55:58 +0200 Subject: [PATCH 0010/1086] style(mobile): tighten guest welcome composition and quick exploration --- .../features/guest/public_home_screen.dart | 211 +++++++++--------- 1 file changed, 100 insertions(+), 111 deletions(-) diff --git a/mobile/lib/features/guest/public_home_screen.dart b/mobile/lib/features/guest/public_home_screen.dart index 8981690a2..0f31bf744 100644 --- a/mobile/lib/features/guest/public_home_screen.dart +++ b/mobile/lib/features/guest/public_home_screen.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import '../../design/makolo_theme.dart'; import 'public_shell.dart'; -/// Public entry point: orientation towards Discover, not a second feed. +/// Guest orientation into Discover, not a second discovery feed. class PublicHomeScreen extends StatefulWidget { const PublicHomeScreen({super.key}); @@ -37,15 +37,16 @@ class _PublicHomeScreenState extends State { final theme = Theme.of(context); final colors = theme.colorScheme; final muted = colors.onSurfaceVariant; - final border = colors.outlineVariant; + final edge = MediaQuery.sizeOf(context).width <= 360 + ? MakoloSpacing.md + : MakoloSpacing.inner; return PublicExperienceShell( current: 0, child: ListView( key: const Key('guest-public-home'), - padding: const EdgeInsets.all(MakoloSpacing.lg), + padding: EdgeInsets.fromLTRB(edge, MakoloSpacing.inner, edge, MakoloSpacing.lg), children: [ - const SizedBox(height: MakoloSpacing.sm), Text( 'DÉCOUVRIR · PRÉPARER · AVANCER', style: theme.textTheme.labelSmall?.copyWith( @@ -54,29 +55,28 @@ class _PublicHomeScreenState extends State { letterSpacing: 1, ), ), - const SizedBox(height: MakoloSpacing.md), + const SizedBox(height: MakoloSpacing.compact), Text( 'Ce que vous voulez faire commence ici.', style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.w800, - height: 1.13, + letterSpacing: -0.6, + height: 1.12, ), ), - const SizedBox(height: MakoloSpacing.md), + const SizedBox(height: MakoloSpacing.sm), Text( 'Trouvez des activités, des services, des occasions et des ' 'personnes ou organisations qui peuvent vous aider à avancer.', - style: theme.textTheme.bodyLarge?.copyWith( + style: theme.textTheme.bodyMedium?.copyWith( color: muted, - height: 1.5, + height: 1.35, ), ), - const SizedBox(height: MakoloSpacing.xl), + const SizedBox(height: MakoloSpacing.lg), Text( 'Rechercher des possibilités', - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w700, - ), + style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: MakoloSpacing.sm), Semantics( @@ -86,8 +86,7 @@ class _PublicHomeScreenState extends State { controller: _search, textInputAction: TextInputAction.search, onSubmitted: (_) => _explore(), - onTapOutside: (_) => - FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), decoration: InputDecoration( hintText: 'Voyager, participer, être accompagné…', hintMaxLines: 2, @@ -100,163 +99,139 @@ class _PublicHomeScreenState extends State { ), filled: true, fillColor: colors.surface, + contentPadding: const EdgeInsets.symmetric( + horizontal: MakoloSpacing.md, + vertical: MakoloSpacing.compact, + ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(MakoloRadii.card), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(MakoloRadii.card), - borderSide: BorderSide(color: border), + borderSide: BorderSide(color: colors.outlineVariant), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(MakoloRadii.card), borderSide: BorderSide(color: colors.primary, width: 2), ), - contentPadding: const EdgeInsets.symmetric( - horizontal: MakoloSpacing.md, - vertical: MakoloSpacing.md, - ), ), ), ), const SizedBox(height: MakoloSpacing.md), - Wrap( - spacing: MakoloSpacing.sm, - runSpacing: MakoloSpacing.sm, + GridView.count( + key: const Key('guest-home-quick-choices'), + crossAxisCount: 2, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: MakoloSpacing.sm, + crossAxisSpacing: MakoloSpacing.sm, + childAspectRatio: 3.35, children: [ - _ExploreChip( + _ExploreChoice( label: 'Voyager', icon: Icons.directions_bus_outlined, - onPressed: () => - _explore(vertical: 'transport', includeQuery: false), + onPressed: () => _explore(vertical: 'transport', includeQuery: false), ), - _ExploreChip( + _ExploreChoice( label: 'Participer', icon: Icons.event_outlined, - onPressed: () => - _explore(vertical: 'event', includeQuery: false), + onPressed: () => _explore(vertical: 'event', includeQuery: false), ), - _ExploreChip( + _ExploreChoice( label: 'Être accompagné', icon: Icons.handshake_outlined, - onPressed: () => - _explore(vertical: 'service', includeQuery: false), + onPressed: () => _explore(vertical: 'service', includeQuery: false), ), - _ExploreChip( + _ExploreChoice( label: 'Tout explorer', icon: Icons.explore_outlined, onPressed: () => _explore(includeQuery: false), ), ], ), - const SizedBox(height: MakoloSpacing.strong), + const SizedBox(height: MakoloSpacing.lg), Container( - padding: const EdgeInsets.all(MakoloSpacing.inner), + key: const Key('guest-home-action-journey'), + padding: const EdgeInsets.symmetric( + horizontal: MakoloSpacing.md, + vertical: MakoloSpacing.inner, + ), decoration: BoxDecoration( - color: colors.surface, + color: colors.primary.withValues(alpha: 0.055), borderRadius: BorderRadius.circular(MakoloRadii.large), - border: Border.all(color: border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Makolo vous accompagne jusqu’à l’action', - style: theme.textTheme.titleLarge?.copyWith( + style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w800, + height: 1.2, ), ), - const SizedBox(height: MakoloSpacing.lg), - const Row( - crossAxisAlignment: CrossAxisAlignment.start, + const SizedBox(height: MakoloSpacing.md), + Row( children: [ - Expanded( - child: _JourneyStep( - number: '01', - label: 'Découvrez', - icon: Icons.explore_outlined, - ), - ), - SizedBox(width: MakoloSpacing.sm), - Expanded( - child: _JourneyStep( - number: '02', - label: 'Préparez', - icon: Icons.fact_check_outlined, - ), - ), - SizedBox(width: MakoloSpacing.sm), - Expanded( - child: _JourneyStep( - number: '03', - label: 'Avancez', - icon: Icons.arrow_outward_rounded, - ), - ), + const Expanded(child: _JourneyStep(number: '01', label: 'Découvrez')), + Icon(Icons.chevron_right_rounded, size: 18, color: muted), + const Expanded(child: _JourneyStep(number: '02', label: 'Préparez')), + Icon(Icons.chevron_right_rounded, size: 18, color: muted), + const Expanded(child: _JourneyStep(number: '03', label: 'Avancez')), ], ), ], ), ), - const SizedBox(height: MakoloSpacing.lg), + const SizedBox(height: MakoloSpacing.md), Material( color: colors.surface, - borderRadius: BorderRadius.circular(MakoloRadii.large), + borderRadius: BorderRadius.circular(MakoloRadii.card), child: InkWell( key: const Key('guest-home-spaces'), - borderRadius: BorderRadius.circular(MakoloRadii.large), onTap: () => context.go('/spaces'), + borderRadius: BorderRadius.circular(MakoloRadii.card), child: Container( - padding: const EdgeInsets.all(MakoloSpacing.inner), + padding: const EdgeInsets.all(MakoloSpacing.md), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(MakoloRadii.large), - border: Border.all(color: border), + borderRadius: BorderRadius.circular(MakoloRadii.card), + border: Border.all(color: colors.outlineVariant), ), child: Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Icons.groups_outlined, - color: colors.primary, - size: 28, - ), - const SizedBox(width: MakoloSpacing.md), + Icon(Icons.groups_outlined, color: colors.primary, size: 26), + const SizedBox(width: MakoloSpacing.compact), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Les Espaces sur Makolo', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w800, - ), + style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w800), ), const SizedBox(height: MakoloSpacing.xs), Text( - 'Découvrez les organisations, équipes et ' - 'communautés présentes sur Makolo.', - style: theme.textTheme.bodyMedium?.copyWith( - color: muted, - ), + 'Organisations, équipes et communautés', + style: theme.textTheme.bodySmall?.copyWith(color: muted), ), ], ), ), - const SizedBox(width: MakoloSpacing.sm), - Icon(Icons.chevron_right_rounded, color: muted), + const SizedBox(width: MakoloSpacing.xs), + Icon(Icons.arrow_forward_ios_rounded, size: 16, color: muted), ], ), ), ), ), - const SizedBox(height: MakoloSpacing.lg), ], ), ); } } -class _ExploreChip extends StatelessWidget { - const _ExploreChip({ +class _ExploreChoice extends StatelessWidget { + const _ExploreChoice({ required this.label, required this.icon, required this.onPressed, @@ -268,39 +243,54 @@ class _ExploreChip extends StatelessWidget { @override Widget build(BuildContext context) { - final colors = Theme.of(context).colorScheme; - return ActionChip( - backgroundColor: colors.surface, - side: BorderSide(color: colors.outlineVariant), - shape: const StadiumBorder(), - avatar: Icon(icon, size: 20, color: colors.primary), - label: Text(label), - onPressed: onPressed, + final theme = Theme.of(context); + final colors = theme.colorScheme; + return Material( + color: colors.surface, + shape: RoundedRectangleBorder( + side: BorderSide(color: colors.outlineVariant), + borderRadius: BorderRadius.circular(MakoloRadii.card), + ), + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(MakoloRadii.card), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: MakoloSpacing.sm), + child: Row( + children: [ + Icon(icon, size: 19, color: colors.primary), + const SizedBox(width: MakoloSpacing.sm), + Expanded( + child: Text( + label, + maxLines: 2, + style: theme.textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.w700, + height: 1.15, + ), + ), + ), + ], + ), + ), + ), ); } } class _JourneyStep extends StatelessWidget { - const _JourneyStep({ - required this.number, - required this.label, - required this.icon, - }); + const _JourneyStep({required this.number, required this.label}); final String number; final String label; - final IconData icon; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colors = theme.colorScheme; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(icon, size: 26, color: colors.primary), - const SizedBox(height: MakoloSpacing.compact), Text( number, style: theme.textTheme.labelMedium?.copyWith( @@ -311,9 +301,8 @@ class _JourneyStep extends StatelessWidget { const SizedBox(height: MakoloSpacing.xs), Text( label, - style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w700, - ), + textAlign: TextAlign.center, + style: theme.textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w700), ), ], ); From eaa62dbfa2d8ce90f80fb4a66876051048abcea4 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 15:56:16 +0200 Subject: [PATCH 0011/1086] style(mobile): protect quick choices text at compact widths --- mobile/lib/features/guest/public_home_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/features/guest/public_home_screen.dart b/mobile/lib/features/guest/public_home_screen.dart index 0f31bf744..a0eb1e0f3 100644 --- a/mobile/lib/features/guest/public_home_screen.dart +++ b/mobile/lib/features/guest/public_home_screen.dart @@ -125,7 +125,7 @@ class _PublicHomeScreenState extends State { physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: MakoloSpacing.sm, crossAxisSpacing: MakoloSpacing.sm, - childAspectRatio: 3.35, + childAspectRatio: 2.65, children: [ _ExploreChoice( label: 'Voyager', From aa4aa714971924d4a47492c26c72815cc7c0a088 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 15:56:31 +0200 Subject: [PATCH 0012/1086] test(mobile): cover guest welcome compact composition --- mobile/test/guest_router_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mobile/test/guest_router_test.dart b/mobile/test/guest_router_test.dart index 1d440f20d..de2bfc87c 100644 --- a/mobile/test/guest_router_test.dart +++ b/mobile/test/guest_router_test.dart @@ -100,6 +100,8 @@ void main() { expect(find.byKey(const Key('guest-public-home')), findsOneWidget); expect(find.text('Ce que vous voulez faire commence ici.'), findsOneWidget); expect(find.byKey(const Key('guest-home-search-submit')), findsOneWidget); + expect(find.byKey(const Key('guest-home-quick-choices')), findsOneWidget); + expect(find.byKey(const Key('guest-home-action-journey')), findsOneWidget); expect(find.text('Accueil'), findsOneWidget); expect(find.text('Espaces'), findsWidgets); expect(find.text('En cours'), findsNothing); From ab4574d05a945136e7aa41e26358533f520949f1 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 17:50:17 +0200 Subject: [PATCH 0013/1086] feat(mobile): keep optional Discovery location separate from proximity --- .../location/discovery_location_context.dart | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 mobile/lib/platform/location/discovery_location_context.dart diff --git a/mobile/lib/platform/location/discovery_location_context.dart b/mobile/lib/platform/location/discovery_location_context.dart new file mode 100644 index 000000000..730f3e95d --- /dev/null +++ b/mobile/lib/platform/location/discovery_location_context.dart @@ -0,0 +1,26 @@ +import 'location_service.dart'; + +/// Optional device context for Discovery; never a geographic search filter. +/// Kept only in process memory, not in a Profile or global business model. +class DiscoveryLocationContext { + DiscoveryLocationContext._(); + + static LocationFix? _fix; + + static LocationFix? get current => _fix; + + static void update(LocationFix? fix) { + _fix = fix; + } + + static Map get parameters { + final fix = _fix; + if (fix == null) return const {}; + return { + 'client_lat': fix.latitude.toString(), + 'client_lon': fix.longitude.toString(), + 'client_accuracy_m': fix.accuracyMeters.toString(), + 'client_observed_at': fix.observedAt.toUtc().toIso8601String(), + }; + } +} From 370a69f74cb5f73307912d85c40aacc7c69fb658 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 17:50:19 +0200 Subject: [PATCH 0014/1086] feat(mobile): concise three-step onboarding with permissions --- .../features/onboarding/onboarding_flow.dart | 80 ++++++++++++++----- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/mobile/lib/features/onboarding/onboarding_flow.dart b/mobile/lib/features/onboarding/onboarding_flow.dart index 2e719eda4..0bd337df7 100644 --- a/mobile/lib/features/onboarding/onboarding_flow.dart +++ b/mobile/lib/features/onboarding/onboarding_flow.dart @@ -2,6 +2,9 @@ import 'package:flutter/material.dart'; import '../../design/makolo_mark.dart'; import '../../design/makolo_theme.dart'; +import '../../platform/location/location_capability.dart'; +import '../../platform/location/discovery_location_context.dart'; +import '../../platform/permissions/permission_gateway.dart'; enum OnboardingExit { skip, @@ -16,9 +19,13 @@ class OnboardingFlow extends StatefulWidget { super.key, required this.isAuthenticated, required this.onComplete, + this.location, + this.permissions, }); final bool isAuthenticated; + final LocationCapability? location; + final PermissionGateway? permissions; final Future Function(OnboardingExit exit) onComplete; @override @@ -27,23 +34,39 @@ class OnboardingFlow extends StatefulWidget { class _OnboardingFlowState extends State { static const _pages = <({String title, String body, IconData icon})>[ - ( - title: 'Découvrez ce qui compte pour vous.', - body: 'Explorez des possibilités réelles pour vivre, faire ou obtenir ce qui compte pour vous.', - icon: Icons.explore_outlined, - ), - ( - title: 'Préparez ce qui est nécessaire.', - body: 'Rassemblez le contexte utile avant le moment d’agir et voyez clairement ce qui reste à faire.', - icon: Icons.inventory_2_outlined, - ), - ( - title: 'Avancez dans l’action réelle.', - body: 'Retrouvez vos démarches, vos prochaines actions et la continuité nécessaire pour aller jusqu’au bout.', - icon: Icons.arrow_forward_rounded, - ), + (title: 'Makolo marche pour vous.', body: 'Découvrez des possibilités, préparez ce qui peut l’être et suivez vos actions réelles.', icon: Icons.explore_outlined), + (title: 'Préparer Makolo', body: 'Choisissez les autorisations à activer.', icon: Icons.tune_rounded), + (title: 'Tout commence par une découverte.', body: 'Événements, formations, voyages, services et opportunités : explorez librement.', icon: Icons.arrow_forward_rounded), ]; + final Map _choices = { + MakoloPermission.locationWhenInUse: false, + MakoloPermission.notifications: false, + MakoloPermission.camera: false, + MakoloPermission.microphone: false, + }; + bool _requesting = false; + + Future _applyPermissions() async { + if (_requesting) return; + setState(() => _requesting = true); + try { + for (final entry in _choices.entries) { + if (!entry.value) continue; + if (entry.key == MakoloPermission.locationWhenInUse) { + final result = await widget.location?.current( + accuracy: MakoloLocationAccuracy.low, + ); + DiscoveryLocationContext.update(result?.fix); + } else { + await widget.permissions?.requestWhenNeeded(entry.key); + } + } + } finally { + if (mounted) setState(() => _requesting = false); + } + } + final PageController _controller = PageController(); int _page = 0; @@ -65,6 +88,8 @@ class _OnboardingFlowState extends State { return; } + if (_page == 1) await _applyPermissions(); + if (!mounted) return; await _controller.nextPage( duration: MakoloMotion.medium, curve: Curves.easeOutCubic, @@ -77,7 +102,7 @@ class _OnboardingFlowState extends State { if (_isLastPage) { primaryLabel = widget.isAuthenticated ? 'Ouvrir Makolo' - : 'Découvrir Makolo'; + : 'Découvrir'; } return Scaffold( @@ -133,6 +158,25 @@ class _OnboardingFlowState extends State { item.body, style: Theme.of(context).textTheme.bodyLarge, ), + if (index == 1) ...[ + const SizedBox(height: MakoloSpacing.lg), + for (final choice in <({MakoloPermission permission, String label, IconData icon})>[ + (permission: MakoloPermission.locationWhenInUse, label: 'Localisation', icon: Icons.location_on_outlined), + (permission: MakoloPermission.notifications, label: 'Notifications', icon: Icons.notifications_none_rounded), + (permission: MakoloPermission.camera, label: 'Appareil photo', icon: Icons.camera_alt_outlined), + (permission: MakoloPermission.microphone, label: 'Microphone', icon: Icons.mic_none_outlined), + ]) + CheckboxListTile( + dense: true, + contentPadding: EdgeInsets.zero, + secondary: Icon(choice.icon), + title: Text(choice.label), + value: _choices[choice.permission] ?? false, + onChanged: _requesting ? null : (value) => setState(() { + _choices[choice.permission] = value ?? false; + }), + ), + ], ], ), ), @@ -167,7 +211,7 @@ class _OnboardingFlowState extends State { const SizedBox(height: MakoloSpacing.lg), FilledButton( key: const Key('onboarding-primary'), - onPressed: _next, + onPressed: _requesting ? null : _next, child: Text(primaryLabel), ), if (!widget.isAuthenticated && _isLastPage) ...[ @@ -178,7 +222,7 @@ class _OnboardingFlowState extends State { child: OutlinedButton( onPressed: () => widget.onComplete(OnboardingExit.signIn), - child: const Text('Se connecter'), + child: const Text('J’ai un compte'), ), ), const SizedBox(width: MakoloSpacing.sm), From 5edb4b2df2740b2ee86d895ee87e61de37cd9adc Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 17:50:22 +0200 Subject: [PATCH 0015/1086] feat(mobile): wire native permissions to onboarding --- mobile/lib/app/launch_gate.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mobile/lib/app/launch_gate.dart b/mobile/lib/app/launch_gate.dart index 59bcd3e6e..0e5cb2c36 100644 --- a/mobile/lib/app/launch_gate.dart +++ b/mobile/lib/app/launch_gate.dart @@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart'; import '../features/onboarding/onboarding_flow.dart'; import '../features/splash/splash_screen.dart'; +import '../platform/permissions/permission_gateway.dart'; import 'launch_policy.dart'; import 'launch_preferences.dart'; import 'providers.dart'; @@ -125,6 +126,8 @@ class _LaunchGateState extends State { _LaunchStage.onboarding => OnboardingFlow( isAuthenticated: widget.runtime.isAuthenticated, + location: widget.runtime.location, + permissions: const PermissionHandlerGateway(), onComplete: _finishOnboarding, ), From f1eea8991e6977459d048bdbf23ae5ec7245a6bf Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 17:50:24 +0200 Subject: [PATCH 0016/1086] feat(mobile): include optional device location in Discovery requests --- mobile/lib/features/discovery/discovery_repository.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mobile/lib/features/discovery/discovery_repository.dart b/mobile/lib/features/discovery/discovery_repository.dart index 41303da2d..7bfed72c7 100644 --- a/mobile/lib/features/discovery/discovery_repository.dart +++ b/mobile/lib/features/discovery/discovery_repository.dart @@ -5,6 +5,7 @@ import 'package:drift/drift.dart'; import '../../data/local/makolo_database.dart'; import '../../data/local/profile_store.dart'; import '../../network/makolo_api_client.dart'; +import '../../platform/location/discovery_location_context.dart'; import '../../sync/freshness.dart'; import '../../sync/projection_contract.dart'; import '../../sync/sync_engine.dart'; @@ -92,6 +93,7 @@ class DiscoveryQuery { if (latitude != null) 'lat': latitude!.toString(), if (longitude != null) 'lon': longitude!.toString(), if (radiusKm != null) 'radius_km': radiusKm!.toString(), + ...DiscoveryLocationContext.parameters, 'page': page.toString(), 'page_size': pageSize.toString(), }; From 5ce24ef827a23ee97460a716f35d580e71409a81 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 17:50:27 +0200 Subject: [PATCH 0017/1086] test(mobile): align onboarding expectations with three-step flow --- mobile/test/onboarding_flow_test.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mobile/test/onboarding_flow_test.dart b/mobile/test/onboarding_flow_test.dart index a630c9425..21eb417af 100644 --- a/mobile/test/onboarding_flow_test.dart +++ b/mobile/test/onboarding_flow_test.dart @@ -19,25 +19,25 @@ void main() { ), ); - expect(find.text('Découvrez ce qui compte vraiment.'), findsOneWidget); + expect(find.text('Makolo marche pour vous.'), findsOneWidget); expect(find.text('Continuer'), findsOneWidget); expect(find.text('1/3'), findsOneWidget); - expect(find.text('Se connecter'), findsNothing); + expect(find.text('J’ai un compte'), findsNothing); await tester.tap(find.text('Continuer')); await tester.pumpAndSettle(); - expect(find.text('Préparez ce qui peut l’être.'), findsOneWidget); + expect(find.text('Préparer Makolo'), findsOneWidget); expect(find.text('2/3'), findsOneWidget); await tester.tap(find.text('Continuer')); await tester.pumpAndSettle(); - expect(find.text('Avancez dans l’action réelle.'), findsOneWidget); + expect(find.text('Tout commence par une découverte.'), findsOneWidget); expect(find.text('3/3'), findsOneWidget); - expect(find.text('Découvrir Makolo'), findsOneWidget); - expect(find.text('Se connecter'), findsOneWidget); + expect(find.text('Découvrir'), findsOneWidget); + expect(find.text('J’ai un compte'), findsOneWidget); expect(find.text('Créer un compte'), findsOneWidget); - await tester.tap(find.text('Découvrir Makolo')); + await tester.tap(find.text('Découvrir')); await tester.pump(); expect(exit, OnboardingExit.continueGuest); @@ -64,7 +64,7 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Ouvrir Makolo'), findsOneWidget); - expect(find.text('Se connecter'), findsNothing); + expect(find.text('J’ai un compte'), findsNothing); expect(find.text('Créer un compte'), findsNothing); await tester.tap(find.text('Ouvrir Makolo')); From 13d2273493e1caabe9fc26b362c94fe7b3407f75 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 17:50:48 +0200 Subject: [PATCH 0018/1086] fix(mobile): import location accuracy type for onboarding --- mobile/lib/features/onboarding/onboarding_flow.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/mobile/lib/features/onboarding/onboarding_flow.dart b/mobile/lib/features/onboarding/onboarding_flow.dart index 0bd337df7..c60fc8385 100644 --- a/mobile/lib/features/onboarding/onboarding_flow.dart +++ b/mobile/lib/features/onboarding/onboarding_flow.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../design/makolo_mark.dart'; import '../../design/makolo_theme.dart'; import '../../platform/location/location_capability.dart'; +import '../../platform/location/location_service.dart'; import '../../platform/location/discovery_location_context.dart'; import '../../platform/permissions/permission_gateway.dart'; From 01d953a0f504c769a1958f2ea0c069d30e5ad54d Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 17:50:51 +0200 Subject: [PATCH 0019/1086] fix(mobile): onboarding Discover opens public Discovery --- mobile/lib/app/launch_gate.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/app/launch_gate.dart b/mobile/lib/app/launch_gate.dart index 0e5cb2c36..38c3efabf 100644 --- a/mobile/lib/app/launch_gate.dart +++ b/mobile/lib/app/launch_gate.dart @@ -104,7 +104,7 @@ class _LaunchGateState extends State { case OnboardingExit.skip: case OnboardingExit.continueGuest: if (!_priorityNavigation) { - widget.router.go('/welcome'); + widget.router.go('/discover'); } case OnboardingExit.continueAuthenticated: From bb69d505aba3469a070ef9468224b4d2b4e17e3d Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 17:51:08 +0200 Subject: [PATCH 0020/1086] fix(mobile): keep onboarding navigable when native permission fails --- .../features/onboarding/onboarding_flow.dart | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/mobile/lib/features/onboarding/onboarding_flow.dart b/mobile/lib/features/onboarding/onboarding_flow.dart index c60fc8385..9f96cdf68 100644 --- a/mobile/lib/features/onboarding/onboarding_flow.dart +++ b/mobile/lib/features/onboarding/onboarding_flow.dart @@ -54,13 +54,20 @@ class _OnboardingFlowState extends State { try { for (final entry in _choices.entries) { if (!entry.value) continue; - if (entry.key == MakoloPermission.locationWhenInUse) { - final result = await widget.location?.current( - accuracy: MakoloLocationAccuracy.low, - ); - DiscoveryLocationContext.update(result?.fix); - } else { - await widget.permissions?.requestWhenNeeded(entry.key); + try { + if (entry.key == MakoloPermission.locationWhenInUse) { + final result = await widget.location?.current( + accuracy: MakoloLocationAccuracy.low, + ); + DiscoveryLocationContext.update(result?.fix); + } else { + await widget.permissions?.requestWhenNeeded(entry.key); + } + } on Object { + // A refused or unavailable native capability must not block entry. + if (entry.key == MakoloPermission.locationWhenInUse) { + DiscoveryLocationContext.update(null); + } } } } finally { From de7b72f9d310b7db4f3575e0ce649f31576df9bc Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 17:53:37 +0200 Subject: [PATCH 0021/1086] fix(mobile): enter guest home after onboarding --- mobile/lib/app/launch_gate.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/app/launch_gate.dart b/mobile/lib/app/launch_gate.dart index 38c3efabf..0e5cb2c36 100644 --- a/mobile/lib/app/launch_gate.dart +++ b/mobile/lib/app/launch_gate.dart @@ -104,7 +104,7 @@ class _LaunchGateState extends State { case OnboardingExit.skip: case OnboardingExit.continueGuest: if (!_priorityNavigation) { - widget.router.go('/discover'); + widget.router.go('/welcome'); } case OnboardingExit.continueAuthenticated: From 5414639fe7c15d165fac2ce469c1eaf7de8fe802 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 17:53:40 +0200 Subject: [PATCH 0022/1086] fix(mobile): label guest onboarding entry accurately --- mobile/lib/features/onboarding/onboarding_flow.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/features/onboarding/onboarding_flow.dart b/mobile/lib/features/onboarding/onboarding_flow.dart index 9f96cdf68..a4cc4c586 100644 --- a/mobile/lib/features/onboarding/onboarding_flow.dart +++ b/mobile/lib/features/onboarding/onboarding_flow.dart @@ -110,7 +110,7 @@ class _OnboardingFlowState extends State { if (_isLastPage) { primaryLabel = widget.isAuthenticated ? 'Ouvrir Makolo' - : 'Découvrir'; + : 'Entrer dans Makolo'; } return Scaffold( From 1bdd9c300c95169fd5cc29fff32abccdfa6f8d21 Mon Sep 17 00:00:00 2001 From: Gilbert243 Date: Wed, 14 Oct 2026 17:53:43 +0200 Subject: [PATCH 0023/1086] test(mobile): reflect guest onboarding entry label --- mobile/test/onboarding_flow_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile/test/onboarding_flow_test.dart b/mobile/test/onboarding_flow_test.dart index 21eb417af..ab8874812 100644 --- a/mobile/test/onboarding_flow_test.dart +++ b/mobile/test/onboarding_flow_test.dart @@ -33,11 +33,11 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Tout commence par une découverte.'), findsOneWidget); expect(find.text('3/3'), findsOneWidget); - expect(find.text('Découvrir'), findsOneWidget); + expect(find.text('Entrer dans Makolo'), findsOneWidget); expect(find.text('J’ai un compte'), findsOneWidget); expect(find.text('Créer un compte'), findsOneWidget); - await tester.tap(find.text('Découvrir')); + await tester.tap(find.text('Entrer dans Makolo')); await tester.pump(); expect(exit, OnboardingExit.continueGuest); From 7aad34d114ddfd697027ee07b13e51770ee8cb8b Mon Sep 17 00:00:00 2001 From: SomafGroup Date: Wed, 14 Oct 2026 17:58:23 +0200 Subject: [PATCH 0024/1086] feat(guest): refine public home and onboarding experience --- .../features/guest/public_home_screen.dart | 63 ++++++++++++++----- templates/core/public_home.html | 2 +- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/mobile/lib/features/guest/public_home_screen.dart b/mobile/lib/features/guest/public_home_screen.dart index a0eb1e0f3..66dc9649d 100644 --- a/mobile/lib/features/guest/public_home_screen.dart +++ b/mobile/lib/features/guest/public_home_screen.dart @@ -45,7 +45,12 @@ class _PublicHomeScreenState extends State { current: 0, child: ListView( key: const Key('guest-public-home'), - padding: EdgeInsets.fromLTRB(edge, MakoloSpacing.inner, edge, MakoloSpacing.lg), + padding: EdgeInsets.fromLTRB( + edge, + MakoloSpacing.inner, + edge, + MakoloSpacing.lg, + ), children: [ Text( 'DÉCOUVRIR · PRÉPARER · AVANCER', @@ -57,7 +62,7 @@ class _PublicHomeScreenState extends State { ), const SizedBox(height: MakoloSpacing.compact), Text( - 'Ce que vous voulez faire commence ici.', + 'Ca vous intéresserait', style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.w800, letterSpacing: -0.6, @@ -76,7 +81,9 @@ class _PublicHomeScreenState extends State { const SizedBox(height: MakoloSpacing.lg), Text( 'Rechercher des possibilités', - style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700), + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + ), ), const SizedBox(height: MakoloSpacing.sm), Semantics( @@ -86,9 +93,10 @@ class _PublicHomeScreenState extends State { controller: _search, textInputAction: TextInputAction.search, onSubmitted: (_) => _explore(), - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), decoration: InputDecoration( - hintText: 'Voyager, participer, être accompagné…', + hintText: 'Trouver…', hintMaxLines: 2, prefixIcon: const Icon(Icons.search_rounded), suffixIcon: IconButton( @@ -130,17 +138,20 @@ class _PublicHomeScreenState extends State { _ExploreChoice( label: 'Voyager', icon: Icons.directions_bus_outlined, - onPressed: () => _explore(vertical: 'transport', includeQuery: false), + onPressed: () => + _explore(vertical: 'transport', includeQuery: false), ), _ExploreChoice( label: 'Participer', icon: Icons.event_outlined, - onPressed: () => _explore(vertical: 'event', includeQuery: false), + onPressed: () => + _explore(vertical: 'event', includeQuery: false), ), _ExploreChoice( label: 'Être accompagné', icon: Icons.handshake_outlined, - onPressed: () => _explore(vertical: 'service', includeQuery: false), + onPressed: () => + _explore(vertical: 'service', includeQuery: false), ), _ExploreChoice( label: 'Tout explorer', @@ -173,11 +184,17 @@ class _PublicHomeScreenState extends State { const SizedBox(height: MakoloSpacing.md), Row( children: [ - const Expanded(child: _JourneyStep(number: '01', label: 'Découvrez')), + const Expanded( + child: _JourneyStep(number: '01', label: 'Découvrez'), + ), Icon(Icons.chevron_right_rounded, size: 18, color: muted), - const Expanded(child: _JourneyStep(number: '02', label: 'Préparez')), + const Expanded( + child: _JourneyStep(number: '02', label: 'Préparez'), + ), Icon(Icons.chevron_right_rounded, size: 18, color: muted), - const Expanded(child: _JourneyStep(number: '03', label: 'Avancez')), + const Expanded( + child: _JourneyStep(number: '03', label: 'Avancez'), + ), ], ), ], @@ -199,7 +216,11 @@ class _PublicHomeScreenState extends State { ), child: Row( children: [ - Icon(Icons.groups_outlined, color: colors.primary, size: 26), + Icon( + Icons.groups_outlined, + color: colors.primary, + size: 26, + ), const SizedBox(width: MakoloSpacing.compact), Expanded( child: Column( @@ -207,18 +228,26 @@ class _PublicHomeScreenState extends State { children: [ Text( 'Les Espaces sur Makolo', - style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w800), + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w800, + ), ), const SizedBox(height: MakoloSpacing.xs), Text( 'Organisations, équipes et communautés', - style: theme.textTheme.bodySmall?.copyWith(color: muted), + style: theme.textTheme.bodySmall?.copyWith( + color: muted, + ), ), ], ), ), const SizedBox(width: MakoloSpacing.xs), - Icon(Icons.arrow_forward_ios_rounded, size: 16, color: muted), + Icon( + Icons.arrow_forward_ios_rounded, + size: 16, + color: muted, + ), ], ), ), @@ -302,7 +331,9 @@ class _JourneyStep extends StatelessWidget { Text( label, textAlign: TextAlign.center, - style: theme.textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w700), + style: theme.textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.w700, + ), ), ], ); diff --git a/templates/core/public_home.html b/templates/core/public_home.html index 52525e678..794f200bf 100644 --- a/templates/core/public_home.html +++ b/templates/core/public_home.html @@ -5,7 +5,7 @@
Découvrir · Préparer · Avancer -

Ce que vous voulez faire commence ici.

+

Ca vous intéresserait

Trouvez des activités, des services, des occasions et des personnes ou organisations qui peuvent vous aider à avancer.