diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b9a408dfc2..a8a2f1abaf 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -437,6 +437,7 @@ jobs:
fi
apt-get update --allow-releaseinfo-change
apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libasound2-dev libunwind-dev
+ apt-get install -y pkg-config libsecret-1-0 libsecret-1-dev
apt-get install -y \
libmpv-dev mpv \
libgstreamer1.0-dev \
@@ -718,6 +719,7 @@ jobs:
flet-map
flet-permission-handler
flet-rive
+ flet-secure-storage
flet-video
flet-webview
)
@@ -835,6 +837,7 @@ jobs:
flet_map \
flet_permission_handler \
flet_rive \
+ flet_secure_storage \
flet_video \
flet_webview; do
uv publish dist/**/${pkg}-*
diff --git a/client/android/app/src/main/AndroidManifest.xml b/client/android/app/src/main/AndroidManifest.xml
index 360eed4270..3d99c9acb9 100644
--- a/client/android/app/src/main/AndroidManifest.xml
+++ b/client/android/app/src/main/AndroidManifest.xml
@@ -15,10 +15,12 @@
+
+
-
+
? args]) async {
flet_flashlight.Extension(),
flet_datatable2.Extension(),
flet_charts.Extension(),
+ flet_secure_storage.Extension(),
// --FAT_CLIENT_START--
// --RIVE_EXTENSION_START--
flet_rive.Extension(),
@@ -90,10 +93,8 @@ void main([List? args]) async {
assetsDir = args[2];
debugPrint("Args contain a path assets directory: $assetsDir}");
}
- } else if (!kDebugMode &&
- (Platform.isWindows || Platform.isMacOS || Platform.isLinux)) {
- throw Exception(
- 'In desktop mode Flet app URL must be provided as a first argument.');
+ } else if (!kDebugMode && (Platform.isWindows || Platform.isMacOS || Platform.isLinux)) {
+ throw Exception('In desktop mode Flet app URL must be provided as a first argument.');
}
}
diff --git a/client/pubspec.yaml b/client/pubspec.yaml
index 5de1042d4b..5bf6455299 100644
--- a/client/pubspec.yaml
+++ b/client/pubspec.yaml
@@ -52,6 +52,9 @@ dependencies:
flet_audio_recorder:
path: ../sdk/python/packages/flet-audio-recorder/src/flutter/flet_audio_recorder
+ flet_charts:
+ path: ../sdk/python/packages/flet-charts/src/flutter/flet_charts
+
flet_datatable2:
path: ../sdk/python/packages/flet-datatable2/src/flutter/flet_datatable2
@@ -70,12 +73,12 @@ dependencies:
flet_permission_handler:
path: ../sdk/python/packages/flet-permission-handler/src/flutter/flet_permission_handler
+ flet_secure_storage:
+ path: ../sdk/python/packages/flet-secure-storage/src/flutter/flet_secure_storage
+
flet_webview:
path: ../sdk/python/packages/flet-webview/src/flutter/flet_webview
- flet_charts:
- path: ../sdk/python/packages/flet-charts/src/flutter/flet_charts
-
cupertino_icons: ^1.0.6
wakelock_plus: ^1.4.0
package_info_plus: ^9.0.0
diff --git a/packages/flet/lib/src/models/control.dart b/packages/flet/lib/src/models/control.dart
index 35772172f0..6c707ba483 100644
--- a/packages/flet/lib/src/models/control.dart
+++ b/packages/flet/lib/src/models/control.dart
@@ -371,35 +371,39 @@ class Control extends ChangeNotifier {
var node = getPatchTarget(op[1]);
var index = op[2];
var value = op[3];
- if (node.obj is! List) {
- throw Exception("Add operation can be applied to lists only: $op");
- }
- node.obj
- .insert(index, _transformIfControl(value, node.control, backend));
- if (shouldNotify) {
- node.control.notify();
+ if (node.obj is Map) {
+ node.obj[index] = _transformIfControl(value, node.control, backend);
+ } else if (node.obj is List) {
+ node.obj.insert(index, _transformIfControl(value, node.control, backend));
+ } else {
+ throw Exception("Add operation can be applied to lists or maps: $op");
}
+ if (shouldNotify) node.control.notify();
} else if (opType == OperationType.remove) {
// REMOVE
var node = getPatchTarget(op[1]);
var index = op[2];
- if (node.obj is! List) {
- throw Exception("Remove operation can be applied to lists only: $op");
- }
- node.obj.removeAt(index);
- if (shouldNotify) {
- node.control.notify();
+ if (node.obj is List) {
+ node.obj.removeAt(index);
+ } else if (node.obj is Map) {
+ node.obj.remove(index);
+ } else {
+ throw Exception("Remove operation can be applied to lists or maps: $op");
}
+ if (shouldNotify) node.control.notify();
} else if (opType == OperationType.move) {
// MOVE
var fromNode = getPatchTarget(op[1]);
var fromIndex = op[2];
var toNode = getPatchTarget(op[3]);
var toIndex = op[4];
- if (fromNode.obj is! List || toNode.obj is! List) {
- throw Exception("Move operation can be applied to lists only: $op");
+ if (fromNode.obj is List && toNode.obj is List) {
+ toNode.obj.insert(toIndex, fromNode.obj.removeAt(fromIndex));
+ } else if (fromNode.obj is Map && toNode.obj is Map) {
+ toNode.obj[toIndex] = fromNode.obj.remove(fromIndex);
+ } else {
+ throw Exception("Move operation can only be applied to lists or maps: $op");
}
- toNode.obj.insert(toIndex, fromNode.obj.removeAt(fromIndex));
if (shouldNotify) {
if (fromNode.control.id != toNode.control.id) {
fromNode.control.notify();
diff --git a/sdk/python/examples/services/secure_storage/basic.py b/sdk/python/examples/services/secure_storage/basic.py
new file mode 100644
index 0000000000..6e65a34fba
--- /dev/null
+++ b/sdk/python/examples/services/secure_storage/basic.py
@@ -0,0 +1,94 @@
+import base64
+import os
+
+import flet as ft
+import flet_secure_storage as fss
+
+
+def main(page: ft.Page):
+ page.vertical_alignment = ft.MainAxisAlignment.CENTER
+ page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
+ storage = fss.SecureStorage(
+ web_options=fss.WebOptions(
+ db_name="customstorage",
+ public_key="publickey",
+ wrap_key=base64.urlsafe_b64encode(os.urandom(32)).decode(),
+ wrap_key_iv=base64.urlsafe_b64encode(os.urandom(16)).decode(),
+ ),
+ android_options=fss.AndroidOptions(
+ reset_on_error=True,
+ migrate_on_algorithm_change=True,
+ enforce_biometrics=True,
+ key_cipher_algorithm=fss.KeyCipherAlgorithm.AES_GCM_NO_PADDING,
+ storage_cipher_algorithm=fss.StorageCipherAlgorithm.AES_GCM_NO_PADDING,
+ ),
+ )
+
+ key = ft.TextField(label="Key", value="example_key")
+ value = ft.TextField(label="Value", value="secret_value")
+ result = ft.Text(theme_style=ft.TextThemeStyle.TITLE_LARGE)
+
+ async def set_value(e):
+ await storage.set(key.value, value.value)
+ result.value = "Value saved"
+ page.update()
+
+ async def get_value(e):
+ result.value = await storage.get(key.value)
+ page.update()
+
+ async def remove_value(e):
+ await storage.remove(key.value)
+ result.value = "Value removed"
+ page.update()
+
+ async def clear_storage(e):
+ await storage.clear()
+ result.value = "Storage cleared"
+ page.update()
+
+ async def contains_key(e):
+ exists = await storage.contains_key(key.value)
+ result.value = f"Key exists: {exists}"
+ page.update()
+
+ async def get_availability(e):
+ is_availability = await storage.get_availability()
+ page.show_dialog(
+ ft.SnackBar(
+ content=ft.Text(
+ value=f"Protected data available: {is_availability}"
+ if is_availability
+ else "Protected data available: None"
+ )
+ )
+ )
+ page.update()
+
+ page.add(
+ ft.Column(
+ alignment=ft.MainAxisAlignment.CENTER,
+ horizontal_alignment=ft.CrossAxisAlignment.CENTER,
+ spacing=10,
+ controls=[
+ result,
+ key,
+ value,
+ ft.Row(
+ width=300,
+ wrap=True,
+ controls=[
+ ft.Button("Set", on_click=set_value),
+ ft.Button("Get", on_click=get_value),
+ ft.Button("Contains key", on_click=contains_key),
+ ft.Button("Remove", on_click=remove_value),
+ ft.Button("Clear", on_click=clear_storage),
+ ft.Button("Check Data Availability", on_click=get_availability),
+ ],
+ ),
+ ],
+ ),
+ )
+
+
+ft.run(main)
diff --git a/sdk/python/packages/flet-secure-storage/LICENSE b/sdk/python/packages/flet-secure-storage/LICENSE
new file mode 100644
index 0000000000..261eeb9e9f
--- /dev/null
+++ b/sdk/python/packages/flet-secure-storage/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/sdk/python/packages/flet-secure-storage/pyproject.toml b/sdk/python/packages/flet-secure-storage/pyproject.toml
new file mode 100644
index 0000000000..96be7d71c7
--- /dev/null
+++ b/sdk/python/packages/flet-secure-storage/pyproject.toml
@@ -0,0 +1,23 @@
+[project]
+name = "flet-secure-storage"
+version = "0.1.0"
+description = "Secure Storage control for Flet"
+authors = [{name = "Appveyor Systems Inc.", email = "hello@flet.dev"}]
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+ "flet",
+]
+
+[project.urls]
+Homepage = "https://flet.dev"
+Documentation = "https://docs.flet.dev/secure-storage"
+Repository = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/flet-secure-storage"
+Issues = "https://github.com/flet-dev/flet/issues"
+
+[tool.setuptools.package-data]
+"flutter.flet_secure_storage" = ["**/*"]
+
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
diff --git a/sdk/python/packages/flet-secure-storage/src/flet_secure_storage/__init__.py b/sdk/python/packages/flet-secure-storage/src/flet_secure_storage/__init__.py
new file mode 100644
index 0000000000..33ce1e2720
--- /dev/null
+++ b/sdk/python/packages/flet-secure-storage/src/flet_secure_storage/__init__.py
@@ -0,0 +1,28 @@
+from flet_secure_storage.secure_storage import SecureStorage, SecureStorageEvent
+from flet_secure_storage.types import (
+ AccessControlFlag,
+ AndroidOptions,
+ AppleOptions,
+ IOSOptions,
+ KeychainAccessibility,
+ KeyCipherAlgorithm,
+ MacOsOptions,
+ StorageCipherAlgorithm,
+ WebOptions,
+ WindowsOptions,
+)
+
+__all__ = [
+ "AccessControlFlag",
+ "AndroidOptions",
+ "AppleOptions",
+ "IOSOptions",
+ "KeyCipherAlgorithm",
+ "KeychainAccessibility",
+ "MacOsOptions",
+ "SecureStorage",
+ "SecureStorageEvent",
+ "StorageCipherAlgorithm",
+ "WebOptions",
+ "WindowsOptions",
+]
diff --git a/sdk/python/packages/flet-secure-storage/src/flet_secure_storage/secure_storage.py b/sdk/python/packages/flet-secure-storage/src/flet_secure_storage/secure_storage.py
new file mode 100644
index 0000000000..6c28e70716
--- /dev/null
+++ b/sdk/python/packages/flet-secure-storage/src/flet_secure_storage/secure_storage.py
@@ -0,0 +1,288 @@
+from dataclasses import dataclass, field
+from typing import Any, Optional
+
+import flet as ft
+from flet.controls.base_control import control
+from flet.controls.services.service import Service
+from flet_secure_storage.types import (
+ AndroidOptions,
+ IOSOptions,
+ MacOsOptions,
+ WebOptions,
+ WindowsOptions,
+)
+
+
+@dataclass
+class SecureStorageEvent(ft.Event["SecureStorage"]):
+ """
+ The event fired by SecureStorage when availability changes.
+ """
+
+ available: Optional[bool]
+ """
+ The availability of secure storage. True if secure storage is available,
+ False if not, None if unknown.
+ """
+
+
+@control("SecureStorage")
+class SecureStorage(Service):
+ """
+ A class to manage secure storage in a Flet application across multiple platforms.
+ """
+
+ ios_options: IOSOptions = field(default_factory=lambda: IOSOptions())
+ """
+ iOS-specific configuration for secure storage.
+ """
+
+ android_options: AndroidOptions = field(default_factory=lambda: AndroidOptions())
+ """
+ Android-specific configuration for secure storage.
+ """
+
+ windows_options: WindowsOptions = field(default_factory=lambda: WindowsOptions())
+ """
+ Windows-specific configuration for secure storage.
+ """
+
+ macos_options: MacOsOptions = field(default_factory=lambda: MacOsOptions())
+ """
+ macOS-specific configuration for secure storage.
+ """
+
+ web_options: WebOptions = field(default_factory=lambda: WebOptions())
+ """
+ Web-specific configuration for secure storage.
+ """
+
+ on_change: Optional[ft.EventHandler["SecureStorageEvent"]] = None
+ """
+ Fires when secure storage availability changes.
+
+ iOS only feature. For unsupported platforms, this event will never fire.
+ The payload is a `SecureStorageEvent` object with the `available` field.
+ """
+
+ async def get_availability(self) -> Optional[bool]:
+ """
+ Gets the current availability status of secure storage.
+
+ iOS and macOS only. On macOS, available only on macOS 12+.
+ On older macOS versions, always returns True.
+ On unsupported platforms, returns None.
+
+ Returns:
+ A boolean indicating storage availability, or None if unsupported.
+ """
+ return await self._invoke_method("get_availability")
+
+ async def set(
+ self,
+ key: str,
+ value: Any,
+ *,
+ web: Optional[WebOptions] = None,
+ ios: Optional[IOSOptions] = None,
+ macos: Optional[MacOsOptions] = None,
+ android: Optional[AndroidOptions] = None,
+ windows: Optional[WindowsOptions] = None,
+ ) -> None:
+ """
+ Stores a value in secure storage under the given key.
+
+ Args:
+ key: The key to store the value under.
+ value: The value to store (cannot be None).
+ web: Optional web-specific configuration.
+ ios: Optional iOS-specific configuration.
+ macos: Optional macOS-specific configuration.
+ android: Optional Android-specific configuration.
+ windows: Optional Windows-specific configuration.
+
+ Raises:
+ ValueError: If `value` is None.
+ """
+ if value is None:
+ raise ValueError("value can't be None")
+ return await self._invoke_method(
+ method_name="set",
+ arguments={
+ "key": key,
+ "value": value,
+ "web": web,
+ "ios": ios,
+ "macos": macos,
+ "android": android,
+ "windows": windows,
+ },
+ )
+
+ async def get(
+ self,
+ key: str,
+ *,
+ web: Optional[WebOptions] = None,
+ ios: Optional[IOSOptions] = None,
+ macos: Optional[MacOsOptions] = None,
+ android: Optional[AndroidOptions] = None,
+ windows: Optional[WindowsOptions] = None,
+ ) -> Optional[str]:
+ """
+ Retrieves the value stored under the given key in secure storage.
+
+ Args:
+ key: The key to retrieve.
+ web: Optional web-specific configuration.
+ ios: Optional iOS-specific configuration.
+ macos: Optional macOS-specific configuration.
+ android: Optional Android-specific configuration.
+ windows: Optional Windows-specific configuration.
+
+ Returns:
+ The stored string value, or None if the key does not exist.
+ """
+ return await self._invoke_method(
+ method_name="get",
+ arguments={
+ "key": key,
+ "web": web,
+ "ios": ios,
+ "macos": macos,
+ "android": android,
+ "windows": windows,
+ },
+ )
+
+ async def get_all(
+ self,
+ *,
+ web: Optional[WebOptions] = None,
+ ios: Optional[IOSOptions] = None,
+ macos: Optional[MacOsOptions] = None,
+ android: Optional[AndroidOptions] = None,
+ windows: Optional[WindowsOptions] = None,
+ ) -> dict[str, str]:
+ """
+ Retrieves all key-value pairs from secure storage.
+
+ Args:
+ web: Optional web-specific configuration.
+ ios: Optional iOS-specific configuration.
+ macos: Optional macOS-specific configuration.
+ android: Optional Android-specific configuration.
+ windows: Optional Windows-specific configuration.
+
+ Returns:
+ A dictionary with all stored key-value pairs.
+ """
+ return await self._invoke_method(
+ method_name="get_all",
+ arguments={
+ "web": web,
+ "ios": ios,
+ "macos": macos,
+ "android": android,
+ "windows": windows,
+ },
+ )
+
+ async def contains_key(
+ self,
+ key: str,
+ *,
+ web: Optional[WebOptions] = None,
+ ios: Optional[IOSOptions] = None,
+ macos: Optional[MacOsOptions] = None,
+ android: Optional[AndroidOptions] = None,
+ windows: Optional[WindowsOptions] = None,
+ ) -> bool:
+ """
+ Checks whether the given key exists in secure storage.
+
+ Args:
+ key: The key to check.
+ web: Optional web-specific configuration.
+ ios: Optional iOS-specific configuration.
+ macos: Optional macOS-specific configuration.
+ android: Optional Android-specific configuration.
+ windows: Optional Windows-specific configuration.
+
+ Returns:
+ True if the key exists, False otherwise.
+ """
+ return await self._invoke_method(
+ method_name="contains_key",
+ arguments={
+ "key": key,
+ "web": web,
+ "ios": ios,
+ "macos": macos,
+ "android": android,
+ "windows": windows,
+ },
+ )
+
+ async def remove(
+ self,
+ key: str,
+ *,
+ web: Optional[WebOptions] = None,
+ ios: Optional[IOSOptions] = None,
+ macos: Optional[MacOsOptions] = None,
+ android: Optional[AndroidOptions] = None,
+ windows: Optional[WindowsOptions] = None,
+ ) -> None:
+ """
+ Removes the value stored under the given key in secure storage.
+
+ Args:
+ key: The key to remove.
+ web: Optional web-specific configuration.
+ ios: Optional iOS-specific configuration.
+ macos: Optional macOS-specific configuration.
+ android: Optional Android-specific configuration.
+ windows: Optional Windows-specific configuration.
+ """
+ return await self._invoke_method(
+ method_name="remove",
+ arguments={
+ "key": key,
+ "web": web,
+ "ios": ios,
+ "macos": macos,
+ "android": android,
+ "windows": windows,
+ },
+ )
+
+ async def clear(
+ self,
+ *,
+ web: Optional[WebOptions] = None,
+ ios: Optional[IOSOptions] = None,
+ macos: Optional[MacOsOptions] = None,
+ android: Optional[AndroidOptions] = None,
+ windows: Optional[WindowsOptions] = None,
+ ) -> None:
+ """
+ Clears all key-value pairs from secure storage.
+
+ Args:
+ web: Optional web-specific configuration.
+ ios: Optional iOS-specific configuration.
+ macos: Optional macOS-specific configuration.
+ android: Optional Android-specific configuration.
+ windows: Optional Windows-specific configuration.
+ """
+ return await self._invoke_method(
+ method_name="clear",
+ arguments={
+ "web": web,
+ "ios": ios,
+ "macos": macos,
+ "android": android,
+ "windows": windows,
+ },
+ )
diff --git a/sdk/python/packages/flet-secure-storage/src/flet_secure_storage/types.py b/sdk/python/packages/flet-secure-storage/src/flet_secure_storage/types.py
new file mode 100644
index 0000000000..3beb9c3f2f
--- /dev/null
+++ b/sdk/python/packages/flet-secure-storage/src/flet_secure_storage/types.py
@@ -0,0 +1,453 @@
+from dataclasses import dataclass, field
+from datetime import datetime
+from enum import Enum
+from typing import Optional
+
+
+class KeychainAccessibility(Enum):
+ """
+ KeyChain accessibility attributes for iOS/macOS platforms.
+
+ These attributes determine when the app can access secure values
+ stored in the Keychain.
+ """
+
+ PASSCODE = "passcode"
+ """
+ The data in the keychain can only be accessed when the device is unlocked.
+
+ Only available if a passcode is set on the device.
+ Items with this attribute do not migrate to a new device.
+ """
+
+ UNLOCKED = "unlocked"
+ """
+ The data in the keychain item can be accessed only while
+ the device is unlocked by the user.
+ """
+
+ UNLOCKED_THIS_DEVICE = "unlocked_this_device"
+ """
+ The data in the keychain item can be accessed only while
+ the device is unlocked by the user.
+
+ Items with this attribute do not migrate to a new device.
+ """
+
+ FIRST_UNLOCK = "first_unlock"
+ """
+ The data in the keychain item cannot be accessed after a restart until
+ the device has been unlocked once by the user.
+
+ Enables access to secure values after the device is unlocked for the first
+ time after a reboot.
+ """
+
+ FIRST_UNLOCK_THIS_DEVICE = "first_unlock_this_device"
+ """
+ The data in the keychain item cannot be accessed after
+ a restart until the device has been unlocked once by the user.
+
+ Items with this attribute do not migrate to a new device.
+
+ Allows access to secure values only after the device is unlocked for the first time
+ since installation on this device.
+ """
+
+
+class AccessControlFlag(Enum):
+ """
+ Keychain access control flags that define security conditions for accessing items.
+
+ These flags can be combined to create complex access control policies using
+ the `access_control_flags` parameter in `IOSOptions` or `MacOsOptions`.
+
+ Rules for combining flags:
+ - Use `AccessControlFlag.OR` to allow access if any condition is met
+ - Use `AccessControlFlag.AND` to require that all specified conditions are met
+ - Only one logical operator (OR/AND) can be used per combination
+ """
+
+ DEVICE_PASSCODE = "devicePasscode"
+ """
+ Constraint to access an item with a passcode.
+ """
+
+ BIOMETRY_ANY = "biometryAny"
+ """
+ Constraint to access an item with biometrics (Touch ID/Face ID).
+ """
+
+ BIOMETRY_CURRENT_SET = "biometryCurrentSet"
+ """
+ Constraint to access an item with the currently enrolled biometrics.
+ """
+
+ USER_PRESENCE = "userPresence"
+ """
+ Constraint to access an item with either biometry or passcode.
+ """
+
+ WATCH = "watch"
+ """
+ Constraint to access an item with a paired watch.
+ """
+
+ OR = "or"
+ """
+ Combine multiple constraints with an OR operation.
+ """
+
+ AND = "and"
+ """
+ Combine multiple constraints with an AND operation.
+ """
+
+ APPLICATION_PASSWORD = "applicationPassword"
+ """
+ Use an application-provided password for encryption.
+ """
+
+ PRIVATE_KEY_USAGE = "privateKeyUsage"
+ """
+ Enable private key usage for signing operations.
+ """
+
+
+class KeyCipherAlgorithm(Enum):
+ """
+ Algorithm used to encrypt/wrap the secret key in Android KeyStore.
+
+ Different algorithms provide different security guarantees and compatibility levels:
+
+ - RSA algorithms wrap the AES encryption key with RSA (no biometric support)
+ - AES algorithm stores the key directly in Android KeyStore
+ (supports biometric authentication)
+
+ See the [AndroidOptions] class for usage examples and combinations.
+ """
+
+ RSA_ECB_PKCS1_PADDING = "RSA_ECB_PKCS1Padding"
+ """
+ Legacy RSA/ECB/PKCS1Padding for backwards compatibility.
+ """
+
+ RSA_ECB_OAEP_WITH_SHA256_AND_MGF1_PADDING = "RSA_ECB_OAEPwithSHA_256andMGF1Padding"
+ """
+ RSA/ECB/OAEPWithSHA-256AndMGF1Padding (API 23+).
+
+ This is the default and recommended algorithm for most use cases.
+ Provides strong authenticated encryption without biometrics.
+ """
+
+ AES_GCM_NO_PADDING = "AES_GCM_NoPadding"
+ """
+ AES/GCM/NoPadding for KeyStore-based key wrapping (supports biometrics).
+
+ Use this algorithm when you need biometric authentication support.
+ Requires API 23+ for basic use, API 28+ for enforced biometric authentication.
+ """
+
+
+class StorageCipherAlgorithm(Enum):
+ """
+ Algorithm used to encrypt stored data on Android.
+
+ Modern applications should use `AES_GCM_NO_PADDING` for better security.
+ The legacy `AES_CBC_PKCS7_PADDING` is provided for backwards compatibility only.
+ """
+
+ AES_CBC_PKCS7_PADDING = "AES_CBC_PKCS7Padding"
+ """
+ Legacy AES/CBC/PKCS7Padding for backwards compatibility.
+ """
+
+ AES_GCM_NO_PADDING = "AES_GCM_NoPadding"
+ """
+ AES/GCM/NoPadding (API 23+).
+
+ This is the default and recommended storage cipher algorithm.
+ Provides authenticated encryption with associated data (AEAD).
+ """
+
+
+@dataclass
+class AndroidOptions:
+ """
+ Specific options for Android platform for secure storage.
+
+ Provides configurable options for encryption, key wrapping, biometric enforcement,
+ and shared preferences naming.
+ """
+
+ reset_on_error: bool = True
+ """
+ When an error is detected, automatically reset all data to prevent fatal errors
+ with unknown keys.
+
+ Be aware that data is PERMANENTLY erased when this occurs.
+ """
+
+ migrate_on_algorithm_change: bool = True
+ """
+ When the encryption algorithm changes, automatically migrate existing data
+ to the new algorithm. Preserves data across algorithm upgrades.
+
+ If False, data may be lost when algorithm changes unless
+ reset_on_error is True.
+ """
+
+ enforce_biometrics: bool = False
+ """
+ Whether to enforce biometric or PIN authentication.
+
+ When True:
+ - The plugin throws an exception if no biometric/PIN is enrolled.
+ - The encryption key is generated with authentication required.
+
+ When False:
+ - The plugin gracefully degrades if biometrics are unavailable.
+ - The key is generated without authentication required.
+ """
+
+ key_cipher_algorithm: KeyCipherAlgorithm = (
+ KeyCipherAlgorithm.RSA_ECB_OAEP_WITH_SHA256_AND_MGF1_PADDING
+ )
+ """
+ Algorithm used to encrypt the secret key.
+
+ Legacy RSA/ECB/PKCS1Padding is available for backwards compatibility.
+ """
+
+ storage_cipher_algorithm: StorageCipherAlgorithm = (
+ StorageCipherAlgorithm.AES_GCM_NO_PADDING
+ )
+ """
+ Algorithm used to encrypt stored data.
+
+ Legacy AES/CBC/PKCS7Padding is available for backwards compatibility.
+ """
+
+ shared_preferences_name: Optional[str] = None
+ """
+ The name of the shared preferences database to use.
+
+ Changing this will prevent access to already saved preferences.
+ """
+
+ preferences_key_prefix: Optional[str] = None
+ """
+ Prefix for shared preference keys. Ensures keys are unique to your app.
+
+ An underscore (_) is added automatically.
+
+ Changing this prevents access to existing preferences.
+ """
+
+ biometric_prompt_title: str = "Authenticate to access"
+ """
+ Title displayed in the biometric authentication prompt.
+ """
+
+ biometric_prompt_subtitle: str = "Use biometrics or device credentials"
+ """
+ Subtitle displayed in the biometric authentication prompt.
+ """
+
+
+@dataclass
+class AppleOptions:
+ """
+ Specific options for Apple platforms (iOS/macOS) for secure storage.
+
+ This class allows configuring keychain access and storage behavior.
+ Use `IOSOptions` for iOS-specific configuration
+ or `MacOsOptions` for macOS-specific configuration.
+
+ Note:
+ - Most options apply to both iOS and macOS
+ - Some options (like `group_id` on macOS) only apply when
+ certain keychain flags are set
+ - See individual option documentation for platform-specific behavior
+ """
+
+ account_name: Optional[str] = "flet_secure_storage_service"
+ """
+ Represents the service or application name associated with the item.
+
+ Typically used to group related keychain items.
+ """
+
+ group_id: Optional[str] = None
+ """
+ Specifies the app group for shared access. Allows multiple apps in the
+ same app group to access the item.
+ """
+
+ accessibility: Optional[KeychainAccessibility] = KeychainAccessibility.UNLOCKED
+ """
+ Defines the accessibility level of the keychain item.
+
+ Controls when the item is accessible (e.g., when device is unlocked
+ or after first unlock).
+ """
+
+ synchronizable: bool = False
+ """
+ Indicates whether the keychain item should be synchronized with iCloud.
+
+ - True: Enables synchronization across user's devices
+ - False: Item stays local to this device only
+ """
+
+ label: Optional[str] = None
+ """
+ A user-visible label for the keychain item.
+ Helps identify the item in keychain management tools.
+ """
+
+ description: Optional[str] = None
+ """
+ A description of the keychain item.
+ Can describe a category of items (shared) or a specific item (unique).
+ """
+
+ comment: Optional[str] = None
+ """
+ A comment associated with the keychain item.
+ Often used for metadata or debugging information.
+ """
+
+ invisible: Optional[bool] = None
+ """
+ Indicates whether the keychain item is hidden from user-visible lists.
+ Can apply to all items in a category (shared) or specific items (unique).
+ """
+
+ is_negative: Optional[bool] = None
+ """
+ Indicates whether the item is a placeholder or a negative entry.
+ Typically unique to individual keychain items.
+ """
+
+ creation_date: Optional[datetime] = None
+ """
+ The creation date of the keychain item.
+ Automatically set by the system when an item is created.
+ """
+
+ last_modified_date: Optional[datetime] = None
+ """
+ The last modification date of the keychain item.
+ Automatically updated when an item is modified.
+ """
+
+ result_limit: Optional[int] = None
+ """
+ Specifies the maximum number of results to return in a query.
+ For example, 1 for a single result, or `None` for all matching results.
+ """
+
+ is_persistent: Optional[bool] = None
+ """
+ Indicates whether to return a persistent reference to the keychain item.
+ Used for persistent access across app sessions.
+ """
+
+ auth_ui_behavior: Optional[str] = None
+ """
+ Controls how authentication UI is presented during secure operations.
+ Determines whether authentication prompts are displayed to the user.
+ """
+
+ access_control_flags: list[AccessControlFlag] = field(default_factory=list)
+ """
+ Keychain access control flags that define security conditions for accessing items.
+ """
+
+
+@dataclass
+class IOSOptions(AppleOptions):
+ """
+ iOS-specific configuration for secure storage.
+
+ All configurable options are inherited from `AppleOptions`.
+ There are currently no iOS-only options.
+ """
+
+
+@dataclass
+class MacOsOptions(AppleOptions):
+ """
+ Specific options for macOS platform.
+ Extends `AppleOptions` and adds the `usesDataProtectionKeychain` parameter.
+ """
+
+ uses_data_protection_keychain: bool = True
+ """
+ Indicates whether the macOS data protection keychain is used.
+ """
+
+
+@dataclass
+class WebOptions:
+ """
+ Specific options for the Web platform for secure storage.
+
+ Configures database, encryption, and storage behavior on web platforms.
+ """
+
+ db_name: str = "FletEncryptedStorage"
+ """
+ The name of the database used for secure storage.
+ """
+
+ public_key: str = "FletSecureStorage"
+ """
+ The public key used for encryption.
+ """
+
+ wrap_key: str = ""
+ """
+ The key used to wrap the encryption key.
+ """
+
+ wrap_key_iv: str = ""
+ """
+ The initialization vector (IV) used for the wrap key.
+ """
+
+ use_session_storage: bool = False
+ """
+ Whether to use session storage instead of local storage.
+ """
+
+
+@dataclass
+class WindowsOptions:
+ """
+ Specific options for Windows platform for secure storage.
+
+ Allows configuring backward compatibility when reading/writing
+ values from previous versions of storage.
+
+ Note:
+ You need the C++ ATL libraries installed along with Visual Studio Build Tools.
+ Download from: https://visualstudio.microsoft.com/downloads/?q=build+tools
+ Make sure the C++ ATL under optional components is installed as well.
+ """
+
+ use_backward_compatibility: bool = False
+ """
+ If True, attempts to read values written by previous versions of the storage.
+ When reading or writing old storage values, they will be automatically
+ migrated to new storage.
+
+ Note:
+ - May introduce performance overhead.
+ - May cause errors for keys with `"`, `<`, `>`, `|`, `:`, `*`, `?`, `/`, `\\`.
+ or any ASCII control characters.
+ - May cause errors for keys containing `/../`, `\\..\\`, or similar patterns.
+ - May cause errors for very long keys (length depends on app's product name,
+ company name, and executing account).
+ """
diff --git a/sdk/python/packages/flet-secure-storage/src/flutter/flet_secure_storage/lib/flet_secure_storage.dart b/sdk/python/packages/flet-secure-storage/src/flutter/flet_secure_storage/lib/flet_secure_storage.dart
new file mode 100644
index 0000000000..faabfd5681
--- /dev/null
+++ b/sdk/python/packages/flet-secure-storage/src/flutter/flet_secure_storage/lib/flet_secure_storage.dart
@@ -0,0 +1,3 @@
+library flet_secure_storage;
+
+export "src/extension.dart" show Extension;
diff --git a/sdk/python/packages/flet-secure-storage/src/flutter/flet_secure_storage/lib/src/extension.dart b/sdk/python/packages/flet-secure-storage/src/flutter/flet_secure_storage/lib/src/extension.dart
new file mode 100644
index 0000000000..46135b14f1
--- /dev/null
+++ b/sdk/python/packages/flet-secure-storage/src/flutter/flet_secure_storage/lib/src/extension.dart
@@ -0,0 +1,15 @@
+import 'package:flet/flet.dart';
+
+import 'secure_storage.dart';
+
+class Extension extends FletExtension {
+ @override
+ FletService? createService(Control control) {
+ switch (control.type) {
+ case "SecureStorage":
+ return SecureStorageService(control: control);
+ default:
+ return null;
+ }
+ }
+}
diff --git a/sdk/python/packages/flet-secure-storage/src/flutter/flet_secure_storage/lib/src/secure_storage.dart b/sdk/python/packages/flet-secure-storage/src/flutter/flet_secure_storage/lib/src/secure_storage.dart
new file mode 100644
index 0000000000..ed194b68b9
--- /dev/null
+++ b/sdk/python/packages/flet-secure-storage/src/flutter/flet_secure_storage/lib/src/secure_storage.dart
@@ -0,0 +1,143 @@
+import 'dart:async';
+
+import 'package:flet/flet.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+
+import 'utils/secure_storage.dart';
+
+class SecureStorageService extends FletService {
+ SecureStorageService({required super.control});
+
+ FlutterSecureStorage? _storage;
+ StreamSubscription? _onSecureDataChanged;
+ AndroidOptions androidOptions = AndroidOptions.defaultOptions;
+ WindowsOptions windowsOptions = WindowsOptions.defaultOptions;
+ LinuxOptions linuxOptions = LinuxOptions.defaultOptions;
+ MacOsOptions macOsOptions = MacOsOptions.defaultOptions;
+ IOSOptions iosOptions = IOSOptions.defaultOptions;
+ WebOptions webOptions = WebOptions.defaultOptions;
+
+ FlutterSecureStorage get storage {
+ return _storage ??= FlutterSecureStorage(
+ lOptions: linuxOptions,
+ aOptions: parseAndroidOptions(control.get