Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@

**Compatibility:** Apps built with `flet build` and the macOS desktop client now require macOS 12 or later, and iOS apps require iOS 15 or later. Custom build templates keep building, because Flutter 3.47 raises the deployment targets during the build; apply the same `Podfile` and Xcode project changes to skip that step. See the [macOS 12 and iOS 15 minimum](/docs/updates/breaking-changes/v1-1-0/minimum-macos-12-ios-15) guide.

* Fix Flet web apps using the `hash` route URL strategy returning `index.html` with a `200 OK` for unknown paths instead of a `404` ([#2796](https://github.com/flet-dev/flet/issues/2796), [#6954](https://github.com/flet-dev/flet/pull/6954)) by @ndonkoHenri.
* Fix `flet.fastapi.app()` and `ft.run(..., export_asgi_app=True)` failing every request with a `500` error when `route_url_strategy` or `web_renderer` is passed as a string, such as `"hash"` ([#6954](https://github.com/flet-dev/flet/pull/6954)) by @ndonkoHenri.
* Fix `page.login(redirect_to_page=True)` not returning to the page's route with the `hash` route URL strategy or under a `proxy_path` ([#6954](https://github.com/flet-dev/flet/pull/6954)) by @ndonkoHenri.
* flet-audio: Fix `Audio` failing to play when it is created without `src` and `src` is set later ([#6133](https://github.com/flet-dev/flet/issues/6133), [#6956](https://github.com/flet-dev/flet/pull/6956)) by @ndonkoHenri.
* flet-audio: Fix `Audio` ignoring `playback_rate` after `src` changes on web ([#6956](https://github.com/flet-dev/flet/pull/6956)) by @ndonkoHenri.
* flet-audio-recorder: Fix `AudioRecorder.start_recording()` not recording to `output_path` on desktop, Android and iOS, where it returned `False`, failed, wrote no file, or recorded into the assets directory ([#5999](https://github.com/flet-dev/flet/issues/5999), [#6955](https://github.com/flet-dev/flet/pull/6955)) by @ndonkoHenri.
Expand Down
29 changes: 15 additions & 14 deletions sdk/python/packages/flet-web/src/flet_web/fastapi/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ def app(

fastapi_app = FastAPI()

static_files = FletStaticFiles(
proxy_path=proxy_path,
assets_dir=assets_dir,
app_name=app_name,
app_short_name=app_short_name,
app_description=app_description,
web_renderer=web_renderer,
route_url_strategy=route_url_strategy,
websocket_endpoint_path=websocket_endpoint,
no_cdn=no_cdn,
)

@fastapi_app.websocket(f"/{websocket_endpoint}")
async def app_handler(websocket: WebSocket):
"""
Expand All @@ -109,6 +121,8 @@ async def app_handler(websocket: WebSocket):
oauth_state_timeout_seconds=oauth_state_timeout_seconds,
upload_endpoint_path=upload_endpoint_path,
secret_key=secret_key,
route_url_strategy=static_files.route_url_strategy,
proxy_path=proxy_path,
).handle(websocket)

if upload_dir:
Expand All @@ -133,20 +147,7 @@ async def oauth_redirect_handler(request: Request):
"""
return await FletOAuth().handle(request)

fastapi_app.mount(
path="/",
app=FletStaticFiles(
proxy_path=proxy_path,
assets_dir=assets_dir,
app_name=app_name,
app_short_name=app_short_name,
app_description=app_description,
web_renderer=web_renderer,
route_url_strategy=route_url_strategy,
websocket_endpoint_path=websocket_endpoint,
no_cdn=no_cdn,
),
)
fastapi_app.mount(path="/", app=static_files)

# Add middleware for custom headers
class CustomHeadersMiddleware(BaseHTTPMiddleware):
Expand Down
12 changes: 12 additions & 0 deletions sdk/python/packages/flet-web/src/flet_web/fastapi/flet_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from flet.controls.base_control import BaseControl
from flet.controls.context import _context_page, context
from flet.controls.exceptions import FletPageDisconnectedException
from flet.controls.types import RouteUrlStrategy
from flet.messaging.connection import Connection
from flet.messaging.protocol import (
ControlEventBody,
Expand Down Expand Up @@ -59,6 +60,9 @@ class FletApp(Connection):
and redirecting to OAuth callback URL.
upload_endpoint_path: Absolute URL of upload endpoint, e.g. `/upload`.
secret_key: Secret key to sign upload requests.
route_url_strategy: Route URL strategy of the served web client.
proxy_path: URL prefix that a proxy in front of the app strips, as given
to `flet_web.fastapi.app()`; part of the page's base path.
"""

def __init__(
Expand All @@ -71,8 +75,12 @@ def __init__(
oauth_state_timeout_seconds: int = DEFAULT_FLET_OAUTH_STATE_TIMEOUT,
upload_endpoint_path: Optional[str] = None,
secret_key: Optional[str] = None,
route_url_strategy: RouteUrlStrategy = RouteUrlStrategy.PATH,
proxy_path: Optional[str] = None,
):
super().__init__()
self.route_url_strategy = route_url_strategy
self.__proxy_path = proxy_path
self.__id = random_string(8)
logger.info(f"New FletApp: {self.__id}")

Expand Down Expand Up @@ -124,6 +132,10 @@ async def handle(self, websocket: WebSocket):
self.pubsubhub = app_manager.get_pubsubhub(self.__main, loop=self.loop)
self.page_url = str(websocket.url).rsplit("/", 1)[0]
self.page_name = websocket.url.path.rsplit("/", 1)[0].lstrip("/")
mount_path = (
(self.__proxy_path or "") + websocket.scope.get("root_path", "")
).strip("/")
self.page_base_path = f"/{mount_path}/" if mount_path else "/"

if not self.__upload_endpoint_path:
self.__upload_endpoint_path = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ class FletStaticFiles(StaticFiles):
app_name: PWA application name.
app_short_name: PWA application short name.
app_description: PWA application description.
web_renderer: Type of web renderer.
route_url_strategy: Routing URL strategy.
web_renderer: Type of web renderer, as a `WebRenderer` or its value, e.g.
`"canvaskit"`.
route_url_strategy: Routing URL strategy, as a `RouteUrlStrategy` or its
value, e.g. `"hash"`.
no_cdn: Whether not load CanvasKit, Pyodide, and fonts from CDN.
websocket_endpoint_path: Path of Flet app WebSocket handler. A path
without a leading `/` is resolved against the app mount path.
Expand All @@ -97,8 +99,8 @@ def __init__(
self.__app_name = app_name
self.__app_short_name = app_short_name
self.__app_description = app_description
self.__web_renderer = web_renderer
self.__route_url_strategy = route_url_strategy
self.__web_renderer = WebRenderer(web_renderer)
self.__route_url_strategy = RouteUrlStrategy(route_url_strategy)
Comment thread
ndonkoHenri marked this conversation as resolved.
self.__no_cdn = no_cdn
self.__websocket_endpoint_path = websocket_endpoint_path
self.__once = Once()
Expand All @@ -119,16 +121,31 @@ def __init__(
logger.info(f"Route URL strategy configured: {self.__route_url_strategy}")
logger.info(f"No CDN configured: {self.__no_cdn}")

@property
def route_url_strategy(self) -> RouteUrlStrategy:
"""
The route URL strategy written into the served `index.html`: the
`route_url_strategy` argument, or `FLET_WEB_ROUTE_URL_STRATEGY` if set.
"""
return self.__route_url_strategy

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
await self.__once.do(self.__config, scope["root_path"])
await super().__call__(scope, receive, send)

def lookup_path(self, path: str) -> tuple[str, Optional[os.stat_result]]:
"""Resolve a static file path, with SPA fallback for client-side routes.

Route-like paths (no extension, or `.html`) that don't match a file
fall back to `index.html` so the Flutter client can handle routing.
Asset-like paths (any other extension) resolve to a real `404`.
With the `path` route URL strategy, route-like paths (no extension, or
`.html`) that don't match a file fall back to `index.html` so the
Flutter client can handle routing. Asset-like paths (any other
extension) resolve to a real `404`.

With the `hash` strategy, routes live in the URL fragment and never
reach the server, so there is no fallback: any path that doesn't match
a file resolves to a real `404` (with a `404.html` from the assets
directory as its body, if there is one), while the mount root still
serves `index.html`.

Args:
path: Requested path, relative to the mounted static root
Expand All @@ -141,7 +158,10 @@ def lookup_path(self, path: str) -> tuple[str, Optional[os.stat_result]]:
logger.debug(f"StaticFiles.lookup_path: {self.__app_mount_path} {path}")
full_path, stat_result = super().lookup_path(path)

if stat_result is not None:
if (
stat_result is not None
or self.__route_url_strategy == RouteUrlStrategy.HASH
):
return full_path, stat_result

# Not found: SPA fallback only for route-like paths.
Expand Down
72 changes: 66 additions & 6 deletions sdk/python/packages/flet/src/flet/controls/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
DeviceOrientation,
Locale,
PagePlatform,
RouteUrlStrategy,
Wrapper,
)
from flet.utils import is_pyodide
Expand Down Expand Up @@ -1204,10 +1205,56 @@ async def login(
authorization: "Optional[type[AT]]" = None,
) -> "AT":
"""
Starts OAuth flow.
Signs the user in with an OAuth provider.

See [Authentication](https://flet.dev/docs/cookbook/authentication)
Without `saved_token`, this starts the OAuth authorization code flow: it
opens the provider's authorization page and returns without waiting for
the user to sign in. The provider then sends the browser to its
`redirect_url`, and Flet exchanges the authorization code for a token,
fetches the user if requested and calls [`on_login`][flet.Page.on_login].
Handling that redirect needs the app to be served by Flet's web server.

With `saved_token`, this restores an earlier sign-in instead, without
opening any page: it refreshes the token if it has expired and can be
refreshed, fetches the user if requested and calls `on_login`.

See the [Authentication](https://flet.dev/docs/cookbook/authentication)
guide for more information and examples.

Args:
provider: The OAuth provider to sign in with.
fetch_user: Whether to fetch the signed-in user's details, available
as the authorization's `user`.
fetch_groups: Whether to also fetch the user's groups into
`user.groups`. Groups are fetched only together with the user.
Of the built-in providers, only GitHub provides them; with the
others, `groups` is empty.
scope: Scopes to request in addition to the provider's default ones.
saved_token: A token from an earlier sign-in to restore, as returned
by [`OAuthToken.to_json()`][flet.auth.OAuthToken.to_json].
on_open_authorization_url: Async function that opens the
authorization URL it receives. By default, a web app opens it in
a popup window, and other apps open it with
[`UrlLauncher.launch_url()`][flet.UrlLauncher.launch_url].
complete_page_html: HTML of the page that the browser shows when the
provider redirects back, if `redirect_to_page` is `False`. By
default, the page tries to close itself and asks the user to
close it.
redirect_to_page: Whether to send the browser back to the app's
current page and route when the provider redirects back, instead
of showing `complete_page_html`. Use it in web apps that open the
authorization page in the same browser tab, for example with
`UrlLauncher().launch_url(url, web_only_window_name="_self")` in
`on_open_authorization_url`.
authorization: Class that performs the authorization. It is created
as `authorization(provider, fetch_user=..., fetch_groups=...,
scope=...)`. Defaults to
[`AuthorizationService`][flet.auth.AuthorizationService].

Returns:
The authorization, also available as [`auth`][flet.Page.auth].
Without `saved_token`, it has no token or user until `on_login`
is called.
"""
if authorization is None:
authorization = _default_authorization_impl()
Expand All @@ -1218,15 +1265,27 @@ async def login(
scope=scope,
)
if saved_token is None:
# The server keeps the state and completion options until the
# provider redirects back, then calls _authorize_callback().
authorization_url, state = self.__authorization.get_authorization_data()
auth_attrs = {"state": state}
if complete_page_html:
auth_attrs["completePageHtml"] = complete_page_html
if redirect_to_page:
up = urlparse(provider.redirect_url)
auth_attrs["completePageUrl"] = up._replace(
path=f"{self.session.connection.page_name}{self.route}"
).geturl()
# The current page on the redirect URL's host, with the route
# in the URL path or after `#`, as the client expects it.
connection = self.session.connection
base_path = connection.page_base_path
redirect_url = urlparse(provider.redirect_url)
if connection.route_url_strategy == RouteUrlStrategy.HASH:
complete_page_url = redirect_url._replace(
path=base_path, query="", fragment=self.route
)
else:
complete_page_url = redirect_url._replace(
path=base_path.rstrip("/") + self.route
)
auth_attrs["completePageUrl"] = complete_page_url.geturl()
self.session.connection.oauth_authorize(auth_attrs)
if on_open_authorization_url:
await on_open_authorization_url(authorization_url)
Expand All @@ -1238,6 +1297,7 @@ async def login(
else:
await UrlLauncher().launch_url(authorization_url)
else:
# Restore the saved sign-in; there is no redirect to wait for.
await self.__authorization.dehydrate_token(saved_token)

e = LoginEvent(name="login", control=self, error="", error_description="")
Expand Down
14 changes: 10 additions & 4 deletions sdk/python/packages/flet/src/flet/messaging/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from concurrent.futures import ThreadPoolExecutor
from typing import Any, Optional

from flet.controls.types import RouteUrlStrategy
from flet.messaging.protocol import Message
from flet.pubsub.pubsub_hub import PubSubHub

Expand All @@ -21,11 +22,16 @@ class Connection:
def __init__(self):
self.page_name: str = ""
self.page_url: Optional[str] = None
# True when the connected client runs on the same machine (or in the
# same process), so DataChannel bytes move at local-IPC / memcpy
# speed. Widgets may use this to prefer uncompressed payloads over
# encoded ones.
self.page_base_path: str = "/"
"""Base path of the page in the browser URL, e.g. `/sub-app/`, which is the
`<base href>` of the served web client."""
self.route_url_strategy: RouteUrlStrategy = RouteUrlStrategy.PATH
"""Whether the client keeps routes in the URL path (`/store`)
or after `#` (`/#/store`)."""
self.local_data_transport: bool = False
"""`True` when the connected client runs on the same machine (or in the
same process), so DataChannel bytes move at local-IPC / memcpy speed.
Widgets may use this to prefer uncompressed payloads over encoded ones."""
self.__pubsubhub = None
self.__loop: Optional[AbstractEventLoop] = None
self.__executor: Optional[ThreadPoolExecutor] = None
Expand Down
2 changes: 1 addition & 1 deletion website/docs/cookbook/navigation-and-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ import flet as ft
ft.run(main, route_url_strategy="hash")
```

For Flet server deployments, you can also set the [`FLET_ROUTE_URL_STRATEGY`](../reference/environment-variables.md#flet_web_route_url_strategy)
For Flet server deployments, you can also set the [`FLET_WEB_ROUTE_URL_STRATEGY`](../reference/environment-variables.md#flet_web_route_url_strategy)
environment variable.

## Declarative Router
Expand Down
Loading