reflex-capacitor 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- reflex_capacitor/__init__.py +8 -0
- reflex_capacitor/android_signing.py +166 -0
- reflex_capacitor/bridge/__init__.py +34 -0
- reflex_capacitor/bridge/_script.py +48 -0
- reflex_capacitor/bridge/api.py +359 -0
- reflex_capacitor/bridge/api.pyi +60 -0
- reflex_capacitor/bridge/assets/bridge.js +805 -0
- reflex_capacitor/bridge/assets/image-editor.js +614 -0
- reflex_capacitor/bridge/inject.py +88 -0
- reflex_capacitor/bridge/ios_plist.py +112 -0
- reflex_capacitor/bridge/log.py +60 -0
- reflex_capacitor/bridge/plugins.py +259 -0
- reflex_capacitor/cli.py +793 -0
- reflex_capacitor/components/__init__.py +5 -0
- reflex_capacitor/components/image_editor.py +92 -0
- reflex_capacitor/config.py +50 -0
- reflex_capacitor/dev_util.py +59 -0
- reflex_capacitor/network_env.py +86 -0
- reflex_capacitor/plugin.py +392 -0
- reflex_capacitor/preflight.py +394 -0
- reflex_capacitor/scaffold/.gitignore +8 -0
- reflex_capacitor/scaffold/capacitor.config.json +8 -0
- reflex_capacitor/scaffold/package.json +13 -0
- reflex_capacitor-0.3.2.dist-info/METADATA +126 -0
- reflex_capacitor-0.3.2.dist-info/RECORD +28 -0
- reflex_capacitor-0.3.2.dist-info/WHEEL +4 -0
- reflex_capacitor-0.3.2.dist-info/entry_points.txt +2 -0
- reflex_capacitor-0.3.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Package a Reflex app as a Capacitor mobile app (remote backend)."""
|
|
2
|
+
|
|
3
|
+
from .bridge import api as mobile
|
|
4
|
+
from .bridge import api # noqa: F401 — re-export module for `from reflex_capacitor.bridge import api`
|
|
5
|
+
from . import components
|
|
6
|
+
from .plugin import CapacitorPlugin as CapacitorPlugin
|
|
7
|
+
|
|
8
|
+
__all__ = ["CapacitorPlugin", "mobile", "api", "components"]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Android release signing helpers for Capacitor Gradle builds."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
SIGNING_BEGIN = "// reflex-capacitor signing begin"
|
|
10
|
+
SIGNING_END = "// reflex-capacitor signing end"
|
|
11
|
+
|
|
12
|
+
KEYSTORE_ENV = "REFLEX_CAPACITOR_KEYSTORE_PATH"
|
|
13
|
+
KEYSTORE_PASSWORD_ENV = "REFLEX_CAPACITOR_KEYSTORE_PASSWORD"
|
|
14
|
+
KEY_ALIAS_ENV = "REFLEX_CAPACITOR_KEY_ALIAS"
|
|
15
|
+
KEY_PASSWORD_ENV = "REFLEX_CAPACITOR_KEY_PASSWORD"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, kw_only=True)
|
|
19
|
+
class AndroidSigningConfig:
|
|
20
|
+
"""Release keystore settings."""
|
|
21
|
+
|
|
22
|
+
keystore_path: Path
|
|
23
|
+
keystore_password: str
|
|
24
|
+
key_alias: str
|
|
25
|
+
key_password: str
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_env(cls) -> AndroidSigningConfig | None:
|
|
29
|
+
"""Load signing config from standard environment variables."""
|
|
30
|
+
path = os.environ.get(KEYSTORE_ENV, "").strip()
|
|
31
|
+
if not path:
|
|
32
|
+
return None
|
|
33
|
+
store_password = os.environ.get(KEYSTORE_PASSWORD_ENV, "").strip()
|
|
34
|
+
key_alias = os.environ.get(KEY_ALIAS_ENV, "").strip()
|
|
35
|
+
key_password = os.environ.get(KEY_PASSWORD_ENV, "").strip()
|
|
36
|
+
if not store_password or not key_alias or not key_password:
|
|
37
|
+
msg = (
|
|
38
|
+
"reflex-capacitor: set all signing env vars when using "
|
|
39
|
+
f"{KEYSTORE_ENV}: {KEYSTORE_PASSWORD_ENV}, {KEY_ALIAS_ENV}, {KEY_PASSWORD_ENV}"
|
|
40
|
+
)
|
|
41
|
+
raise ValueError(msg)
|
|
42
|
+
keystore_path = Path(path).expanduser().resolve()
|
|
43
|
+
if not keystore_path.is_file():
|
|
44
|
+
msg = f"reflex-capacitor: keystore not found at {keystore_path}"
|
|
45
|
+
raise FileNotFoundError(msg)
|
|
46
|
+
return cls(
|
|
47
|
+
keystore_path=keystore_path,
|
|
48
|
+
keystore_password=store_password,
|
|
49
|
+
key_alias=key_alias,
|
|
50
|
+
key_password=key_password,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def write_keystore_properties(android_root: Path, signing: AndroidSigningConfig) -> Path:
|
|
55
|
+
"""Write ``android/keystore.properties`` (gitignored by Capacitor template)."""
|
|
56
|
+
store_path = signing.keystore_path
|
|
57
|
+
try:
|
|
58
|
+
rel = store_path.relative_to(android_root)
|
|
59
|
+
store_file = str(rel).replace("\\", "/")
|
|
60
|
+
except ValueError:
|
|
61
|
+
store_file = str(store_path).replace("\\", "/")
|
|
62
|
+
|
|
63
|
+
props_path = android_root / "keystore.properties"
|
|
64
|
+
props_path.write_text(
|
|
65
|
+
"\n".join(
|
|
66
|
+
[
|
|
67
|
+
f"storeFile={store_file}",
|
|
68
|
+
f"storePassword={signing.keystore_password}",
|
|
69
|
+
f"keyAlias={signing.key_alias}",
|
|
70
|
+
f"keyPassword={signing.key_password}",
|
|
71
|
+
"",
|
|
72
|
+
]
|
|
73
|
+
),
|
|
74
|
+
encoding="utf-8",
|
|
75
|
+
)
|
|
76
|
+
return props_path
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _signing_snippet() -> str:
|
|
80
|
+
return f"""
|
|
81
|
+
{SIGNING_BEGIN}
|
|
82
|
+
def rcKeystorePropertiesFile = rootProject.file("keystore.properties")
|
|
83
|
+
def rcKeystoreProperties = new Properties()
|
|
84
|
+
if (rcKeystorePropertiesFile.exists()) {{
|
|
85
|
+
rcKeystoreProperties.load(new FileInputStream(rcKeystorePropertiesFile))
|
|
86
|
+
}}
|
|
87
|
+
{SIGNING_END}
|
|
88
|
+
""".strip()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _signing_configs_block() -> str:
|
|
92
|
+
return """
|
|
93
|
+
signingConfigs {
|
|
94
|
+
release {
|
|
95
|
+
if (rcKeystorePropertiesFile.exists()) {
|
|
96
|
+
keyAlias rcKeystoreProperties['keyAlias']
|
|
97
|
+
keyPassword rcKeystoreProperties['keyPassword']
|
|
98
|
+
storeFile file(rcKeystoreProperties['storeFile'])
|
|
99
|
+
storePassword rcKeystoreProperties['storePassword']
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
""".strip()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def apply_release_signing(android_root: Path, signing: AndroidSigningConfig) -> None:
|
|
107
|
+
"""Configure Gradle release signing via keystore.properties + build.gradle patch."""
|
|
108
|
+
write_keystore_properties(android_root, signing)
|
|
109
|
+
gradle_path = android_root / "app" / "build.gradle"
|
|
110
|
+
if not gradle_path.is_file():
|
|
111
|
+
msg = f"reflex-capacitor: missing {gradle_path} — run `reflex-capacitor init` first"
|
|
112
|
+
raise FileNotFoundError(msg)
|
|
113
|
+
patch_app_build_gradle(gradle_path)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def patch_app_build_gradle(gradle_path: Path) -> None:
|
|
117
|
+
"""Idempotently patch ``android/app/build.gradle`` for release signing."""
|
|
118
|
+
text = gradle_path.read_text(encoding="utf-8")
|
|
119
|
+
if SIGNING_BEGIN in text and "signingConfig signingConfigs.release" in text:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
snippet = _signing_snippet()
|
|
123
|
+
if SIGNING_BEGIN not in text:
|
|
124
|
+
marker = "android {"
|
|
125
|
+
idx = text.find(marker)
|
|
126
|
+
if idx == -1:
|
|
127
|
+
msg = f"reflex-capacitor: could not find `android {{` in {gradle_path}"
|
|
128
|
+
raise ValueError(msg)
|
|
129
|
+
insert_at = idx + len(marker)
|
|
130
|
+
text = text[:insert_at] + "\n" + snippet + text[insert_at:]
|
|
131
|
+
|
|
132
|
+
if "signingConfigs {" not in text:
|
|
133
|
+
marker = "android {"
|
|
134
|
+
idx = text.find(marker)
|
|
135
|
+
if idx == -1:
|
|
136
|
+
msg = f"reflex-capacitor: could not find `android {{` in {gradle_path}"
|
|
137
|
+
raise ValueError(msg)
|
|
138
|
+
insert_at = idx + len(marker)
|
|
139
|
+
if SIGNING_BEGIN in text:
|
|
140
|
+
end = text.find(SIGNING_END, insert_at)
|
|
141
|
+
if end != -1:
|
|
142
|
+
insert_at = end + len(SIGNING_END)
|
|
143
|
+
text = text[:insert_at] + "\n" + _signing_configs_block() + text[insert_at:]
|
|
144
|
+
|
|
145
|
+
if "signingConfig signingConfigs.release" not in text:
|
|
146
|
+
release_marker = "release {"
|
|
147
|
+
idx = text.find(release_marker)
|
|
148
|
+
if idx == -1:
|
|
149
|
+
msg = f"reflex-capacitor: could not find release buildType in {gradle_path}"
|
|
150
|
+
raise ValueError(msg)
|
|
151
|
+
insert_at = idx + len(release_marker)
|
|
152
|
+
text = (
|
|
153
|
+
text[:insert_at]
|
|
154
|
+
+ "\n signingConfig signingConfigs.release"
|
|
155
|
+
+ text[insert_at:]
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
gradle_path.write_text(text, encoding="utf-8")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def release_output_paths(android_root: Path, *, aab: bool) -> list[Path]:
|
|
162
|
+
"""Return expected Gradle release artifact paths."""
|
|
163
|
+
outputs = android_root / "app" / "build" / "outputs"
|
|
164
|
+
if aab:
|
|
165
|
+
return sorted((outputs / "bundle" / "release").glob("*.aab"))
|
|
166
|
+
return sorted((outputs / "apk" / "release").glob("*.apk"))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Reflex ↔ Capacitor native bridge (Python API + JS assets + injection)."""
|
|
2
|
+
|
|
3
|
+
from reflex_capacitor.bridge import api as mobile
|
|
4
|
+
|
|
5
|
+
from . import api
|
|
6
|
+
from .inject import install_bridge
|
|
7
|
+
from .log import log_bridge
|
|
8
|
+
from .plugins import (
|
|
9
|
+
ALL_PLUGIN_IDS,
|
|
10
|
+
CAPACITOR_PLUGIN_PACKAGES,
|
|
11
|
+
CORE_PLUGIN_IDS,
|
|
12
|
+
EXTENDED_PLUGIN_IDS,
|
|
13
|
+
apply_package_json_deps,
|
|
14
|
+
copy_plugin_vendor_scripts,
|
|
15
|
+
ensure_android_notification_permission,
|
|
16
|
+
resolve_plugin_ids,
|
|
17
|
+
vendor_script_filename,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"ALL_PLUGIN_IDS",
|
|
22
|
+
"CAPACITOR_PLUGIN_PACKAGES",
|
|
23
|
+
"CORE_PLUGIN_IDS",
|
|
24
|
+
"EXTENDED_PLUGIN_IDS",
|
|
25
|
+
"api",
|
|
26
|
+
"mobile",
|
|
27
|
+
"install_bridge",
|
|
28
|
+
"log_bridge",
|
|
29
|
+
"apply_package_json_deps",
|
|
30
|
+
"copy_plugin_vendor_scripts",
|
|
31
|
+
"ensure_android_notification_permission",
|
|
32
|
+
"resolve_plugin_ids",
|
|
33
|
+
"vendor_script_filename",
|
|
34
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Helpers for generating rx.call_script snippets targeting __REFLEX_CAPACITOR__."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import reflex as rx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def call_bridge(
|
|
12
|
+
method: str,
|
|
13
|
+
args: dict[str, Any] | None = None,
|
|
14
|
+
*,
|
|
15
|
+
callback: Any = None,
|
|
16
|
+
) -> rx.event.EventSpec:
|
|
17
|
+
"""Call an async method on ``window.__REFLEX_CAPACITOR__``.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
method: Bridge method name (e.g. ``"notify"``).
|
|
21
|
+
args: JSON-serializable arguments object passed to the method.
|
|
22
|
+
callback: Optional Reflex handler for the resolved return value.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A Reflex event that runs the bridge call in the webview.
|
|
26
|
+
"""
|
|
27
|
+
payload = json.dumps(args or {})
|
|
28
|
+
script = (
|
|
29
|
+
"(async () => {"
|
|
30
|
+
" const b = window.__REFLEX_CAPACITOR__;"
|
|
31
|
+
" if (!b) {"
|
|
32
|
+
" console.warn('reflex-capacitor: bridge not loaded');"
|
|
33
|
+
" return { ok: false, error: 'bridge_not_loaded' };"
|
|
34
|
+
" }"
|
|
35
|
+
" try {"
|
|
36
|
+
f" return await b.{method}({payload});"
|
|
37
|
+
" } catch (e) {"
|
|
38
|
+
" console.error('reflex-capacitor: bridge call failed', e);"
|
|
39
|
+
" return { ok: false, error: String(e) };"
|
|
40
|
+
" }"
|
|
41
|
+
"})()"
|
|
42
|
+
)
|
|
43
|
+
return rx.call_script(script, callback=callback)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def call_bridge_void(method: str, args: dict[str, Any] | None = None) -> rx.event.EventSpec:
|
|
47
|
+
"""Call a bridge method without using the return value."""
|
|
48
|
+
return call_bridge(method, args)
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""Python → Capacitor bridge API for Reflex event handlers.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
import reflex as rx
|
|
6
|
+
from reflex_capacitor import mobile
|
|
7
|
+
|
|
8
|
+
rx.button("Notify", on_click=mobile.notify("Hello", "From Reflex"))
|
|
9
|
+
rx.button("Share", on_click=mobile.share(text="Check this out"))
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any, Literal
|
|
15
|
+
|
|
16
|
+
import reflex as rx
|
|
17
|
+
|
|
18
|
+
from ._script import call_bridge, call_bridge_void
|
|
19
|
+
|
|
20
|
+
ImpactStyle = Literal["HEAVY", "MEDIUM", "LIGHT"]
|
|
21
|
+
NotificationStyle = Literal["SUCCESS", "WARNING", "ERROR"]
|
|
22
|
+
StatusBarStyle = Literal["DARK", "LIGHT", "DEFAULT"]
|
|
23
|
+
BackButtonMode = Literal["emit", "exit", "history"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def notify(title: str, body: str = "") -> rx.event.EventSpec:
|
|
27
|
+
"""Show a local OS notification (requires Local Notifications plugin)."""
|
|
28
|
+
return call_bridge_void("notify", {"title": title, "body": body})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def toast(text: str, *, duration: ToastDuration = "short") -> rx.event.EventSpec:
|
|
32
|
+
"""Show a native toast banner."""
|
|
33
|
+
return call_bridge_void("toast", {"text": text, "duration": duration})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def haptics_impact(*, style: ImpactStyle = "MEDIUM", callback: Any = None) -> rx.event.EventSpec:
|
|
37
|
+
"""Trigger a haptic impact (very short tick — may be hard to feel on some Android phones)."""
|
|
38
|
+
return call_bridge("hapticsImpact", {"style": style}, callback=callback)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def haptics_notification(*, type: NotificationStyle = "SUCCESS", callback: Any = None) -> rx.event.EventSpec:
|
|
42
|
+
"""Trigger a haptic notification pattern (slightly longer than impact)."""
|
|
43
|
+
return call_bridge("hapticsNotification", {"type": type}, callback=callback)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def haptics_vibrate(*, duration_ms: int = 300, callback: Any = None) -> rx.event.EventSpec:
|
|
47
|
+
"""Vibrate for ``duration_ms`` — more noticeable than impact for testing."""
|
|
48
|
+
return call_bridge("hapticsVibrate", {"duration": duration_ms}, callback=callback)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def share(
|
|
52
|
+
*,
|
|
53
|
+
title: str | None = None,
|
|
54
|
+
text: str | None = None,
|
|
55
|
+
url: str | None = None,
|
|
56
|
+
dialog_title: str | None = None,
|
|
57
|
+
) -> rx.event.EventSpec:
|
|
58
|
+
"""Open the system share sheet."""
|
|
59
|
+
args: dict[str, str] = {}
|
|
60
|
+
if title is not None:
|
|
61
|
+
args["title"] = title
|
|
62
|
+
if text is not None:
|
|
63
|
+
args["text"] = text
|
|
64
|
+
if url is not None:
|
|
65
|
+
args["url"] = url
|
|
66
|
+
if dialog_title is not None:
|
|
67
|
+
args["dialogTitle"] = dialog_title
|
|
68
|
+
return call_bridge_void("share", args)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def clipboard_write(text: str) -> rx.event.EventSpec:
|
|
72
|
+
"""Write text to the system clipboard."""
|
|
73
|
+
return call_bridge_void("clipboardWrite", {"text": text})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def clipboard_read(callback: Any) -> rx.event.EventSpec:
|
|
77
|
+
"""Read clipboard text and pass ``{value: str}`` to the callback handler."""
|
|
78
|
+
return call_bridge("clipboardRead", callback=callback)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def status_bar_set_style(*, style: StatusBarStyle = "DARK") -> rx.event.EventSpec:
|
|
82
|
+
"""Set the status bar icon/text style."""
|
|
83
|
+
return call_bridge_void("statusBarSetStyle", {"style": style})
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def status_bar_hide() -> rx.event.EventSpec:
|
|
87
|
+
"""Hide the status bar."""
|
|
88
|
+
return call_bridge_void("statusBarHide")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def status_bar_show() -> rx.event.EventSpec:
|
|
92
|
+
"""Show the status bar."""
|
|
93
|
+
return call_bridge_void("statusBarShow")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def splash_hide() -> rx.event.EventSpec:
|
|
97
|
+
"""Hide the splash screen after the app has loaded."""
|
|
98
|
+
return call_bridge_void("splashHide")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def device_info(callback: Any) -> rx.event.EventSpec:
|
|
102
|
+
"""Fetch device info and pass the result to the callback handler."""
|
|
103
|
+
return call_bridge("deviceInfo", callback=callback)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def network_status(callback: Any) -> rx.event.EventSpec:
|
|
107
|
+
"""Fetch network status and pass the result to the callback handler."""
|
|
108
|
+
return call_bridge("networkStatus", callback=callback)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def app_exit() -> rx.event.EventSpec:
|
|
112
|
+
"""Exit the app (platform-dependent)."""
|
|
113
|
+
return call_bridge_void("appExit")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def diagnostics(callback: Any) -> rx.event.EventSpec:
|
|
117
|
+
"""Fetch bridge diagnostics (platform, loaded plugins, log count)."""
|
|
118
|
+
return call_bridge("getDiagnostics", callback=callback)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def platform_info(callback: Any) -> rx.event.EventSpec:
|
|
122
|
+
"""Return runtime platform flags from the WebView shell.
|
|
123
|
+
|
|
124
|
+
Callback receives::
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
"platform": "ios" | "android" | "web",
|
|
128
|
+
"isNative": bool,
|
|
129
|
+
"isAndroid": bool,
|
|
130
|
+
"isIos": bool,
|
|
131
|
+
"isWeb": bool,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Use this in Reflex State to branch UI or call different ``mobile.*`` handlers.
|
|
135
|
+
"""
|
|
136
|
+
return call_bridge("platformInfo", callback=callback)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def bridge_logs(limit: int = 50, callback: Any = None) -> rx.event.EventSpec:
|
|
140
|
+
"""Fetch recent client-side bridge log entries from the WebView."""
|
|
141
|
+
return call_bridge("getLogs", {"limit": limit}, callback=callback)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def clear_logs() -> rx.event.EventSpec:
|
|
145
|
+
"""Clear the in-WebView bridge log buffer."""
|
|
146
|
+
return call_bridge_void("clearLogs")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# --- Phase 3 P1 ---
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def pref_set(key: str, value: str, *, callback: Any = None) -> rx.event.EventSpec:
|
|
153
|
+
"""Store a string in Capacitor Preferences (device-local key/value)."""
|
|
154
|
+
return call_bridge("prefSet", {"key": key, "value": value}, callback=callback)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def pref_get(key: str, callback: Any) -> rx.event.EventSpec:
|
|
158
|
+
"""Read a Preferences value; callback receives ``{key, value}``."""
|
|
159
|
+
return call_bridge("prefGet", {"key": key}, callback=callback)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def take_photo(
|
|
163
|
+
callback: Any,
|
|
164
|
+
*,
|
|
165
|
+
quality: int = 90,
|
|
166
|
+
save_to_gallery: bool = False,
|
|
167
|
+
) -> rx.event.EventSpec:
|
|
168
|
+
"""Capture a photo.
|
|
169
|
+
|
|
170
|
+
Callback receives ``{dataUrl, webPath, format, saved, ...}``.
|
|
171
|
+
|
|
172
|
+
By default the image stays in memory as ``dataUrl`` (no cloud, not saved to disk).
|
|
173
|
+
Set ``save_to_gallery=True`` to also write into the system photo gallery on
|
|
174
|
+
Android/iOS (local only, no upload).
|
|
175
|
+
"""
|
|
176
|
+
return call_bridge(
|
|
177
|
+
"takePhoto",
|
|
178
|
+
{"quality": quality, "saveToGallery": save_to_gallery},
|
|
179
|
+
callback=callback,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def pick_images(callback: Any, *, limit: int = 1, quality: int = 90) -> rx.event.EventSpec:
|
|
184
|
+
"""Pick images from the gallery; callback receives ``{photos: [...]}``."""
|
|
185
|
+
return call_bridge("pickImages", {"limit": limit, "quality": quality}, callback=callback)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_current_position(
|
|
189
|
+
callback: Any,
|
|
190
|
+
*,
|
|
191
|
+
enable_high_accuracy: bool = False,
|
|
192
|
+
timeout_ms: int = 30000,
|
|
193
|
+
) -> rx.event.EventSpec:
|
|
194
|
+
"""Get GPS coordinates; callback receives lat/lon/accuracy.
|
|
195
|
+
|
|
196
|
+
Defaults to network/coarse first (faster indoors). Set ``enable_high_accuracy=True``
|
|
197
|
+
to prefer GPS (slower, needs clear sky).
|
|
198
|
+
"""
|
|
199
|
+
return call_bridge(
|
|
200
|
+
"getCurrentPosition",
|
|
201
|
+
{"enableHighAccuracy": enable_high_accuracy, "timeout": timeout_ms},
|
|
202
|
+
callback=callback,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def keyboard_show(*, callback: Any = None) -> rx.event.EventSpec:
|
|
207
|
+
"""Show the soft keyboard."""
|
|
208
|
+
return call_bridge("keyboardShow", callback=callback)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def keyboard_hide(*, callback: Any = None) -> rx.event.EventSpec:
|
|
212
|
+
"""Hide the soft keyboard."""
|
|
213
|
+
return call_bridge("keyboardHide", callback=callback)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def browser_open(url: str, *, callback: Any = None) -> rx.event.EventSpec:
|
|
217
|
+
"""Open a URL in the in-app browser (SFSafariViewController / Custom Tabs)."""
|
|
218
|
+
return call_bridge("browserOpen", {"url": url}, callback=callback)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def fs_write(
|
|
222
|
+
path: str,
|
|
223
|
+
data: str,
|
|
224
|
+
*,
|
|
225
|
+
directory: str = "DATA",
|
|
226
|
+
callback: Any = None,
|
|
227
|
+
) -> rx.event.EventSpec:
|
|
228
|
+
"""Write UTF-8 text to the app sandbox (Capacitor Filesystem directory)."""
|
|
229
|
+
return call_bridge("fsWrite", {"path": path, "data": data, "directory": directory}, callback=callback)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def fs_read(path: str, callback: Any, *, directory: str = "DATA") -> rx.event.EventSpec:
|
|
233
|
+
"""Read UTF-8 text from the app sandbox."""
|
|
234
|
+
return call_bridge("fsRead", {"path": path, "directory": directory}, callback=callback)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def invoke(
|
|
238
|
+
plugin: str,
|
|
239
|
+
method: str,
|
|
240
|
+
args: dict[str, Any] | None = None,
|
|
241
|
+
*,
|
|
242
|
+
callback: Any = None,
|
|
243
|
+
) -> rx.event.EventSpec:
|
|
244
|
+
"""Call a Capacitor plugin method directly (extension point for custom plugins)."""
|
|
245
|
+
return call_bridge(
|
|
246
|
+
"invoke",
|
|
247
|
+
{"plugin": plugin, "method": method, "args": args or {}},
|
|
248
|
+
callback=callback,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def editor_options(
|
|
253
|
+
*,
|
|
254
|
+
enable_crop: bool = True,
|
|
255
|
+
enable_rotate: bool = True,
|
|
256
|
+
enable_compress: bool = True,
|
|
257
|
+
enable_watermark: bool = False,
|
|
258
|
+
watermark_text: str = "",
|
|
259
|
+
max_width: int = 1920,
|
|
260
|
+
quality: int = 85,
|
|
261
|
+
aspect_ratio: float | None = None,
|
|
262
|
+
save_to_sandbox: bool = False,
|
|
263
|
+
sandbox_path: str = "edited/photo.jpg",
|
|
264
|
+
return_data_url: bool = True,
|
|
265
|
+
) -> dict[str, Any]:
|
|
266
|
+
"""Build editor option dict for :func:`capture_and_edit` / :func:`edit_image`."""
|
|
267
|
+
from reflex_capacitor.components.image_editor import ImageEditorOptions
|
|
268
|
+
|
|
269
|
+
return ImageEditorOptions(
|
|
270
|
+
enable_crop=enable_crop,
|
|
271
|
+
enable_rotate=enable_rotate,
|
|
272
|
+
enable_compress=enable_compress,
|
|
273
|
+
enable_watermark=enable_watermark,
|
|
274
|
+
watermark_text=watermark_text,
|
|
275
|
+
max_width=max_width,
|
|
276
|
+
quality=quality,
|
|
277
|
+
aspect_ratio=aspect_ratio,
|
|
278
|
+
save_to_sandbox=save_to_sandbox,
|
|
279
|
+
sandbox_path=sandbox_path,
|
|
280
|
+
return_data_url=return_data_url,
|
|
281
|
+
).to_bridge()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def capture_and_edit(
|
|
285
|
+
callback: Any,
|
|
286
|
+
*,
|
|
287
|
+
source: Literal["prompt", "camera", "gallery"] = "prompt",
|
|
288
|
+
editor: dict[str, Any] | None = None,
|
|
289
|
+
) -> rx.event.EventSpec:
|
|
290
|
+
"""Pick/capture an image then open the built-in editor (device-local processing)."""
|
|
291
|
+
return call_bridge(
|
|
292
|
+
"captureAndEdit",
|
|
293
|
+
{"source": source, "editor": editor or editor_options()},
|
|
294
|
+
callback=callback,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def edit_image(
|
|
299
|
+
callback: Any,
|
|
300
|
+
*,
|
|
301
|
+
data_url: str | None = None,
|
|
302
|
+
web_path: str | None = None,
|
|
303
|
+
editor: dict[str, Any] | None = None,
|
|
304
|
+
) -> rx.event.EventSpec:
|
|
305
|
+
"""Open the built-in editor for an existing ``dataUrl`` or Capacitor ``webPath``."""
|
|
306
|
+
args: dict[str, Any] = {"editor": editor or editor_options()}
|
|
307
|
+
if data_url is not None:
|
|
308
|
+
args["dataUrl"] = data_url
|
|
309
|
+
if web_path is not None:
|
|
310
|
+
args["webPath"] = web_path
|
|
311
|
+
return call_bridge("editImage", args, callback=callback)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def setup_native_listeners(
|
|
315
|
+
*,
|
|
316
|
+
back_button: BackButtonMode = "emit",
|
|
317
|
+
callback: Any = None,
|
|
318
|
+
) -> rx.event.EventSpec:
|
|
319
|
+
"""Register native → WebView listeners (app lifecycle, back button, keyboard).
|
|
320
|
+
|
|
321
|
+
Call once on app load. Events are retrieved via :func:`poll_native_events`.
|
|
322
|
+
|
|
323
|
+
``back_button``:
|
|
324
|
+
- ``emit`` — queue event for Reflex (default; blocks auto-exit on Android)
|
|
325
|
+
- ``exit`` — quit the app immediately
|
|
326
|
+
- ``history`` — ``window.history.back()`` when possible
|
|
327
|
+
"""
|
|
328
|
+
return call_bridge(
|
|
329
|
+
"setupNativeListeners",
|
|
330
|
+
{"backButton": back_button},
|
|
331
|
+
callback=callback,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def poll_native_events(callback: Any) -> rx.event.EventSpec:
|
|
336
|
+
"""Drain queued native events since the last poll.
|
|
337
|
+
|
|
338
|
+
Callback receives ``{events: [{ts, type, detail}, ...]}``.
|
|
339
|
+
"""
|
|
340
|
+
return call_bridge("drainNativeEvents", callback=callback)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def push_register(*, callback: Any = None) -> rx.event.EventSpec:
|
|
344
|
+
"""Request push permission and register for FCM / APNs (requires push-notifications plugin).
|
|
345
|
+
|
|
346
|
+
Registration token arrives as a ``pushRegistration`` native event via
|
|
347
|
+
:func:`poll_native_events`.
|
|
348
|
+
"""
|
|
349
|
+
return call_bridge("pushRegister", callback=callback)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def push_check_permissions(*, callback: Any = None) -> rx.event.EventSpec:
|
|
353
|
+
"""Check remote push notification permission status."""
|
|
354
|
+
return call_bridge("pushCheckPermissions", callback=callback)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def push_request_permissions(*, callback: Any = None) -> rx.event.EventSpec:
|
|
358
|
+
"""Request remote push notification permission from the user."""
|
|
359
|
+
return call_bridge("pushRequestPermissions", callback=callback)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Type stubs for reflex_capacitor.bridge.api."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
import reflex as rx
|
|
6
|
+
|
|
7
|
+
ImpactStyle = Literal["HEAVY", "MEDIUM", "LIGHT"]
|
|
8
|
+
NotificationStyle = Literal["SUCCESS", "WARNING", "ERROR"]
|
|
9
|
+
StatusBarStyle = Literal["DARK", "LIGHT", "DEFAULT"]
|
|
10
|
+
ToastDuration = Literal["short", "long"]
|
|
11
|
+
|
|
12
|
+
def notify(title: str, body: str = "") -> rx.event.EventSpec: ...
|
|
13
|
+
def toast(text: str, *, duration: ToastDuration = "short") -> rx.event.EventSpec: ...
|
|
14
|
+
def haptics_impact(*, style: ImpactStyle = "MEDIUM", callback: Any = None) -> rx.event.EventSpec: ...
|
|
15
|
+
def haptics_notification(*, type: NotificationStyle = "SUCCESS", callback: Any = None) -> rx.event.EventSpec: ...
|
|
16
|
+
def haptics_vibrate(*, duration_ms: int = 300, callback: Any = None) -> rx.event.EventSpec: ...
|
|
17
|
+
def share(
|
|
18
|
+
*,
|
|
19
|
+
title: str | None = None,
|
|
20
|
+
text: str | None = None,
|
|
21
|
+
url: str | None = None,
|
|
22
|
+
dialog_title: str | None = None,
|
|
23
|
+
) -> rx.event.EventSpec: ...
|
|
24
|
+
def clipboard_write(text: str) -> rx.event.EventSpec: ...
|
|
25
|
+
def clipboard_read(callback: Any) -> rx.event.EventSpec: ...
|
|
26
|
+
def status_bar_set_style(*, style: StatusBarStyle = "DARK") -> rx.event.EventSpec: ...
|
|
27
|
+
def status_bar_hide() -> rx.event.EventSpec: ...
|
|
28
|
+
def status_bar_show() -> rx.event.EventSpec: ...
|
|
29
|
+
def splash_hide() -> rx.event.EventSpec: ...
|
|
30
|
+
def device_info(callback: Any) -> rx.event.EventSpec: ...
|
|
31
|
+
def network_status(callback: Any) -> rx.event.EventSpec: ...
|
|
32
|
+
def app_exit() -> rx.event.EventSpec: ...
|
|
33
|
+
def diagnostics(callback: Any) -> rx.event.EventSpec: ...
|
|
34
|
+
def platform_info(callback: Any) -> rx.event.EventSpec: ...
|
|
35
|
+
def bridge_logs(limit: int = 50, callback: Any = None) -> rx.event.EventSpec: ...
|
|
36
|
+
def clear_logs() -> rx.event.EventSpec: ...
|
|
37
|
+
def pref_set(key: str, value: str, *, callback: Any = None) -> rx.event.EventSpec: ...
|
|
38
|
+
def pref_get(key: str, callback: Any) -> rx.event.EventSpec: ...
|
|
39
|
+
def take_photo(
|
|
40
|
+
callback: Any, *, quality: int = 90, save_to_gallery: bool = False
|
|
41
|
+
) -> rx.event.EventSpec: ...
|
|
42
|
+
def pick_images(callback: Any, *, limit: int = 1, quality: int = 90) -> rx.event.EventSpec: ...
|
|
43
|
+
def get_current_position(callback: Any, *, enable_high_accuracy: bool = False, timeout_ms: int = 30000) -> rx.event.EventSpec: ...
|
|
44
|
+
def keyboard_show(*, callback: Any = None) -> rx.event.EventSpec: ...
|
|
45
|
+
def keyboard_hide(*, callback: Any = None) -> rx.event.EventSpec: ...
|
|
46
|
+
def browser_open(url: str, *, callback: Any = None) -> rx.event.EventSpec: ...
|
|
47
|
+
def fs_write(
|
|
48
|
+
path: str, data: str, *, directory: str = "DATA", callback: Any = None
|
|
49
|
+
) -> rx.event.EventSpec: ...
|
|
50
|
+
def fs_read(path: str, callback: Any, *, directory: str = "DATA") -> rx.event.EventSpec: ...
|
|
51
|
+
def invoke(
|
|
52
|
+
plugin: str, method: str, args: dict[str, Any] | None = None, *, callback: Any = None
|
|
53
|
+
) -> rx.event.EventSpec: ...
|
|
54
|
+
def setup_native_listeners(
|
|
55
|
+
*, back_button: Literal["emit", "exit", "history"] = "emit", callback: Any = None
|
|
56
|
+
) -> rx.event.EventSpec: ...
|
|
57
|
+
def poll_native_events(callback: Any) -> rx.event.EventSpec: ...
|
|
58
|
+
def push_register(*, callback: Any = None) -> rx.event.EventSpec: ...
|
|
59
|
+
def push_check_permissions(*, callback: Any = None) -> rx.event.EventSpec: ...
|
|
60
|
+
def push_request_permissions(*, callback: Any = None) -> rx.event.EventSpec: ...
|