Skip to content

Commit c2a1ad6

Browse files
ndonkoHenrixiaocai2011FeodorFitsner
authored
feat(flet-ads): ConsentManager for AdMob UMP consent (#6615)
* Add package version handling and runtime variable references Extended workflows to handle package versions by introducing `pkg_version`. Updated staging process to pass version details. Enhanced docs with runtime equivalents for specific environment variables, improving clarity and usability. * initial commit * add example * docs * Update runtime environment variable references in docs Refine `environment-variables.md` to align runtime equivalent references with updated API methods for improved accuracy and clarity. * remove timeout * update changelog * Add AdMob test app IDs to examples * update changelog * Download x64 arch of Android SDK on Windows ARM64 computers * 在android_sdk.py中令cmdline-tools-url检测到windows使用arm64架构时返回与x64相同的下载链接,利用转译层运行 * Add comment for ARM64 Windows device handling Added comment for ARM64 Windows devices in android_sdk.py. --------- Co-authored-by: xiaocai2011 <134185030+xiaocai2011@users.noreply.github.com> Co-authored-by: Feodor Fitsner <feodor@appveyor.com>
1 parent 6ca6b48 commit c2a1ad6

27 files changed

Lines changed: 716 additions & 4171 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* **In-process Python transport (`dart_bridge` FFI).** `package:flet` gains a third protocol transport alongside the UDS / TCP socket servers: it can run Flet's MsgPack protocol over an in-process `dart_bridge` byte channel via a `FletApp(channelBuilder: …)` seam (the `flet` package stays Python-independent — it doesn't depend on `serious_python` or know about `PythonBridge`; the embedder wires the channel). `serious_python` >= 3.0.0 uses this seam to embed the Python interpreter **in-process** instead of talking to it over a localhost socket, and the `flet build` template migrates from sockets to the FFI transport. On Android, where the OS may keep the process alive across a back-button quit and restart only the Dart VM, the transport rebinds to the new VM's `dart_bridge` ports on a session-restart signal (`libdart_bridge` >= 1.3.0) — the Python process and its in-memory state are preserved and the Flet session is rebuilt from `REGISTER_CLIENT` by @FeodorFitsner.
88
* Add `flet clean` command that deletes the `build` directory of a Flet app — the Flutter bootstrap project, cached artifacts, and generated output — in a single step ([#6233](https://github.com/flet-dev/flet/issues/6233)) by @ndonkoHenri.
99
* Add `compression_quality` to `FilePicker.pick_files()` for selecting the image compression quality used by supported platforms ([#6573](https://github.com/flet-dev/flet/pull/6573)) by @ndonkoHenri.
10+
* Add `ConsentManager` to `flet-ads` for gathering user consent (e.g. GDPR/EEA) via Google's User Messaging Platform (UMP) before requesting ads ([#6569](https://github.com/flet-dev/flet/issues/6569), [#6615](https://github.com/flet-dev/flet/pull/6615)) by @ndonkoHenri.
1011

1112
### Improvements
1213

sdk/python/examples/extensions/ads/banner_ad_and_interstitial_ad/pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,10 @@ platforms = ["ios", "android"]
2525
org = "dev.flet"
2626
company = "Flet"
2727
copyright = "Copyright (C) 2023-2026 by Flet"
28+
29+
# AdMob test app ID — replace with your own for production.
30+
[tool.flet.android.meta_data]
31+
"com.google.android.gms.ads.APPLICATION_ID" = "ca-app-pub-3940256099942544~3347511713"
32+
33+
[tool.flet.ios.info]
34+
GADApplicationIdentifier = "ca-app-pub-3940256099942544~3347511713"
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import flet as ft
2+
import flet_ads as fta
3+
4+
5+
def main(page: ft.Page):
6+
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
7+
8+
# mobile-only
9+
supported = not page.web and page.platform.is_mobile()
10+
if supported:
11+
page.services.append(consent_manager := fta.ConsentManager())
12+
13+
async def refresh_status():
14+
consent_status = await consent_manager.get_consent_status()
15+
privacy_status = await consent_manager.get_privacy_options_requirement_status()
16+
can_request = await consent_manager.can_request_ads()
17+
status.value = (
18+
f"Consent status: {consent_status.name}\n"
19+
f"Privacy options required: {privacy_status.name}\n"
20+
f"Can request ads: {can_request}"
21+
)
22+
# Only surface the privacy options entry point when we are required to.
23+
privacy_button.visible = (
24+
privacy_status == fta.PrivacyOptionsRequirementStatus.REQUIRED
25+
)
26+
page.update()
27+
28+
async def gather_consent(e: ft.Event[ft.OutlinedButton]):
29+
status.value = "Gathering consent…"
30+
status.update()
31+
try:
32+
# Remember to remove the debug settings in production.
33+
await consent_manager.request_consent_info_update(
34+
fta.ConsentRequestParameters(
35+
consent_debug_settings=fta.ConsentDebugSettings(
36+
debug_geography=fta.DebugGeography.EEA,
37+
# test_identifiers=["<HASHED_ID>"], # for physical devices only
38+
),
39+
)
40+
)
41+
# Loads and shows the consent form only if consent is required.
42+
await consent_manager.load_and_show_consent_form_if_required()
43+
await refresh_status()
44+
# At this point, if `can_request_ads()` is True, it is safe to start
45+
# loading ads (e.g. fta.BannerAd / fta.InterstitialAd).
46+
except Exception as ex:
47+
status.value = f"Error: {ex}"
48+
status.update()
49+
50+
async def show_privacy_options(e: ft.Event[ft.OutlinedButton]):
51+
try:
52+
await consent_manager.show_privacy_options_form()
53+
await refresh_status()
54+
except Exception as ex:
55+
status.value = f"Error: {ex}"
56+
page.update()
57+
58+
async def reset_consent(e: ft.Event[ft.OutlinedButton]):
59+
# For testing only: resets consent so you can replay the flow.
60+
try:
61+
await consent_manager.reset()
62+
status.value = 'Consent reset. Tap "Gather consent" to begin again.'
63+
privacy_button.visible = False
64+
except Exception as ex:
65+
status.value = f"Error: {ex}"
66+
page.update()
67+
68+
page.add(
69+
ft.SafeArea(
70+
content=ft.Column(
71+
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
72+
controls=[
73+
ft.Text(
74+
"Gather user consent (e.g. GDPR/EEA) with Google's User "
75+
"Messaging Platform before requesting ads.",
76+
text_align=ft.TextAlign.CENTER,
77+
),
78+
ft.OutlinedButton(
79+
content="Gather consent",
80+
on_click=gather_consent,
81+
disabled=not supported,
82+
),
83+
privacy_button := ft.OutlinedButton(
84+
content="Show privacy options form",
85+
visible=False,
86+
on_click=show_privacy_options,
87+
),
88+
ft.OutlinedButton(
89+
content="Reset consent (testing)",
90+
on_click=reset_consent,
91+
disabled=not supported,
92+
),
93+
ft.Divider(),
94+
status := ft.Text('Tap "Gather consent" to begin.'),
95+
],
96+
)
97+
),
98+
)
99+
100+
101+
if __name__ == "__main__":
102+
ft.run(main)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[project]
2+
name = "ads-consent"
3+
version = "1.0.0"
4+
description = "Gather user consent for ads with Google's User Messaging Platform (UMP)."
5+
requires-python = ">=3.10"
6+
keywords = ["ads", "consent", "ump", "gdpr", "admob", "mobile", "privacy"]
7+
authors = [{ name = "Flet team", email = "hello@flet.dev" }]
8+
dependencies = ["flet", "flet-ads"]
9+
10+
[dependency-groups]
11+
dev = ["flet-cli", "flet-desktop", "flet-web"]
12+
13+
[tool.flet.gallery]
14+
categories = ["Marketing/Ads"]
15+
16+
[tool.flet.metadata]
17+
title = "AdMob Consent (UMP)"
18+
controls = ["SafeArea", "Column", "OutlinedButton", "Text", "Divider", "ConsentManager"]
19+
layout_pattern = "inline-actions"
20+
complexity = "basic"
21+
features = ["consent gathering", "consent form", "privacy options form", "consent status"]
22+
23+
[tool.flet]
24+
platforms = ["ios", "android"]
25+
org = "dev.flet"
26+
company = "Flet"
27+
copyright = "Copyright (C) 2023-2026 by Flet"
28+
29+
# AdMob test app ID — replace with your own for production.
30+
[tool.flet.android.meta_data]
31+
"com.google.android.gms.ads.APPLICATION_ID" = "ca-app-pub-3940256099942544~3347511713"
32+
33+
[tool.flet.ios.info]
34+
GADApplicationIdentifier = "ca-app-pub-3940256099942544~3347511713"

sdk/python/packages/flet-ads/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## 0.86.0
9+
10+
### Added
11+
12+
- New `ConsentManager` service for gathering and managing user consent for ads (e.g. GDPR/EEA) via Google's User Messaging Platform (UMP): request consent updates, present the consent and privacy options forms, and read the consent status before requesting ads ([#6569](https://github.com/flet-dev/flet/issues/6569), [#6615](https://github.com/flet-dev/flet/pull/6615)) by @ndonkoHenri.
13+
814
## 0.82.0
915

1016
### Changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
from flet_ads.banner_ad import BannerAd
22
from flet_ads.base_ad import BaseAd
3+
from flet_ads.consent_manager import ConsentManager
34
from flet_ads.interstitial_ad import InterstitialAd
45
from flet_ads.types import (
56
AdRequest,
7+
ConsentDebugSettings,
8+
ConsentRequestParameters,
9+
ConsentStatus,
10+
DebugGeography,
611
PaidAdEvent,
712
PrecisionType,
13+
PrivacyOptionsRequirementStatus,
814
)
915

1016
__all__ = [
1117
"AdRequest",
1218
"BannerAd",
1319
"BaseAd",
20+
"ConsentDebugSettings",
21+
"ConsentManager",
22+
"ConsentRequestParameters",
23+
"ConsentStatus",
24+
"DebugGeography",
1425
"InterstitialAd",
1526
"PaidAdEvent",
1627
"PrecisionType",
28+
"PrivacyOptionsRequirementStatus",
1729
]

sdk/python/packages/flet-ads/src/flet_ads/base_ad.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ class BaseAd(ft.BaseControl):
6666
Called when this ad is clicked.
6767
"""
6868

69-
def before_update(self):
69+
def init(self):
70+
super().init()
7071
if self.page.web or not self.page.platform.is_mobile():
7172
raise ft.FletUnsupportedPlatformException(
7273
f"{self.__class__.__name__} is only supported on "
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
from typing import Optional
2+
3+
import flet as ft
4+
from flet_ads.types import (
5+
ConsentRequestParameters,
6+
ConsentStatus,
7+
PrivacyOptionsRequirementStatus,
8+
)
9+
10+
__all__ = ["ConsentManager"]
11+
12+
13+
@ft.control("ConsentManager")
14+
class ConsentManager(ft.Service):
15+
"""
16+
Gathers and manages user consent for ads using
17+
Google's User Messaging Platform (UMP).
18+
19+
Use this service to request consent information, present the consent form
20+
(for example, the GDPR/EEA consent dialog) and the privacy options form,
21+
and query the consent status before requesting ads.
22+
23+
A typical flow looks like this:
24+
25+
1. :meth:`request_consent_info_update` (at every app launch);
26+
2. :meth:`load_and_show_consent_form_if_required` (shows the form only if needed);
27+
3. :meth:`can_request_ads` (to decide whether to start loading ads).
28+
29+
More info in the [AdMob documentation](https://developers.google.com/admob/flutter/privacy).
30+
31+
Raises:
32+
FletUnsupportedPlatformException: When any of its methods are called on
33+
a web and/or non-mobile platform.
34+
""" # noqa: E501
35+
36+
def init(self):
37+
super().init()
38+
if self.page.web or not self.page.platform.is_mobile():
39+
raise ft.FletUnsupportedPlatformException(
40+
f"{self.__class__.__name__} is only supported on "
41+
f"Mobile (Android and iOS)"
42+
)
43+
44+
async def request_consent_info_update(
45+
self, params: Optional[ConsentRequestParameters] = None
46+
):
47+
"""
48+
Requests an update of the user's consent information.
49+
50+
This should be called (and awaited) at every app launch, before
51+
loading/showing a consent form or reading the consent status.
52+
53+
Args:
54+
params: Parameters such as debug settings (useful for
55+
testing the form during development) or the under-age tag.
56+
If `None`, default parameters are used.
57+
"""
58+
await self._invoke_method("request_consent_info_update", {"params": params})
59+
60+
async def is_consent_form_available(self) -> bool:
61+
"""
62+
Checks whether a consent form is available to be loaded and shown.
63+
64+
Note:
65+
:meth:`request_consent_info_update` should be awaited before calling
66+
this method.
67+
68+
Returns:
69+
`True` if a consent form is available, `False` otherwise.
70+
"""
71+
return await self._invoke_method("is_consent_form_available")
72+
73+
async def get_consent_status(self) -> ConsentStatus:
74+
"""
75+
Get the user’s consent status.
76+
77+
This value is cached between app sessions and can be read before
78+
requesting an update of the consent information.
79+
80+
Returns:
81+
The user's current consent status.
82+
"""
83+
return ConsentStatus(await self._invoke_method("get_consent_status"))
84+
85+
async def can_request_ads(self) -> bool:
86+
"""
87+
Indicates whether the app has gathered enough consent to request ads.
88+
89+
Returns:
90+
`True` if ads can be requested, `False` otherwise.
91+
"""
92+
return bool(await self._invoke_method("can_request_ads"))
93+
94+
async def get_privacy_options_requirement_status(
95+
self,
96+
) -> PrivacyOptionsRequirementStatus:
97+
"""
98+
Gets the requirement status for showing a privacy options entry point.
99+
100+
Returns:
101+
Whether a privacy options entry point (for example, a button
102+
that calls :meth:`show_privacy_options_form`) is required.
103+
"""
104+
return PrivacyOptionsRequirementStatus(
105+
await self._invoke_method("get_privacy_options_requirement_status")
106+
)
107+
108+
async def load_and_show_consent_form_if_required(self):
109+
"""
110+
Loads a consent form and immediately shows it if consent is required.
111+
112+
If consent is not required, this method completes without showing
113+
anything. This is the simplest way to gather consent: call
114+
:meth:`request_consent_info_update` first, then this method.
115+
"""
116+
await self._invoke_method("load_and_show_consent_form_if_required")
117+
118+
async def show_privacy_options_form(self):
119+
"""
120+
Presents the privacy options form to the user.
121+
122+
This lets users change or withdraw their consent after the initial
123+
choice. Only present it when :meth:`get_privacy_options_requirement_status`
124+
returns :attr:`flet_ads.PrivacyOptionsRequirementStatus.REQUIRED`.
125+
"""
126+
await self._invoke_method("show_privacy_options_form")
127+
128+
async def reset(self):
129+
"""
130+
Resets the consent state.
131+
132+
Warning:
133+
This is intended only for testing, allowing you to simulate a first-time
134+
user. It should not be used in production.
135+
"""
136+
await self._invoke_method("reset")

sdk/python/packages/flet-ads/src/flet_ads/native_ad.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class NativeAd(BannerAd):
2727
ValueError: When neither :attr:`factory_id` nor `template_style` is set.
2828
"""
2929

30-
def before_update(self):
31-
super().before_update()
30+
def init(self):
31+
super().init()
3232
if self.factory_id is None and self.template_style is None:
3333
raise ValueError("factory_id or template_style must be set")

0 commit comments

Comments
 (0)