From 396a6e07dbc52366b92249070652d966b44d475e Mon Sep 17 00:00:00 2001 From: Alex Hope-O'Connor Date: Tue, 5 May 2026 00:23:22 +1000 Subject: [PATCH] Improve portal WiFi handoff and bump WiFiManager to v2.0.19. Expose redirect details after connect-on-save so clients can land on the station IP before the AP closes, hide the mobile nav scrollbar, and document the updated API contract. --- README.md | 24 ++ lib/WiFiManager/include/WiFiManager.h | 26 +- lib/WiFiManager/include/templates/CSS.h | 28 +- .../include/templates/PortalAppJS.h | 240 ++++++++++++++---- lib/WiFiManager/src/WiFiManager.cpp | 54 +++- lib/WiFiManager/src/WiFiManagerHandlers.cpp | 43 +++- library.json | 2 +- test/test_wifimanager/test_main.cpp | 2 + test/test_wifimanager/test_main.h | 2 + .../tests/test_api_json_and_captive.cpp | 40 +++ 10 files changed, 380 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 4b7facd..25bb480 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ This fork currently includes the following architectural improvements: - **Capability-driven UI flags** in bootstrap/API payloads so features like info, update, erase, and action visibility can be controlled by backend state. - **Portal bootstrap contract v2**: nested `brand`, `context`, `pages`, `actions`, `layout`, `extraHomeCards`; Wi-Fi meta params include `kind` (`field` | `html`) for first-class custom HTML parameters. - **SPA-native feedback UX** using in-DOM dialog/toast behavior rather than page-based action flows. +- **Connect-on-save handoff**: when portal save triggers a station connect, `/api/wifi/connect-status` now reports a brief success state with `stationIp` and `redirectUrl` so the SPA can show the new address and redirect before the AP shuts down. - **Request-scoped shell rendering**: the root portal page is built for each `GET /` from `WM_ROOT_SHELL_TEMPLATE` using a fresh placeholder registry. Shell inputs are `%PAGE_TITLE%`, `%STYLES%`, `%BOOTSTRAP_JSON%`, `%PORTAL_APP_JS%`, and `%PORTAL_APPEND_JS%` — filled in `WiFiManagerHandlers` from WiFiManager state and embedded assets (not from a server-wide template registry). - **Customization via WiFiManager `portal*` APIs** (`portalSetBrandTitle`, `portalSetPageInfoVisible`, `portalSetLayoutParamsLocation`, `portalAddParameter`, asset hooks, etc.) and JSON under `/api/...`, not by exposing placeholder-registry mutation to consumers. - A clearer separation between: @@ -140,6 +141,29 @@ wm.portalAppendJs( ); ``` +## WiFi Connect Status API + +When `portalSetBehaviorConnectOnSave(true)` is enabled, saving WiFi credentials queues a station join and the SPA polls `GET /api/wifi/connect-status`. + +Response shape: + +```json +{ + "state": "waiting | success | failed", + "message": "human readable status", + "wifiStatus": "WL_CONNECTED", + "stationIp": "192.168.1.42", + "redirectUrl": "http://192.168.1.42/" +} +``` + +Notes: + +- `stationIp` and `redirectUrl` are present only after a successful station join. +- If the portal HTTP server is not on port `80`, `redirectUrl` includes the active port. +- On success, WiFiManager keeps the portal alive briefly so the client can read the success payload and navigate to the new device address before the captive AP is shut down. +- This improves the handoff on typical home networks, but it is not a universal guarantee: client captive-portal helpers, browser behavior, DHCP timing, and network isolation can still affect whether the redirect completes automatically. + ## Dependencies This fork depends on **DFTE** ([Device Framework Template Engine](https://github.com/alexhopeoconnor/DFTE)) and **ESP32Async/ESPAsyncWebServer**. diff --git a/lib/WiFiManager/include/WiFiManager.h b/lib/WiFiManager/include/WiFiManager.h index 3e2d8c9..f7f94c3 100644 --- a/lib/WiFiManager/include/WiFiManager.h +++ b/lib/WiFiManager/include/WiFiManager.h @@ -147,7 +147,7 @@ #define WM_STRING(x) WM_STRING2(x) // WiFiManager version -const char WM_VERSION_STR[] PROGMEM = "v2.0.18"; +const char WM_VERSION_STR[] PROGMEM = "v2.0.19"; // #include #ifdef ESP_IDF_VERSION @@ -965,7 +965,26 @@ protected: _scan.completionPending = true; _scan.completionResult = completionResult; } - void wmTestSetPortalActive(bool active) { configPortalActive = active; } + void wmTestSetPortalActive(bool active) { + configPortalActive = active; + // Keep the test helper aligned with real portal startup so timeout-based + // assertions are anchored to "now" instead of device boot. + if (active) { + _configPortalStart = millis(); + } + } + void wmTestSetPortalConnectSuccess(const String& message, const String& stationIp, uint8_t status = WL_CONNECTED) { + _cpConnectState = wm_cp_connect_state_t::success; + _cpConnectMessage = message; + _cpConnectStationIp = stationIp; + _cpConnectStatus = status; + } + void wmTestSetPortalConnectFailure(const String& message, uint8_t status = WL_CONNECT_FAILED) { + _cpConnectState = wm_cp_connect_state_t::failed; + _cpConnectMessage = message; + _cpConnectStationIp = ""; + _cpConnectStatus = status; + } void wmTestSetConnectPending(bool active); void wmTestSetScanLifecycleBlocked(bool blocked) { _scanLifecycleBlocked = blocked; } void wmTestSetScanGenerations(uint32_t generation, uint32_t runningGeneration, uint32_t completionGeneration) { @@ -987,6 +1006,7 @@ protected: delaying, starting, waiting, + success_waiting_close, success, failed }; @@ -1001,6 +1021,8 @@ protected: unsigned long _cpConnectStartedAt = 0; unsigned long _cpConnectDelayUntil = 0; unsigned long _cpConnectTimeoutMs = 0; + unsigned long _cpConnectCloseAt = 0; + String _cpConnectStationIp; WiFiManagerEventCallback _eventCallback = nullptr; //helpers (rendering methods moved to WiFiManagerHandlers) diff --git a/lib/WiFiManager/include/templates/CSS.h b/lib/WiFiManager/include/templates/CSS.h index 20e45ca..4dce478 100644 --- a/lib/WiFiManager/include/templates/CSS.h +++ b/lib/WiFiManager/include/templates/CSS.h @@ -57,15 +57,22 @@ const char CSS_STYLE[] PROGMEM = "