From 0cb9653d813079d447bc9c8844e63ba65722c6f7 Mon Sep 17 00:00:00 2001 From: Marc-Andrieu Date: Fri, 6 Mar 2026 21:38:40 +0100 Subject: [PATCH 01/10] Dependencies --- pubspec.lock | 10 +++++----- pubspec.yaml | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 06f217b15..4b22c6b95 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1438,18 +1438,18 @@ packages: dependency: "direct main" description: name: universal_html - sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" + sha256: c0bcae5c733c60f26c7dfc88b10b0fd27cbcc45cb7492311cdaa6067e21c9cd4 url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.0" universal_io: dependency: transitive description: name: universal_io - sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.1" universal_platform: dependency: transitive description: @@ -1579,7 +1579,7 @@ packages: source: hosted version: "1.1.1" web: - dependency: transitive + dependency: "direct main" description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" diff --git a/pubspec.yaml b/pubspec.yaml index 30f5b39bc..e5fbaaa00 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,9 +70,10 @@ dependencies: timeago: ^3.7.0 timezone: ^0.10.0 tuple: ^2.0.0 - universal_html: ^2.0.8 + universal_html: ^2.3.0 url_launcher: ^6.2.5 uuid: ^4.5.1 + web: ^1.1.1 webview_flutter: ^4.10.0 yaml: ^3.1.3 From b1fd3c089cde04ac6c7409f806dc29e71172647b Mon Sep 17 00:00:00 2001 From: Marc-Andrieu Date: Fri, 6 Mar 2026 21:40:26 +0100 Subject: [PATCH 02/10] Headers compatible with WASM-multithreading + no cross-origin isolation --- nginx.conf | 6 ++++++ web_dev_config.yaml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/nginx.conf b/nginx.conf index a3330e786..cbd367e36 100644 --- a/nginx.conf +++ b/nginx.conf @@ -59,9 +59,15 @@ http { } location / { + # Disable caching to ensure the client gets the latest version add_header Cache-Control 'no-store'; add_header Cache-Control 'no-cache'; add_header Cache-Control 'must-revalidate'; + + # WebAssembly + add_header Cross-Origin-Embedder-Policy 'credentialless'; + add_header Cross-Origin-Opener-Policy 'unsafe-none'; + expires 0; try_files $uri $uri/ /index.html; } diff --git a/web_dev_config.yaml b/web_dev_config.yaml index 0e85c0fc6..bfc3dc82c 100644 --- a/web_dev_config.yaml +++ b/web_dev_config.yaml @@ -1,5 +1,11 @@ server: port: 3000 headers: + # Caching - name: Cache-Control value: no-store, no-cache, must-revalidate + # WebAssembly + - name: Cross-Origin-Embedder-Policy + value: credentialless + - name: Cross-Origin-Opener-Policy + value: unsafe-none From 0dddc431ede5c28c0d3d4170798b6008f5b4bf31 Mon Sep 17 00:00:00 2001 From: Marc-Andrieu Date: Fri, 6 Mar 2026 23:42:49 +0100 Subject: [PATCH 03/10] It works, at last (but tests shall fail) --- lib/auth/providers/openid_provider.dart | 14 ++++++------ .../ui/pages/fund_page/confirm_button.dart | 22 +++++++++---------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/auth/providers/openid_provider.dart b/lib/auth/providers/openid_provider.dart index 1e7c40bc7..987f33462 100644 --- a/lib/auth/providers/openid_provider.dart +++ b/lib/auth/providers/openid_provider.dart @@ -13,7 +13,8 @@ import 'package:titan/tools/cache/cache_manager.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/repository/repository.dart'; import 'dart:convert'; -import 'package:universal_html/html.dart' as html; +import 'package:web/web.dart' as web; +import 'dart:js_interop'; final authTokenProvider = StateNotifierProvider>>( @@ -140,7 +141,6 @@ class OpenIdTokenProvider } Future getTokenFromRequest() async { - html.WindowBase? popupWin; final codeVerifier = generateRandomString(128); final authUrl = @@ -149,7 +149,7 @@ class OpenIdTokenProvider state = const AsyncValue.loading(); try { if (kIsWeb) { - popupWin = html.window.open( + web.Window? popupWin = web.window.open( authUrl, "Hyperion", "width=800, height=900, scrollbars=yes", @@ -168,6 +168,7 @@ class OpenIdTokenProvider } checkWindowClosed(); + completer.future.then((_) { state.maybeWhen( loading: () { @@ -210,10 +211,9 @@ class OpenIdTokenProvider } } - html.window.onMessage.listen((event) { - if (event.data.toString().contains('code=')) { - login(event.data); - } + web.window.onMessage.listen((event) { + final data = (event.data as JSString).toDart; + if (data.contains('code=')) login(data); }); } else { AuthorizationTokenResponse resp = await appAuth diff --git a/lib/mypayment/ui/pages/fund_page/confirm_button.dart b/lib/mypayment/ui/pages/fund_page/confirm_button.dart index 946b7fb5a..4cde4b55a 100644 --- a/lib/mypayment/ui/pages/fund_page/confirm_button.dart +++ b/lib/mypayment/ui/pages/fund_page/confirm_button.dart @@ -12,7 +12,8 @@ import 'package:titan/mypayment/providers/my_wallet_provider.dart'; import 'package:titan/mypayment/providers/tos_provider.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:universal_html/html.dart' as html; +import 'package:web/web.dart' as web; +import 'dart:js_interop'; import 'package:url_launcher/url_launcher.dart'; class ConfirmFundButton extends ConsumerWidget { @@ -60,13 +61,11 @@ class ConfirmFundButton extends ConsumerWidget { } void helloAssoCallback(String fundingUrl) async { - html.WindowBase? popupWin = - html.window.open( - fundingUrl, - "HelloAsso", - "width=800, height=900, scrollbars=yes", - ) - as html.WindowBase?; + web.Window? popupWin = web.window.open( + fundingUrl, + "HelloAsso", + "width=800, height=900, scrollbars=yes", + ); if (popupWin == null) { displayToastWithContext(TypeMsg.error, "Veuillez autoriser les popups"); @@ -99,10 +98,9 @@ class ConfirmFundButton extends ConsumerWidget { Navigator.pop(context, code); } - html.window.onMessage.listen((event) { - if (event.data.toString().contains('code=')) { - login(event.data); - } + web.window.onMessage.listen((event) { + final data = (event.data as JSString).toDart; + if (data.contains('code=')) login(data); }); } From f26747dd3fdb42c1e28cc230a42563948f356228 Mon Sep 17 00:00:00 2001 From: Marc-Andrieu Date: Sat, 7 Mar 2026 14:34:05 +0100 Subject: [PATCH 04/10] Nginx fixes --- Dockerfile | 2 +- nginx.conf | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index cbb7592e1..eeec49a47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM nginx:1.28.0-alpine3.21-slim +FROM nginx:1.28.2-alpine3.23-slim COPY nginx.conf /etc/nginx/nginx.conf COPY ./build/web/ /app/html RUN find /app/html/ -type f -size +512c -regex '.*\.\(html\|css\|js\|json\|svg\|ttf\|otf\|woff2\|wasm\|mjs\|symbols\|yaml\|env\)' -exec gzip -k9 {} + \ No newline at end of file diff --git a/nginx.conf b/nginx.conf index cbd367e36..5fbcacbf3 100644 --- a/nginx.conf +++ b/nginx.conf @@ -13,6 +13,12 @@ http { server_tokens off; keepalive_timeout 65; + include mime.types; + types { + text/javascript mjs; + } + default_type application/octet-stream; + gzip on; gzip_disable "msie6"; gzip_vary on; @@ -50,14 +56,14 @@ http { # Path to the root of your installation root /app/html; - + # Redirect on 404 error error_page 404 @home; - + location @home { return 301 /; } - + location / { # Disable caching to ensure the client gets the latest version add_header Cache-Control 'no-store'; From a29574dc6766c1b38e5b2d7c1ff83a5105f6ee03 Mon Sep 17 00:00:00 2001 From: Marc-Andrieu Date: Fri, 13 Mar 2026 10:30:57 +0100 Subject: [PATCH 05/10] It works, we shall not touch this code anymore, it is a miracle of Nature --- lib/auth/providers/openid_provider.dart | 160 +++++++++++------- .../ui/pages/fund_page/confirm_button.dart | 79 +++++---- lib/tools/web-window-callback/else.dart | 12 ++ lib/tools/web-window-callback/html.dart | 35 ++++ lib/tools/web-window-callback/js_interop.dart | 36 ++++ .../web_window_with_callback.dart | 3 + 6 files changed, 231 insertions(+), 94 deletions(-) create mode 100644 lib/tools/web-window-callback/else.dart create mode 100644 lib/tools/web-window-callback/html.dart create mode 100644 lib/tools/web-window-callback/js_interop.dart create mode 100644 lib/tools/web-window-callback/web_window_with_callback.dart diff --git a/lib/auth/providers/openid_provider.dart b/lib/auth/providers/openid_provider.dart index 987f33462..ad727cb98 100644 --- a/lib/auth/providers/openid_provider.dart +++ b/lib/auth/providers/openid_provider.dart @@ -13,8 +13,7 @@ import 'package:titan/tools/cache/cache_manager.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/repository/repository.dart'; import 'dart:convert'; -import 'package:web/web.dart' as web; -import 'dart:js_interop'; +import 'package:titan/tools/web-window-callback/web_window_with_callback.dart'; final authTokenProvider = StateNotifierProvider>>( @@ -149,72 +148,113 @@ class OpenIdTokenProvider state = const AsyncValue.loading(); try { if (kIsWeb) { - web.Window? popupWin = web.window.open( + webWindowWithCallback( authUrl, "Hyperion", - "width=800, height=900, scrollbars=yes", + () { + state.maybeWhen( + loading: () { + state = AsyncValue.data({tokenKey: "", refreshTokenKey: ""}); + }, + orElse: () {}, + ); + }, + (String data) async { + final receivedUri = Uri.parse(data); + final token = receivedUri.queryParameters["code"]; + try { + if (token != null && token.isNotEmpty) { + final resp = await openIdRepository.getToken( + token, + clientId, + redirectURL.toString(), + codeVerifier, + "authorization_code", + ); + final accessToken = resp[tokenKey]!; + final refreshToken = resp[refreshTokenKey]!; + await _secureStorage.write(key: tokenName, value: refreshToken); + state = AsyncValue.data({ + tokenKey: accessToken, + refreshTokenKey: refreshToken, + }); + } else { + throw Exception('Wrong credentials'); + } + } on TimeoutException catch (_) { + throw Exception('No response from server'); + } catch (e) { + rethrow; + } + }, ); - final completer = Completer(); - void checkWindowClosed() { - if (popupWin != null && popupWin!.closed == true) { - completer.complete(); - } else { - Future.delayed( - const Duration(milliseconds: 100), - checkWindowClosed, - ); - } - } + // web.Window? popupWin = web.window.open( + // authUrl, + // "Hyperion", + // "width=800, height=900, scrollbars=yes", + // ); - checkWindowClosed(); + // final completer = Completer(); + // void checkWindowClosed() { + // if (popupWin != null && popupWin!.closed == true) { + // completer.complete(); + // } else { + // Future.delayed( + // const Duration(milliseconds: 100), + // checkWindowClosed, + // ); + // } + // } - completer.future.then((_) { - state.maybeWhen( - loading: () { - state = AsyncValue.data({tokenKey: "", refreshTokenKey: ""}); - }, - orElse: () {}, - ); - }); + // checkWindowClosed(); - void login(String data) async { - final receivedUri = Uri.parse(data); - final token = receivedUri.queryParameters["code"]; - if (popupWin != null) { - popupWin!.close(); - popupWin = null; - } - try { - if (token != null && token.isNotEmpty) { - final resp = await openIdRepository.getToken( - token, - clientId, - redirectURL.toString(), - codeVerifier, - "authorization_code", - ); - final accessToken = resp[tokenKey]!; - final refreshToken = resp[refreshTokenKey]!; - await _secureStorage.write(key: tokenName, value: refreshToken); - state = AsyncValue.data({ - tokenKey: accessToken, - refreshTokenKey: refreshToken, - }); - } else { - throw Exception('Wrong credentials'); - } - } on TimeoutException catch (_) { - throw Exception('No response from server'); - } catch (e) { - rethrow; - } - } + // completer.future.then((_) { + // state.maybeWhen( + // loading: () { + // state = AsyncValue.data({tokenKey: "", refreshTokenKey: ""}); + // }, + // orElse: () {}, + // ); + // }); - web.window.onMessage.listen((event) { - final data = (event.data as JSString).toDart; - if (data.contains('code=')) login(data); - }); + // void login(String data) async { + // final receivedUri = Uri.parse(data); + // final token = receivedUri.queryParameters["code"]; + // if (popupWin != null) { + // popupWin!.close(); + // popupWin = null; + // } + // try { + // if (token != null && token.isNotEmpty) { + // final resp = await openIdRepository.getToken( + // token, + // clientId, + // redirectURL.toString(), + // codeVerifier, + // "authorization_code", + // ); + // final accessToken = resp[tokenKey]!; + // final refreshToken = resp[refreshTokenKey]!; + // await _secureStorage.write(key: tokenName, value: refreshToken); + // state = AsyncValue.data({ + // tokenKey: accessToken, + // refreshTokenKey: refreshToken, + // }); + // } else { + // throw Exception('Wrong credentials'); + // } + // } on TimeoutException catch (_) { + // throw Exception('No response from server'); + // } catch (e) { + // rethrow; + // } + // } + + // web.window.onMessage.listen((event) { + // final data = (event.data as JSString).toDart; + // if (data.contains('code=')) login(data); + // }); } else { AuthorizationTokenResponse resp = await appAuth .authorizeAndExchangeCode( diff --git a/lib/mypayment/ui/pages/fund_page/confirm_button.dart b/lib/mypayment/ui/pages/fund_page/confirm_button.dart index 4cde4b55a..d5e264078 100644 --- a/lib/mypayment/ui/pages/fund_page/confirm_button.dart +++ b/lib/mypayment/ui/pages/fund_page/confirm_button.dart @@ -12,9 +12,8 @@ import 'package:titan/mypayment/providers/my_wallet_provider.dart'; import 'package:titan/mypayment/providers/tos_provider.dart'; import 'package:titan/tools/functions.dart'; import 'package:titan/tools/ui/builders/waiting_button.dart'; -import 'package:web/web.dart' as web; -import 'dart:js_interop'; import 'package:url_launcher/url_launcher.dart'; +import 'package:titan/tools/web-window-callback/web_window_with_callback.dart'; class ConfirmFundButton extends ConsumerWidget { const ConfirmFundButton({super.key}); @@ -60,31 +59,10 @@ class ConfirmFundButton extends ConsumerWidget { } } - void helloAssoCallback(String fundingUrl) async { - web.Window? popupWin = web.window.open( - fundingUrl, - "HelloAsso", - "width=800, height=900, scrollbars=yes", - ); - - if (popupWin == null) { - displayToastWithContext(TypeMsg.error, "Veuillez autoriser les popups"); - return; - } - - final completer = Completer(); - void checkWindowClosed() { - if (popupWin.closed == true) { - completer.complete(); - } else { - Future.delayed(const Duration(milliseconds: 100), checkWindowClosed); - } - } - - checkWindowClosed(); - completer.future.then((_) {}); - - void login(String data) async { + void helloAssoCallback2(String fundingUrl) async { + webWindowWithCallback(fundingUrl, "HelloAsso", () {}, ( + String data, + ) async { final receivedUri = Uri.parse(data); final code = receivedUri.queryParameters["code"]; if (code == "succeeded") { @@ -94,16 +72,49 @@ class ConfirmFundButton extends ConsumerWidget { } else { displayToastWithContext(TypeMsg.error, "Paiement annulé"); } - popupWin.close(); Navigator.pop(context, code); - } - - web.window.onMessage.listen((event) { - final data = (event.data as JSString).toDart; - if (data.contains('code=')) login(data); }); } + // void helloAssoCallback(String fundingUrl) async { + // web.Window? popupWin = web.window.open( + // fundingUrl, + // "HelloAsso", + // "width=800, height=900, scrollbars=yes", + // ); + + // final completer = Completer(); + // void checkWindowClosed() { + // if (popupWin.closed == true) { + // completer.complete(); + // } else { + // Future.delayed(const Duration(milliseconds: 100), checkWindowClosed); + // } + // } + + // checkWindowClosed(); + // completer.future.then((_) {}); + + // void login(String data) async { + // final receivedUri = Uri.parse(data); + // final code = receivedUri.queryParameters["code"]; + // if (code == "succeeded") { + // displayToastWithContext(TypeMsg.msg, "Paiement effectué avec succès"); + // myWalletNotifier.getMyWallet(); + // myHistoryNotifier.getHistory(); + // } else { + // displayToastWithContext(TypeMsg.error, "Paiement annulé"); + // } + // popupWin.close(); + // Navigator.pop(context, code); + // } + + // web.window.onMessage.listen((event) { + // final data = (event.data as JSString).toDart; + // if (data.contains('code=')) login(data); + // }); + // } + return WaitingButton( onTap: () async { if (!minValidFundAmount) { @@ -133,7 +144,7 @@ class ConfirmFundButton extends ConsumerWidget { fundAmountNotifier.setFundAmount(""); Navigator.pop(context); if (kIsWeb) { - helloAssoCallback(fundingUrl.url); + helloAssoCallback2(fundingUrl.url); return; } tryLaunchUrl(fundingUrl.url); diff --git a/lib/tools/web-window-callback/else.dart b/lib/tools/web-window-callback/else.dart new file mode 100644 index 000000000..49451207c --- /dev/null +++ b/lib/tools/web-window-callback/else.dart @@ -0,0 +1,12 @@ +import 'dart:async'; + +void webWindowWithCallback( + String windowUrl, + String windowName, + void Function() completerFutureCallback, + Future Function(String) login, +) async { + throw UnsupportedError( + "`webWindowWithCallback()` is not implemented for the current platform", + ); +} diff --git a/lib/tools/web-window-callback/html.dart b/lib/tools/web-window-callback/html.dart new file mode 100644 index 000000000..9feadd84b --- /dev/null +++ b/lib/tools/web-window-callback/html.dart @@ -0,0 +1,35 @@ +import 'dart:async'; +import 'package:universal_html/universal_html.dart'; + +void webWindowWithCallback( + String windowUrl, + String windowName, + void Function() completerFutureCallback, + Future Function(String) login, +) async { + WindowBase popupWin = window.open( + windowUrl, + windowName, + "width=800, height=900, scrollbars=yes", + ); + + final completer = Completer(); + void checkWindowClosed() { + if (popupWin.closed == true) { + completer.complete(); + } else { + Future.delayed(const Duration(milliseconds: 100), checkWindowClosed); + } + } + + checkWindowClosed(); + completer.future.then((_) => completerFutureCallback()); + + window.onMessage.listen((event) { + final data = event.data.toString(); + if (data.contains('code=')) { + login(data); + popupWin.close(); + } + }); +} diff --git a/lib/tools/web-window-callback/js_interop.dart b/lib/tools/web-window-callback/js_interop.dart new file mode 100644 index 000000000..84f614b6e --- /dev/null +++ b/lib/tools/web-window-callback/js_interop.dart @@ -0,0 +1,36 @@ +import 'dart:async'; +import 'package:web/web.dart'; +import 'dart:js_interop'; + +void webWindowWithCallback( + String windowUrl, + String windowName, + void Function() completerFutureCallback, + Future Function(String) login, +) async { + Window? popupWin = window.open( + windowUrl, + windowName, + "width=800, height=900, scrollbars=yes", + ); + + final completer = Completer(); + void checkWindowClosed() { + if (popupWin!.closed == true) { + completer.complete(); + } else { + Future.delayed(const Duration(milliseconds: 100), checkWindowClosed); + } + } + + checkWindowClosed(); + completer.future.then((_) => completerFutureCallback()); + + window.onMessage.listen((event) { + final data = (event.data as JSString).toDart; + if (data.contains('code=')) { + login(data); + popupWin!.close(); + } + }); +} diff --git a/lib/tools/web-window-callback/web_window_with_callback.dart b/lib/tools/web-window-callback/web_window_with_callback.dart new file mode 100644 index 000000000..a1b608310 --- /dev/null +++ b/lib/tools/web-window-callback/web_window_with_callback.dart @@ -0,0 +1,3 @@ +export 'else.dart' + if (dart.library.js_interop) 'js_interop.dart' + if (dart.library.html) 'html.dart'; From 99283e1f8cb2e942c2c7b7a3e1846e18f109a92c Mon Sep 17 00:00:00 2001 From: Marc-Andrieu Date: Fri, 13 Mar 2026 10:44:09 +0100 Subject: [PATCH 06/10] Code quality: named params --- lib/auth/providers/openid_provider.dart | 4 +-- .../ui/pages/fund_page/confirm_button.dart | 34 +++++++++++-------- lib/tools/web-window-callback/else.dart | 14 ++++---- lib/tools/web-window-callback/html.dart | 10 +++--- lib/tools/web-window-callback/js_interop.dart | 10 +++--- 5 files changed, 38 insertions(+), 34 deletions(-) diff --git a/lib/auth/providers/openid_provider.dart b/lib/auth/providers/openid_provider.dart index ad727cb98..eff71ad17 100644 --- a/lib/auth/providers/openid_provider.dart +++ b/lib/auth/providers/openid_provider.dart @@ -151,7 +151,7 @@ class OpenIdTokenProvider webWindowWithCallback( authUrl, "Hyperion", - () { + completerFutureCallback: () { state.maybeWhen( loading: () { state = AsyncValue.data({tokenKey: "", refreshTokenKey: ""}); @@ -159,7 +159,7 @@ class OpenIdTokenProvider orElse: () {}, ); }, - (String data) async { + loginCallback: (String data) async { final receivedUri = Uri.parse(data); final token = receivedUri.queryParameters["code"]; try { diff --git a/lib/mypayment/ui/pages/fund_page/confirm_button.dart b/lib/mypayment/ui/pages/fund_page/confirm_button.dart index d5e264078..d0f23da76 100644 --- a/lib/mypayment/ui/pages/fund_page/confirm_button.dart +++ b/lib/mypayment/ui/pages/fund_page/confirm_button.dart @@ -60,20 +60,26 @@ class ConfirmFundButton extends ConsumerWidget { } void helloAssoCallback2(String fundingUrl) async { - webWindowWithCallback(fundingUrl, "HelloAsso", () {}, ( - String data, - ) async { - final receivedUri = Uri.parse(data); - final code = receivedUri.queryParameters["code"]; - if (code == "succeeded") { - displayToastWithContext(TypeMsg.msg, "Paiement effectué avec succès"); - myWalletNotifier.getMyWallet(); - myHistoryNotifier.getHistory(); - } else { - displayToastWithContext(TypeMsg.error, "Paiement annulé"); - } - Navigator.pop(context, code); - }); + webWindowWithCallback( + fundingUrl, + "HelloAsso", + completerFutureCallback: () {}, + loginCallback: (String data) async { + final receivedUri = Uri.parse(data); + final code = receivedUri.queryParameters["code"]; + if (code == "succeeded") { + displayToastWithContext( + TypeMsg.msg, + "Paiement effectué avec succès", + ); + myWalletNotifier.getMyWallet(); + myHistoryNotifier.getHistory(); + } else { + displayToastWithContext(TypeMsg.error, "Paiement annulé"); + } + Navigator.pop(context, code); + }, + ); } // void helloAssoCallback(String fundingUrl) async { diff --git a/lib/tools/web-window-callback/else.dart b/lib/tools/web-window-callback/else.dart index 49451207c..5ec73b31e 100644 --- a/lib/tools/web-window-callback/else.dart +++ b/lib/tools/web-window-callback/else.dart @@ -2,11 +2,9 @@ import 'dart:async'; void webWindowWithCallback( String windowUrl, - String windowName, - void Function() completerFutureCallback, - Future Function(String) login, -) async { - throw UnsupportedError( - "`webWindowWithCallback()` is not implemented for the current platform", - ); -} + String windowName, { + required void Function() completerFutureCallback, + required Future Function(String) loginCallback, +}) async => throw UnsupportedError( + "`webWindowWithCallback()` is not implemented for the current platform", +); diff --git a/lib/tools/web-window-callback/html.dart b/lib/tools/web-window-callback/html.dart index 9feadd84b..d45947283 100644 --- a/lib/tools/web-window-callback/html.dart +++ b/lib/tools/web-window-callback/html.dart @@ -3,10 +3,10 @@ import 'package:universal_html/universal_html.dart'; void webWindowWithCallback( String windowUrl, - String windowName, - void Function() completerFutureCallback, - Future Function(String) login, -) async { + String windowName, { + required void Function() completerFutureCallback, + required Future Function(String) loginCallback, +}) async { WindowBase popupWin = window.open( windowUrl, windowName, @@ -28,7 +28,7 @@ void webWindowWithCallback( window.onMessage.listen((event) { final data = event.data.toString(); if (data.contains('code=')) { - login(data); + loginCallback(data); popupWin.close(); } }); diff --git a/lib/tools/web-window-callback/js_interop.dart b/lib/tools/web-window-callback/js_interop.dart index 84f614b6e..0e6bdb915 100644 --- a/lib/tools/web-window-callback/js_interop.dart +++ b/lib/tools/web-window-callback/js_interop.dart @@ -4,10 +4,10 @@ import 'dart:js_interop'; void webWindowWithCallback( String windowUrl, - String windowName, - void Function() completerFutureCallback, - Future Function(String) login, -) async { + String windowName, { + required void Function() completerFutureCallback, + required Future Function(String) loginCallback, +}) async { Window? popupWin = window.open( windowUrl, windowName, @@ -29,7 +29,7 @@ void webWindowWithCallback( window.onMessage.listen((event) { final data = (event.data as JSString).toDart; if (data.contains('code=')) { - login(data); + loginCallback(data); popupWin!.close(); } }); From 2acb6ac20c498a041b57779f0d491fa82149d16a Mon Sep 17 00:00:00 2001 From: Marc-Andrieu Date: Fri, 13 Mar 2026 16:17:13 +0100 Subject: [PATCH 07/10] Fix: remove the old ways that I had commented --- lib/auth/providers/openid_provider.dart | 67 ------------------- .../ui/pages/fund_page/confirm_button.dart | 39 ----------- 2 files changed, 106 deletions(-) diff --git a/lib/auth/providers/openid_provider.dart b/lib/auth/providers/openid_provider.dart index eff71ad17..709c0fc4f 100644 --- a/lib/auth/providers/openid_provider.dart +++ b/lib/auth/providers/openid_provider.dart @@ -188,73 +188,6 @@ class OpenIdTokenProvider } }, ); - - // web.Window? popupWin = web.window.open( - // authUrl, - // "Hyperion", - // "width=800, height=900, scrollbars=yes", - // ); - - // final completer = Completer(); - // void checkWindowClosed() { - // if (popupWin != null && popupWin!.closed == true) { - // completer.complete(); - // } else { - // Future.delayed( - // const Duration(milliseconds: 100), - // checkWindowClosed, - // ); - // } - // } - - // checkWindowClosed(); - - // completer.future.then((_) { - // state.maybeWhen( - // loading: () { - // state = AsyncValue.data({tokenKey: "", refreshTokenKey: ""}); - // }, - // orElse: () {}, - // ); - // }); - - // void login(String data) async { - // final receivedUri = Uri.parse(data); - // final token = receivedUri.queryParameters["code"]; - // if (popupWin != null) { - // popupWin!.close(); - // popupWin = null; - // } - // try { - // if (token != null && token.isNotEmpty) { - // final resp = await openIdRepository.getToken( - // token, - // clientId, - // redirectURL.toString(), - // codeVerifier, - // "authorization_code", - // ); - // final accessToken = resp[tokenKey]!; - // final refreshToken = resp[refreshTokenKey]!; - // await _secureStorage.write(key: tokenName, value: refreshToken); - // state = AsyncValue.data({ - // tokenKey: accessToken, - // refreshTokenKey: refreshToken, - // }); - // } else { - // throw Exception('Wrong credentials'); - // } - // } on TimeoutException catch (_) { - // throw Exception('No response from server'); - // } catch (e) { - // rethrow; - // } - // } - - // web.window.onMessage.listen((event) { - // final data = (event.data as JSString).toDart; - // if (data.contains('code=')) login(data); - // }); } else { AuthorizationTokenResponse resp = await appAuth .authorizeAndExchangeCode( diff --git a/lib/mypayment/ui/pages/fund_page/confirm_button.dart b/lib/mypayment/ui/pages/fund_page/confirm_button.dart index d0f23da76..568f86464 100644 --- a/lib/mypayment/ui/pages/fund_page/confirm_button.dart +++ b/lib/mypayment/ui/pages/fund_page/confirm_button.dart @@ -82,45 +82,6 @@ class ConfirmFundButton extends ConsumerWidget { ); } - // void helloAssoCallback(String fundingUrl) async { - // web.Window? popupWin = web.window.open( - // fundingUrl, - // "HelloAsso", - // "width=800, height=900, scrollbars=yes", - // ); - - // final completer = Completer(); - // void checkWindowClosed() { - // if (popupWin.closed == true) { - // completer.complete(); - // } else { - // Future.delayed(const Duration(milliseconds: 100), checkWindowClosed); - // } - // } - - // checkWindowClosed(); - // completer.future.then((_) {}); - - // void login(String data) async { - // final receivedUri = Uri.parse(data); - // final code = receivedUri.queryParameters["code"]; - // if (code == "succeeded") { - // displayToastWithContext(TypeMsg.msg, "Paiement effectué avec succès"); - // myWalletNotifier.getMyWallet(); - // myHistoryNotifier.getHistory(); - // } else { - // displayToastWithContext(TypeMsg.error, "Paiement annulé"); - // } - // popupWin.close(); - // Navigator.pop(context, code); - // } - - // web.window.onMessage.listen((event) { - // final data = (event.data as JSString).toDart; - // if (data.contains('code=')) login(data); - // }); - // } - return WaitingButton( onTap: () async { if (!minValidFundAmount) { From 0a1c4ab5e3e1132eea7309e09573fac4c3f7033a Mon Sep 17 00:00:00 2001 From: Marc-Andrieu Date: Fri, 13 Mar 2026 16:19:05 +0100 Subject: [PATCH 08/10] Fix: otherwise the conversion from event.data to a string fails statistically (most events aren't "stringifiable") --- lib/tools/web-window-callback/js_interop.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/tools/web-window-callback/js_interop.dart b/lib/tools/web-window-callback/js_interop.dart index 0e6bdb915..3c5b0c267 100644 --- a/lib/tools/web-window-callback/js_interop.dart +++ b/lib/tools/web-window-callback/js_interop.dart @@ -16,7 +16,7 @@ void webWindowWithCallback( final completer = Completer(); void checkWindowClosed() { - if (popupWin!.closed == true) { + if (popupWin?.closed == true) { completer.complete(); } else { Future.delayed(const Duration(milliseconds: 100), checkWindowClosed); @@ -27,10 +27,12 @@ void webWindowWithCallback( completer.future.then((_) => completerFutureCallback()); window.onMessage.listen((event) { - final data = (event.data as JSString).toDart; - if (data.contains('code=')) { - loginCallback(data); - popupWin!.close(); - } + try { + final data = (event.data as JSString).toDart; + if (data.contains('code=')) { + loginCallback(data); + popupWin?.close(); + } + } catch (_) {} }); } From 919880f45eea1abfce857ff850beacc8aa4739fe Mon Sep 17 00:00:00 2001 From: Marc-Andrieu Date: Sat, 14 Mar 2026 10:15:17 +0100 Subject: [PATCH 09/10] Add the `--wasm` flag where it should be used --- .github/workflows/release-web.yml | 4 +-- .vscode/launch.json | 44 +++++++++++++++++++++++++++---- README.md | 8 ++++-- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index ab13299d2..6ee682ce9 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -103,11 +103,11 @@ jobs: - name: Build Alpha 🔧 if: needs.extract-version.outputs.isAlpha == 'true' - run: flutter build web --release --dart-define=flavor=alpha + run: flutter build web --release --dart-define=flavor=alpha --wasm - name: Build Prod 🔧 if: needs.extract-version.outputs.isAlpha == 'false' - run: flutter build web --release --dart-define=flavor=prod + run: flutter build web --release --dart-define=flavor=prod --wasm - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.vscode/launch.json b/.vscode/launch.json index c00853d44..50f3e5629 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,29 +5,63 @@ "version": "0.2.0", "configurations": [ { - "name": "Titan dev", + "name": "Titan dev (JS)", "request": "launch", "type": "dart", "args": ["--flavor", "dev", "--dart-define", "flavor=dev"] }, { - "name": "Titan dev (profile mode)", + "name": "Titan dev (JS, profile mode)", "request": "launch", "type": "dart", "flutterMode": "profile", "args": ["--flavor", "dev", "--dart-define", "flavor=dev"] }, { - "name": "Titan alpha", + "name": "Titan alpha (JS)", "request": "launch", "type": "dart", "args": ["--flavor", "alpha", "--dart-define", "flavor=alpha"] }, { - "name": "Titan prod", + "name": "Titan dev (WASM)", "request": "launch", "type": "dart", - "args": ["--flavor", "prod", "--dart-define", "flavor=prod"] + "args": [ + "--flavor", + "dev", + "--dart-define", + "flavor=dev", + "--wasm", + "--no-cross-origin-isolation" + ] + }, + { + "name": "Titan dev (WASM, profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile", + "args": [ + "--flavor", + "dev", + "--dart-define", + "flavor=dev", + "--wasm", + "--no-cross-origin-isolation" + ] + }, + { + "name": "Titan alpha (WASM)", + "request": "launch", + "type": "dart", + "args": [ + "--flavor", + "alpha", + "--dart-define", + "flavor=alpha", + "--wasm", + "--no-cross-origin-isolation" + ] } ] } diff --git a/README.md b/README.md index 438fc3ea2..2c0c1af8b 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ flutter run --flavor alpha More generally you can use: ```bash -flutter run --flavor [ -d ] +flutter run --flavor [ -d ] [ --wasm --no-cross-origin-isolation ] ``` - Where the flavor can be any of `dev`, `alpha`, or `prod` (whose policy is to only accept the prod client). @@ -196,6 +196,8 @@ flutter run --flavor alpha -d chrome flutter run --flavor dev -d web-server ``` +- The optional duo `--wasm --no-cross-origin-isolation` runs a version compiled to WebAssembly instead of Javascript. + ### Check the app is running @@ -280,9 +282,11 @@ flutter build {target} --flavor={flavor} Currently flavor are not supported for Flutter for web, you should use: ``` -flutter build web --dart-define=flavor={flavor} +flutter build web --dart-define=flavor={flavor} [--wasm] ``` +The optional `--wasm` flag enables Titan to be also compiled to WebAssembly (besides the Javascript compilation). + ### Notifications setup Notifications are handled using the Firebase Cloud Messaging API. On mobile platforms, a valid notification configuration is required to debug Titan. Notifications are disabled on web builds. From d138106b2b2757ab13792cf5776579199ce71871 Mon Sep 17 00:00:00 2001 From: Marc-Andrieu Date: Fri, 27 Mar 2026 00:13:21 +0100 Subject: [PATCH 10/10] Fix: helloAssoCallback name --- lib/mypayment/ui/pages/fund_page/confirm_button.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mypayment/ui/pages/fund_page/confirm_button.dart b/lib/mypayment/ui/pages/fund_page/confirm_button.dart index 568f86464..2cf6bc657 100644 --- a/lib/mypayment/ui/pages/fund_page/confirm_button.dart +++ b/lib/mypayment/ui/pages/fund_page/confirm_button.dart @@ -59,7 +59,7 @@ class ConfirmFundButton extends ConsumerWidget { } } - void helloAssoCallback2(String fundingUrl) async { + void helloAssoCallback(String fundingUrl) async { webWindowWithCallback( fundingUrl, "HelloAsso", @@ -111,7 +111,7 @@ class ConfirmFundButton extends ConsumerWidget { fundAmountNotifier.setFundAmount(""); Navigator.pop(context); if (kIsWeb) { - helloAssoCallback2(fundingUrl.url); + helloAssoCallback(fundingUrl.url); return; } tryLaunchUrl(fundingUrl.url);