Compare commits

..

3 Commits

Author SHA1 Message Date
c80b7a9a5f fix 2026-04-14 21:36:15 +02:00
db153bb38d removed "+" symbol from sign up email 2026-04-14 14:09:05 +02:00
50c4529cba fix 2026-04-14 12:49:43 +02:00
1848 changed files with 57633 additions and 95805 deletions

View File

@@ -0,0 +1,31 @@
Extension Discovery Cache
=========================
This folder is used by `package:extension_discovery` to cache lists of
packages that contains extensions for other packages.
DO NOT USE THIS FOLDER
----------------------
* Do not read (or rely) the contents of this folder.
* Do write to this folder.
If you're interested in the lists of extensions stored in this folder use the
API offered by package `extension_discovery` to get this information.
If this package doesn't work for your use-case, then don't try to read the
contents of this folder. It may change, and will not remain stable.
Use package `extension_discovery`
---------------------------------
If you want to access information from this folder.
Feel free to delete this folder
-------------------------------
Files in this folder act as a cache, and the cache is discarded if the files
are older than the modification time of `.dart_tool/package_config.json`.
Hence, it should never be necessary to clear this cache manually, if you find a
need to do please file a bug.

View File

@@ -0,0 +1 @@
{"version":2,"entries":[{"package":"sf_app_platform_mono_repo","rootUri":"../","packageUri":"lib/"}]}

View File

@@ -0,0 +1 @@
{"version":2,"entries":[{"package":"sf_app_platform_mono_repo","rootUri":"../","packageUri":"lib/"}]}

View File

@@ -0,0 +1,364 @@
{
"configVersion": 2,
"packages": [
{
"name": "ansi_styles",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/ansi_styles-0.3.2+1",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "args",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/args-2.7.0",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "async",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/async-2.13.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "characters",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/characters-1.4.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "charcode",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/charcode-1.4.0",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "checked_yaml",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/checked_yaml-2.0.4",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "cli_launcher",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/cli_launcher-0.3.2+1",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "cli_util",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/cli_util-0.4.2",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "collection",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/collection-1.19.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "conventional_commit",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/conventional_commit-0.6.1+1",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "ffi",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/ffi-2.1.4",
"packageUri": "lib/",
"languageVersion": "3.7"
},
{
"name": "file",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/file-7.0.1",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "flutter",
"rootUri": "file:///Users/juliandalcalaf/Development/flutter/packages/flutter",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "flutter_secure_storage",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "flutter_secure_storage_linux",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "flutter_secure_storage_macos",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "flutter_secure_storage_platform_interface",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage_platform_interface-1.1.2",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "flutter_secure_storage_web",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "flutter_secure_storage_windows",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "flutter_web_plugins",
"rootUri": "file:///Users/juliandalcalaf/Development/flutter/packages/flutter_web_plugins",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "glob",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/glob-2.1.3",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "graphs",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/graphs-2.3.2",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "http",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/http-1.5.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "http_parser",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/http_parser-4.1.2",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "io",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/io-1.0.5",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "js",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/js-0.6.7",
"packageUri": "lib/",
"languageVersion": "2.19"
},
{
"name": "json_annotation",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/json_annotation-4.9.0",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "material_color_utilities",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1",
"packageUri": "lib/",
"languageVersion": "2.17"
},
{
"name": "melos",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/melos-6.3.3",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "meta",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/meta-1.16.0",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "mustache_template",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/mustache_template-2.0.2",
"packageUri": "lib/",
"languageVersion": "3.7"
},
{
"name": "path",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path-1.9.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "path_provider",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider-2.1.5",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "path_provider_android",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_android-2.2.20",
"packageUri": "lib/",
"languageVersion": "3.9"
},
{
"name": "path_provider_foundation",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.3",
"packageUri": "lib/",
"languageVersion": "3.9"
},
{
"name": "path_provider_linux",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1",
"packageUri": "lib/",
"languageVersion": "2.19"
},
{
"name": "path_provider_platform_interface",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_platform_interface-2.1.2",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "path_provider_windows",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "platform",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/platform-3.1.6",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "plugin_platform_interface",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/plugin_platform_interface-2.1.8",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "pool",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/pool-1.5.2",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "process",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/process-5.0.5",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "prompts",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/prompts-2.0.0",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "pub_semver",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/pub_semver-2.2.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "pub_updater",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/pub_updater-0.5.0",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "pubspec_parse",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/pubspec_parse-1.5.0",
"packageUri": "lib/",
"languageVersion": "3.6"
},
{
"name": "sky_engine",
"rootUri": "file:///Users/juliandalcalaf/Development/flutter/bin/cache/pkg/sky_engine",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "source_span",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/source_span-1.10.1",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "stack_trace",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/stack_trace-1.12.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "string_scanner",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/string_scanner-1.4.1",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "term_glyph",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/term_glyph-1.2.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "typed_data",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/typed_data-1.4.0",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "vector_math",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/vector_math-2.2.0",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "web",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/web-1.1.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "win32",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/win32-5.15.0",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "xdg_directories",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/xdg_directories-1.1.0",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "yaml",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/yaml-3.1.3",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "yaml_edit",
"rootUri": "file:///Users/juliandalcalaf/.pub-cache/hosted/pub.dev/yaml_edit-2.2.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "sf_app_platform_mono_repo",
"rootUri": "../",
"packageUri": "lib/",
"languageVersion": "3.0"
}
],
"generator": "pub",
"generatorVersion": "3.9.2",
"flutterRoot": "file:///Users/juliandalcalaf/Development/flutter",
"flutterVersion": "3.35.7",
"pubCache": "file:///Users/juliandalcalaf/.pub-cache"
}

View File

@@ -0,0 +1,491 @@
{
"roots": [
"sf_app_platform_mono_repo"
],
"packages": [
{
"name": "sf_app_platform_mono_repo",
"version": "0.0.0",
"dependencies": [
"flutter_secure_storage"
],
"devDependencies": [
"melos"
]
},
{
"name": "flutter_secure_storage",
"version": "9.2.4",
"dependencies": [
"flutter",
"flutter_secure_storage_linux",
"flutter_secure_storage_macos",
"flutter_secure_storage_platform_interface",
"flutter_secure_storage_web",
"flutter_secure_storage_windows",
"meta"
]
},
{
"name": "melos",
"version": "6.3.3",
"dependencies": [
"ansi_styles",
"args",
"async",
"cli_launcher",
"cli_util",
"collection",
"conventional_commit",
"file",
"glob",
"graphs",
"http",
"meta",
"mustache_template",
"path",
"platform",
"pool",
"prompts",
"pub_semver",
"pub_updater",
"pubspec_parse",
"string_scanner",
"yaml",
"yaml_edit"
]
},
{
"name": "meta",
"version": "1.16.0",
"dependencies": []
},
{
"name": "flutter_secure_storage_windows",
"version": "3.1.2",
"dependencies": [
"ffi",
"flutter",
"flutter_secure_storage_platform_interface",
"path",
"path_provider",
"win32"
]
},
{
"name": "flutter_secure_storage_web",
"version": "1.2.1",
"dependencies": [
"flutter",
"flutter_secure_storage_platform_interface",
"flutter_web_plugins",
"js"
]
},
{
"name": "flutter_secure_storage_platform_interface",
"version": "1.1.2",
"dependencies": [
"flutter",
"plugin_platform_interface"
]
},
{
"name": "flutter_secure_storage_macos",
"version": "3.1.3",
"dependencies": [
"flutter",
"flutter_secure_storage_platform_interface"
]
},
{
"name": "flutter_secure_storage_linux",
"version": "1.2.3",
"dependencies": [
"flutter",
"flutter_secure_storage_platform_interface"
]
},
{
"name": "flutter",
"version": "0.0.0",
"dependencies": [
"characters",
"collection",
"material_color_utilities",
"meta",
"sky_engine",
"vector_math"
]
},
{
"name": "yaml_edit",
"version": "2.2.2",
"dependencies": [
"collection",
"meta",
"source_span",
"yaml"
]
},
{
"name": "yaml",
"version": "3.1.3",
"dependencies": [
"collection",
"source_span",
"string_scanner"
]
},
{
"name": "string_scanner",
"version": "1.4.1",
"dependencies": [
"source_span"
]
},
{
"name": "pubspec_parse",
"version": "1.5.0",
"dependencies": [
"checked_yaml",
"collection",
"json_annotation",
"pub_semver",
"yaml"
]
},
{
"name": "pub_updater",
"version": "0.5.0",
"dependencies": [
"http",
"json_annotation",
"process",
"pub_semver"
]
},
{
"name": "pub_semver",
"version": "2.2.0",
"dependencies": [
"collection"
]
},
{
"name": "prompts",
"version": "2.0.0",
"dependencies": [
"charcode",
"io"
]
},
{
"name": "pool",
"version": "1.5.2",
"dependencies": [
"async",
"stack_trace"
]
},
{
"name": "platform",
"version": "3.1.6",
"dependencies": []
},
{
"name": "path",
"version": "1.9.1",
"dependencies": []
},
{
"name": "mustache_template",
"version": "2.0.2",
"dependencies": []
},
{
"name": "http",
"version": "1.5.0",
"dependencies": [
"async",
"http_parser",
"meta",
"web"
]
},
{
"name": "graphs",
"version": "2.3.2",
"dependencies": [
"collection"
]
},
{
"name": "glob",
"version": "2.1.3",
"dependencies": [
"async",
"collection",
"file",
"path",
"string_scanner"
]
},
{
"name": "file",
"version": "7.0.1",
"dependencies": [
"meta",
"path"
]
},
{
"name": "conventional_commit",
"version": "0.6.1+1",
"dependencies": []
},
{
"name": "collection",
"version": "1.19.1",
"dependencies": []
},
{
"name": "cli_util",
"version": "0.4.2",
"dependencies": [
"meta",
"path"
]
},
{
"name": "cli_launcher",
"version": "0.3.2+1",
"dependencies": [
"path",
"yaml"
]
},
{
"name": "async",
"version": "2.13.0",
"dependencies": [
"collection",
"meta"
]
},
{
"name": "args",
"version": "2.7.0",
"dependencies": []
},
{
"name": "ansi_styles",
"version": "0.3.2+1",
"dependencies": []
},
{
"name": "win32",
"version": "5.15.0",
"dependencies": [
"ffi"
]
},
{
"name": "path_provider",
"version": "2.1.5",
"dependencies": [
"flutter",
"path_provider_android",
"path_provider_foundation",
"path_provider_linux",
"path_provider_platform_interface",
"path_provider_windows"
]
},
{
"name": "ffi",
"version": "2.1.4",
"dependencies": []
},
{
"name": "js",
"version": "0.6.7",
"dependencies": [
"meta"
]
},
{
"name": "flutter_web_plugins",
"version": "0.0.0",
"dependencies": [
"flutter"
]
},
{
"name": "plugin_platform_interface",
"version": "2.1.8",
"dependencies": [
"meta"
]
},
{
"name": "sky_engine",
"version": "0.0.0",
"dependencies": []
},
{
"name": "vector_math",
"version": "2.2.0",
"dependencies": []
},
{
"name": "material_color_utilities",
"version": "0.11.1",
"dependencies": [
"collection"
]
},
{
"name": "characters",
"version": "1.4.0",
"dependencies": []
},
{
"name": "source_span",
"version": "1.10.1",
"dependencies": [
"collection",
"path",
"term_glyph"
]
},
{
"name": "json_annotation",
"version": "4.9.0",
"dependencies": [
"meta"
]
},
{
"name": "checked_yaml",
"version": "2.0.4",
"dependencies": [
"json_annotation",
"source_span",
"yaml"
]
},
{
"name": "process",
"version": "5.0.5",
"dependencies": [
"file",
"path",
"platform"
]
},
{
"name": "io",
"version": "1.0.5",
"dependencies": [
"meta",
"path",
"string_scanner"
]
},
{
"name": "charcode",
"version": "1.4.0",
"dependencies": []
},
{
"name": "stack_trace",
"version": "1.12.1",
"dependencies": [
"path"
]
},
{
"name": "web",
"version": "1.1.1",
"dependencies": []
},
{
"name": "http_parser",
"version": "4.1.2",
"dependencies": [
"collection",
"source_span",
"string_scanner",
"typed_data"
]
},
{
"name": "path_provider_windows",
"version": "2.3.0",
"dependencies": [
"ffi",
"flutter",
"path",
"path_provider_platform_interface"
]
},
{
"name": "path_provider_platform_interface",
"version": "2.1.2",
"dependencies": [
"flutter",
"platform",
"plugin_platform_interface"
]
},
{
"name": "path_provider_linux",
"version": "2.2.1",
"dependencies": [
"ffi",
"flutter",
"path",
"path_provider_platform_interface",
"xdg_directories"
]
},
{
"name": "path_provider_foundation",
"version": "2.4.3",
"dependencies": [
"flutter",
"path_provider_platform_interface"
]
},
{
"name": "path_provider_android",
"version": "2.2.20",
"dependencies": [
"flutter",
"path_provider_platform_interface"
]
},
{
"name": "term_glyph",
"version": "1.2.2",
"dependencies": []
},
{
"name": "typed_data",
"version": "1.4.0",
"dependencies": [
"collection"
]
},
{
"name": "xdg_directories",
"version": "1.1.0",
"dependencies": [
"meta",
"path"
]
}
],
"configVersion": 1
}

Binary file not shown.

1
.dart_tool/version Normal file
View File

@@ -0,0 +1 @@
3.35.7

35
.gitignore vendored
View File

@@ -1,35 +0,0 @@
# Dart / Flutter workspace caches (regenerated by `flutter pub get` / `dart pub get`)
.dart_tool/
**/.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
# Per-package build outputs
**/build/
**/coverage/
# Flutter ephemeral plugin symlinks and helpers (regenerated on pub get)
**/ios/Flutter/ephemeral/
**/linux/flutter/ephemeral/
**/macos/Flutter/ephemeral/
**/windows/flutter/ephemeral/
# Flutter iOS build config (regenerated on pub get; contains machine-specific paths)
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/flutter_export_environment.sh
# IDE
.idea/
*.iml
.vscode/
# App config (contains API keys, passed via --dart-define-from-file)
apps/mobile_app/config/*.json
# Golden test diff outputs (transient artifacts written when goldens fail)
**/test/failures/
# macOS
.DS_Store
**/.DS_Store

2
.idea/.name generated
View File

@@ -1 +1 @@
sf_app_platform_mono_repo
sf-app-platform

37
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/account/melos_account.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/account/melos_account.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/activity/melos_activity.iml" filepath="$PROJECT_DIR$/modules/activity/melos_activity.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/auth/melos_auth.iml" filepath="$PROJECT_DIR$/modules/auth/melos_auth.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/control_panel/melos_control_panel.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/control_panel/melos_control_panel.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/customer_service/melos_customer_service.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/customer_service/melos_customer_service.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/dashboard_shell/melos_dashboard_shell.iml" filepath="$PROJECT_DIR$/modules/dashboard_shell/melos_dashboard_shell.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/design_system/melos_design_system.iml" filepath="$PROJECT_DIR$/packages/design_system/melos_design_system.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/device_management/melos_device_management.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/device_management/melos_device_management.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/flutter_treezor_entrust_sdk_bridge/melos_flutter_treezor_entrust_sdk_bridge.iml" filepath="$PROJECT_DIR$/packages/flutter_treezor_entrust_sdk_bridge/melos_flutter_treezor_entrust_sdk_bridge.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/flutter_treezor_entrust_sdk_bridge/example/melos_flutter_treezor_entrust_sdk_bridge_example.iml" filepath="$PROJECT_DIR$/packages/flutter_treezor_entrust_sdk_bridge/example/melos_flutter_treezor_entrust_sdk_bridge_example.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/fonts/melos_fonts.iml" filepath="$PROJECT_DIR$/packages/fonts/melos_fonts.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/home/melos_home.iml" filepath="$PROJECT_DIR$/modules/home/melos_home.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/melos_legacy.iml" filepath="$PROJECT_DIR$/modules/legacy/melos_legacy.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/legacy_auth/melos_legacy_auth.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/legacy_auth/melos_legacy_auth.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/legacy_dashboard_shell/melos_legacy_dashboard_shell.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/legacy_dashboard_shell/melos_legacy_dashboard_shell.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/packages/legacy_shared/melos_legacy_shared.iml" filepath="$PROJECT_DIR$/modules/legacy/packages/legacy_shared/melos_legacy_shared.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/location/melos_location.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/location/melos_location.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/navigation/melos_navigation.iml" filepath="$PROJECT_DIR$/packages/navigation/melos_navigation.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/notifications/melos_notifications.iml" filepath="$PROJECT_DIR$/modules/notifications/melos_notifications.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/payments/melos_payments.iml" filepath="$PROJECT_DIR$/packages/payments/melos_payments.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/profile/melos_profile.iml" filepath="$PROJECT_DIR$/modules/profile/melos_profile.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/sca_treezor/melos_sca_treezor.iml" filepath="$PROJECT_DIR$/packages/sca_treezor/melos_sca_treezor.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/legacy/modules/settings/melos_settings.iml" filepath="$PROJECT_DIR$/modules/legacy/modules/settings/melos_settings.iml" />
<module fileurl="file://$PROJECT_DIR$/apps/mobile_app/melos_sf_app_platform.iml" filepath="$PROJECT_DIR$/apps/mobile_app/melos_sf_app_platform.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/sf_infrastructure/melos_sf_infrastructure.iml" filepath="$PROJECT_DIR$/packages/sf_infrastructure/melos_sf_infrastructure.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/sf_localizations/melos_sf_localizations.iml" filepath="$PROJECT_DIR$/packages/sf_localizations/melos_sf_localizations.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/sf_shared/melos_sf_shared.iml" filepath="$PROJECT_DIR$/packages/sf_shared/melos_sf_shared.iml" />
<module fileurl="file://$PROJECT_DIR$/modules/splash/melos_splash.iml" filepath="$PROJECT_DIR$/modules/splash/melos_splash.iml" />
<module fileurl="file://$PROJECT_DIR$/packages/utils/melos_utils.iml" filepath="$PROJECT_DIR$/packages/utils/melos_utils.iml" />
<module fileurl="file://$PROJECT_DIR$/melos_sf-app-platform.iml" filepath="$PROJECT_DIR$/melos_sf-app-platform.iml" />
</modules>
</component>
</project>

View File

@@ -6,7 +6,6 @@
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos bootstrap" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -6,7 +6,6 @@
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos clean" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter Run -&gt; 'flutter_treezor_entrust_sdk_bridge_example'" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="filePath" value="$PROJECT_DIR$/packages/flutter_treezor_entrust_sdk_bridge/example/lib/main.dart" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,7 +1,7 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter Test -&gt; 'auth'" type="FlutterTestConfigType" factoryName="Flutter Test">
<option name="testDir" value="$PROJECT_DIR$/modules/payment/modules/auth/test" />
<option name="testDir" value="$PROJECT_DIR$/modules/auth/test" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,7 +1,7 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter Test -&gt; 'dashboard_shell'" type="FlutterTestConfigType" factoryName="Flutter Test">
<option name="testDir" value="$PROJECT_DIR$/modules/payment/modules/dashboard_shell/test" />
<option name="testDir" value="$PROJECT_DIR$/modules/dashboard_shell/test" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,7 +1,7 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter Test -&gt; 'home'" type="FlutterTestConfigType" factoryName="Flutter Test">
<option name="testDir" value="$PROJECT_DIR$/modules/payment/modules/home/test" />
<option name="testDir" value="$PROJECT_DIR$/modules/home/test" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,7 +1,7 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter Test -&gt; 'notifications'" type="FlutterTestConfigType" factoryName="Flutter Test">
<option name="testDir" value="$PROJECT_DIR$/modules/payment/modules/notifications/test" />
<option name="testDir" value="$PROJECT_DIR$/modules/notifications/test" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,7 +1,7 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Flutter Test -&gt; 'profile'" type="FlutterTestConfigType" factoryName="Flutter Test">
<option name="testDir" value="$PROJECT_DIR$/modules/payment/modules/profile/test" />
<option name="testDir" value="$PROJECT_DIR$/modules/profile/test" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,12 +0,0 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'analyze'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run analyze" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -1,12 +0,0 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'app:dev'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run app:dev" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -1,12 +0,0 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'app:prod'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run app:prod" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -1,12 +0,0 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'app:staging'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run app:staging" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -1,12 +1,11 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'test'" type="ShConfigurationType">
<configuration default="false" name="Melos Run -&gt; 'bootstrap'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run test" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<option name="SCRIPT_TEXT" value="melos run bootstrap" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,12 +0,0 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'check-deps'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run check-deps" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -1,12 +0,0 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'clean'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run clean" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -1,12 +0,0 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'format'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run format" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -1,12 +0,0 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'format:check'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run format:check" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -1,12 +0,0 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'generate'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run generate" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -1,12 +0,0 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'outdated'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run outdated" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

View File

@@ -1,12 +0,0 @@
<!-- Generated by Melos -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Melos Run -&gt; 'sync-deps'" type="ShConfigurationType">
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="SCRIPT_TEXT" value="melos run sync-deps" />
<option name="EXECUTE_IN_TERMINAL" value="true"/>
<method v="2" />
</configuration>
</component>

67
.vscode/launch.json vendored
View File

@@ -2,88 +2,39 @@
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
//
// Configurations are split between (Legacy) and (Payment) variants.
// (Legacy) is the default and matches historical behavior; (Payment)
// boots straight into the Treezor wallet flow via APP_MODE=payment.
"version": "0.2.0",
"configurations": [
{
"name": "SF Development (Legacy)",
"name": "SF Development",
"cwd": "apps/mobile_app",
"request": "launch",
"type": "dart",
"program": "lib/main_development.dart",
"toolArgs": [
"args": [
"--flavor",
"development",
"--dart-define-from-file=config/development.json",
"--dart-define=APP_MODE=legacy"
"--dart-define-from-file=config/development.json"
]
},
{
"name": "SF Development (Payment)",
"name": "SF Staging",
"cwd": "apps/mobile_app",
"request": "launch",
"type": "dart",
"program": "lib/main_development.dart",
"toolArgs": [
"--flavor",
"development",
"--dart-define-from-file=config/development.json",
"--dart-define=APP_MODE=payment"
]
},
{
"name": "SF Staging (Legacy)",
"cwd": "apps/mobile_app",
"request": "launch",
"type": "dart",
"program": "lib/main_staging.dart",
"toolArgs": [
"args": [
"--flavor",
"staging",
"--dart-define-from-file=config/staging.json",
"--dart-define=APP_MODE=legacy"
"--dart-define-from-file=config/staging.json"
]
},
{
"name": "SF Staging (Payment)",
"name": "SF Production",
"cwd": "apps/mobile_app",
"request": "launch",
"type": "dart",
"program": "lib/main_staging.dart",
"toolArgs": [
"--flavor",
"staging",
"--dart-define-from-file=config/staging.json",
"--dart-define=APP_MODE=payment"
]
},
{
"name": "SF Production (Legacy)",
"cwd": "apps/mobile_app",
"request": "launch",
"type": "dart",
"program": "lib/main_production.dart",
"toolArgs": [
"args": [
"--flavor",
"production",
"--dart-define-from-file=config/production.json",
"--dart-define=APP_MODE=legacy"
]
},
{
"name": "SF Production (Payment)",
"cwd": "apps/mobile_app",
"request": "launch",
"type": "dart",
"program": "lib/main_production.dart",
"toolArgs": [
"--flavor",
"production",
"--dart-define-from-file=config/production.json",
"--dart-define=APP_MODE=payment"
"--dart-define-from-file=config/production.json"
]
}
]

View File

@@ -1,195 +0,0 @@
# Integración Videollamadas Juphoon — Resumen del progreso
## Contexto
SaveFamily S.L (Bizkaia) está integrando videollamadas y chat entre su app móvil Flutter y sus smartwatches infantiles (RTOS/Android), usando el SDK de Juphoon (`jc_sdk`).
**Actores:**
- Grupo SaveFamily S.L — Cliente, dueño de la app y backend
- Shenzhen i365-Tech Co., Limited (Jane Zhang, Carmen) — Fabricante hardware, intermediario comercial
- Juphoon/JUQU (Allen) — Proveedor del SDK de videollamadas
- SeTracker — Proveedor del firmware del reloj y servidores auxiliares
**Cotización aprobada: $8,835** (Integración $2,200 + Chat $2,950 + Cloud Photo Album $735 + Encryption $2,950)
---
## Lo que se hizo
### 1. Análisis de documentación (3 rondas)
- **Ronda 1** (31-03-2026): 50 preguntas técnicas → 27/50 respondidas (54%)
- **Ronda 2** (01-04-2026): 17 preguntas generales → 17/17 respondidas (calidad desigual)
- **Ronda 3** (09-04-2026): Documentación oficial SDK recibida — Quickstart V1.1 Flutter (13 páginas), sequence diagrams, protocolo TCP, connection/mutual dialing process
- Documentos generados: análisis completo, conclusiones, preguntas bilingües ES/EN, análisis cruzado de respuestas
### 2. Cuenta Juphoon Cloud creada (16-04-2026)
- Consola: juphoon.com (+34)
- App creada: "SaveFamily" (tipo IoT, escenario Smartwatch)
- **AppKey:** `9efcf2d889dc8a0320925096`
- **AppSecret:** `ui7pr73ggl5rr0gf01np` (solo backend)
- **AES_KEY IoT:** `8e3637pG7E9144E0` (solo backend)
- Token auth activado en consola
### 3. Paquete `packages/videocall_sdk/` creado
Wrapper 100% del `jc_sdk` v2.16.5 con arquitectura sólida (patrón `sca_treezor` del monorepo):
- **7 servicios** cubriendo toda la API pública del SDK:
- `VideocallClient` → JCClient (auth, login, logout, messaging)
- `VideocallCallService` → JCCall (llamadas 1-to-1)
- `VideocallDeviceService` → JCMediaDevice (cámara, mic, speaker)
- `VideocallChannelService` → JCMediaChannel (llamadas grupales)
- `VideocallPushService` → JCPush (push notifications)
- `VideocallNetService` → JCNet (estado de red)
- `VideocallLogService` → JCLog (logging)
- **Constructor injection** (no singletons estáticos)
- **GetIt module** (`videocallSdkModule(config)`)
- **`VideocallSdkManager`** orquestador de inicialización (Client → Device → Call/Channel/Push)
- **`VideocallSdkConfig`** abstracto para config por entorno
- **Riverpod providers** + StreamProviders para UI reactiva
- **Callbacks del SDK → Dart Streams**
### 4. Permisos nativos configurados
- **Android:** RECORD_AUDIO, ACCESS_WIFI_STATE, MODIFY_AUDIO_SETTINGS, BLUETOOTH + uses-feature (camera, bluetooth) + ProGuard rules (juphoon, justalk)
- **iOS:** NSMicrophoneUsageDescription, NSPhotoLibraryUsageDescription, NSCameraUsageDescription actualizado + Podfile GCC_PREPROCESSOR_DEFINITIONS (PERMISSION_CAMERA, PHOTOS, MICROPHONE)
### 5. AppKey configurado por entorno
- `juphoonAppKey` en development.json, staging.json, production.json
- `Environment.juphoonAppKey` via `String.fromEnvironment()`
- `SaveFamilyVideocallConfig` implementa `VideocallSdkConfig`
- `videocallSdkModule(config)` integrado en `init_app.dart`
### 6. Feature `videocall/` creada en device_management
Feature completa siguiendo el patrón del monorepo (builder + domain + data + presentation):
**Domain:**
- `videocall_error.dart` — enums de error/success/screenMode
- `videocall_participant.dart` — entidad Freezed para participantes grupales
- `videocall_signaling_repository.dart` — interface señalización backend
**Data:**
- `videocall_signaling_datasource.dart` — interface
- `videocall_signaling_datasource_impl.dart` — placeholder (TODO cuando backend dé spec)
- `videocall_signaling_repository_impl.dart` — impl
**State:**
- `videocall_view_state.dart` — Freezed state 1-to-1 (screenMode, sdk ready, mic/speaker/camera, canvas, error/success events)
- `videocall_view_model.dart` — Notifier 1-to-1 (init, login, call, answer, hangup, mute, speaker, camera, streams del SDK)
- `group_call_view_state.dart` — Freezed state grupal
- `group_call_view_model.dart` — Notifier grupal (join, leave, participants, streams)
**Widgets:**
- `video_view_widget.dart` — renderiza JCMediaDeviceVideoCanvas (iOS/Android)
- `call_controls_widget.dart` — mic, speaker, camera, hangup (botones circulares)
- `call_status_indicator.dart` — "Llamando...", "Conectando..."
- `incoming_call_overlay.dart` — aceptar/rechazar llamada entrante (fullscreen)
- `participant_tile_widget.dart` — tile individual con video + nombre
- `participant_grid_widget.dart` — grid responsivo de participantes
**Screen:**
- `videocall_screen.dart` — 4 modos: idle (input userID + botón llamar), outgoing (llamando...), incoming (overlay aceptar/rechazar), inCall (video fullscreen + PIP + controles)
**Routing:**
- `videocall_builder.dart` — GoRouter builder
- Ruta: `/legacy/dashboard/device_management/videocall`
### 7. Code review realizado
Score: **6/10 — Request changes**
**Issues identificados (pendientes de corregir):**
1. Hardcoded test credentials (`p_test1/test123`) en UI de producción → guardar con `kDebugMode`
2. `_onCallItemRemove` llama async sin await → race condition
3. Todos los errores mapean a `I18n.errorGeneric` → sin diferenciación para el usuario
4. `videocall_screen.dart` (310 líneas) demasiado grande → extraer `_IdleView` y `_InCallView` a ficheros separados como `ConsumerWidget`
5. `group_call_view_model.dart` es dead code (no lo consume ninguna screen)
6. Signaling placeholder con `throw UnimplementedError` → cambiar a no-op
7. `VideocallParticipant` (domain) expone tipo SDK (`JCMediaDeviceVideoCanvas`) → mover al ViewModel
---
## Dónde quedamos
- **Rama:** `feature/videocall-sdk-integration`
- Los cambios del paquete `videocall_sdk` están **commiteados y pusheados** (3 commits)
- Los cambios de la feature están en disco pero **sin commitear** (necesitan correcciones del code review)
- `fusion-app` avanzó y revirtió algunos cambios compartidos (permisos, rutas) → hay que re-sincronizar
---
## Pendiente
### Correcciones del code review
- [ ] Guardar test credentials con `kDebugMode`
- [ ] Fix async race en `_onCallItemRemove`
- [ ] Implementar mensajes de error diferenciados
- [ ] Extraer `_IdleView` y `_InCallView` a ficheros separados
- [ ] Integrar o excluir group call ViewModel
- [ ] Cambiar signaling placeholder de throw a no-op
- [ ] Remover SDK type de domain entity
### Pruebas APP↔APP (primera llamada real)
- [ ] Login con 2 userIDs de prueba (`p_test1`, `p_test2`)
- [ ] Videollamada entre dos teléfonos físicos
- [ ] Probar incoming call, reject, hangup, mute, camera switch
- [ ] Probar app cerrada en iOS (riesgo #1 — push/background)
### Integración con backend
- [ ] Obtener API REST del backend SaveFamily para señalización
- [ ] Definir formato userID con backend (`p_<cuenta>` + sanitización emails)
- [ ] Implementar datasource de señalización
### Pruebas APP↔Reloj
- [ ] Llamada APP → Reloj
- [ ] Llamada Reloj → APP
- [ ] Llamadas grupales
### Producción
- [ ] Token auth (backend genera tokens con AppSecret)
- [ ] AppKeys separadas por entorno
- [ ] Push/background iOS (PushKit + CallKit si necesario)
---
## 3 riesgos abiertos antes del pago ($8,835)
| # | Riesgo | Estado |
|---|---|---|
| 1 | **Push/background iOS** — la doc no menciona FCM/APNs, CallKit ni ConnectionService. App cerrada = no recibe llamadas. Posible deal-breaker | ❌ Sin respuesta |
| 2 | **GDPR sin DPA** — servidores UE pero sin DPA, sin control routing, datos de menores | ❌ Email enviado 01-04, sin respuesta |
| 3 | **Chat sin spec** — $2,950 sin lista de features, "mira SeTracker2" | ❌ Sin spec |
| + | **Encryption** — $2,950 pagados, cero documentación del módulo | ❌ Sin spec |
---
## Arquitectura confirmada
```
APP (Flutter + jc_sdk) ←→ Juphoon Cloud (solo media)
↕ API REST
Backend SaveFamily ←→ Backend i365/SeTracker ←→ Smartwatch (firmware + jrtc_* C API)
↕ TCP plano
```
- El "Server" del protocolo TCP es **i365**, NO Juphoon
- Juphoon Cloud **solo rutea audio/video** (media plane)
- La señalización (quién llama a quién) va por el backend
## Naming conventions (protocolo TCP)
| Tipo | Formato | Ejemplo |
|---|---|---|
| Watch userID | `w_` + IMEI | `w_000078932675810` |
| Mobile userID | `p_` + APP account | `p_abc10086` |
| Group room | `did` + `_group` | `0245423235_group` |
| Single room | `did` + `_` + APP account | `0245423235_abc10086` |
`@` y `.` se reemplazan por `_` en room numbers y userIDs.
## Documentación de referencia
- Quickstart V1.1: `~/Downloads/Video call API_ Juphoon Flutter SDK quickstart V1.1.pdf`
- TCP Protocol: `~/Downloads/Juphoon Video Call TCP Protocol.docx`
- Connection process: `~/Downloads/video call connection process Rev2.docx`
- Mutual dialing: `~/Downloads/video call mutual dialing process.docx`
- Schematics: `~/Downloads/schematics _2025.03.26 (2)/`
- pub.dev: https://pub.dev/packages/jc_sdk
- Consola Juphoon: https://developer.juphoon.com

View File

@@ -3,11 +3,6 @@ import java.io.FileInputStream
plugins {
id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
id("com.google.firebase.firebase-perf")
id("com.google.firebase.crashlytics")
// END: FlutterFire Configuration
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
@@ -27,9 +22,6 @@ android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
// Required by flutter_local_notifications (and any future libs that
// need Java 8+ APIs on older Android API levels).
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
@@ -98,13 +90,3 @@ android {
flutter {
source = "../.."
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
// Required by AntelopAwareMessagingService to forward FCM pushes to the
// Antelop SDK. The Antelop AAR (com.entrust.antelop:antelop) is brought
// in transitively through the flutter_treezor_entrust_sdk_bridge plugin.
implementation(platform("com.google.firebase:firebase-bom:33.4.0"))
implementation("com.google.firebase:firebase-messaging")
implementation("com.entrust.antelop:antelop:2.6.4")
}

View File

@@ -1,48 +0,0 @@
{
"project_info": {
"project_number": "535646668726",
"project_id": "sf-platform-pre",
"storage_bucket": "sf-platform-pre.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:535646668726:android:c3a09d6c26f0cdf95e6317",
"android_client_info": {
"package_name": "com.savefamily.app.dev"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyAzo8E_L6iUYWmK1BDFpNqRri1df6CqJiY"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:535646668726:android:b87245b807258e3e5e6317",
"android_client_info": {
"package_name": "com.savefamily.app.stag"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyAzo8E_L6iUYWmK1BDFpNqRri1df6CqJiY"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@@ -21,11 +21,3 @@
-dontwarn com.huawei.hms.location.LocationServices
-dontwarn com.huawei.hms.push.RemoteMessage
-dontwarn com.huawei.hms.security.SecComponentInstallWizard
## Juphoon jc_sdk
-dontwarn com.juphoon.*
-keep class com.juphoon.**{*;}
-dontwarn com.justalk.*
-keep class com.justalk.**{*;}
-keepattributes InnerClasses
-keep class **.R$* {*;}

View File

@@ -1,26 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<application
android:label="@string/app_name"
@@ -50,29 +34,6 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="sf_default_channel" />
<!-- Wrap FCM with Antelop SDK forwarding (see AntelopAwareMessagingService). -->
<service
android:name=".AntelopAwareMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- Disable Antelop's stock FCM service so AntelopAwareMessagingService is the only handler. -->
<service
android:name="fr.antelop.exposed.DefaultAntelopFirebaseMessagingService"
tools:node="remove" />
<!-- Disable the firebase_messaging plugin's FCM service so AntelopAwareMessagingService is the only handler. -->
<service
android:name="io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingService"
tools:node="remove" />
</application>
<!-- Required to query activities that can process text, see:

View File

@@ -1,33 +0,0 @@
package com.savefamily.app
import com.google.firebase.messaging.RemoteMessage
import fr.antelop.sdk.firebase.AntelopFirebaseMessagingUtil
import io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingService
/**
* FCM service that gives the Antelop SDK first dibs on every push, then
* delegates the rest to the firebase_messaging Flutter plugin so Dart still
* receives the notifications it expects.
*
* Without this, only one FirebaseMessagingService can win the
* com.google.firebase.MESSAGING_EVENT intent — and once we added the
* firebase_messaging plugin, its FlutterFirebaseMessagingService started
* winning over Antelop's DefaultAntelopFirebaseMessagingService, leaving the
* SDK forever waiting for activation pushes that never reached it.
*/
class AntelopAwareMessagingService : FlutterFirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val handled = AntelopFirebaseMessagingUtil.onMessageReceived(
applicationContext,
remoteMessage,
)
if (!handled) {
super.onMessageReceived(remoteMessage)
}
}
override fun onNewToken(token: String) {
super.onNewToken(token)
AntelopFirebaseMessagingUtil.onTokenRefresh(applicationContext)
}
}

View File

@@ -1,29 +0,0 @@
{
"project_info": {
"project_number": "950566980029",
"project_id": "sf-platform-pro",
"storage_bucket": "sf-platform-pro.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:950566980029:android:75a7c10b6259d09681aad4",
"android_client_info": {
"package_name": "com.savefamily.app"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyDkjNdOAK0ype7wgdgiC1BCKV_pP4s_mlA"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

File diff suppressed because one or more lines are too long

View File

@@ -21,9 +21,6 @@ plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("com.google.gms.google-services") version "4.4.2" apply false
id("com.google.firebase.firebase-perf") version "1.4.2" apply false
id("com.google.firebase.crashlytics") version "3.0.2" apply false
}
include(":app")

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,5 @@
{
"env": "development",
"apiBaseUrl": "https://api-neki-b2b.neki.es/gateway/api/",
"apiOrigin": "https://neki-b2b.neki.es"
}

View File

@@ -0,0 +1,5 @@
{
"env": "production",
"apiBaseUrl": "https://api-neki-b2b.neki.es/gateway/api/",
"apiOrigin": "https://neki-b2b.neki.es"
}

View File

@@ -0,0 +1,5 @@
{
"env": "staging",
"apiBaseUrl": "https://api-platform.pre.savefamilygps.net/gateway/api/",
"apiOrigin": "https://platform.pre.savefamilygps.net"
}

View File

@@ -1,637 +0,0 @@
# Catálogo de Analíticas — SaveFamily (módulo legacy)
> Documento para el equipo de Marketing. Describe cada evento de Firebase
> Analytics que la app envía desde el módulo legacy: qué significa, cuándo se
> dispara, qué parámetros trae, y qué insight ofrece.
>
> **Ambiente:** Eventos visibles en vivo en Firebase Console Analytics
> **DebugView** (para builds debug/profile con el flag de debug activado).
> Los reportes históricos están en **Realtime**, **Engagement Events** y
> **Engagement Pages and screens**.
>
> **Parámetro común:** Cada evento incluye automáticamente un parámetro
> `consent_status` (`true` / `false`) para permitir filtrado por
> consentimiento GDPR cuando corresponda.
---
## Índice
1. [User Properties (propiedades del usuario)](#user-properties)
2. [Screen Views (vistas de pantalla automáticas)](#screen-views)
3. [Autenticación (`legacy_auth_*`)](#autenticación)
4. [Cuenta (`legacy_account_*`)](#cuenta)
5. [Dispositivo — Setup / alta (`legacy_device_setup_*`)](#dispositivo--setup)
6. [Dispositivo — Funciones (`legacy_device_*`)](#dispositivo--funciones)
7. [Contactos del dispositivo (`legacy_contacts_*`)](#contactos-del-dispositivo)
8. [Ajustes (`legacy_settings_*`)](#ajustes)
9. [Soporte (`legacy_support_*`)](#soporte)
10. [Onboarding (`legacy_onboarding_*`)](#onboarding)
11. [Panel principal (`legacy_control_panel_*`)](#panel-principal)
12. [Ubicación y mapa (`legacy_location_*`)](#ubicación-y-mapa)
---
## User Properties
Son propiedades que se setean una sola vez por usuario (al hacer login) y
sirven para **segmentar** a los usuarios en los reportes. Cualquier evento
puede cruzarse por estas dimensiones en Firebase Analytics.
| Propiedad | Descripción | Valores ejemplo | Cuándo se setea |
|---|---|---|---|
| `env` | Ambiente de la app | `development`, `staging`, `production` | Al arrancar la app |
| `user_id` (interna) | Identificador único del usuario | UUID del backend | Al confirmar el login (después del 2FA) |
| `user_role` | Rol del usuario en el backend | `client`, `admin`, etc. | Al login |
| `user_language` | Idioma preferido del usuario | `es`, `en`, `fr`, `de`, `it`, `pt` | Al login |
| `user_signup_date` | Fecha de creación de la cuenta (ISO 8601 UTC) | `2024-04-07T10:34:42.000Z` | Al login |
| `user_has_phone` | Si tiene teléfono registrado | `true` / `false` | Al login |
| `user_has_api_key` | Si tiene una API key asignada (usuario técnico) | `true` / `false` | Al login |
> **Nota futura:** Cuando se lance el plan premium, se agregará
> `user_plan` (`free` / `premium` / `family`) para segmentar la base por
> plan.
---
## Screen Views
Cada vez que el usuario navega a una pantalla, Firebase recibe un evento
automático `screen_view` con el parámetro `screen_name` igual al nombre
lógico de la ruta (no el nombre de clase Flutter).
**Esto se captura automáticamente**, sin instrumentación manual en cada
pantalla, mediante un listener del router. **También captura los cambios de
tab del bottom navigation** (home device functions mapa chat).
### Pantallas del módulo legacy que se trackean
| Screen name | Pantalla |
|---|---|
| `splash` | Pantalla de carga inicial |
| `legacy_onboarding` | Intro/onboarding |
| `legacy_login` | Pantalla de login |
| `legacy_signup` | Alta de cuenta |
| `legacy_recover_password` | Recuperación de contraseña |
| `legacy_device_setup` | Wizard de alta de reloj/dispositivo |
| `legacy_request_link_phone` | Inicio de vinculación de teléfono |
| `legacy_verify_link_phone_code` | Verificación del código OTP |
| `control_panel` | Dashboard principal (home del legacy) |
| `customer_service` | Pantalla de soporte |
| `account_settings` | Menú de cuenta |
| `personal_data` | Editar datos personales |
| `change_password` | Cambiar contraseña |
| `linked_devices` | Dispositivos vinculados a la cuenta |
| `app_users` | Sub-usuarios de la app |
| `delete_account` | Flujo de eliminación de cuenta |
| `device_management` | Menú de gestión del dispositivo |
| `scheduled_activities` | Actividades programadas |
| `contacts` | Contactos del dispositivo |
| `edit_contact` | Editar un contacto |
| `health` | Salud (ritmo cardíaco, SpO2) |
| `remote_connection` | Conexión remota (cámara, llamada) |
| `locate_device` | Localizar dispositivo |
| `rewards` | Recompensas |
| `activity_meter` | Medidor de actividad (pasos) |
| `apps_use` | Uso de apps |
| `volume_control` | Control de volumen |
| `call_history` | Historial de llamadas |
| `background_image` | Imagen de fondo del dispositivo |
| `legacy_location` | Mapa de ubicación |
| `legacy_chat` | Chat (placeholder) |
| `settings` | Menú de ajustes |
| `alarm` | Alarmas |
| `remote_management` | Gestión remota |
| `sos_agenda` | Contactos SOS |
| `sound` | Sonido del dispositivo |
| `sync_clock` | Sincronización de reloj |
| `app_store` | Gestión de apps instaladas |
| `battery` | Batería / modo nocturno |
| `block_phone` | Bloqueo de teléfono (whitelist) |
| `disable_functions` | Desactivar funciones (teclado, GPS) |
| `language` | Idioma del dispositivo |
| `legacy_notifications` | Notificaciones del dispositivo |
| `remote_on_off` | Encendido/apagado remoto |
| `alerts` | Alertas |
| `timezone` | Zona horaria |
| `wifi_settings` | Configuración WiFi |
**Insight para marketing:** Con estas screen_view podés construir funnels
(ej: `legacy_login control_panel device_management locate_device`) y
medir tiempos entre pantallas, rebotes y pantallas más visitadas.
---
## Autenticación
Prefijo `legacy_auth_*` — cubre login, 2FA, signup, recuperación de
contraseña, vinculación de teléfono y logout.
### Login
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_auth_login_attempt` | El usuario pulsa "Iniciar sesión" después de validar el formulario en el cliente. | — | Tope del funnel de login. Usar como base del "100 %" del funnel. |
| `legacy_auth_login_success` | El backend aceptó email + contraseña. Aún falta el 2FA. | — | Credenciales válidas. Usar para medir la calidad de la contraseña/email. |
| `legacy_auth_login_failure` | El backend rechazó las credenciales o hubo un error de red. | `reason` (mensaje de error) | Fricción. Analizar los `reason` más frecuentes para detectar problemas. |
### 2FA (doble factor)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_auth_2fa_requested` | El backend envió el código 2FA al usuario. | — | Usuario pasó el primer paso del login. |
| `legacy_auth_2fa_verified` | El código 2FA fue verificado y la sesión está activa. | — | Login exitoso. Fin del funnel de login. |
| `legacy_auth_2fa_failure` | El código 2FA fue rechazado (incorrecto, expirado). | `reason` | Fricción en el 2FA. Si es muy alto, puede indicar problemas con la entrega del código. |
| `legacy_auth_2fa_resend` | El usuario pidió reenviar el código. | — | Indica que no le llegó el primero. Útil para medir problemas de entrega. |
### Signup
El signup es un wizard de **2 pasos** (`step_index` 0, 1):
- **Paso 0 — Datos personales:** nombre, apellido, email, teléfono (con picker de país), aceptación de términos.
- **Paso 1 — Contraseña:** password y repeat password con validación de reglas.
El `language` se infiere del locale del dispositivo al momento del submit.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_auth_signup_started` | El usuario envió el formulario final (submit del paso 1). | — | Top del funnel de request al backend. |
| `legacy_auth_signup_completed` | El backend creó la cuenta exitosamente. | — | Conversión de nueva cuenta. |
| `legacy_auth_signup_failed` | Error en el alta (email ya existe, datos inválidos, error de red). | `reason` | Drop-off del signup. Analizar `reason` para ver si hay patrones. |
| `legacy_auth_signup_step_completed` | El usuario avanzó a un paso siguiente (validación pasó). | `step_index` (0, 1) — el paso que JUSTO terminó | Funnel interno del signup. Permite ver cuántos completan paso 0 y llegan al 1. |
| `legacy_auth_signup_step_back` | El usuario tocó "atrás" dentro del wizard. | `step_index` — el paso del que vuelve | Indica que el usuario quiere corregir algo — fricción. |
| `legacy_auth_signup_step_validation_failed` | El usuario tocó "siguiente" pero la validación del formulario lo rechazó. | `step_index` | **Muy valioso:** dice en qué paso hay más problemas de validación. Si step 0 falla: nombre/apellido, email, teléfono o términos. Si step 1 falla: reglas de contraseña. |
### Recuperación de contraseña
Flujo **exclusivo por email** (se removió la opción de SMS/teléfono).
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_auth_password_reset_requested` | El usuario inició el flujo de recuperar contraseña tipeando su email. | — | Fricción: alguien no recuerda su contraseña. |
| `legacy_auth_password_reset_email_sent` | El backend confirmó el envío del email de recuperación. | — | Confirma que el email salió. Cruzar con `reset_requested` para medir fallas. |
| `legacy_auth_password_reset_completed` | El usuario guardó la nueva contraseña exitosamente. | — | **Conversión final** del funnel de recuperación. |
| `legacy_auth_password_reset_failed` | Error al intentar guardar la nueva contraseña. | `reason` (`unequal_passwords`, `too_short`, `no_capitals`, `no_numbers`, `no_special_chars`, o mensaje del backend) | Permite ver qué reglas de validación molestan más a los usuarios. |
### Vinculación de teléfono
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_auth_link_phone_code_requested` | El usuario envió su número y pidió el código OTP. | — | Inicio del flujo de linking. |
| `legacy_auth_link_phone_code_request_failed` | Falló el pedido del código al backend. | `reason` | Fricción inicial. |
| `legacy_auth_link_phone_code_verified` | El código OTP fue verificado con éxito. | — | Número vinculado. |
| `legacy_auth_link_phone_code_verification_failed` | Falló la verificación (código incorrecto o expirado). | `reason` | Fricción en el segundo paso. |
### Logout
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_auth_logout` | El usuario cerró sesión y la app limpió la sesión local. | — | Señal de fin de sesión. Cruzar con duración de sesión para ver patrones de uso. |
---
## Cuenta
Prefijo `legacy_account_*` — cubre edición de perfil, contraseña,
dispositivos vinculados, usuarios de la app y **eliminación de cuenta
(señal crítica de churn)**.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_account_personal_data_edited` | El usuario guardó cambios en sus datos personales (nombre, apellido, teléfono). | — | Engagement con la cuenta. |
| `legacy_account_password_changed` | Cambio de contraseña exitoso. | — | Señal de buen hábito de seguridad. |
| `legacy_account_password_change_failed` | El cambio de contraseña falló. | `reason` | Fricción. |
| `legacy_account_linked_device_unlinked` | El usuario quitó un dispositivo vinculado de su cuenta. | — | Posible señal temprana de desuso del dispositivo. |
| `legacy_account_linked_device_renamed` | El usuario renombró un dispositivo vinculado (editó el carrier name). | — | Personalización / engagement con la gestión de dispositivos. |
| `legacy_account_app_user_delete_triggered` | El usuario tocó "eliminar" en la pantalla de app users. | — | Nota técnica: la implementación actual borra al usuario logueado (parece ser placeholder). El evento se mantiene para medir demanda del feature. |
| `legacy_account_deletion_initiated` | **CHURN SIGNAL** — El usuario entró al flujo "Eliminar cuenta". | — | Top del funnel de churn. |
| `legacy_account_deletion_confirmed` | El usuario confirmó la eliminación y la API call está en progreso. | — | El usuario quiere realmente irse. |
| `legacy_account_deletion_completed` | El backend confirmó la eliminación. | — | Usuario perdido. |
| `legacy_account_deletion_cancelled` | El usuario canceló antes de confirmar la eliminación. | — | Save: el usuario se arrepintió. Útil para medir efectividad de pantallas de retención. |
---
## Dispositivo — Setup
Prefijo `legacy_device_setup_*`**el momento aha del producto**: vincular
un reloj/dispositivo del hijo a la cuenta del padre/madre.
### Funnel del wizard
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_setup_started` | El usuario entró al wizard de alta de dispositivo. | — | Top del funnel de activación. |
| `legacy_device_setup_step_completed` | El usuario completó un paso del wizard. | `step` (`intro`, `link_info`, `scan_watch`, `profile`), `duration_seconds` (cuánto tardó en ese paso) | Permite ver dónde se abandona más el wizard **Y cuánto tiempo pasan los usuarios en cada paso** — fricción directa. |
| `legacy_device_setup_completed` | El dispositivo se creó exitosamente y está vinculado. | `child_gender` (M/F/other), `relation_type` (mother/father/etc), `child_age_years` | **Conversión de activación + demográficos del usuario final**. Marketing puede construir **personas reales** con estos 3 params: género, edad y relación con el adulto que compró. |
| `legacy_device_setup_failed` | Falló un paso del wizard. | `at_step` (en qué paso falló), `reason` (error) | Señal para el equipo técnico de dónde hay problemas. |
| `legacy_device_setup_cancelled` | El usuario volvió atrás y abandonó el wizard. | `at_step` | Drop-off del wizard. |
### Entrada del código del reloj (QR vs. manual)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_setup_qr_scanned` | El usuario escaneó exitosamente el código QR del reloj. | — | Método "rápido". Si su ratio baja, el QR scanner puede estar fallando. |
| `legacy_device_setup_manual_code_entered` | El usuario avanzó con el código tipeado manualmente (no escaneó). | — | Fallback. Si crece mucho el ratio vs QR, invertir en mejorar la UX del scanner. |
### Familias con múltiples hijos
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_setup_reset_for_new_kid` | Después de terminar un alta, el usuario tocó "agregar otro hijo". | — | **Señal de familia con múltiples hijos**. Estos usuarios típicamente tienen mayor retention y LTV — son el mejor segmento. |
---
## Dispositivo — Funciones
Prefijo `legacy_device_*` — acciones sobre el dispositivo ya vinculado.
Mide qué features del producto se usan más.
### Localización del dispositivo (comando "find")
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_locate_requested` | El usuario pulsó el botón de localizar (intento). | — | Uso del feature principal del producto. Top del mini-funnel de localización. |
| `legacy_device_locate_success` | El comando de localizar fue enviado con éxito al backend. | — | El dispositivo va a sonar. Conversión del mini-funnel. |
| `legacy_device_locate_failure` | El comando de localizar falló (error del backend o de red). | `reason` | Problema técnico al localizar. Drop-off del mini-funnel. |
### Conexión remota (cámara + llamadas)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_remote_connection_started` | El usuario entró a la pantalla de conexión remota. | — | Intención de interactuar remotamente. |
| `legacy_device_remote_connection_photo_taken` | El usuario pidió una foto de la cámara remota. | — | Feature avanzada. Permite medir uso de la cámara del reloj. |
| `legacy_device_remote_connection_call_initiated` | El usuario inició una llamada bidireccional. | — | Feature crítica: llamar al niño. |
| `legacy_device_remote_connection_picture_viewed` | El usuario navegó entre fotos de la cámara remota. | `direction` (`next`, `prev`, `direct`) | Engagement con la galería: cuántas fotos revisa el padre después de pedirlas. |
### Volumen del dispositivo
Cada envío del formulario dispara **un evento por tipo de volumen que
efectivamente cambió** (media, ringtone, alarm) — si el usuario movió solo
el media, solo se manda ese. Permite medir qué tipo de sonido configuran
más los padres.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_volume_control_changed` | El usuario guardó un cambio de volumen en el dispositivo, se emite 1 vez por cada tipo modificado. | `type` (`media`, `ringtone`, `alarm`), `level` (0-100) | Configuración. Cruzar `type` para ver cuál se ajusta más. |
### Imagen de fondo del reloj
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_background_image_changed` | El usuario seleccionó una imagen existente como fondo. | — | Personalización. |
| `legacy_device_background_image_uploaded` | El usuario subió una foto personal como fondo. | — | Alta personalización — indicador de engagement. |
### Actividades programadas (alarmas personalizadas del dispositivo)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_scheduled_activity_added` | El usuario agregó una actividad programada. | `week_day` (0-6, 0 = domingo), `period` (`HH:mm-HH:mm`) | **Dato muy útil:** permite ver qué horarios programan los padres (desayuno, colegio, deberes, etc) y qué días. |
| `legacy_device_scheduled_activity_updated` | El usuario editó una actividad programada. | `week_day`, `period` | Refinamiento de configuración. |
| `legacy_device_scheduled_activity_removed` | El usuario eliminó una actividad programada. | — | |
### Recompensas
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_rewards_granted` | El usuario asignó minutos de recompensa al dispositivo. | `amount` (cantidad de minutos) | Gamificación / recompensas de uso. |
### Podómetro (Activity Meter)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_activity_pedometer_toggled` | El usuario activó/desactivó el contador de pasos. | `enabled` (`true` / `false`) | |
| `legacy_device_activity_meter_time_range_changed` | El usuario cambió el rango de fechas en la pantalla de pasos. | `range` (`today`, `seven_days`, `thirty_days`, `custom`) | **Engagement profundo:** el padre no solo abre la pantalla, sino que investiga distintos períodos. |
### Salud (ritmo cardíaco / SpO2)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_health_heart_rate_frequency_changed` | El usuario cambió la frecuencia de medición del ritmo cardíaco. | `frequency_seconds` | Personalización de monitoreo de salud. |
| `legacy_device_health_measurement_started` | El usuario inició una medición manual de ritmo cardíaco. | — | Interés en datos de salud del niño. |
| `legacy_device_health_time_range_changed` | El usuario cambió el rango de fechas en la pantalla de salud. | `range` | Engagement profundo: padres revisando el historial de salud. |
### Uso de aplicaciones (Apps Use)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_apps_use_time_range_changed` | El usuario cambió el rango de fechas en la pantalla de uso de apps. | `range`, `total_duration_seconds` (total acumulado del período), `top_app_name` (app más usada en ese período) | **El evento más rico del módulo.** Permite a marketing segmentar directo: "padres cuyos hijos usan más TikTok que YouTube", "familias con uso > 4hs/día", etc. |
### Historial de llamadas
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_device_call_history_filter_changed` | El usuario cambió el filtro del historial. | `filter` (`all`, `incoming`, `outgoing`, `missed`) | **Cuando se filtra `missed` es señal de preocupación** del padre: busca llamadas perdidas del hijo. |
---
## Contactos del dispositivo
Prefijo `legacy_contacts_*` — contactos permitidos para llamadas desde el
dispositivo del niño.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_contacts_added` | El usuario agregó un contacto al dispositivo. | `total_count` (cantidad total de contactos DESPUÉS del add) | Configuración inicial o expansión de la agenda. El `total_count` permite segmentar "padres con agenda chica vs grande". |
| `legacy_contacts_edited` | El usuario editó un contacto existente. | — | |
| `legacy_contacts_deleted` | El usuario eliminó un contacto del dispositivo. | `total_count` (cantidad total DESPUÉS del delete) | |
---
## Ajustes
Prefijo `legacy_settings_*` — configuración general del dispositivo
(alarmas, SOS, bloqueos, idioma, red, etc).
### Alarmas
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_settings_alarm_added` | Alarma nueva creada. | `time` (`HH:mm`) | Uso del feature de alarma. El `time` permite ver qué horarios son más populares (despertador matutino, hora del colegio, etc). |
| `legacy_settings_alarm_updated` | Alarma existente editada. | `time` (el NUEVO `HH:mm`) | Refinamiento. |
| `legacy_settings_alarm_removed` | Alarma eliminada. | — | |
### Contactos SOS
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_settings_sos_contact_added` | Contacto SOS agregado. | `total_count` | Configuración de seguridad. Muy importante. |
| `legacy_settings_sos_contact_removed` | Contacto SOS removido. | `total_count` | |
### Whitelist del teléfono (bloqueo de llamadas)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_settings_block_phone_contact_added` | Contacto agregado a la whitelist de llamadas permitidas. | `total_count` | Control parental. |
| `legacy_settings_block_phone_contact_removed` | Contacto removido de la whitelist. | `total_count` | |
### Control parental (funciones desactivadas)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_settings_disable_functions_changed` | El usuario guardó cambios en la pantalla de funciones desactivadas. | — | Engagement con control parental (evento agregado). |
| `legacy_settings_disable_functions_keyboard_toggled` | Se guardó con el teclado habilitado/deshabilitado. | `enabled` | Control granular. |
| `legacy_settings_disable_functions_gps_toggled` | Se guardó con el GPS habilitado/deshabilitado. | `enabled` | Control granular. |
### Otros ajustes del dispositivo
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_settings_language_changed` | Se cambió el idioma del dispositivo. | `language` (ej. `es`, `en`) | |
| `legacy_settings_alerts_configured` | El usuario guardó cambios en las alertas. | `alert_count` (cuántas alertas activas), `alerts_enabled` (lista separada por comas truncada a 100 chars) | Permite ver qué alertas son más populares y cuántas alertas promedio configuran los padres. |
| `legacy_settings_timezone_changed` | Se cambió la zona horaria. | `timezone` | |
| `legacy_settings_wifi_added` | Se agregó una red WiFi permitida. | `total_count` | |
| `legacy_settings_wifi_removed` | Se eliminó una red WiFi permitida. | `total_count` | |
| `legacy_settings_sound_changed` | Se cambió el modo de sonido del dispositivo. | `mode` (`normal` / `silent` / `vibrate`) | Preferencia de perfil sonoro del niño. |
| `legacy_settings_sync_clock_triggered` | El usuario disparó una sincronización manual del reloj del dispositivo. | — | |
| `legacy_settings_battery_night_mode_toggled` | El usuario activó/desactivó el modo nocturno (ahorro de batería). | `enabled` | |
### Gestión remota del dispositivo (comandos destructivos)
Estos eventos son **muy importantes** para churn analysis. Un
`factory_reset` típicamente precede a un desvinculado y potencial churn.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_settings_remote_management_shutdown` | El usuario apagó el dispositivo remotamente. | — | Acción poco frecuente. |
| `legacy_settings_remote_management_restart` | El usuario reinició el dispositivo remotamente. | — | Típicamente usado cuando hay problemas técnicos. |
| `legacy_settings_remote_management_factory_reset` | **CHURN SIGNAL** — El usuario reseteó el dispositivo a fábrica. | — | Borra el dispositivo. Frecuentemente precede un `legacy_account_linked_device_unlinked` y luego `legacy_account_deletion_*`. Cruzar para medir correlación. |
---
## Soporte
Prefijo `legacy_support_*` — solo 1 evento hoy, medirá la demanda de
soporte.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_support_contact_initiated` | El usuario tocó el botón para contactar soporte (ej. abrir el cliente de email). | `channel` (`email` hoy; en el futuro también `phone`, `whatsapp`), `country` (país seleccionado en el formulario) | Demanda de soporte **por país**: permite ver dónde se originan más tickets. Nota: mide la **intención** de contactar, no confirma envío. |
---
## Onboarding
Prefijo `legacy_onboarding_*` — los slides de intro iniciales de la app.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_onboarding_step_changed` | El usuario pasó a un nuevo slide del intro. | `step_index` (número de slide, empieza en 0) | Medir cuántos slides el usuario ve antes de empezar. |
---
## Panel principal
Prefijo `legacy_control_panel_*` — acciones en el home del legacy.
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_control_panel_device_selected` | El usuario cambió el dispositivo activo (útil cuando hay varios hijos). | `total_devices` (cuántos dispositivos tiene vinculados) | Qué dispositivo está monitoreando activamente. El `total_devices` permite **segmentar por tamaño de familia** (1 hijo, 2 hijos, 3+). |
| `legacy_control_panel_positions_refreshed` | El usuario tiró del pull-to-refresh o tocó "actualizar" en el dashboard. | — | Preocupación activa del usuario. Indicador de engagement alto. |
---
## Ubicación y mapa
Prefijo `legacy_location_*`**el feature más rico del producto**. Acá
capturamos toda la interacción del usuario con el mapa: ver el trayecto,
crear zonas seguras, ver lugares frecuentes, cambiar frecuencia de
actualización, etc.
### Geofences (zonas seguras) — CRUD básico
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_geofence_created` | Se creó una geofence (API confirmó). | — | **Conversión final** del funnel de creación. |
| `legacy_location_geofence_updated` | Se actualizó una geofence existente. | — | Refinamiento de configuración de zonas. |
| `legacy_location_geofence_deleted` | Se eliminó una geofence. | — | |
### Lugares frecuentes — CRUD básico
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_frequent_place_created` | Se creó un lugar frecuente (API confirmó). | — | **Conversión final** del funnel. |
| `legacy_location_frequent_place_updated` | Se actualizó un lugar frecuente. | — | |
| `legacy_location_frequent_place_deleted` | Se eliminó un lugar frecuente. | — | |
### Funnel de creación de lugares (geofences y frequent places)
Este es el funnel más valioso del módulo de ubicación. Permite medir
**cuánta gente empieza a crear una zona vs. cuánta termina**.
```
legacy_location_place_creation_started (top: 100 %)
legacy_location_point_confirmed (paso 1 completado)
legacy_location_radius_confirmed (solo geofences — paso 2)
legacy_location_geofence_created (bottom: API OK)
o legacy_location_frequent_place_created
```
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_place_creation_started` | El usuario tocó "agregar zona" o "agregar lugar frecuente". | `mode` (`geofence` / `frequent_place`) | Top del funnel. |
| `legacy_location_point_confirmed` | El usuario tocó el mapa para fijar el centro del lugar. | `mode` | Paso 1 del funnel completado. |
| `legacy_location_radius_confirmed` | El usuario confirmó el radio de la geofence (solo aplica a geofences). | `radius` (metros), `is_editing` (`true` si estaba editando una existente, `false` si es nueva) | Paso 2 del funnel completado. Permite también **analizar qué tamaños de zonas eligen los usuarios** (radios más comunes casa, escuela, etc.). |
| `legacy_location_place_creation_cancelled` | El usuario salió del flujo de creación/edición antes de terminar. | `mode`, `at_step` (`picking_point` o `adjusting_radius`) | **Drop-off del funnel**. El `at_step` dice exactamente dónde lo perdimos. |
### Exploración y edición
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_geofence_selected` | El usuario tocó una geofence del mapa para verla. | — | Engagement: el usuario está mirando sus zonas. |
| `legacy_location_geofence_dismissed` | El usuario cerró el popup de la geofence sin hacer nada. | — | "Miró pero no editó". Indicador de exploración. |
| `legacy_location_geofence_edit_started` | El usuario tocó "editar" en una geofence seleccionada. | — | Intención de editar. Mid-funnel de edición. |
| `legacy_location_frequent_place_selected` | El usuario tocó un lugar frecuente para verlo. | — | Engagement. |
| `legacy_location_frequent_place_dismissed` | El usuario cerró el popup del lugar frecuente. | — | |
| `legacy_location_history_position_selected` | El usuario tocó un punto del historial de ubicaciones en el mapa. | — | Inspección detallada del trayecto. |
| `legacy_location_history_position_dismissed` | El usuario cerró el detalle del punto de historial. | — | |
### Historial de ubicaciones
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_history_loaded` | El usuario cargó el historial para un rango de fechas. | — | Interés en el historial. |
| `legacy_location_history_cleared` | El usuario limpió el trayecto del mapa. | — | |
### Frecuencia de ubicación (privacidad vs. precisión)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_frequency_updated` | El usuario cambió cada cuánto el dispositivo manda su posición. | `frequency_seconds` (ej. `60`, `300`, `900`) | **Dato súper útil:** indica preferencia entre privacidad y precisión/batería. Correlacionar con retention. |
### Capas del mapa (toggles de visibilidad)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_map_geofences_toggled` | El usuario mostró/ocultó las geofences en el mapa. | `visible` (`true` / `false`) | |
| `legacy_location_map_frequent_places_toggled` | El usuario mostró/ocultó los lugares frecuentes. | `visible` | |
| `legacy_location_map_route_trail_toggled` | El usuario mostró/ocultó la línea del trayecto histórico. | `visible` | |
### Modo "seguir en vivo"
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_following_toggled` | El usuario activó/desactivó el modo "seguir dispositivo" (el mapa se re-centra automáticamente). | `enabled` (`true` / `false`) | **Engagement alto:** el usuario está viendo al hijo en tiempo real. Correlacionar con horarios (ej. entrada/salida del cole). |
### UI del mapa (chrome)
| Evento | Cuándo se dispara | Parámetros | Qué significa para marketing |
|---|---|---|---|
| `legacy_location_map_actions_expanded` | El usuario abrió/cerró el drawer de acciones del mapa. | `expanded` (`true` / `false`) | Indica conocimiento de la UI. |
| `legacy_location_map_zoomed` | El usuario hizo zoom in/out y se quedó en ese nivel (con debounce de 1 segundo para no spamear). | `zoom` (nivel de zoom final) | Nivel de detalle con el que los usuarios miran el mapa. Un zoom alto indica "me importa ver dónde exactamente está". |
| `legacy_location_map_style_changed` | El usuario eligió otro estilo visual para el mapa desde el selector de capas. | `style` (`standard` / `voyager` / `light` / `dark` / `satellite`) | Personalización de la experiencia. **Satellite** es el más usado por padres que quieren ver edificios reales. |
| `legacy_location_map_center_tapped` | El usuario tocó el botón "centrar en el dispositivo" del mapa. | — | Acción de re-centrado manual. Indica que el mapa se desplazó y el usuario quiere volver al hijo. |
| `legacy_location_map_refresh_tapped` | El usuario tocó el botón de refresco dentro del mapa (distinto del pull-to-refresh del control panel). | — | **Engagement intenso:** el usuario quiere la posición más reciente AHORA. Suele dispararse en momentos de ansiedad. |
| `legacy_location_shared` | El usuario tocó "compartir ubicación" — abre el share sheet nativo para mandar la posición del hijo a otra app. | — | **Acción viral del producto.** Es la más importante para crecimiento orgánico: indica que el usuario está mandando data del producto a contactos fuera de la app (familia, pareja, abuelos). |
| `legacy_location_list_sheet_opened` | El usuario abrió el bottom sheet con la lista de geofences, lugares frecuentes e historial. | — | Quiere explorar todo lo que tiene configurado. Mid-funnel de gestión. |
| `legacy_location_history_type_filter_changed` | El usuario filtró el historial por tipo de posición. | `type` (`gps` / `wifi` / `sos` / `all` cuando limpia el filtro) | Indica interés en una fuente de datos específica. **`sos`** filtrado es señal de un evento crítico que el usuario está investigando. |
---
## Cómo usar este catálogo
### Para construir funnels
Tomá un evento "inicio" y uno "fin" en Firebase Analytics Engagement
**Funnels** y comparalos:
- **Signup:** `legacy_auth_signup_started _completed`
- **Login:** `legacy_auth_login_attempt _2fa_verified`
- **Activación (aha moment):** `legacy_device_setup_started _completed`
- **Creación de zona segura:** `legacy_location_place_creation_started _geofence_created`
- **Churn:** `legacy_account_deletion_initiated _deletion_completed`
### Para segmentar audiencias
En **Audiences** podés filtrar por user properties (`user_language`,
`user_has_phone`, etc.) y cruzarlo con cualquiera de estos eventos.
### Para detectar problemas
Filtrar por los eventos con `_failed` o `_failure` y mirar los `reason`
más frecuentes en la pestaña Events Parameter.
### Para medir engagement diario
Los eventos `legacy_control_panel_positions_refreshed`,
`legacy_location_following_toggled` y las screen_views del mapa son los
indicadores más fuertes de usuarios activos y preocupados.
---
## Eventos propuestos para el futuro (NO implementados aún)
Esta sección es la **wishlist** para cuando existan los features o lleguen
las decisiones pendientes.
### Cuando exista el plan premium/suscripción
- `purchase` / `purchase_subscription` (con `value`, `currency`, `transaction_id`)
- `action_click_gopremium` (botón de upgrade)
- `subscription_error_payment` / `subscription_canceled_payment`
- User property `user_plan` (`free` / `premium` / `family`)
### Limit popups / free-tier walls
- `legacy_limit_hit` con `limit_type` (max_devices, max_contacts, etc.)
- `legacy_limit_popup_shown`
- `legacy_limit_popup_upgrade_clicked`
### Referral / invitación
- `legacy_referral_screen_viewed`
- `legacy_referral_code_shared` (con `channel`)
- `legacy_referral_signup_completed`
### NPS / rating
- `legacy_nps_prompt_shown`
- `legacy_nps_score_submitted` (con `score` 010)
- `legacy_app_rating_submitted`
### Push notification engagement
- `legacy_notification_received` (background)
- `legacy_notification_opened` (tap app abre)
- `legacy_notification_dismissed`
### Aha moments
- `legacy_first_device_connected` (primera vez que el usuario vincula un dispositivo — requiere persistencia de "primera vez")
- `legacy_first_session_completed`
### A/B testing
- `ab_test_<experiment_name>` (cuando empecemos experimentos con Remote Config)
### Errores de API / health técnica
- `legacy_api_error` con `endpoint`, `status_code` (detectar endpoints flakey)
- `legacy_session_expired`
---
## Referencias técnicas
- **Proyecto Firebase:** `sf-platform-pre` (para dev+staging) / `sf-platform-prod` (pendiente de crear)
- **Package Dart:** `packages/sf_tracking/`
- **Mixins:** Cada grupo de eventos vive en un mixin aparte dentro del package (`auth_tracking.dart`, `location_tracking.dart`, etc).
- **GDPR:** Cada evento incluye automáticamente el parámetro `consent_status` para permitir filtrado post-hoc en BigQuery cuando se implemente el consent screen.
- **Ambiente:** `env` se setea como user property (`development` / `staging` / `production`), por lo que **todos los reportes pueden filtrarse por ambiente** y producción no se va a mezclar con testing.
---
## Changelog del catálogo
- **2026-04-07** — Creación inicial. 61 eventos del módulo legacy implementados y validados en device físico (iPhone 14 Pro iOS 18 + Samsung Galaxy A55 Android 15).
- **2026-04-07** — Se agregaron 16 eventos nuevos al módulo de ubicación (funnel de creación, exploración, edición, follow mode, map zoom debounced, history).
- **2026-04-07** — Se expandió el tracking de `device_management` con 8 eventos nuevos y 3 enriquecimientos de parámetros:
- NUEVOS: `legacy_device_locate_success/failure`, `legacy_device_remote_connection_picture_viewed`, `legacy_device_activity_meter_time_range_changed`, `legacy_device_health_time_range_changed`, `legacy_device_apps_use_time_range_changed` (con total_duration_seconds y top_app_name), `legacy_device_call_history_filter_changed`.
- ENRIQUECIDOS: `legacy_device_volume_control_changed` ahora dispara un evento por cada tipo (media/ringtone/alarm) que efectivamente cambió; `legacy_device_scheduled_activity_added/updated` ahora incluyen `week_day` y `period`; `legacy_contacts_added/deleted` ahora incluyen `total_count`.
- **2026-04-07** — Se expandió el tracking de `settings` con 3 eventos nuevos y 7 enriquecimientos de parámetros:
- NUEVOS: `legacy_settings_remote_management_shutdown/restart/factory_reset` (churn signal crítico).
- ENRIQUECIDOS: `legacy_settings_alarm_added/updated` ahora incluyen `time`; `legacy_settings_sos_contact_added/removed` incluyen `total_count`; `legacy_settings_block_phone_contact_added/removed` incluyen `total_count`; `legacy_settings_wifi_added/removed` incluyen `total_count`; `legacy_settings_sound_changed` incluye `mode`; `legacy_settings_alerts_configured` incluye `alert_count` y `alerts_enabled`.
- **2026-04-07** — Se expandió el tracking de `device_setup` con 3 eventos nuevos y 2 enriquecimientos críticos:
- NUEVOS: `legacy_device_setup_qr_scanned`, `legacy_device_setup_manual_code_entered`, `legacy_device_setup_reset_for_new_kid` (señal de familias con múltiples hijos).
- ENRIQUECIDOS: `legacy_device_setup_step_completed` ahora incluye `duration_seconds` (tiempo por paso — fricción directa); `legacy_device_setup_completed` ahora incluye `child_gender`, `relation_type`, `child_age_years` ( demográficos del usuario final para personas de marketing).
- **2026-04-07** — Se expandió el tracking de `legacy_auth` con 3 eventos nuevos para el funnel interno del signup:
- NUEVOS: `legacy_auth_signup_step_completed`, `legacy_auth_signup_step_back`, `legacy_auth_signup_step_validation_failed` (originalmente con `step_index` 0-2; reducido a 0-1 en abril 2026 al simplificar el signup).
- **2026-04-07** — Pasada final de cobertura en `legacy_auth`, `account`, `support`, `control_panel`: 6 eventos nuevos y 2 enriquecimientos.
- NUEVOS AUTH: `legacy_auth_password_reset_completed`, `legacy_auth_password_reset_failed` (con `reason` granular), `legacy_auth_link_phone_code_request_failed`, `legacy_auth_link_phone_code_verification_failed`.
- NUEVOS ACCOUNT: `legacy_account_linked_device_renamed`, `legacy_account_app_user_delete_triggered`.
- ENRIQUECIDOS: `legacy_support_contact_initiated` ahora incluye `country` además de `channel`; `legacy_control_panel_device_selected` ahora incluye `total_devices` (proxy de tamaño de familia).
- **2026-04-07** — Se expandió la cobertura de los widgets del módulo `location` con 6 eventos nuevos sobre acciones top-level del mapa:
- NUEVOS: `legacy_location_map_style_changed` (selector de capas), `legacy_location_map_center_tapped`, `legacy_location_map_refresh_tapped`, `legacy_location_shared` ( acción viral del producto), `legacy_location_list_sheet_opened`, `legacy_location_history_type_filter_changed` (con `type` para detectar interés en posiciones SOS).
- **2026-04-15** — Cambios de producto en `legacy_auth`:
- **Signup reducido a 2 pasos** (antes 3). Se quitaron los campos de documento, fecha de nacimiento, lugar de nacimiento, país de nacimiento, relación con el niño y dirección completa. El request al backend ahora solo incluye `firstName`, `lastName`, `email`, `phone` (E.164), `language` (del locale del dispositivo) y `password`. `step_index` de los eventos `legacy_auth_signup_step_*` pasa de 0-2 a 0-1.
- **Recover password solo por email**: se eliminó la UI de teléfono móvil en ambos screens del flujo (`request_recovery` y `new_password`). Los eventos del flujo se mantienen igual pero ahora siempre corresponden al canal email. Se eliminó del state `recoveryFormat` (ya no hay rama SMS).
- **User properties (Firebase Analytics)** ahora se sincronizan solo en shells autenticados (dashboards legacy y payment), no en rutas públicas. Los eventos en sí no cambian — solo se movió el disparador de la sync para evitar llamadas espurias a `/auth/me` en login/signup/recover_password.

View File

@@ -1,353 +0,0 @@
# Catálogo de Eventos — SaveFamily
> Documento para el equipo de Marketing. Lista todos los eventos que la app registra y describe el momento exacto en que se dispara cada uno.
---
## Índice
1. [Pantallas de la app](#pantallas-de-la-app)
2. [Autenticación](#autenticación)
3. [Cuenta](#cuenta)
4. [Alta de dispositivo (reloj/wearable del niño)](#alta-de-dispositivo)
5. [Funciones del dispositivo](#funciones-del-dispositivo)
6. [Contactos del dispositivo](#contactos-del-dispositivo)
7. [Ajustes del dispositivo](#ajustes-del-dispositivo)
8. [Soporte](#soporte)
9. [Onboarding](#onboarding)
10. [Panel principal (home)](#panel-principal)
11. [Ubicación y mapa](#ubicación-y-mapa)
---
## Pantallas de la app
Cada vez que el usuario navega a una pantalla, queda registrada
automáticamente. También se registran los cambios entre pestañas del menú
inferior.
Pantallas registradas:
- Pantalla de carga inicial
- Onboarding / intro
- Login
- Alta de cuenta (signup)
- Recuperación de contraseña
- Wizard de alta de reloj/dispositivo
- Inicio de vinculación de teléfono
- Verificación del código de vinculación
- Dashboard principal (home)
- Soporte / atención al cliente
- Menú de cuenta
- Editar datos personales
- Cambiar contraseña
- Dispositivos vinculados a la cuenta
- Sub-usuarios de la app
- Eliminación de cuenta
- Menú de gestión del dispositivo
- Actividades programadas
- Contactos
- Editar contacto
- Salud (ritmo cardíaco, oxígeno en sangre)
- Conexión remota (cámara y llamada)
- Localizar dispositivo
- Recompensas
- Medidor de actividad / pasos
- Uso de aplicaciones
- Control de volumen
- Historial de llamadas
- Imagen de fondo del dispositivo
- Mapa de ubicación
- Chat
- Menú de ajustes
- Alarmas
- Gestión remota
- Contactos SOS
- Sonido del dispositivo
- Sincronización del reloj
- Gestión de apps instaladas
- Batería / modo nocturno
- Bloqueo de teléfono (whitelist)
- Desactivar funciones (teclado, GPS)
- Idioma del dispositivo
- Notificaciones del dispositivo
- Encendido/apagado remoto
- Alertas
- Zona horaria
- Configuración WiFi
---
## Autenticación
### Login
- **legacy_auth_login_attempt** — El usuario pulsa "Iniciar sesión" después de completar el formulario.
- **legacy_auth_login_success** — Email y contraseña aceptados (todavía falta el segundo factor).
- **legacy_auth_login_failure** — El intento de login fue rechazado.
### Doble factor (2FA)
- **legacy_auth_2fa_requested** — Se le envió el código de verificación al usuario.
- **legacy_auth_2fa_verified** — El código fue aceptado y la sesión está activa (login completado).
- **legacy_auth_2fa_failure** — El código fue rechazado (incorrecto o expirado).
- **legacy_auth_2fa_resend** — El usuario pidió que le reenvíen el código.
### Alta de cuenta (signup)
El alta es un wizard de 2 pasos (datos personales → contraseña).
- **legacy_auth_signup_started** — El usuario envió el formulario final del alta.
- **legacy_auth_signup_completed** — La cuenta se creó exitosamente.
- **legacy_auth_signup_failed** — El alta falló.
- **legacy_auth_signup_step_completed** — El usuario completó un paso del wizard y avanzó al siguiente.
- **legacy_auth_signup_step_back** — El usuario volvió al paso anterior dentro del wizard.
- **legacy_auth_signup_step_validation_failed** — El usuario intentó avanzar pero el formulario tenía errores.
### Recuperación de contraseña
Flujo exclusivo por email (no hay opción de SMS).
- **legacy_auth_password_reset_requested** — El usuario inició el flujo de "olvidé mi contraseña" tipeando su email.
- **legacy_auth_password_reset_email_sent** — Se envió el email con el enlace de recuperación.
- **legacy_auth_password_reset_completed** — El usuario guardó exitosamente su nueva contraseña.
- **legacy_auth_password_reset_failed** — El intento de guardar la nueva contraseña falló.
### Vinculación de teléfono
- **legacy_auth_link_phone_code_requested** — El usuario envió su número y pidió el código.
- **legacy_auth_link_phone_code_request_failed** — Falló el envío del código.
- **legacy_auth_link_phone_code_verified** — El código fue verificado, número vinculado.
- **legacy_auth_link_phone_code_verification_failed** — El código no fue aceptado.
### Cierre de sesión
- **legacy_auth_logout** — El usuario cerró sesión.
---
## Cuenta
- **legacy_account_personal_data_edited** — El usuario guardó cambios en sus datos personales (nombre, apellido, teléfono).
- **legacy_account_password_changed** — El usuario cambió su contraseña exitosamente.
- **legacy_account_password_change_failed** — El cambio de contraseña falló.
- **legacy_account_linked_device_unlinked** — El usuario quitó un dispositivo vinculado de su cuenta.
- **legacy_account_linked_device_renamed** — El usuario renombró un dispositivo vinculado.
- **legacy_account_app_user_delete_triggered** — El usuario tocó "eliminar" en la pantalla de sub-usuarios.
- **legacy_account_deletion_initiated** — El usuario entró al flujo de "eliminar cuenta". Señal temprana de churn.
- **legacy_account_deletion_confirmed** — El usuario confirmó la eliminación.
- **legacy_account_deletion_completed** — La cuenta se eliminó.
- **legacy_account_deletion_cancelled** — El usuario canceló antes de confirmar la eliminación.
---
## Alta de dispositivo
Vincular el reloj/dispositivo del niño a la cuenta del adulto.
### Wizard de alta
- **legacy_device_setup_started** — El usuario entró al wizard de alta de dispositivo.
- **legacy_device_setup_step_completed** — El usuario completó un paso del wizard. Se registra cuánto tiempo tardó en ese paso.
- **legacy_device_setup_completed** — El dispositivo quedó vinculado. Se registra género y edad del niño y la relación con el adulto.
- **legacy_device_setup_failed** — Falló el alta del dispositivo.
- **legacy_device_setup_cancelled** — El usuario abandonó el wizard.
### Cómo se introdujo el código del reloj
- **legacy_device_setup_qr_scanned** — El usuario escaneó el código QR del reloj.
- **legacy_device_setup_manual_code_entered** — El usuario tipeó el código manualmente.
### Familias con varios hijos
- **legacy_device_setup_reset_for_new_kid** — Después de terminar un alta, el usuario eligió "agregar otro hijo".
---
## Funciones del dispositivo
### Localizar dispositivo
- **legacy_device_locate_requested** — El usuario pulsó el botón de localizar.
- **legacy_device_locate_success** — La orden de localizar se envió al dispositivo.
- **legacy_device_locate_failure** — La orden de localizar falló.
### Conexión remota (cámara y llamada)
- **legacy_device_remote_connection_started** — El usuario abrió la pantalla de conexión remota.
- **legacy_device_remote_connection_photo_taken** — El usuario pidió una foto desde la cámara remota.
- **legacy_device_remote_connection_call_initiated** — El usuario inició una llamada con el dispositivo.
- **legacy_device_remote_connection_picture_viewed** — El usuario navegó entre las fotos tomadas remotamente.
### Volumen del dispositivo
- **legacy_device_volume_control_changed** — El usuario guardó un cambio de volumen. Se dispara una vez por cada tipo modificado (multimedia, tono de llamada, alarma).
### Imagen de fondo del reloj
- **legacy_device_background_image_changed** — El usuario eligió una imagen existente como fondo.
- **legacy_device_background_image_uploaded** — El usuario subió una foto personal como fondo.
### Actividades programadas (rutinas en el dispositivo)
- **legacy_device_scheduled_activity_added** — El usuario creó una nueva actividad programada. Se registra el día de la semana y el horario.
- **legacy_device_scheduled_activity_updated** — El usuario editó una actividad programada.
- **legacy_device_scheduled_activity_removed** — El usuario eliminó una actividad programada.
### Recompensas
- **legacy_device_rewards_granted** — El usuario otorgó minutos de recompensa al dispositivo. Se registra la cantidad de minutos.
### Podómetro / pasos
- **legacy_device_activity_pedometer_toggled** — El usuario activó o desactivó el contador de pasos.
- **legacy_device_activity_meter_time_range_changed** — El usuario cambió el rango de fechas en la pantalla de pasos (hoy, 7 días, 30 días, personalizado).
### Salud (ritmo cardíaco / oxígeno en sangre)
- **legacy_device_health_heart_rate_frequency_changed** — El usuario cambió la frecuencia con la que se mide el ritmo cardíaco.
- **legacy_device_health_measurement_started** — El usuario inició una medición manual.
- **legacy_device_health_time_range_changed** — El usuario cambió el rango de fechas en la pantalla de salud.
### Uso de aplicaciones del dispositivo
- **legacy_device_apps_use_time_range_changed** — El usuario cambió el rango de fechas en la pantalla de uso de apps. Se registra el tiempo total acumulado y la app más usada del período.
### Historial de llamadas
- **legacy_device_call_history_filter_changed** — El usuario filtró el historial (todas, entrantes, salientes, perdidas). Filtrar perdidas suele ser señal de preocupación del adulto.
---
## Contactos del dispositivo
Contactos permitidos para llamar al/desde el dispositivo del niño.
- **legacy_contacts_added** — El usuario agregó un contacto. Se registra cuántos contactos tiene en total.
- **legacy_contacts_edited** — El usuario editó un contacto existente.
- **legacy_contacts_deleted** — El usuario eliminó un contacto. Se registra el total restante.
---
## Ajustes del dispositivo
### Alarmas
- **legacy_settings_alarm_added** — El usuario creó una alarma. Se registra la hora.
- **legacy_settings_alarm_updated** — El usuario editó una alarma.
- **legacy_settings_alarm_removed** — El usuario eliminó una alarma.
### Contactos SOS
- **legacy_settings_sos_contact_added** — El usuario agregó un contacto SOS.
- **legacy_settings_sos_contact_removed** — El usuario eliminó un contacto SOS.
### Whitelist de llamadas (bloqueo de teléfono)
- **legacy_settings_block_phone_contact_added** — El usuario agregó un contacto a la lista de llamadas permitidas.
- **legacy_settings_block_phone_contact_removed** — El usuario quitó un contacto de la lista de llamadas permitidas.
### Control parental (funciones desactivadas)
- **legacy_settings_disable_functions_changed** — El usuario guardó cambios en la pantalla de funciones desactivadas.
- **legacy_settings_disable_functions_keyboard_toggled** — El usuario activó o desactivó el teclado.
- **legacy_settings_disable_functions_gps_toggled** — El usuario activó o desactivó el GPS.
### Otros ajustes
- **legacy_settings_language_changed** — El usuario cambió el idioma del dispositivo.
- **legacy_settings_alerts_configured** — El usuario guardó cambios en las alertas. Se registra cuántas alertas activas tiene y cuáles están encendidas.
- **legacy_settings_timezone_changed** — El usuario cambió la zona horaria.
- **legacy_settings_wifi_added** — El usuario agregó una red WiFi permitida.
- **legacy_settings_wifi_removed** — El usuario eliminó una red WiFi permitida.
- **legacy_settings_sound_changed** — El usuario cambió el modo de sonido del dispositivo (normal / silencio / vibración).
- **legacy_settings_sync_clock_triggered** — El usuario disparó una sincronización manual del reloj.
- **legacy_settings_battery_night_mode_toggled** — El usuario activó o desactivó el modo nocturno (ahorro de batería).
### Gestión remota del dispositivo
- **legacy_settings_remote_management_shutdown** — El usuario apagó el dispositivo a distancia.
- **legacy_settings_remote_management_restart** — El usuario reinició el dispositivo a distancia.
- **legacy_settings_remote_management_factory_reset** — El usuario restauró el dispositivo a fábrica. Suele preceder al desvinculado y al churn.
---
## Soporte
- **legacy_support_contact_initiated** — El usuario tocó el botón para contactar a soporte. Se registra el canal (email) y el país seleccionado en el formulario.
---
## Onboarding
- **legacy_onboarding_step_changed** — El usuario pasó a un nuevo slide del intro inicial.
---
## Panel principal
- **legacy_control_panel_device_selected** — El usuario cambió el dispositivo activo (útil cuando hay varios hijos). Se registra cuántos dispositivos tiene vinculados.
- **legacy_control_panel_positions_refreshed** — El usuario refrescó manualmente el dashboard (pull-to-refresh o botón de actualizar).
---
## Ubicación y mapa
### Zonas seguras (geofences)
- **legacy_location_geofence_created** — Se creó una nueva zona segura.
- **legacy_location_geofence_updated** — Se editó una zona segura existente.
- **legacy_location_geofence_deleted** — Se eliminó una zona segura.
### Lugares frecuentes
- **legacy_location_frequent_place_created** — Se creó un nuevo lugar frecuente.
- **legacy_location_frequent_place_updated** — Se editó un lugar frecuente existente.
- **legacy_location_frequent_place_deleted** — Se eliminó un lugar frecuente.
### Funnel de creación de lugares (zonas y frecuentes)
- **legacy_location_place_creation_started** — El usuario tocó "agregar zona" o "agregar lugar frecuente".
- **legacy_location_point_confirmed** — El usuario fijó el centro del lugar tocando el mapa.
- **legacy_location_radius_confirmed** — El usuario confirmó el radio de la zona segura. Se registra el tamaño del radio.
- **legacy_location_place_creation_cancelled** — El usuario abandonó el flujo de creación o edición. Se registra en qué paso lo dejó.
### Exploración y edición
- **legacy_location_geofence_selected** — El usuario tocó una zona segura del mapa para verla.
- **legacy_location_geofence_dismissed** — El usuario cerró el detalle de la zona sin hacer cambios.
- **legacy_location_geofence_edit_started** — El usuario tocó "editar" en una zona seleccionada.
- **legacy_location_frequent_place_selected** — El usuario tocó un lugar frecuente para verlo.
- **legacy_location_frequent_place_dismissed** — El usuario cerró el detalle del lugar frecuente.
- **legacy_location_history_position_selected** — El usuario tocó un punto del historial de ubicaciones en el mapa.
- **legacy_location_history_position_dismissed** — El usuario cerró el detalle del punto del historial.
### Historial de ubicaciones
- **legacy_location_history_loaded** — El usuario cargó el historial de ubicaciones para un rango de fechas.
- **legacy_location_history_cleared** — El usuario limpió el trayecto del mapa.
### Frecuencia de ubicación
- **legacy_location_frequency_updated** — El usuario cambió cada cuánto el dispositivo manda su posición.
### Capas del mapa
- **legacy_location_map_geofences_toggled** — El usuario mostró u ocultó las zonas seguras en el mapa.
- **legacy_location_map_frequent_places_toggled** — El usuario mostró u ocultó los lugares frecuentes en el mapa.
- **legacy_location_map_route_trail_toggled** — El usuario mostró u ocultó la línea del trayecto histórico.
### Modo "seguir en vivo"
- **legacy_location_following_toggled** — El usuario activó o desactivó el modo "seguir dispositivo" (el mapa se re-centra automáticamente sobre el niño).
### Acciones del mapa
- **legacy_location_map_actions_expanded** — El usuario abrió o cerró el menú de acciones del mapa.
- **legacy_location_map_zoomed** — El usuario hizo zoom y se quedó en ese nivel.
- **legacy_location_map_style_changed** — El usuario eligió otro estilo de mapa (estándar, claro, oscuro, satélite, etc.).
- **legacy_location_map_center_tapped** — El usuario tocó "centrar en el dispositivo" para volver el mapa sobre el niño.
- **legacy_location_map_refresh_tapped** — El usuario tocó refrescar dentro del mapa para pedir la posición más reciente.
- **legacy_location_shared** — El usuario compartió la ubicación del niño hacia otra app (familia, pareja, abuelos). Acción viral del producto.
- **legacy_location_list_sheet_opened** — El usuario abrió la lista con todas sus zonas, lugares frecuentes e historial.
- **legacy_location_history_type_filter_changed** — El usuario filtró el historial por tipo de posición (GPS, WiFi, SOS). Filtrar SOS suele indicar que está investigando un evento crítico.

View File

@@ -1,282 +0,0 @@
# Integración Videollamadas — Juphoon jc_sdk
## Estado general
| Fase | Estado |
|---|---|
| 1. SDK wrapper (`videocall_sdk`) | ✅ Completado |
| 2. Configuración nativa (permisos) | ✅ Completado |
| 3. Configuración por entorno (AppKey) | ✅ Completado |
| 4. Feature videocall (UI + lógica) | ⏳ Pendiente |
| 5. Pruebas APP↔APP | ⏳ Pendiente |
| 6. Integración con backend (señalización) | ⏳ Pendiente |
| 7. Pruebas APP↔Reloj | ⏳ Pendiente |
| 8. Token auth (producción) | ⏳ Pendiente |
| 9. Push/background iOS | ⏳ Pendiente (sin doc del proveedor) |
| 10. Chat | ⏳ Pendiente (sin spec del proveedor) |
---
## Fase 1: SDK wrapper — ✅
Paquete `packages/videocall_sdk/` con wrap 100% de `jc_sdk` v2.16.5.
### Arquitectura (patrón sca_treezor)
- Constructor injection (no singletons estáticos)
- GetIt module (`videocallSdkModule(config)`)
- `VideocallSdkManager` orquestador de inicialización
- `VideocallSdkConfig` abstracto para config por entorno
- Riverpod providers + StreamProviders para UI reactiva
- Callbacks del SDK → Dart Streams
### Servicios (7 total, cobertura 100%)
- `VideocallClient` → JCClient (auth, login, logout, messaging)
- `VideocallCallService` → JCCall (llamadas 1-to-1)
- `VideocallDeviceService` → JCMediaDevice (cámara, mic, speaker)
- `VideocallChannelService` → JCMediaChannel (llamadas grupales)
- `VideocallPushService` → JCPush (push notifications)
- `VideocallNetService` → JCNet (estado de red)
- `VideocallLogService` → JCLog (logging)
### Estructura
```
packages/videocall_sdk/lib/src/
├── config/videocall_sdk_config.dart
├── di/videocall_sdk_module.dart
├── manager/videocall_sdk_manager.dart
├── models/ (call_state, call_direction, videocall_item, etc.)
├── services/ (7 servicios)
└── providers/videocall_providers.dart
```
---
## Fase 2: Permisos nativos — ✅
### Android (`AndroidManifest.xml`)
- [x] INTERNET (ya existía)
- [x] ACCESS_NETWORK_STATE (ya existía)
- [x] ACCESS_WIFI_STATE
- [x] CAMERA (ya existía)
- [x] RECORD_AUDIO
- [x] MODIFY_AUDIO_SETTINGS
- [x] BLUETOOTH
- [x] uses-feature: hardware.camera
- [x] uses-feature: hardware.camera.autofocus
- [x] uses-feature: hardware.bluetooth (optional)
### Android (`proguard-rules.pro`)
- [x] -keep com.juphoon.**
- [x] -keep com.justalk.**
- [x] -keepattributes InnerClasses
### iOS (`Info.plist`)
- [x] NSCameraUsageDescription (actualizado: QR + videollamadas)
- [x] NSMicrophoneUsageDescription
- [x] NSPhotoLibraryUsageDescription
### iOS (`Podfile`)
- [x] PERMISSION_CAMERA=1
- [x] PERMISSION_PHOTOS=1
- [x] PERMISSION_MICROPHONE=1
---
## Fase 3: Config por entorno — ✅
- [x] `juphoonAppKey` en development.json, staging.json, production.json
- [x] `Environment.juphoonAppKey` via `String.fromEnvironment()`
- [x] `SaveFamilyVideocallConfig` implementa `VideocallSdkConfig`
- [x] `videocallSdkModule(config)` llamado en `init_app.dart`
- [ ] AppKeys separadas por entorno (por ahora las 3 usan la misma clave de dev)
---
## Fase 4: Feature videocall — ⏳ SIGUIENTE
Feature en `modules/legacy/modules/device_management/lib/src/features/videocall/`
### Por hacer
- [ ] `videocall_builder.dart` — GoRouter builder
- [ ] `domain/entities/videocall_entity.dart` — Freezed entity
- [ ] `domain/entities/videocall_error.dart` — Error enum con i18n
- [ ] `presentation/state/videocall_view_model.dart` — Notifier
- [ ] `presentation/state/videocall_view_state.dart` — Freezed state
- [ ] `presentation/videocall_screen.dart` — Pantalla de llamada
- [ ] `presentation/widgets/local_video_view.dart` — Video local
- [ ] `presentation/widgets/remote_video_view.dart` — Video remoto
- [ ] `presentation/widgets/call_controls.dart` — Botones (colgar, mute, cámara)
- [ ] `presentation/widgets/incoming_call_dialog.dart` — Dialog llamada entrante
- [ ] Providers en `core/providers/`
- [ ] Ruta en GoRouter (`app_router.dart`)
- [ ] Runtime permissions (pedir cámara/mic en runtime)
---
## Fase 5: Pruebas APP↔APP — ⏳
- [ ] Login con 2 userIDs de prueba (`p_test1`, `p_test2`)
- [ ] Llamada de voz APP→APP
- [ ] Videollamada APP→APP
- [ ] Responder llamada entrante
- [ ] Rechazar llamada
- [ ] Colgar durante llamada
- [ ] Mute/unmute micrófono
- [ ] Cambiar cámara frontal/trasera
- [ ] Speaker on/off
- [ ] Llamada perdida (onMissedCallItem)
- [ ] Verificar desfase versión SDK (quickstart 1.0.2 vs pub.dev 2.16.5)
---
## Fase 6: Integración backend — ⏳
- [ ] Obtener documentación API REST del backend SaveFamily para señalización
- [ ] Endpoint para iniciar llamada → notificar al reloj
- [ ] Endpoint para recibir notificación de llamada entrante del reloj
- [ ] Formato userID definido con backend (`p_<cuenta>` vs `w_<IMEI>`)
- [ ] Sanitización de emails (@ → _ en userIDs)
---
## Fase 7: Pruebas APP↔Reloj — ⏳
- [ ] Llamada APP → Reloj
- [ ] Llamada Reloj → APP
- [ ] Videollamada grupal (JCMediaChannel)
- [ ] Límite 5 min de llamada
- [ ] Registro IMEI (protocolo RYIMEI) — lo hace el backend
---
## Fase 8: Token auth — ⏳
- [ ] Backend implementa generación de tokens Juphoon (usa AppSecret)
- [ ] App pide token al backend antes del login
- [ ] Token se pasa como `password` en `VideocallClient.login()`
- [ ] Activar Token鉴权 en consola Juphoon (ya está activo)
Nota: para dev/testing no es necesario — `autoCreateAccount = true` en LoginParam permite login con cualquier password.
---
## Fase 9: Push/Background iOS — ⏳ RIESGO
**Problema:** No hay documentación de cómo recibir llamadas con la app cerrada en iOS.
- [ ] Probar qué pasa cuando la app está cerrada y llega una llamada (fase 5)
- [ ] Si no funciona: investigar PushKit + CallKit
- [ ] Verificar si `JCPush` del SDK resuelve esto
- [ ] Consultar pestaña "消息通知服务" en la consola Juphoon
- [ ] Si es deal-breaker: escalar antes del pago ($8,835)
---
## Fase 10: Chat — ⏳ SIN SPEC
- [ ] Obtener especificación del módulo de chat del proveedor
- [ ] Determinar si usa JCMediaChannel (SDK) o API propia
- [ ] $2,950 pagados sin lista de features
---
## Credenciales Juphoon Cloud
| Campo | Valor | Quién lo usa |
|---|---|---|
| AppKey | `9efcf2d889dc8a0320925096` | App Flutter + Backend |
| AppSecret | `ui7pr73ggl5rr0gf01np` | Solo Backend |
| AES_KEY (IoT) | `8e3637pG7E9144E0` | Solo Backend |
| Consola | juphoon.com (+34 603675786) | Julián |
---
## Naming conventions (protocolo TCP)
| Tipo | Formato | Ejemplo |
|---|---|---|
| Watch userID | `w_` + IMEI | `w_000078932675810` |
| Mobile userID | `p_` + APP account | `p_abc10086` |
| Group room | `did` + `_group` | `0245423235_group` |
| Single room | `did` + `_` + APP account | `0245423235_abc10086` |
Nota: `@` y `.` se reemplazan por `_` en room numbers y userIDs.
---
## Documentación de referencia
- Quickstart V1.1: `~/Downloads/Video call API_ Juphoon Flutter SDK quickstart V1.1.pdf`
- TCP Protocol: `~/Downloads/Juphoon Video Call TCP Protocol.docx`
- Connection process: `~/Downloads/video call connection process Rev2.docx`
- Mutual dialing: `~/Downloads/video call mutual dialing process.docx`
- Schematics: `~/Downloads/schematics _2025.03.26 (2)/`
- pub.dev: https://pub.dev/packages/jc_sdk
- Consola: https://developer.juphoon.com
---
## Flujos de llamada (protocolo TCP + Juphoon SDK)
### APP → Reloj (outgoing)
1. App envía `VIDEO_CALL_REQUEST` al backend con `chatType`, `appAccount`, `roomNumber`, `sessionId`
2. Backend reenvía la notificación al reloj via TCP
3. App inicia audio/cámara y llama al watch account via SDK (`startCall(userId: "w_<IMEI>")`)
4. Reloj contesta → SDK notifica via `callItemUpdateStream` (estado `isTalking`)
5. App envía `VIDEO_CALL_ROOM_COUNT_REQUEST` con `type` (0/1), `count: 2`, `room_num`
### Reloj → APP (incoming)
1. Reloj envía notificación de llamada al backend
2. Backend notifica a la app (requiere app abierta con SDK inicializado, ver Fase 9)
3. SDK detecta llamada entrante via `callItemAddStream` con `CallDirection.incoming`
4. Usuario acepta → `answerCall()` → SDK conecta
5. Reloj reporta participantes al backend
### Colgar / Rechazar
- **Colgar (en llamada):** `hangUp()` en SDK + `VIDEO_CALL_CANCEL` al backend
- **Rechazar (incoming):** `hangUp()` en SDK + `VIDEO_CALL_REFUSE` al backend con `appAccount`, `roomNumber`
### Convenciones de nombres
| Campo | Formato | Ejemplo |
|---|---|---|
| Watch userID | `w_` + IMEI | `w_000078932675810` |
| Mobile userID | `p_` + email sanitizado | `p_user_example_com` |
| Room (single) | `deviceId` + `_` + appAccount | `0245423235_p_user_example_com` |
| Room (group) | `deviceId` + `_group` | `0245423235_group` |
| Session ID | `deviceId` + `_` + epoch en segundos | `0245423235_1714150800` |
Sanitización: `@` y `.` se reemplazan por `_` en userIDs y roomNumbers.
### Configuración del SDK por tipo de dispositivo
- RTOS watches: `MediaConfig.MODE_RTOS`
- Android watches: `MediaConfig.MODE_INTELLIGENT_HARDWARE`
- Se determina con `device.capabilities.system` (`isRtos` / `isAndroid`)
### Auto-login
- userId: `p_` + email sanitizado (ej: `p_julian_test_com`)
- password: `user.id` (UUID del usuario padre)
- En dev/testing `autoCreateAccount = true` permite login con cualquier password
---
## Limitaciones actuales
### Recepción de llamadas requiere app abierta
La app debe estar en primer plano con el SDK inicializado y el client logueado para recibir llamadas entrantes. Si el app está en background o cerrada, las llamadas no llegan. Esto se resuelve en Fase 9 (Push/Background).
### Sin timeout de llamada
El protocolo menciona un límite de 5 min por llamada, pero no está implementado en la app. El reloj podría manejar el corte por su lado.
---
## Pendientes por verificar
- **chatType**: El protocolo TCP usa `0` (single) y `1` (multi) como enteros. Nuestra app envía `"single"`/`"multi"` como strings en el JSON del comando. Verificar que el backend hace la conversión correctamente antes de enviar al reloj via TCP.

View File

@@ -1 +0,0 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"sf-platform-pre","appId":"1:535646668726:android:b87245b807258e3e5e6317","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"sf-platform-pre","appId":"1:535646668726:ios:5172d626d02dfe215e6317","uploadDebugSymbols":true,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options_dev.dart":{"projectId":"sf-platform-pre","configurations":{"android":"1:535646668726:android:c3a09d6c26f0cdf95e6317","ios":"1:535646668726:ios:524afa641f61d7cb5e6317"}},"lib/firebase_options_staging.dart":{"projectId":"sf-platform-pre","configurations":{"android":"1:535646668726:android:b87245b807258e3e5e6317","ios":"1:535646668726:ios:5172d626d02dfe215e6317"}},"lib/firebase_options_prod.dart":{"projectId":"sf-platform-pro","configurations":{"android":"1:950566980029:android:75a7c10b6259d09681aad4","ios":"1:950566980029:ios:987b4f0b9e9b897481aad4"}}}}}}

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '15.0'
platform :ios, '13.4'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -45,14 +45,5 @@ end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
## Juphoon jc_sdk: enable camera, photos and microphone permissions
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
'PERMISSION_CAMERA=1',
'PERMISSION_PHOTOS=1',
'PERMISSION_MICROPHONE=1'
]
end
end
end

View File

@@ -1,250 +1,22 @@
PODS:
- audioplayers_darwin (0.0.1):
- Flutter
- FlutterMacOS
- Firebase/CoreOnly (12.9.0):
- FirebaseCore (~> 12.9.0)
- Firebase/Crashlytics (12.9.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 12.9.0)
- Firebase/Messaging (12.9.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.9.0)
- Firebase/Performance (12.9.0):
- Firebase/CoreOnly
- FirebasePerformance (~> 12.9.0)
- Firebase/RemoteConfig (12.9.0):
- Firebase/CoreOnly
- FirebaseRemoteConfig (~> 12.9.0)
- firebase_analytics (12.2.0):
- firebase_core
- FirebaseAnalytics (= 12.9.0)
- Flutter
- firebase_core (4.6.0):
- Firebase/CoreOnly (= 12.9.0)
- Flutter
- firebase_crashlytics (5.1.0):
- Firebase/Crashlytics (= 12.9.0)
- firebase_core
- Flutter
- firebase_messaging (16.1.3):
- Firebase/Messaging (= 12.9.0)
- firebase_core
- Flutter
- firebase_performance (0.11.2):
- Firebase/Performance (= 12.9.0)
- firebase_core
- Flutter
- firebase_remote_config (6.3.0):
- Firebase/RemoteConfig (= 12.9.0)
- firebase_core
- Flutter
- FirebaseABTesting (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseAnalytics (12.9.0):
- FirebaseAnalytics/Default (= 12.9.0)
- FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/Default (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- GoogleAppMeasurement/Default (= 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseCore (12.9.0):
- FirebaseCoreInternal (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseCoreInternal (12.9.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseCrashlytics (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- FirebaseRemoteConfigInterop (~> 12.9.0)
- FirebaseSessions (~> 12.9.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- FirebaseInstallations (12.9.0):
- FirebaseCore (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- FirebasePerformance (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- FirebaseRemoteConfig (~> 12.9.0)
- FirebaseSessions (~> 12.9.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- FirebaseRemoteConfig (12.9.0):
- FirebaseABTesting (~> 12.9.0)
- FirebaseCore (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- FirebaseRemoteConfigInterop (~> 12.9.0)
- FirebaseSharedSwift (~> 12.9.0)
- GoogleUtilities/Environment (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseRemoteConfigInterop (12.9.0)
- FirebaseSessions (12.9.0):
- FirebaseCore (~> 12.9.0)
- FirebaseCoreExtension (~> 12.9.0)
- FirebaseInstallations (~> 12.9.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesSwift (~> 2.1)
- FirebaseSharedSwift (12.9.0)
- Flutter (1.0.0)
- flutter_contacts (0.0.1):
- Flutter
- flutter_image_compress_common (1.0.0):
- Flutter
- Mantle
- SDWebImage
- SDWebImageWebPCoder
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (2.4.3):
- Flutter
- flutter_ringtone_player (0.0.1):
- Flutter
- flutter_treezor_entrust_sdk_bridge (0.0.1):
- Flutter
- GoogleAdsOnDeviceConversion (3.2.0):
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Core (12.9.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Default (12.9.0):
- GoogleAdsOnDeviceConversion (~> 3.2.0)
- GoogleAppMeasurement/Core (= 12.9.0)
- GoogleAppMeasurement/IdentitySupport (= 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/IdentitySupport (12.9.0):
- GoogleAppMeasurement/Core (= 12.9.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/MethodSwizzler (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- image_picker_ios (0.0.1):
- Flutter
- jc_sdk (0.0.1):
- Flutter
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
- libwebp/sharpyuv (= 1.5.0)
- libwebp/webp (= 1.5.0)
- libwebp/demux (1.5.0):
- libwebp/webp
- libwebp/mux (1.5.0):
- libwebp/demux
- libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0):
- libwebp/sharpyuv
- Mantle (2.2.0):
- Mantle/extobjc (= 2.2.0)
- Mantle/extobjc (2.2.0)
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- PromisesObjC (2.4.0)
- PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0)
- record_ios (1.2.0):
- Flutter
- SDWebImage (5.21.7):
- SDWebImage/Core (= 5.21.7)
- SDWebImage/Core (5.21.7)
- SDWebImageWebPCoder (0.15.0):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
@@ -252,168 +24,51 @@ PODS:
- FlutterMacOS
DEPENDENCIES:
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- firebase_performance (from `.symlinks/plugins/firebase_performance/ios`)
- firebase_remote_config (from `.symlinks/plugins/firebase_remote_config/ios`)
- Flutter (from `Flutter`)
- flutter_contacts (from `.symlinks/plugins/flutter_contacts/ios`)
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_ringtone_player (from `.symlinks/plugins/flutter_ringtone_player/ios`)
- flutter_treezor_entrust_sdk_bridge (from `.symlinks/plugins/flutter_treezor_entrust_sdk_bridge/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- jc_sdk (from `.symlinks/plugins/jc_sdk/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- record_ios (from `.symlinks/plugins/record_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS:
trunk:
- Firebase
- FirebaseABTesting
- FirebaseAnalytics
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- FirebaseCrashlytics
- FirebaseInstallations
- FirebaseMessaging
- FirebasePerformance
- FirebaseRemoteConfig
- FirebaseRemoteConfigInterop
- FirebaseSessions
- FirebaseSharedSwift
- GoogleAdsOnDeviceConversion
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleUtilities
- libwebp
- Mantle
- nanopb
- PromisesObjC
- PromisesSwift
- SDWebImage
- SDWebImageWebPCoder
EXTERNAL SOURCES:
audioplayers_darwin:
:path: ".symlinks/plugins/audioplayers_darwin/darwin"
firebase_analytics:
:path: ".symlinks/plugins/firebase_analytics/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_crashlytics:
:path: ".symlinks/plugins/firebase_crashlytics/ios"
firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios"
firebase_performance:
:path: ".symlinks/plugins/firebase_performance/ios"
firebase_remote_config:
:path: ".symlinks/plugins/firebase_remote_config/ios"
Flutter:
:path: Flutter
flutter_contacts:
:path: ".symlinks/plugins/flutter_contacts/ios"
flutter_image_compress_common:
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_ringtone_player:
:path: ".symlinks/plugins/flutter_ringtone_player/ios"
flutter_treezor_entrust_sdk_bridge:
:path: ".symlinks/plugins/flutter_treezor_entrust_sdk_bridge/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
jc_sdk:
:path: ".symlinks/plugins/jc_sdk/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/darwin"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
record_ios:
:path: ".symlinks/plugins/record_ios/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
audioplayers_darwin: f15e209a3e856d1a7edcf98dc029f484fead2242
Firebase: 065f2bb395062046623036d8e6dc857bc2521d56
firebase_analytics: 42693ebf35c4d330b74abcb46ca80351703644e0
firebase_core: 98bcc1bd1a097bcb8b1ed6e091de3039802527c4
firebase_crashlytics: 2fd6c030ca2f91e8d3b13d2e6e9a08a282c9d259
firebase_messaging: e24e69d994d53e46fd794143544841877bd85a53
firebase_performance: 39d7f9632628c64cacd9e9808d4783cffd83eaa2
firebase_remote_config: 0d060eef0fdfb288ffc41903ba9a60bb963755ea
FirebaseABTesting: a399ffe546392a39b19a5c2fb28bd8ea178a6f47
FirebaseAnalytics: cd7d01d352f3c237c9a0e31552c257cd0b0c0352
FirebaseCore: 428912f751178b06bef0a1793effeb4a5e09a9b8
FirebaseCoreExtension: e911052d59cd0da237a45d706fc0f81654f035c1
FirebaseCoreInternal: b321eafae5362113bc182956fafc9922cfc77b72
FirebaseCrashlytics: 43913d587ef07beaf5db703baa61eacf9554658c
FirebaseInstallations: 7b64ffd006032b2b019a59b803858df5112d9eaa
FirebaseMessaging: 7d6cdbff969127c4151c824fe432f0e301210f15
FirebasePerformance: 94f614453614d8bb2a1a0177f3a1a6d2dbf4c504
FirebaseRemoteConfig: a2f6545e41551ffb520241d38b5d3d6776c9ebe8
FirebaseRemoteConfigInterop: 765ee19cd2bfa8e54937c8dae901eb634ad6787d
FirebaseSessions: a2d06fd980431fda934c7a543901aca05fc4edcc
FirebaseSharedSwift: 9d2fa84a46676302b89dbd5e6e62bce2fe376909
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_contacts: edb1c5ce76aa433e20e6cb14c615f4c0b66e0983
flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e
flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
flutter_ringtone_player: 15eba85187230b87b2512f0e1b92225618bc03e7
flutter_treezor_entrust_sdk_bridge: 4c2c94fb74ab57576e8d49f5f2a4b214e41141fe
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
GoogleAppMeasurement: fce7c1c90640d2f9f5c56771f71deacb2ba3f98c
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
jc_sdk: 3c77f6d7e5e052e2960c47629f612127585779cf
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
record_ios: 26294aaa39e4bb7665b0fef78bdc23d723b432f2
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4
PODFILE CHECKSUM: 88fd88ec59f7f53cf74c06ffd99479aec395968a
PODFILE CHECKSUM: 02dccdf227cb9aef09ff0299e4898a8a19004223
COCOAPODS: 1.16.2

View File

@@ -18,7 +18,6 @@
AA0000011234567800000001 /* AntelopRelease.plist in Resources */ = {isa = PBXBuildFile; fileRef = AA0000011234567800000002 /* AntelopRelease.plist */; };
AA5000010000000000000001 /* AntelopRelease-development.plist in Resources */ = {isa = PBXBuildFile; fileRef = AA5000010000000000000002 /* AntelopRelease-development.plist */; };
AA5000010000000000000003 /* AntelopRelease-staging.plist in Resources */ = {isa = PBXBuildFile; fileRef = AA5000010000000000000004 /* AntelopRelease-staging.plist */; };
D6B9158A899AF56C44180233 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B8D66015CBEA02CDD29EB55 /* GoogleService-Info.plist */; };
FB256274E508EC552E337980 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B56AB2467FA9548370ACF02 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
@@ -54,7 +53,6 @@
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3B8D66015CBEA02CDD29EB55 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
401E1064C971570DADB8AA9B /* Pods-RunnerTests.profile-development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile-development.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile-development.xcconfig"; sourceTree = "<group>"; };
4B56AB2467FA9548370ACF02 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4E688A593FA9E76BDD0DFBFB /* Pods-Runner.debug-staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-staging.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-staging.xcconfig"; sourceTree = "<group>"; };
@@ -146,7 +144,6 @@
331C8082294A63A400263BE5 /* RunnerTests */,
CB8808A12E373F2255B5FC16 /* Pods */,
BE496D7F3574271661ADBDCE /* Frameworks */,
3B8D66015CBEA02CDD29EB55 /* GoogleService-Info.plist */,
);
sourceTree = "<group>";
};
@@ -255,11 +252,8 @@
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
F0758EB530B1A8787EB3F30B /* Copy GoogleService-Info */,
AA0000022345678900000001 /* Copy AntelopRelease */,
437F5EA1E5D92D7C421FD996 /* [CP] Embed Pods Frameworks */,
791C3CA41F1AAEE1267769C8 /* [CP] Copy Pods Resources */,
0F0F4E82D9AA0B3E11014E72 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */,
);
buildRules = (
);
@@ -328,31 +322,12 @@
AA0000011234567800000001 /* AntelopRelease.plist in Resources */,
AA5000010000000000000001 /* AntelopRelease-development.plist in Resources */,
AA5000010000000000000003 /* AntelopRelease-staging.plist in Resources */,
D6B9158A899AF56C44180233 /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
0F0F4E82D9AA0B3E11014E72 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\"";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\n#!/bin/bash\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:${PUB_CACHE}/bin:$HOME/.pub-cache/bin\"\n\nif [ -z \"$PODS_ROOT\" ] || [ ! -d \"$PODS_ROOT/FirebaseCrashlytics\" ]; then\n # Cannot use \"BUILD_DIR%/Build/*\" as per Firebase documentation, it points to \"flutter-project/build/ios/*\" path which doesn't have run script\n DERIVED_DATA_PATH=$(echo \"$BUILD_ROOT\" | sed -E 's|(.*DerivedData/[^/]+).*|\\1|')\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"${DERIVED_DATA_PATH}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\nelse\n PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT=\"$PODS_ROOT/FirebaseCrashlytics/run\"\nfi\n\n# Command to upload symbols script used to upload symbols to Firebase server\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PATH_TO_CRASHLYTICS_UPLOAD_SCRIPT\" --platform=ios --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n";
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -462,42 +437,6 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
F0758EB530B1A8787EB3F30B /* Copy GoogleService-Info */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Copy GoogleService-Info";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/scripts/copy-google-service-plist.sh\"";
};
AA0000022345678900000001 /* Copy AntelopRelease */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Copy AntelopRelease";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/scripts/copy-antelop-release-plist.sh\"";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -591,7 +530,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -604,7 +543,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-development";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-development;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Runner/Runner-development.entitlements";
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -722,7 +661,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -773,7 +712,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -788,7 +727,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-development";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-development;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Runner/Runner-development.entitlements";
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -812,7 +751,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-development";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-development;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Runner/Runner-development.entitlements";
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -880,7 +819,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -937,7 +876,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -988,7 +927,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -1042,7 +981,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -1096,7 +1035,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -1148,7 +1087,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -1161,7 +1100,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-staging";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-staging;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Runner/Runner-staging.entitlements";
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -1185,7 +1124,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-production";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-production;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -1209,7 +1148,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-staging";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-staging;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Runner/Runner-staging.entitlements";
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -1232,7 +1171,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-production";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-production;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -1255,7 +1194,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-staging";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-staging;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "Runner/Runner-staging.entitlements";
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -1278,7 +1217,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-production";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-production;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";

View File

@@ -23,16 +23,12 @@ import AntelopSDK
override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
AntelopAppDelegate.shared.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
// Forward to FlutterAppDelegate so Firebase Messaging can capture the APNs token via swizzling.
super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
}
override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
if AntelopAppDelegate.shared.didReceiveRemoteNotification(userInfo, fetchCompletionHandler: completionHandler) {
guard !AntelopAppDelegate.shared.didReceiveRemoteNotification(userInfo, fetchCompletionHandler: completionHandler) else {
return
}
// Forward to FlutterAppDelegate so Firebase Messaging can deliver the notification to Dart.
super.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler)
}
override func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyBeijehJIznndwIUlbMkj6reYT4z-WHGfQ</string>
<key>GCM_SENDER_ID</key>
<string>535646668726</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.savefamily.app.dev</string>
<key>PROJECT_ID</key>
<string>sf-platform-pre</string>
<key>STORAGE_BUCKET</key>
<string>sf-platform-pre.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:535646668726:ios:524afa641f61d7cb5e6317</string>
</dict>
</plist>

View File

@@ -50,11 +50,7 @@
<key>NSContactsUsageDescription</key>
<string>Necesitamos acceso a tus contactos para seleccionar números de teléfono.</string>
<key>NSCameraUsageDescription</key>
<string>Necesitamos la cámara para escanear códigos QR y realizar videollamadas</string>
<key>NSMicrophoneUsageDescription</key>
<string>Necesitamos el micrófono para realizar videollamadas</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Necesitamos acceso a la galería de fotos para compartir imágenes</string>
<string>Necesitamos la cámara para escanear códigos QR</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Usamos tu ubicación para verificar la seguridad de las transacciones.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>

View File

@@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.savefamily.app.stag</string>

View File

@@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>production</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.savefamily.app.prod</string>

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyBeijehJIznndwIUlbMkj6reYT4z-WHGfQ</string>
<key>GCM_SENDER_ID</key>
<string>535646668726</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.savefamily.app.dev</string>
<key>PROJECT_ID</key>
<string>sf-platform-pre</string>
<key>STORAGE_BUCKET</key>
<string>sf-platform-pre.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:535646668726:ios:524afa641f61d7cb5e6317</string>
</dict>
</plist>

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyC0_d7Z6uVHHKhaf7JHRROaY6g2mvvpOXU</string>
<key>GCM_SENDER_ID</key>
<string>950566980029</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.savefamily.app</string>
<key>PROJECT_ID</key>
<string>sf-platform-pro</string>
<key>STORAGE_BUCKET</key>
<string>sf-platform-pro.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:950566980029:ios:987b4f0b9e9b897481aad4</string>
</dict>
</plist>

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyBeijehJIznndwIUlbMkj6reYT4z-WHGfQ</string>
<key>GCM_SENDER_ID</key>
<string>535646668726</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.savefamily.app.stag</string>
<key>PROJECT_ID</key>
<string>sf-platform-pre</string>
<key>STORAGE_BUCKET</key>
<string>sf-platform-pre.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:535646668726:ios:5172d626d02dfe215e6317</string>
</dict>
</plist>

View File

@@ -1,85 +0,0 @@
#!/usr/bin/env ruby
#
# Adds a "Copy GoogleService-Info" Run Script Build Phase to the Runner target.
# The script copies ios/flavors/{flavor}/GoogleService-Info.plist to the .app
# bundle based on the build CONFIGURATION (Debug-development, Release-staging, etc.).
#
# Idempotent: if the build phase already exists, does nothing.
#
# Usage:
# ruby ios/scripts/add-copy-google-service-build-phase.rb
require 'xcodeproj'
PROJECT_PATH = File.expand_path('../../Runner.xcodeproj', __FILE__)
TARGET_NAME = 'Runner'
PHASE_NAME = 'Copy GoogleService-Info'
SHELL_SCRIPT = '"${SRCROOT}/scripts/copy-google-service-plist.sh"'
project = Xcodeproj::Project.open(PROJECT_PATH)
target = project.targets.find { |t| t.name == TARGET_NAME }
unless target
abort "ERROR: Target '#{TARGET_NAME}' not found in project."
end
# Check if the build phase already exists (idempotency)
existing = target.build_phases.find do |phase|
phase.is_a?(Xcodeproj::Project::Object::PBXShellScriptBuildPhase) && phase.name == PHASE_NAME
end
if existing
puts "OK: Build phase '#{PHASE_NAME}' already exists. No changes needed."
exit 0
end
# Create the new build phase
phase = target.new_shell_script_build_phase(PHASE_NAME)
phase.shell_path = '/bin/sh'
phase.shell_script = SHELL_SCRIPT
phase.input_paths = []
phase.output_paths = []
phase.run_only_for_deployment_postprocessing = '0'
# Move it before the embed frameworks phase (or at the end if no such phase)
# Order: Sources -> Frameworks -> Resources -> ... -> ThinBinary -> CopyGoogleService -> EmbedPodsFrameworks -> CopyPodsResources
build_phases = target.build_phases
# Find the index of "Thin Binary" if it exists
thin_binary_idx = build_phases.find_index do |p|
p.respond_to?(:name) && p.name == 'Thin Binary'
end
# Find the index of "[CP] Embed Pods Frameworks" if it exists
embed_pods_idx = build_phases.find_index do |p|
p.respond_to?(:name) && p.name && p.name.include?('Embed Pods Frameworks')
end
# Remove the just-added phase from its current position (it gets appended at the end)
build_phases.delete(phase)
# Insert at the right spot
target_idx = if thin_binary_idx && embed_pods_idx && thin_binary_idx < embed_pods_idx
# Place between Thin Binary and Embed Pods Frameworks
embed_pods_idx
elsif thin_binary_idx
# Place right after Thin Binary
thin_binary_idx + 1
elsif embed_pods_idx
# Place right before Embed Pods Frameworks
embed_pods_idx
else
# Append at the end
build_phases.length
end
build_phases.insert(target_idx, phase)
project.save
puts "OK: Added build phase '#{PHASE_NAME}' at position #{target_idx}."
puts "Build phases order:"
target.build_phases.each_with_index do |p, i|
name = p.respond_to?(:name) && p.name ? p.name : p.class.name
puts " #{i}: #{name}"
end

View File

@@ -1,46 +0,0 @@
#!/bin/bash
#
# Copies the correct AntelopRelease.plist into the .app bundle based on the
# active build CONFIGURATION (Debug-development, Release-staging, etc.). The
# Antelop SDK reads `AntelopRelease.plist` by a fixed name at runtime, so the
# per-flavor variants (AntelopRelease-development.plist,
# AntelopRelease-staging.plist) must be copied over that fixed name.
#
# Source layout: ios/Runner/AntelopRelease.plist (production),
# ios/Runner/AntelopRelease-development.plist,
# ios/Runner/AntelopRelease-staging.plist.
set -e
echo "Configuration: ${CONFIGURATION}"
if [[ $CONFIGURATION =~ \-([^-]*)$ ]]; then
flavor=${BASH_REMATCH[1]}
else
echo "warning: Could not extract flavor from CONFIGURATION='${CONFIGURATION}', defaulting to 'production'"
flavor="production"
fi
echo "Flavor: $flavor"
case "$flavor" in
development|staging)
SRC="${PROJECT_DIR}/Runner/AntelopRelease-${flavor}.plist"
;;
production)
SRC="${PROJECT_DIR}/Runner/AntelopRelease.plist"
;;
*)
echo "warning: Unknown flavor '${flavor}', falling back to AntelopRelease.plist"
SRC="${PROJECT_DIR}/Runner/AntelopRelease.plist"
;;
esac
if [ ! -f "$SRC" ]; then
echo "error: ${SRC} not found"
exit 1
fi
DEST="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/AntelopRelease.plist"
echo "Copying ${SRC} -> ${DEST}"
cp "${SRC}" "${DEST}"

View File

@@ -1,35 +0,0 @@
#!/bin/bash
#
# Copies the correct GoogleService-Info.plist into the .app bundle
# based on the active build CONFIGURATION (Debug-development,
# Release-staging, etc.). Reads from ios/flavors/{flavor}/GoogleService-Info.plist
# and writes to the final bundle.
#
# Add this as a Run Script Build Phase in Xcode AFTER "Thin Binary" and
# BEFORE "[CP] Embed Pods Frameworks" (or near the end of the phases).
set -e
echo "Configuration: ${CONFIGURATION}"
# Extract flavor from the build configuration name (everything after the last "-")
if [[ $CONFIGURATION =~ \-([^-]*)$ ]]; then
flavor=${BASH_REMATCH[1]}
else
echo "warning: Could not extract flavor from CONFIGURATION='${CONFIGURATION}', defaulting to 'development'"
flavor="development"
fi
echo "Flavor: $flavor"
GOOGLESERVICE_INFO_PLIST=GoogleService-Info.plist
GOOGLESERVICE_INFO_FILE="${PROJECT_DIR}/flavors/${flavor}/${GOOGLESERVICE_INFO_PLIST}"
if [ ! -f "$GOOGLESERVICE_INFO_FILE" ]; then
echo "error: ${GOOGLESERVICE_INFO_FILE} not found"
exit 1
fi
PLIST_DESTINATION="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app"
echo "Copying ${GOOGLESERVICE_INFO_FILE} -> ${PLIST_DESTINATION}/${GOOGLESERVICE_INFO_PLIST}"
cp "${GOOGLESERVICE_INFO_FILE}" "${PLIST_DESTINATION}/${GOOGLESERVICE_INFO_PLIST}"

View File

@@ -2,8 +2,6 @@ abstract class Environment {
static const env = String.fromEnvironment('env', defaultValue: 'development');
static const apiBaseUrl = String.fromEnvironment('apiBaseUrl');
static const apiOrigin = String.fromEnvironment('apiOrigin');
static const wsUrl = String.fromEnvironment('wsUrl');
static const juphoonAppKey = String.fromEnvironment('juphoonAppKey');
// --- Fase 2: Firebase & Sentry ---
// static const sentryDsn = String.fromEnvironment('sentryDsn');

View File

@@ -1,11 +1,9 @@
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'environment.dart';
class SaveFamilyEnvConfig implements EnvConfig {
class QuestiaEnvConfig implements EnvConfig {
@override
String get apiBaseUrl => Environment.apiBaseUrl;
@override
String get apiOrigin => Environment.apiOrigin;
@override
String get wsUrl => Environment.wsUrl;
}

View File

@@ -1,14 +0,0 @@
import 'package:videocall_sdk/videocall_sdk.dart';
import 'environment.dart';
class SaveFamilyVideocallConfig implements VideocallSdkConfig {
@override
String get appKey => Environment.juphoonAppKey;
@override
String get serverAddress => '';
@override
CreateParam? get createParam => null;
}

View File

@@ -1,3 +0,0 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
late ProviderContainer appProviderContainer;

View File

@@ -1,73 +0,0 @@
import 'package:flutter/material.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:url_launcher/url_launcher.dart';
import 'app_version_check.dart';
Future<void> showAppUpdateDialog(
BuildContext context, {
required AvailableUpdate result,
VoidCallback? onDismiss,
VoidCallback? onUpdateTapped,
}) {
final isForce = result is ForceUpdate;
return showDialog<void>(
context: context,
barrierDismissible: !isForce,
builder: (dialogContext) {
return PopScope(
canPop: !isForce,
child: AlertDialog(
title: Text(
dialogContext.translate(
isForce
? I18n.appUpdateRequiredTitle
: I18n.appUpdateAvailableTitle,
),
),
content: Text(
result.message.isNotEmpty
? result.message
: dialogContext.translate(
isForce
? I18n.appUpdateRequiredMessage
: I18n.appUpdateAvailableMessage,
),
),
actions: [
if (!isForce)
TextButton(
onPressed: () {
Navigator.of(dialogContext).pop();
onDismiss?.call();
},
child: Text(dialogContext.translate(I18n.appUpdateLater)),
),
TextButton(
onPressed: () => _launchStore(result.storeUrl, onUpdateTapped),
child: Text(dialogContext.translate(I18n.appUpdateNow)),
),
],
),
);
},
);
}
Future<void> _launchStore(String storeUrl, VoidCallback? onTapped) async {
onTapped?.call();
try {
final uri = Uri.tryParse(storeUrl);
if (uri == null) {
debugPrint('[AppUpdateDialog] invalid store URL: $storeUrl');
return;
}
final launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!launched) {
debugPrint('[AppUpdateDialog] launchUrl returned false for $storeUrl');
}
} catch (e) {
debugPrint('[AppUpdateDialog] launchUrl failed: $e');
}
}

View File

@@ -1,123 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_app_platform/navigation/app_router.dart';
import 'package:sf_tracking/sf_tracking.dart';
import 'app_update_dialog.dart';
import 'app_version_check.dart';
class AppUpdateGate extends ConsumerStatefulWidget {
const AppUpdateGate({super.key, required this.child});
final Widget child;
@override
ConsumerState<AppUpdateGate> createState() => _AppUpdateGateState();
}
class _AppUpdateGateState extends ConsumerState<AppUpdateGate> {
bool _dialogVisible = false;
VoidCallback? _pendingRouterListener;
@override
void dispose() {
_detachPendingRouterListener();
super.dispose();
}
void _detachPendingRouterListener() {
final listener = _pendingRouterListener;
if (listener != null) {
appRouter.routerDelegate.removeListener(listener);
_pendingRouterListener = null;
}
}
bool _isStableRoute() {
final path = appRouter.routerDelegate.currentConfiguration.uri.path;
return path.startsWith(AppRoutes.dashboard) ||
path.startsWith(AppRoutes.legacyDashboard);
}
void _onResultEmitted(AppVersionCheckResult result) {
if (result is! AvailableUpdate) {
_detachPendingRouterListener();
return;
}
if (_dialogVisible) return;
if (_isStableRoute()) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_show(result);
});
return;
}
_detachPendingRouterListener();
void onChange() {
if (!_isStableRoute()) return;
_detachPendingRouterListener();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_show(result);
});
}
_pendingRouterListener = onChange;
appRouter.routerDelegate.addListener(onChange);
}
void _show(AvailableUpdate result) {
if (!mounted) return;
if (_dialogVisible) return;
final ctx = appRouter.routerDelegate.navigatorKey.currentContext;
if (ctx == null) return;
final tracking = ref.read(sfTrackingProvider);
final kind = _kindLabel(result);
tracking.appUpdateDialogShown(
kind: kind,
latestBuild: result.latestBuild,
currentBuild: result.currentBuild,
);
_dialogVisible = true;
showAppUpdateDialog(
ctx,
result: result,
onDismiss: () {
if (result is SoftUpdate) {
tracking.appUpdateDialogDismissed(latestBuild: result.latestBuild);
ref.read(appVersionCheckProvider.notifier).markSoftDismissed(result);
}
},
onUpdateTapped: () => tracking.appUpdateCtaTapped(
kind: kind,
latestBuild: result.latestBuild,
),
).whenComplete(() {
_dialogVisible = false;
});
}
String _kindLabel(AvailableUpdate result) {
return switch (result) {
SoftUpdate() => 'soft',
ForceUpdate() => 'force',
};
}
@override
Widget build(BuildContext context) {
ref.listen<AsyncValue<AppVersionCheckResult>>(
appVersionCheckProvider,
(previous, next) {
next.whenData(_onResultEmitted);
},
);
return widget.child;
}
}

View File

@@ -1,61 +0,0 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'app_version_check_result.dart';
import 'app_version_check_service.dart';
export 'app_version_check_result.dart';
export 'app_version_check_service.dart';
export 'dismissed_build_store.dart';
export 'remote_config_reader.dart';
final appVersionCheckServiceProvider = Provider<AppVersionCheckService>((ref) {
return AppVersionCheckService();
});
class AppVersionCheck extends AsyncNotifier<AppVersionCheckResult> {
Future<void>? _inflight;
AppVersionCheckService get _service =>
ref.read(appVersionCheckServiceProvider);
@override
Future<AppVersionCheckResult> build() {
return _service.check();
}
Future<void> refresh() => _runSerialized(() async {
state = AsyncData(await _service.check());
});
Future<void> markSoftDismissed(SoftUpdate result) =>
_runSerialized(() async {
await _service.markSoftDismissed(result.latestBuild);
state = const AsyncData(NoUpdate());
});
Future<void> _runSerialized(Future<void> Function() op) async {
final previous = _inflight;
final completer = Completer<void>();
_inflight = completer.future;
try {
if (previous != null) {
try {
await previous;
} catch (_) {}
}
await op();
} finally {
completer.complete();
if (identical(_inflight, completer.future)) {
_inflight = null;
}
}
}
}
final appVersionCheckProvider =
AsyncNotifierProvider<AppVersionCheck, AppVersionCheckResult>(
AppVersionCheck.new,
);

View File

@@ -1,39 +0,0 @@
sealed class AppVersionCheckResult {
const AppVersionCheckResult();
}
class NoUpdate extends AppVersionCheckResult {
const NoUpdate();
}
sealed class AvailableUpdate extends AppVersionCheckResult {
const AvailableUpdate({
required this.message,
required this.storeUrl,
required this.latestBuild,
required this.currentBuild,
});
final String message;
final String storeUrl;
final int latestBuild;
final int currentBuild;
}
class SoftUpdate extends AvailableUpdate {
const SoftUpdate({
required super.message,
required super.storeUrl,
required super.latestBuild,
required super.currentBuild,
});
}
class ForceUpdate extends AvailableUpdate {
const ForceUpdate({
required super.message,
required super.storeUrl,
required super.latestBuild,
required super.currentBuild,
});
}

View File

@@ -1,88 +0,0 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'app_version_check_result.dart';
import 'dismissed_build_store.dart';
import 'remote_config_keys.dart';
import 'remote_config_reader.dart';
typedef CurrentBuildLoader = Future<int> Function();
Future<int> defaultCurrentBuildLoader() async {
final info = await PackageInfo.fromPlatform();
return int.tryParse(info.buildNumber) ?? 0;
}
class AppVersionCheckService {
AppVersionCheckService({
RemoteConfigReader? remoteConfig,
DismissedBuildStore? dismissedStore,
CurrentBuildLoader? currentBuildLoader,
bool? isIos,
}) : _remoteConfig = remoteConfig ?? FirebaseRemoteConfigReader(),
_dismissedStore = dismissedStore ?? SharedPrefsDismissedBuildStore(),
_currentBuildLoader = currentBuildLoader ?? defaultCurrentBuildLoader,
_isIos = isIos ?? Platform.isIOS;
final RemoteConfigReader _remoteConfig;
final DismissedBuildStore _dismissedStore;
final CurrentBuildLoader _currentBuildLoader;
final bool _isIos;
Future<AppVersionCheckResult> check() async {
try {
final currentBuild = await _currentBuildLoader();
try {
await _remoteConfig.fetchAndActivate();
} catch (e) {
debugPrint('[AppVersionCheck] RC fetch failed: $e');
}
final minRequired = _remoteConfig.getInt(RemoteConfigKeys.minRequiredBuild);
final latest = _remoteConfig.getInt(RemoteConfigKeys.latestBuild);
final forceFlag = _remoteConfig.getBool(RemoteConfigKeys.updateForce);
final message = _remoteConfig.getString(RemoteConfigKeys.updateMessage);
final storeUrl = _isIos
? _remoteConfig.getString(RemoteConfigKeys.updateUrlIos)
: _remoteConfig.getString(RemoteConfigKeys.updateUrlAndroid);
if (forceFlag || currentBuild < minRequired) {
return ForceUpdate(
message: message,
storeUrl: storeUrl,
latestBuild: latest,
currentBuild: currentBuild,
);
}
if (currentBuild < latest) {
final dismissedFor = await _dismissedStore.read();
if (latest <= dismissedFor) {
return const NoUpdate();
}
return SoftUpdate(
message: message,
storeUrl: storeUrl,
latestBuild: latest,
currentBuild: currentBuild,
);
}
return const NoUpdate();
} catch (e) {
debugPrint('[AppVersionCheck] check failed: $e');
return const NoUpdate();
}
}
Future<void> markSoftDismissed(int latestBuild) async {
try {
await _dismissedStore.write(latestBuild);
} catch (e) {
debugPrint('[AppVersionCheck] markSoftDismissed failed: $e');
}
}
}

View File

@@ -1,22 +0,0 @@
import 'package:shared_preferences/shared_preferences.dart';
abstract class DismissedBuildStore {
Future<int> read();
Future<void> write(int latestBuild);
}
class SharedPrefsDismissedBuildStore implements DismissedBuildStore {
static const _key = 'app_update_dismissed_for_latest_build';
@override
Future<int> read() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getInt(_key) ?? 0;
}
@override
Future<void> write(int latestBuild) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_key, latestBuild);
}
}

View File

@@ -1,10 +0,0 @@
class RemoteConfigKeys {
const RemoteConfigKeys._();
static const minRequiredBuild = 'min_required_build';
static const latestBuild = 'latest_build';
static const updateForce = 'update_force';
static const updateMessage = 'update_message';
static const updateUrlIos = 'update_url_ios';
static const updateUrlAndroid = 'update_url_android';
}

View File

@@ -1,27 +0,0 @@
import 'package:firebase_remote_config/firebase_remote_config.dart';
abstract class RemoteConfigReader {
Future<void> fetchAndActivate();
int getInt(String key);
bool getBool(String key);
String getString(String key);
}
class FirebaseRemoteConfigReader implements RemoteConfigReader {
FirebaseRemoteConfigReader([FirebaseRemoteConfig? rc])
: _rc = rc ?? FirebaseRemoteConfig.instance;
final FirebaseRemoteConfig _rc;
@override
Future<void> fetchAndActivate() => _rc.fetchAndActivate();
@override
int getInt(String key) => _rc.getInt(key);
@override
bool getBool(String key) => _rc.getBool(key);
@override
String getString(String key) => _rc.getString(key);
}

View File

@@ -1,20 +0,0 @@
/// Compile-time constant that controls which app the splash screen
/// navigates to when the app starts.
///
/// Set via `--dart-define=APP_MODE=payment` (or `legacy`) at launch time.
/// Defaults to `legacy` to preserve historical behavior when no flag is
/// passed (e.g. `flutter run` from CLI without arguments).
///
/// Used only for local development to switch between the legacy app
/// (watch/device control) and the payment app (Treezor wallet) without
/// needing separate flavors or entry points.
const String appMode = String.fromEnvironment(
'APP_MODE',
defaultValue: 'legacy',
);
/// Whether the app should boot into the payment (Treezor wallet) flow.
bool get isPaymentMode => appMode == 'payment';
/// Whether the app should boot into the legacy (watch/device) flow.
bool get isLegacyMode => appMode == 'legacy';

View File

@@ -1,73 +0,0 @@
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_performance/firebase_performance.dart';
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:flutter/foundation.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart';
import '../config/env/environment_enum.dart';
import '../firebase_options_dev.dart' as dev_options;
import '../firebase_options_prod.dart' as prod_options;
import '../firebase_options_staging.dart' as staging_options;
import 'app_version_check/remote_config_keys.dart';
Future<void> setupFirebase(EnvironmentEnum env) async {
final FirebaseOptions options;
switch (env) {
case EnvironmentEnum.development:
options = dev_options.DefaultFirebaseOptions.currentPlatform;
case EnvironmentEnum.staging:
options = staging_options.DefaultFirebaseOptions.currentPlatform;
case EnvironmentEnum.production:
options = prod_options.DefaultFirebaseOptions.currentPlatform;
}
await Firebase.initializeApp(options: options);
// Report crashes in ALL builds (debug + release) so we catch issues during testing too.
// TODO(gdpr): wire `enabled` to real consent once the fix in backlog lands.
final crashlytics = FirebaseCrashlyticsService(enabled: true);
FlutterError.onError = (details) =>
crashlytics.recordFlutterError(details, fatal: true);
PlatformDispatcher.instance.onError = (error, stack) {
crashlytics.recordError(error, stack, fatal: true);
return true;
};
await FirebaseAnalytics.instance.setUserProperty(
name: 'env',
value: env.name,
);
final remoteConfig = FirebaseRemoteConfig.instance;
await remoteConfig.setConfigSettings(
RemoteConfigSettings(
fetchTimeout: const Duration(minutes: 1),
minimumFetchInterval: kDebugMode
? const Duration(minutes: 1)
: const Duration(hours: 12),
),
);
await remoteConfig.setDefaults(<String, Object>{
RemoteConfigKeys.minRequiredBuild: 0,
RemoteConfigKeys.latestBuild: 0,
RemoteConfigKeys.updateForce: false,
RemoteConfigKeys.updateMessage: '',
RemoteConfigKeys.updateUrlIos: 'https://apps.apple.com/app/id6759875039',
RemoteConfigKeys.updateUrlAndroid:
'https://play.google.com/store/apps/details?id=com.savefamily.app',
BrandLinksKeys.privacyPolicyUrl:
'https://savefamilygps.com/pages/politica-de-privacidad-reloj-gps-infantil-localizador-savefamily',
BrandLinksKeys.corporateWebsiteUrl: 'https://www.savefamilygps.com/',
BrandLinksKeys.helpCenterUrl: 'https://savefamilygpshelp.zendesk.com/hc/es',
BrandLinksKeys.supportEmail: 'info@savefamilygps.com',
});
try {
await remoteConfig.fetchAndActivate();
} catch (e) {
debugPrint('[Firebase] RemoteConfig fetch failed: $e');
}
FirebasePerformance.instance.setPerformanceCollectionEnabled(true);
}

View File

@@ -1,13 +0,0 @@
class IncomingCallNotificationConfig {
const IncomingCallNotificationConfig._();
static const String channelId = 'sf_incoming_call_channel_v2';
static const String legacyChannelId = 'sf_incoming_call_channel';
static const String channelName = 'Videollamadas entrantes';
static const String channelDescription =
'Notificaciones tipo llamada para videollamadas entrantes desde el reloj.';
static const int notificationId = 1001;
static const String actionAccept = 'accept';
static const String actionReject = 'reject';
static const String systemRingtoneUri = 'content://settings/system/ringtone';
}

View File

@@ -1,63 +0,0 @@
import 'package:shared_preferences/shared_preferences.dart';
class IncomingCallStrings {
const IncomingCallStrings({
required this.title,
required this.body,
required this.acceptLabel,
required this.rejectLabel,
required this.channelName,
required this.channelDescription,
});
final String title;
final String body;
final String acceptLabel;
final String rejectLabel;
final String channelName;
final String channelDescription;
}
class IncomingCallStringsCache {
const IncomingCallStringsCache._();
static const _keyTitle = 'incoming_call.title';
static const _keyBody = 'incoming_call.body';
static const _keyAccept = 'incoming_call.accept';
static const _keyReject = 'incoming_call.reject';
static const _keyChannelName = 'incoming_call.channel_name';
static const _keyChannelDescription = 'incoming_call.channel_description';
static const _fallback = IncomingCallStrings(
title: 'Videollamada entrante',
body: 'El reloj te está llamando',
acceptLabel: 'Aceptar',
rejectLabel: 'Rechazar',
channelName: 'Videollamadas entrantes',
channelDescription:
'Notificaciones tipo llamada para videollamadas entrantes desde el reloj.',
);
static Future<void> save(IncomingCallStrings strings) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_keyTitle, strings.title);
await prefs.setString(_keyBody, strings.body);
await prefs.setString(_keyAccept, strings.acceptLabel);
await prefs.setString(_keyReject, strings.rejectLabel);
await prefs.setString(_keyChannelName, strings.channelName);
await prefs.setString(_keyChannelDescription, strings.channelDescription);
}
static Future<IncomingCallStrings> load() async {
final prefs = await SharedPreferences.getInstance();
return IncomingCallStrings(
title: prefs.getString(_keyTitle) ?? _fallback.title,
body: prefs.getString(_keyBody) ?? _fallback.body,
acceptLabel: prefs.getString(_keyAccept) ?? _fallback.acceptLabel,
rejectLabel: prefs.getString(_keyReject) ?? _fallback.rejectLabel,
channelName: prefs.getString(_keyChannelName) ?? _fallback.channelName,
channelDescription: prefs.getString(_keyChannelDescription) ??
_fallback.channelDescription,
);
}
}

View File

@@ -1,54 +1,36 @@
import 'package:design_system/design_system.dart';
import 'package:get_it/get_it.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get_it/get_it.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:navigation/navigation.dart';
import 'package:design_system/design_system.dart';
import 'package:sca_treezor/sca_treezor.dart';
import 'package:sf_app_platform/config/env/environment_enum.dart';
import 'package:sf_app_platform/config/env/save_family_env_config.dart';
import 'package:sf_app_platform/config/env/save_family_videocall_config.dart';
import 'package:sf_app_platform/core/app_provider_container.dart';
import 'package:sf_app_platform/core/config/app_mode.dart';
import 'package:sf_app_platform/core/firebase_init.dart';
import 'package:sf_app_platform/core/notifications_init.dart';
import 'package:sf_app_platform/config/env/questia_env_config.dart';
import 'package:sf_app_platform/navigation/app_router.dart';
import 'package:sf_app_platform/save_family_app.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_tracking/sf_tracking.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:videocall_sdk/videocall_sdk.dart';
Future<void> initApp(EnvironmentEnum env) async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await initializeDateFormatting();
tz.initializeTimeZones();
final sharedPreferences = await SharedPreferences.getInstance();
navigationModule();
scaTreezorModule();
videocallSdkModule(SaveFamilyVideocallConfig());
configureAppRouter();
themePackages();
await setupFirebase(env);
// --- Fase 2: Firebase ---
// await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// TODO Fase 2: await initSentry(env);
configureAppRouter();
onRouterReady();
// --- Fase 2: Sentry ---
// await initSentry(env);
await configureDependencies(
SaveFamilyEnvConfig(),
QuestiaEnvConfig(),
log: env.isDevelopment || kDebugMode,
onTokenExpired: isPaymentMode
? () => appRouter.go(AppRoutes.scaTreezor)
: null,
onTokenExpired: () => appRouter.go(AppRoutes.scaTreezor),
onUnauthorized: () async {
final currentLocation =
appRouter.routerDelegate.currentConfiguration.uri.path;
@@ -57,25 +39,9 @@ Future<void> initApp(EnvironmentEnum env) async {
await GetIt.I<TreezorWalletConnectionService>().logout();
} catch (_) {}
await clearSessionData();
WidgetsBinding.instance.addPostFrameCallback((_) {
appRouter.go(isPaymentMode ? AppRoutes.login : AppRoutes.legacyLogin);
});
appRouter.go(AppRoutes.login);
},
);
appProviderContainer = ProviderContainer(
overrides: [
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
],
);
await setupNotifications();
initSfTracking();
runApp(
UncontrolledProviderScope(
container: appProviderContainer,
child: const SaveFamilyApp(),
),
);
runApp(const ProviderScope(child: SaveFamilyApp()));
}

View File

@@ -1,409 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:chat/chat.dart';
import 'package:device_management/device_management.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_app_platform/core/app_provider_container.dart';
import 'package:sf_app_platform/core/incoming_call_notification_config.dart';
import 'package:sf_app_platform/core/incoming_call_strings_cache.dart';
import 'package:sf_app_platform/navigation/app_router.dart';
// iOS limitation: incoming-call UX requires PushKit + CallKit + a VoIP cert,
// which we don't have yet. On iOS the full-screen call UI and ring-while-killed
// behaviour will not work — only the standard notification banner.
// See TODO(videocall-ios-callkit) for the migration path.
//
// TODO(push-data-only): backend sends hybrid pushes (notification + data).
// In background/killed the SDK auto-shows the `notification` payload using
// `sf_default_channel`. For VIDEO_CALL_FROM that produces a duplicate notif
// alongside our custom incoming-call notif (ringtone + full-screen). Backend
// must drop the `notification` field at minimum for VIDEO_CALL_FROM pushes,
// ideally for all commands. When that happens, this handler should construct
// title/body locally with i18n for every command (CHAT_MESSAGE, ALERT, etc.)
// and call _localNotifications.show(...) directly.
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
debugPrint('[FCM-bg] messageId=${message.messageId}');
debugPrint('[FCM-bg] notification=${message.notification?.title} | ${message.notification?.body}');
debugPrint('[FCM-bg] data=${message.data}');
if (message.data['command'] == 'VIDEO_CALL_FROM') {
await _showIncomingCallNotification(message.data);
}
}
const String _localChannelId = 'sf_default_channel';
const String _localChannelName = 'General';
const String _localChannelDescription =
'General notifications shown while the app is in the foreground.';
final FlutterLocalNotificationsPlugin _localNotifications =
FlutterLocalNotificationsPlugin();
Map<String, dynamic>? _pendingNotificationData;
bool _routerReady = false;
ProviderSubscription<VideocallIncomingArgs?>? _incomingProviderSub;
Future<void> setupNotifications() async {
final messaging = FirebaseMessaging.instance;
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
await messaging.requestPermission(alert: true, badge: true, sound: true);
await messaging.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
);
await _initLocalNotifications();
_subscribeToIncomingProvider();
FirebaseMessaging.onMessage.listen(_onForegroundMessage);
FirebaseMessaging.onMessageOpenedApp.listen(_onMessageOpenedApp);
final initialMessage = await messaging.getInitialMessage();
if (initialMessage != null) {
_onMessageOpenedApp(initialMessage);
}
}
void onRouterReady() {
_routerReady = true;
final pending = _pendingNotificationData;
if (pending != null) {
_pendingNotificationData = null;
_handleNotificationNavigation(pending);
}
}
Future<void> _initLocalNotifications() async {
const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
const iosInit = DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
);
const initSettings = InitializationSettings(
android: androidInit,
iOS: iosInit,
);
await _localNotifications.initialize(
initSettings,
onDidReceiveNotificationResponse: _onLocalNotificationTapped,
);
const channel = AndroidNotificationChannel(
_localChannelId,
_localChannelName,
description: _localChannelDescription,
importance: Importance.high,
);
final strings = await IncomingCallStringsCache.load();
final callChannel = AndroidNotificationChannel(
IncomingCallNotificationConfig.channelId,
strings.channelName,
description: strings.channelDescription,
importance: Importance.max,
playSound: true,
sound: const UriAndroidNotificationSound(
IncomingCallNotificationConfig.systemRingtoneUri,
),
enableVibration: true,
enableLights: true,
);
final androidPlugin = _localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
await androidPlugin?.deleteNotificationChannel(
IncomingCallNotificationConfig.legacyChannelId,
);
await androidPlugin?.createNotificationChannel(channel);
await androidPlugin?.createNotificationChannel(callChannel);
}
void _subscribeToIncomingProvider() {
_incomingProviderSub?.close();
_incomingProviderSub = appProviderContainer.listen<VideocallIncomingArgs?>(
videocallIncomingProvider,
(_, next) {
if (next == null) {
_dismissIncomingCallNotification();
}
},
);
}
Future<void> _showIncomingCallNotification(Map<String, dynamic> data) async {
final roomNumber = data['roomNumber'] as String?;
final appAccount = data['appAccount'] as String?;
final sessionId = data['sessionId'] as String?;
if (roomNumber == null || appAccount == null || sessionId == null) return;
final strings = await IncomingCallStringsCache.load();
final payload = jsonEncode(data);
await _localNotifications.show(
IncomingCallNotificationConfig.notificationId,
strings.title,
strings.body,
NotificationDetails(
android: AndroidNotificationDetails(
IncomingCallNotificationConfig.channelId,
strings.channelName,
channelDescription: strings.channelDescription,
importance: Importance.max,
priority: Priority.max,
category: AndroidNotificationCategory.call,
fullScreenIntent: true,
ongoing: true,
autoCancel: false,
playSound: true,
sound: const UriAndroidNotificationSound(
IncomingCallNotificationConfig.systemRingtoneUri,
),
enableVibration: true,
visibility: NotificationVisibility.public,
actions: <AndroidNotificationAction>[
AndroidNotificationAction(
IncomingCallNotificationConfig.actionAccept,
strings.acceptLabel,
showsUserInterface: true,
cancelNotification: true,
),
AndroidNotificationAction(
IncomingCallNotificationConfig.actionReject,
strings.rejectLabel,
showsUserInterface: false,
cancelNotification: true,
),
],
),
iOS: const DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
interruptionLevel: InterruptionLevel.timeSensitive,
),
),
payload: payload,
);
}
Future<void> _dismissIncomingCallNotification() async {
await _localNotifications.cancel(
IncomingCallNotificationConfig.notificationId,
);
}
// TODO(push-data-only): when backend stops sending the `notification` field,
// stop reading message.notification.title/body below. Instead, build title/body
// from message.data (e.g., data['carrierName']) plus i18n strings — same way
// _showIncomingCallNotification already does for VIDEO_CALL_FROM. Then we can
// drop the early-return when notification is null.
void _onForegroundMessage(RemoteMessage message) {
debugPrint('[FCM-fg] messageId=${message.messageId}');
debugPrint('[FCM-fg] notification=${message.notification?.title} | ${message.notification?.body}');
debugPrint('[FCM-fg] data=${message.data}');
if (message.data['command'] == 'VIDEO_CALL_FROM') {
_showIncomingCallNotification(message.data);
_handleNotificationNavigation(message.data);
return;
}
if (_shouldSuppressChatNotification(message.data)) {
debugPrint('[FCM-fg] chat notification suppressed by active context');
return;
}
final notification = message.notification;
if (notification == null) return;
final notificationId = message.messageId?.hashCode ?? 0;
_localNotifications.show(
notificationId,
notification.title,
notification.body,
const NotificationDetails(
android: AndroidNotificationDetails(
_localChannelId,
_localChannelName,
channelDescription: _localChannelDescription,
importance: Importance.high,
priority: Priority.high,
),
iOS: DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
),
),
payload: jsonEncode(message.data),
);
}
/// Suppresses chat notifications while the user is actively reading the chat
/// surface — same UX as WhatsApp/Telegram.
///
/// - Chat list → suppress (the list updates reactively via the WebSocket).
/// - Conversation matching the incoming chatId → suppress (the message arrives
/// through the reactive stream).
/// - Conversation viewing a different chat → show (cross-chat notification).
/// - Outside the chat surface → show.
bool _shouldSuppressChatNotification(Map<String, dynamic> data) {
if (data['command'] != 'CHAT_MESSAGE') return false;
final context = appProviderContainer.read(chatContextProvider);
if (context is ChatContextOutsideChat) return false;
if (context is ChatContextList) return true;
final deviceIdentificator = data['deviceIdentificator'] as String?;
if (deviceIdentificator == null || deviceIdentificator.isEmpty) return false;
final incomingChatId = appProviderContainer
.read(chatDeeplinkServiceProvider)
.resolveClientChatId(
chatId: data['chatId'] as String?,
deviceIdentificator: deviceIdentificator,
);
if (incomingChatId == null) return false;
return context is ChatContextConversation && context.chatId == incomingChatId;
}
void _onMessageOpenedApp(RemoteMessage message) {
debugPrint('[FCM-tap] messageId=${message.messageId}');
debugPrint('[FCM-tap] notification=${message.notification?.title} | ${message.notification?.body}');
debugPrint('[FCM-tap] data=${message.data}');
_handleNotificationNavigation(message.data);
}
// TODO(videocall-callkit-migration): tap "Accept" hoy abre el app y muestra
// IncomingView (doble-tap para entrar a la llamada). WhatsApp/Telegram entran
// directo porque usan CallKit/CallStyle nativos. Migración completa a
// flutter_callkit_incoming bloqueada por VoIP cert de Apple. Plan y opciones
// en docs/videocall-callkit-migration.md.
void _onLocalNotificationTapped(NotificationResponse response) {
final payload = response.payload;
if (payload == null || payload.isEmpty) return;
Map<String, dynamic> data;
try {
data = jsonDecode(payload) as Map<String, dynamic>;
} catch (_) {
return;
}
if (data['command'] == 'VIDEO_CALL_FROM' &&
response.actionId == IncomingCallNotificationConfig.actionReject) {
_rejectIncomingCallFromNotification(data);
return;
}
_handleNotificationNavigation(data);
}
Future<void> _rejectIncomingCallFromNotification(
Map<String, dynamic> data,
) async {
final roomNumber = data['roomNumber'] as String?;
final appAccount = data['appAccount'] as String?;
if (roomNumber == null || appAccount == null) {
appProviderContainer.read(videocallIncomingProvider.notifier).clear();
return;
}
final chatType = data['chatType'] == 'multi'
? VideocallChatType.multi
: VideocallChatType.single;
final deviceId = parseDeviceIdFromRoom(roomNumber);
try {
await appProviderContainer
.read(videocallSignalingRepositoryProvider)
.refuseCall(
deviceIdentificator: deviceId,
chatType: chatType,
appAccount: appAccount,
roomNumber: roomNumber,
);
} catch (error) {
debugPrint('[Notification] refuseCall from notif failed: $error');
}
appProviderContainer.read(videocallIncomingProvider.notifier).clear();
}
void _handleNotificationNavigation(Map<String, dynamic> data) {
if (!_routerReady) {
_pendingNotificationData = data;
return;
}
final currentLocation =
appRouter.routerDelegate.currentConfiguration.uri.path;
if (!currentLocation.startsWith(AppRoutes.legacyDashboard)) return;
Map<String, dynamic> resolved = data;
final pushData = data['pushData'];
if (pushData is String && pushData.isNotEmpty) {
try {
resolved = jsonDecode(pushData) as Map<String, dynamic>;
} catch (_) {}
}
final command = resolved['command'] as String? ?? data['command'] as String?;
switch (command) {
case 'ALERT':
appRouter.go(AppRoutes.deviceNotifications);
case 'CHAT_MESSAGE':
_openChatFromIncoming(resolved);
case 'VIDEO_CALL_FROM':
final chatType = resolved['chatType'] as String?;
final appAccount = resolved['appAccount'] as String?;
final roomNumber = resolved['roomNumber'] as String?;
final sessionId = resolved['sessionId'] as String?;
if (roomNumber == null || appAccount == null || sessionId == null) {
return;
}
appProviderContainer.read(videocallIncomingProvider.notifier).set(
VideocallIncomingArgs(
roomNumber: roomNumber,
appAccount: appAccount,
sessionId: sessionId,
chatType: chatType == 'multi'
? VideocallChatType.multi
: VideocallChatType.single,
),
);
appRouter.go(AppRoutes.videocall);
}
}
Future<void> _openChatFromIncoming(Map<String, dynamic> data) async {
final deviceIdentificator = data['deviceIdentificator'] as String?;
if (deviceIdentificator == null || deviceIdentificator.isEmpty) {
appRouter.go(AppRoutes.legacyChat);
return;
}
final outcome = await appProviderContainer
.read(chatDeeplinkServiceProvider)
.prepareIncomingChat(
chatId: data['chatId'] as String?,
deviceIdentificator: deviceIdentificator,
);
if (outcome == null) {
appRouter.go(AppRoutes.legacyChat);
return;
}
appRouter.go(AppRoutes.legacyChatConversationFor(outcome.chatId));
}

View File

@@ -1,68 +0,0 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options_dev.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyAzo8E_L6iUYWmK1BDFpNqRri1df6CqJiY',
appId: '1:535646668726:android:c3a09d6c26f0cdf95e6317',
messagingSenderId: '535646668726',
projectId: 'sf-platform-pre',
storageBucket: 'sf-platform-pre.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyBeijehJIznndwIUlbMkj6reYT4z-WHGfQ',
appId: '1:535646668726:ios:524afa641f61d7cb5e6317',
messagingSenderId: '535646668726',
projectId: 'sf-platform-pre',
storageBucket: 'sf-platform-pre.firebasestorage.app',
iosBundleId: 'com.savefamily.app.dev',
);
}

View File

@@ -1,68 +0,0 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options_prod.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyDkjNdOAK0ype7wgdgiC1BCKV_pP4s_mlA',
appId: '1:950566980029:android:75a7c10b6259d09681aad4',
messagingSenderId: '950566980029',
projectId: 'sf-platform-pro',
storageBucket: 'sf-platform-pro.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyC0_d7Z6uVHHKhaf7JHRROaY6g2mvvpOXU',
appId: '1:950566980029:ios:987b4f0b9e9b897481aad4',
messagingSenderId: '950566980029',
projectId: 'sf-platform-pro',
storageBucket: 'sf-platform-pro.firebasestorage.app',
iosBundleId: 'com.savefamily.app',
);
}

View File

@@ -1,68 +0,0 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options_staging.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyAzo8E_L6iUYWmK1BDFpNqRri1df6CqJiY',
appId: '1:535646668726:android:b87245b807258e3e5e6317',
messagingSenderId: '535646668726',
projectId: 'sf-platform-pre',
storageBucket: 'sf-platform-pre.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyBeijehJIznndwIUlbMkj6reYT4z-WHGfQ',
appId: '1:535646668726:ios:5172d626d02dfe215e6317',
messagingSenderId: '535646668726',
projectId: 'sf-platform-pre',
storageBucket: 'sf-platform-pre.firebasestorage.app',
iosBundleId: 'com.savefamily.app.stag',
);
}

View File

@@ -7,7 +7,6 @@ import 'package:dashboard_shell/dashboard_builder.dart';
import 'package:device_management/device_management.dart';
import 'package:control_panel/control_panel.dart';
import 'package:legacy_dashboard_shell/legacy_dashboard_builder.dart';
import 'package:chat/chat.dart';
import 'package:location/location.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
@@ -18,8 +17,6 @@ import 'package:notifications/notifications.dart';
import 'package:payments/payments.dart';
import 'package:profile/profile.dart';
import 'package:settings/settings.dart';
import 'package:sf_app_platform/core/config/app_mode.dart';
import 'package:sf_app_platform/widgets/user_identity_listener.dart';
import 'package:splash/splash.dart';
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
@@ -45,28 +42,7 @@ final _dashboardProfileNavKey =
late final GoRouter appRouter;
/// Maps the splash's session check result to the destination route based
/// on the active [appMode]. Set `--dart-define=APP_MODE=payment` (or use
/// the `(Payment)` launch configurations) to boot into the payment app.
const _legacySplashRouteMap = <InitialRoute, String>{
InitialRoute.onboarding: AppRoutes.legacyOnboarding,
InitialRoute.login: AppRoutes.legacyLogin,
InitialRoute.home: AppRoutes.controlPanel,
InitialRoute.deviceSetup: AppRoutes.legacyDeviceSetup,
};
const _paymentSplashRouteMap = <InitialRoute, String>{
InitialRoute.onboarding: AppRoutes.onboarding,
InitialRoute.login: AppRoutes.login,
InitialRoute.home: AppRoutes.dashboardHome,
InitialRoute.deviceSetup: AppRoutes.deviceSetup,
};
void configureAppRouter() {
final splashRouteMap = isPaymentMode
? _paymentSplashRouteMap
: _legacySplashRouteMap;
appRouter = GoRouter(
navigatorKey: rootNavigatorKey,
initialLocation: AppRoutes.splash,
@@ -75,13 +51,11 @@ void configureAppRouter() {
GoRoute(
path: AppRoutes.splash,
name: 'splash',
pageBuilder: SplashBuilder(routeMap: splashRouteMap).buildPage,
pageBuilder: SplashBuilder().buildPage,
),
StatefulShellRoute.indexedStack(
builder: (context, state, navShell) {
return UserIdentityListener(
child: LegacyDashboardBuilder().build(context, navShell),
);
return LegacyDashboardBuilder().build(context, navShell);
},
branches: [
StatefulShellBranch(
@@ -97,11 +71,6 @@ void configureAppRouter() {
name: 'customer_service',
pageBuilder: CustomerServiceBuilder().buildPage,
),
GoRoute(
path: 'notifications',
name: 'legacy_notifications',
pageBuilder: const DeviceNotificationsBuilder().buildPage,
),
GoRoute(
path: 'account_settings',
name: 'account_settings',
@@ -198,44 +167,11 @@ void configureAppRouter() {
name: 'volume_control',
pageBuilder: const VolumeControlBuilder().buildPage,
),
GoRoute(
path: 'do_not_disturb',
name: 'do_not_disturb',
pageBuilder: const DoNotDisturbBuilder().buildPage,
),
GoRoute(
path: 'call_history',
name: 'call_history',
pageBuilder: const CallHistoryBuilder().buildPage,
),
GoRoute(
path: 'background_image',
name: 'background_image',
pageBuilder: const BackgroundImageBuilder().buildPage,
),
GoRoute(
path: 'installed_apps',
name: 'installed_apps',
pageBuilder: const InstalledAppsBuilder().buildPage,
routes: [
GoRoute(
path: 'schedules',
name: 'app_usage_schedules',
pageBuilder:
const AppUsageSchedulesBuilder().buildPage,
),
],
),
GoRoute(
path: 'videocall',
name: 'videocall',
pageBuilder: const VideocallBuilder().buildPage,
),
GoRoute(
path: 'friends',
name: 'friends',
pageBuilder: const FriendsBuilder().buildPage,
),
],
),
],
@@ -250,20 +186,19 @@ void configureAppRouter() {
),
],
),
// TODO: Añadir branch para Chat (tab 4)
StatefulShellBranch(
navigatorKey: _legacyChatNavKey,
routes: [
GoRoute(
path: AppRoutes.legacyChat,
path: '${AppRoutes.legacyDashboard}/chat',
name: 'legacy_chat',
pageBuilder: const ChatListBuilder().buildPage,
routes: [
GoRoute(
path: 'conversation/:chatId',
name: 'legacy_chat_conversation',
pageBuilder: const ChatConversationBuilder().buildPage,
pageBuilder: (context, state) => MaterialPage<void>(
key: state.pageKey,
child: const Scaffold(
body: Center(child: Text('Chat - Coming soon')),
),
],
),
),
],
),
@@ -275,11 +210,6 @@ void configureAppRouter() {
name: 'settings',
pageBuilder: SettingsBuilder().buildPage,
routes: [
GoRoute(
path: 'appearance',
name: 'appearance',
pageBuilder: AppearanceBuilder().buildPage,
),
GoRoute(
path: 'alarm',
name: 'alarm',
@@ -330,15 +260,20 @@ void configureAppRouter() {
name: 'language',
pageBuilder: LanguageBuilder().buildPage,
),
GoRoute(
path: 'legacy_notifications',
name: 'legacy_notifications',
pageBuilder: LegacyNotificationsBuilder().buildPage,
),
GoRoute(
path: 'remote_on_off',
name: 'remote_on_off',
pageBuilder: RemoteOnOffBuilder().buildPage,
),
GoRoute(
path: 'alerts',
name: 'alerts',
pageBuilder: AlertsBuilder().buildPage,
path: 'sms_alert',
name: 'sms_alert',
pageBuilder: SmsAlertBuilder().buildPage,
),
GoRoute(
path: 'timezone',
@@ -440,9 +375,7 @@ void configureAppRouter() {
),
StatefulShellRoute.indexedStack(
builder: (context, state, navShell) {
return UserIdentityListener(
child: DashboardBuilder().build(context, navShell),
);
return DashboardBuilder().build(context, navShell);
},
branches: [
StatefulShellBranch(
@@ -491,22 +424,26 @@ void configureAppRouter() {
GoRoute(
path: 'edit',
name: 'home_edit_child_profile',
pageBuilder: const EditChildProfileBuilder().buildPage,
pageBuilder:
const EditChildProfileBuilder().buildPage,
),
GoRoute(
path: 'set-pin',
name: 'home_set_card_pin',
pageBuilder: const SetCardPinBuilder().buildPage,
pageBuilder:
const SetCardPinBuilder().buildPage,
),
GoRoute(
path: 'change-pin',
name: 'home_change_card_pin',
pageBuilder: const ChangeCardPinBuilder().buildPage,
pageBuilder:
const ChangeCardPinBuilder().buildPage,
),
GoRoute(
path: 'renew-card',
name: 'home_renew_card',
pageBuilder: const RenewCardBuilder().buildPage,
pageBuilder:
const RenewCardBuilder().buildPage,
),
],
),
@@ -555,7 +492,8 @@ void configureAppRouter() {
GoRoute(
path: 'edit-personal-data',
name: 'profile_edit_personal_data',
pageBuilder: const EditPersonalDataBuilder().buildPage,
pageBuilder:
const EditPersonalDataBuilder().buildPage,
),
GoRoute(
path: 'payment-methods',

View File

@@ -2,21 +2,20 @@ import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart' show WidgetRef;
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
class LegacyHeartbeatService {
LegacyHeartbeatService({
required WidgetRef ref,
required QuestiaRepository repository,
required void Function() onUnauthorized,
}) : _ref = ref,
_onUnauthorized = onUnauthorized;
}) : _repository = repository,
_onUnauthorized = onUnauthorized;
final WidgetRef _ref;
final QuestiaRepository _repository;
final void Function() _onUnauthorized;
Timer? _timer;
static const _interval = Duration(minutes: 2);
static const _interval = Duration(minutes: 3);
void start() {
if (_timer != null) return;
@@ -33,8 +32,8 @@ class LegacyHeartbeatService {
Future<void> _beat() async {
try {
await _ref.read(legacyDevicesProvider.notifier).refresh();
debugPrint('[LegacyHeartbeat] devices refreshed');
await _repository.get<dynamic>('/auth/me');
debugPrint('[LegacyHeartbeat] /auth/me => OK');
} catch (e) {
debugPrint('[LegacyHeartbeat] error: $e');
if (e is DioException && e.response?.statusCode == 401) {

View File

@@ -1,13 +1,7 @@
import 'package:auth/auth.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:sf_app_platform/core/app_version_check/app_update_gate.dart';
import 'package:sf_app_platform/core/app_version_check/app_version_check.dart';
import 'package:sf_app_platform/core/config/app_mode.dart';
import 'package:sf_app_platform/core/incoming_call_strings_cache.dart';
import 'package:sf_app_platform/navigation/app_router.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_app_platform/providers/app_state_provider.dart';
@@ -17,11 +11,9 @@ import 'package:sf_app_platform/providers/wallet_heartbeat_service.dart';
import 'package:get_it/get_it.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
import 'package:fonts/fonts.dart';
import 'package:videocall_sdk/videocall_sdk.dart';
class SaveFamilyApp extends ConsumerStatefulWidget {
const SaveFamilyApp({super.key});
@@ -32,82 +24,46 @@ class SaveFamilyApp extends ConsumerStatefulWidget {
class SaveFamilyAppState extends ConsumerState<SaveFamilyApp>
with WidgetsBindingObserver {
WalletHeartbeatService? _walletHeartbeat;
LegacyHeartbeatService? _legacyHeartbeat;
SfRouterListener? _trackingRouterListener;
WebSocketService? _webSocket;
late final WalletHeartbeatService walletHeartbeat;
late final LegacyHeartbeatService legacyHeartbeat;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_trackingRouterListener = SfRouterListener(
listenable: appRouter.routerDelegate,
currentScreenName: () {
final config = appRouter.routerDelegate.currentConfiguration;
if (config.matches.isEmpty) return null;
return config.last.route.name;
},
tracking: sfTracking,
walletHeartbeat = WalletHeartbeatService(
repository: ref.read(treezorRepositoryProvider),
sessionLocal: SessionLocalDatasourceImpl(),
onError: () => appRouter.go(AppRoutes.scaTreezor),
);
legacyHeartbeat = LegacyHeartbeatService(
repository: GetIt.I<QuestiaRepository>(),
onUnauthorized: () {
clearSessionData();
appRouter.go(AppRoutes.login);
},
);
if (isPaymentMode) {
_walletHeartbeat = WalletHeartbeatService(
repository: ref.read(treezorRepositoryProvider),
sessionLocal: SessionLocalDatasourceImpl(),
onError: () => appRouter.go(AppRoutes.scaTreezor),
);
}
if (isLegacyMode) {
_legacyHeartbeat = LegacyHeartbeatService(
ref: ref,
onUnauthorized: () {
clearSessionData();
WidgetsBinding.instance.addPostFrameCallback((_) {
appRouter.go(AppRoutes.legacyLogin);
});
},
);
_webSocket = GetIt.I<WebSocketService>();
appRouter.routerDelegate.addListener(_onRouteChanged);
}
onBeforeSessionCleared = () {
_walletHeartbeat?.stop();
_legacyHeartbeat?.stop();
_webSocket?.disconnect();
GetIt.I<VideocallClient>().logout();
FirebaseMessaging.instance.deleteToken().catchError((Object e) {
debugPrint('[FCM] deleteToken on logout failed: $e');
});
walletHeartbeat.stop();
legacyHeartbeat.stop();
};
appRouter.routerDelegate.addListener(_onRouteChanged);
}
void _onRouteChanged() {
final heartbeat = _legacyHeartbeat;
if (heartbeat == null) return;
final location = appRouter.routerDelegate.currentConfiguration.uri.path;
if (location.startsWith(AppRoutes.legacyDashboard)) {
heartbeat.start();
_webSocket?.connect();
legacyHeartbeat.start();
} else {
heartbeat.stop();
_webSocket?.disconnect();
legacyHeartbeat.stop();
}
}
@override
void dispose() {
if (isLegacyMode) {
appRouter.routerDelegate.removeListener(_onRouteChanged);
}
_trackingRouterListener?.dispose();
_walletHeartbeat?.stop();
_legacyHeartbeat?.stop();
_webSocket?.disconnect();
appRouter.routerDelegate.removeListener(_onRouteChanged);
walletHeartbeat.stop();
legacyHeartbeat.stop();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@@ -117,16 +73,12 @@ class SaveFamilyAppState extends ConsumerState<SaveFamilyApp>
debugPrint('State: $state');
ref.read(appLifecycleStateProvider.notifier).setState(state);
if (state == AppLifecycleState.resumed) {
_walletHeartbeat?.start();
if (isLegacyMode) {
_onRouteChanged();
}
// walletHeartbeat.start();
_onRouteChanged();
ref.read(permissionsProvider.notifier).checkPermissions();
ref.read(appVersionCheckProvider.notifier).refresh();
} else if (state == AppLifecycleState.paused) {
_walletHeartbeat?.stop();
_legacyHeartbeat?.stop();
_webSocket?.disconnect();
// walletHeartbeat.stop();
legacyHeartbeat.stop();
}
super.didChangeAppLifecycleState(state);
}
@@ -134,76 +86,32 @@ class SaveFamilyAppState extends ConsumerState<SaveFamilyApp>
@override
Widget build(BuildContext context) {
SizeUtils.init(context: context);
ref.watch(pushTokenRefreshListenerProvider);
// Theme wiring:
// - Legacy mode: new `legacy_theme` package (Material 3 + light/dark/system).
// - Payment mode: unchanged behaviour (seed-based ColorScheme, light only).
final ThemeData lightTheme;
final ThemeData? darkTheme;
final ThemeMode themeMode;
if (isLegacyMode) {
final legacyThemeState = ref.watch(legacyThemeNotifierProvider);
lightTheme = LegacyAppTheme.light;
darkTheme = LegacyAppTheme.dark;
themeMode = legacyThemeState.themeMode;
} else {
lightTheme = ThemeData(
return MaterialApp.router(
title: 'SaveFamily',
theme: ThemeData(
fontFamily: AppFonts.stolzl,
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF329E95)),
);
darkTheme = null;
themeMode = ThemeMode.light;
}
return AppUpdateGate(
child: MaterialApp.router(
title: 'SaveFamily',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: themeMode,
routerConfig: appRouter,
debugShowCheckedModeBanner: false,
localizationsDelegates: [
SFLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [for (final lang in supportedLanguages) Locale(lang)],
localeResolutionCallback: (locale, supportedLocales) {
if (locale == null) return supportedLocales.first;
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode) {
return supportedLocale;
}
),
routerConfig: appRouter,
debugShowCheckedModeBanner: false,
localizationsDelegates: [
// CountryLocalizations.delegate,
SFLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [for (final lang in supportedLanguages) Locale(lang)],
localeResolutionCallback: (locale, supportedLocales) {
if (locale == null) return supportedLocales.first;
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode) {
return supportedLocale;
}
return supportedLocales.first;
},
builder: (context, child) {
_cacheIncomingCallStrings(context);
return child ?? const SizedBox.shrink();
},
),
);
}
String? _cachedIncomingCallLocale;
void _cacheIncomingCallStrings(BuildContext context) {
final localeCode = context.locale.languageCode;
if (_cachedIncomingCallLocale == localeCode) return;
_cachedIncomingCallLocale = localeCode;
IncomingCallStringsCache.save(
IncomingCallStrings(
title: context.translate(I18n.videocallIncomingVideo),
body: context.translate(I18n.videocallIncomingPushBody),
acceptLabel: context.translate(I18n.videocallAccept),
rejectLabel: context.translate(I18n.videocallReject),
channelName: context.translate(I18n.videocallNotificationChannelName),
channelDescription:
context.translate(I18n.videocallNotificationChannelDescription),
),
}
return supportedLocales.first;
},
);
}
}

View File

@@ -1,28 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart';
class UserIdentityListener extends ConsumerWidget {
final Widget child;
const UserIdentityListener({super.key, required this.child});
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<AsyncValue<UserEntity>>(userInfoProvider, (_, next) {
next.whenData((user) {
UserInfoTrackingListener(ref.read(sfTrackingProvider)).onUserChanged(
userId: user.id,
role: user.role,
language: user.language,
createdAtMillis: user.createdAt,
hasPhone: user.phone.isNotEmpty,
hasApiKey: user.hasApiKey,
);
});
});
return child;
}
}

View File

@@ -0,0 +1 @@
/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/

View File

@@ -0,0 +1 @@
/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/share_plus-10.1.4/

View File

@@ -0,0 +1 @@
/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/

View File

@@ -0,0 +1 @@
/Users/juliandalcalaf/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/

View File

@@ -6,21 +6,9 @@
#include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <record_linux/record_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) record_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
record_linux_plugin_register_with_registrar(record_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@@ -3,9 +3,6 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
file_selector_linux
record_linux
url_launcher_linux
)

1629
apps/mobile_app/pubspec.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,6 @@ description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
resolution: workspace
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
@@ -17,7 +16,7 @@ resolution: workspace
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+10
version: 1.0.0+5
environment:
sdk: ^3.9.2
@@ -39,17 +38,17 @@ dependencies:
#modules dependencies go here
activity:
path: ../../modules/payment/modules/activity
path: ../../modules/activity
auth:
path: ../../modules/payment/modules/auth
path: ../../modules/auth
home:
path: ../../modules/payment/modules/home
profile:
path: ../../modules/payment/modules/profile
path: ../../modules/home
profile:
path: ../../modules/profile
notifications:
path: ../../modules/payment/modules/notifications
path: ../../modules/notifications
dashboard_shell:
path: ../../modules/payment/modules/dashboard_shell
path: ../../modules/dashboard_shell
legacy_dashboard_shell:
path: ../../modules/legacy/modules/legacy_dashboard_shell
control_panel:
@@ -64,14 +63,10 @@ dependencies:
path: ../../modules/legacy/modules/device_management
location:
path: ../../modules/legacy/modules/location
chat:
path: ../../modules/legacy/modules/chat
legacy_auth:
path: ../../modules/legacy/modules/legacy_auth
settings:
path: ../../modules/legacy/modules/settings
legacy_theme:
path: ../../modules/legacy/packages/legacy_theme
#packages dependencies go here
navigation:
path: ../../packages/navigation
@@ -83,8 +78,6 @@ dependencies:
path: ../../packages/fonts
sf_shared:
path: ../../packages/sf_shared
sf_tracking:
path: ../../packages/sf_tracking
sf_infrastructure:
path: ../../packages/sf_infrastructure
utils:
@@ -95,33 +88,15 @@ dependencies:
path: ../../packages/sca_treezor
payments:
path: ../../packages/payments
videocall_sdk:
path: ../../packages/videocall_sdk
#dependencies go here
cupertino_icons: ^1.0.8
flutter_svg: ^2.2.2
intl: ^0.20.2
timezone: ^0.10.1
flutter_svg: ^2.2.1
go_router_builder: ^4.1.1
build_runner: ^2.7.1
country_code_picker: ^3.4.1
flutter_native_splash: ^2.4.7
permission_handler: ^12.0.1
dio: ^5.9.2
# Firebase
firebase_core: ^4.6.0
firebase_crashlytics: ^5.1.0
firebase_analytics: ^12.2.0
firebase_remote_config: ^6.3.0
package_info_plus: ^8.3.1
url_launcher: ^6.3.2
shared_preferences: ^2.5.5
firebase_messaging: ^16.1.3
firebase_performance: ^0.11.2
# Notifications (foreground display + tap handling)
flutter_local_notifications: ^19.4.2
dev_dependencies:
flutter_test:
sdk: flutter
@@ -148,7 +123,6 @@ flutter:
# To add assets to your application, add an assets section, like this:
assets:
- assets/shared/images/
- assets/shared/animations/
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images

View File

@@ -0,0 +1,56 @@
# melos_managed_dependency_overrides: account,activity,auth,customer_service,dashboard_shell,design_system,flutter_treezor_entrust_sdk_bridge,fonts,home,legacy_dashboard_shell,legacy_shared,navigation,notifications,payments,profile,sca_treezor,sf_infrastructure,sf_localizations,sf_shared,splash,utils,control_panel,device_management,legacy_auth,location
# melos_managed_dependency_overrides: settings
# melos_managed_dependency_overrides: account,activity,auth,customer_service,dashboard_shell,design_system,flutter_treezor_entrust_sdk_bridge,fonts,home,legacy_dashboard_shell,legacy_shared,navigation,notifications,payments,profile,sca_treezor,sf_infrastructure,sf_localizations,sf_shared,splash,utils,control_panel,device_management
dependency_overrides:
account:
path: ../../modules/legacy/modules/account
activity:
path: ../../modules/activity
auth:
path: ../../modules/auth
control_panel:
path: ../../modules/legacy/modules/control_panel
customer_service:
path: ../../modules/legacy/modules/customer_service
dashboard_shell:
path: ../../modules/dashboard_shell
design_system:
path: ../../packages/design_system
device_management:
path: ../../modules/legacy/modules/device_management
flutter_treezor_entrust_sdk_bridge:
path: ../../packages/flutter_treezor_entrust_sdk_bridge
fonts:
path: ../../packages/fonts
home:
path: ../../modules/home
legacy_auth:
path: ../../modules/legacy/modules/legacy_auth
legacy_dashboard_shell:
path: ../../modules/legacy/modules/legacy_dashboard_shell
legacy_shared:
path: ../../modules/legacy/packages/legacy_shared
location:
path: ../../modules/legacy/modules/location
navigation:
path: ../../packages/navigation
notifications:
path: ../../modules/notifications
payments:
path: ../../packages/payments
profile:
path: ../../modules/profile
sca_treezor:
path: ../../packages/sca_treezor
settings:
path: ../../modules/legacy/modules/settings
sf_infrastructure:
path: ../../packages/sf_infrastructure
sf_localizations:
path: ../../packages/sf_localizations
sf_shared:
path: ../../packages/sf_shared
splash:
path: ../../modules/splash
utils:
path: ../../packages/utils

View File

@@ -1,308 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:sf_app_platform/core/app_version_check/app_version_check.dart';
import 'package:sf_app_platform/core/app_version_check/remote_config_keys.dart';
class _FakeRemoteConfig implements RemoteConfigReader {
_FakeRemoteConfig({
this.minRequired = 0,
this.latest = 0,
this.force = false,
this.message = '',
this.iosUrl = 'ios-url',
this.androidUrl = 'android-url',
this.fetchThrows = false,
});
int minRequired;
int latest;
bool force;
String message;
String iosUrl;
String androidUrl;
bool fetchThrows;
int fetchCallCount = 0;
@override
Future<void> fetchAndActivate() async {
fetchCallCount++;
if (fetchThrows) throw Exception('boom');
}
@override
int getInt(String key) => switch (key) {
RemoteConfigKeys.minRequiredBuild => minRequired,
RemoteConfigKeys.latestBuild => latest,
_ => 0,
};
@override
bool getBool(String key) =>
key == RemoteConfigKeys.updateForce ? force : false;
@override
String getString(String key) => switch (key) {
RemoteConfigKeys.updateMessage => message,
RemoteConfigKeys.updateUrlIos => iosUrl,
RemoteConfigKeys.updateUrlAndroid => androidUrl,
_ => '',
};
}
class _FakeDismissedStore implements DismissedBuildStore {
_FakeDismissedStore({this.value = 0, this.writeThrows = false});
int value;
bool writeThrows;
int writeCallCount = 0;
@override
Future<int> read() async => value;
@override
Future<void> write(int latestBuild) async {
writeCallCount++;
if (writeThrows) throw Exception('write boom');
value = latestBuild;
}
}
AppVersionCheckService _buildService({
required _FakeRemoteConfig rc,
required _FakeDismissedStore store,
required int currentBuild,
bool isIos = false,
}) {
return AppVersionCheckService(
remoteConfig: rc,
dismissedStore: store,
currentBuildLoader: () async => currentBuild,
isIos: isIos,
);
}
void main() {
group('AppVersionCheckService.check', () {
test('returns NoUpdate when current build matches latest and no force',
() async {
final service = _buildService(
rc: _FakeRemoteConfig(latest: 7),
store: _FakeDismissedStore(),
currentBuild: 7,
);
final result = await service.check();
expect(result, isA<NoUpdate>());
});
test('returns NoUpdate when nothing requires attention', () async {
final service = _buildService(
rc: _FakeRemoteConfig(),
store: _FakeDismissedStore(),
currentBuild: 7,
);
final result = await service.check();
expect(result, isA<NoUpdate>());
});
test('returns SoftUpdate when current build is behind latest', () async {
final service = _buildService(
rc: _FakeRemoteConfig(latest: 8, message: 'New stuff'),
store: _FakeDismissedStore(),
currentBuild: 7,
);
final result = await service.check();
expect(result, isA<SoftUpdate>());
final soft = result as SoftUpdate;
expect(soft.latestBuild, 8);
expect(soft.currentBuild, 7);
expect(soft.message, 'New stuff');
});
test('returns ForceUpdate when current build is below min required',
() async {
final service = _buildService(
rc: _FakeRemoteConfig(minRequired: 8, latest: 8),
store: _FakeDismissedStore(),
currentBuild: 7,
);
final result = await service.check();
expect(result, isA<ForceUpdate>());
final force = result as ForceUpdate;
expect(force.latestBuild, 8);
expect(force.currentBuild, 7);
});
test('returns ForceUpdate when force flag is true even with current build matching',
() async {
final service = _buildService(
rc: _FakeRemoteConfig(force: true, latest: 7),
store: _FakeDismissedStore(),
currentBuild: 7,
);
final result = await service.check();
expect(result, isA<ForceUpdate>());
});
test('force flag wins over soft when both conditions are met', () async {
final service = _buildService(
rc: _FakeRemoteConfig(force: true, latest: 8),
store: _FakeDismissedStore(),
currentBuild: 7,
);
final result = await service.check();
expect(result, isA<ForceUpdate>());
});
test('returns NoUpdate when latest is dismissed', () async {
final service = _buildService(
rc: _FakeRemoteConfig(latest: 8),
store: _FakeDismissedStore(value: 8),
currentBuild: 7,
);
final result = await service.check();
expect(result, isA<NoUpdate>());
});
test('returns NoUpdate when dismissed value is greater than latest',
() async {
final service = _buildService(
rc: _FakeRemoteConfig(latest: 8),
store: _FakeDismissedStore(value: 10),
currentBuild: 7,
);
final result = await service.check();
expect(result, isA<NoUpdate>());
});
test('returns SoftUpdate when latest is greater than dismissed', () async {
final service = _buildService(
rc: _FakeRemoteConfig(latest: 9),
store: _FakeDismissedStore(value: 8),
currentBuild: 7,
);
final result = await service.check();
expect(result, isA<SoftUpdate>());
});
test('dismiss persistence does NOT block force update', () async {
final service = _buildService(
rc: _FakeRemoteConfig(force: true),
store: _FakeDismissedStore(value: 9999),
currentBuild: 7,
);
final result = await service.check();
expect(result, isA<ForceUpdate>());
});
test('uses iOS store URL when isIos is true', () async {
final service = _buildService(
rc: _FakeRemoteConfig(latest: 8, iosUrl: 'apple', androidUrl: 'play'),
store: _FakeDismissedStore(),
currentBuild: 7,
isIos: true,
);
final result = await service.check() as SoftUpdate;
expect(result.storeUrl, 'apple');
});
test('uses Android store URL when isIos is false', () async {
final service = _buildService(
rc: _FakeRemoteConfig(latest: 8, iosUrl: 'apple', androidUrl: 'play'),
store: _FakeDismissedStore(),
currentBuild: 7,
isIos: false,
);
final result = await service.check() as SoftUpdate;
expect(result.storeUrl, 'play');
});
test('returns NoUpdate when fetchAndActivate throws', () async {
final service = _buildService(
rc: _FakeRemoteConfig(fetchThrows: true, latest: 8),
store: _FakeDismissedStore(),
currentBuild: 7,
);
final result = await service.check();
expect(result, isA<SoftUpdate>(),
reason:
'fetch failure should fall back to cached values, not return none');
});
test('check still calls fetchAndActivate', () async {
final rc = _FakeRemoteConfig();
final service = _buildService(
rc: rc,
store: _FakeDismissedStore(),
currentBuild: 7,
);
await service.check();
expect(rc.fetchCallCount, 1);
});
test('returns NoUpdate when current build loader throws', () async {
final service = AppVersionCheckService(
remoteConfig: _FakeRemoteConfig(),
dismissedStore: _FakeDismissedStore(),
currentBuildLoader: () async => throw Exception('package_info crash'),
);
final result = await service.check();
expect(result, isA<NoUpdate>());
});
});
group('AppVersionCheckService.markSoftDismissed', () {
test('writes the latest build to the dismiss store', () async {
final store = _FakeDismissedStore();
final service = _buildService(
rc: _FakeRemoteConfig(),
store: store,
currentBuild: 7,
);
await service.markSoftDismissed(8);
expect(store.value, 8);
expect(store.writeCallCount, 1);
});
test('swallows write errors silently', () async {
final store = _FakeDismissedStore(writeThrows: true);
final service = _buildService(
rc: _FakeRemoteConfig(),
store: store,
currentBuild: 7,
);
await expectLater(service.markSoftDismissed(8), completes);
});
});
}

View File

@@ -1,167 +0,0 @@
workflows:
staging_android:
name: Staging Android
instance_type: linux_x2
environment:
flutter: $FLUTTER_VERSION
android_signing:
- upload_keystore
groups:
- codemagic-staging
- codemagic-common-vars
triggering:
events:
- push
branch_patterns:
- pattern: fusion-app
include: true
scripts:
- name: Setup project (melos bootstrap)
script: sh ./codemagic_scripts/project_setup.sh
- name: Analyze + test (bloqueante)
script: sh ./codemagic_scripts/analyze_and_test.sh
- name: Print versions
script: sh ./codemagic_scripts/print_versions.sh android staging
- name: Android dependencies
script: sh ./codemagic_scripts/android_dependencies.sh
- name: Set up key.properties
script: sh ./codemagic_scripts/android_key_properties.sh
- name: Build Android app (APP_MODE=legacy)
script: sh ./codemagic_scripts/flutter_build.sh android staging legacy
- name: Mark success
script: touch ~/SUCCESS
artifacts:
- apps/mobile_app/build/app/outputs/bundle/stagingRelease/app-staging-release.aab
publishing:
google_play:
track: internal
credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
submit_as_draft: false
scripts:
- name: Report status
script: sh ./codemagic_scripts/build_status.sh android staging
staging_ios:
name: Staging iOS
instance_type: mac_mini_m2
integrations:
app_store_connect: SaveFamily ASC Key
environment:
flutter: $FLUTTER_VERSION
xcode: latest
ios_signing:
distribution_type: app_store
bundle_identifier: com.savefamily.app.stag
groups:
- codemagic-staging
- codemagic-common-vars
triggering:
events:
- push
branch_patterns:
- pattern: fusion-app
include: true
scripts:
- name: Setup project (melos bootstrap)
script: sh ./codemagic_scripts/project_setup.sh
- name: Analyze + test (bloqueante)
script: sh ./codemagic_scripts/analyze_and_test.sh
- name: iOS signing + CocoaPods
script: sh ./codemagic_scripts/ios_signing_cocoapods.sh
- name: Print versions
script: sh ./codemagic_scripts/print_versions.sh ios staging
- name: Build iOS app (APP_MODE=legacy)
script: sh ./codemagic_scripts/flutter_build.sh ios staging legacy
- name: Mark success
script: touch ~/SUCCESS
artifacts:
- apps/mobile_app/build/ios/ipa/*.ipa
publishing:
app_store_connect:
auth: integration
submit_to_testflight: true
scripts:
- name: Report status
script: sh ./codemagic_scripts/build_status.sh ios staging
production_android:
name: Production Android
instance_type: linux_x2
environment:
flutter: $FLUTTER_VERSION
android_signing:
- upload_keystore
groups:
- codemagic-production
- codemagic-common-vars
triggering:
events:
- tag
scripts:
- name: Setup project (melos bootstrap)
script: sh ./codemagic_scripts/project_setup.sh
- name: Analyze + test (bloqueante)
script: sh ./codemagic_scripts/analyze_and_test.sh
- name: Print versions
script: sh ./codemagic_scripts/print_versions.sh android production
- name: Android dependencies
script: sh ./codemagic_scripts/android_dependencies.sh
- name: Set up key.properties
script: sh ./codemagic_scripts/android_key_properties.sh
- name: Build Android app (APP_MODE=legacy)
script: sh ./codemagic_scripts/flutter_build.sh android production legacy
- name: Mark success
script: touch ~/SUCCESS
artifacts:
- apps/mobile_app/build/app/outputs/bundle/productionRelease/app-production-release.aab
publishing:
google_play:
track: internal
credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
submit_as_draft: true
scripts:
- name: Report status
script: sh ./codemagic_scripts/build_status.sh android production
production_ios:
name: Production iOS
instance_type: mac_mini_m2
integrations:
app_store_connect: SaveFamily ASC Key
environment:
flutter: $FLUTTER_VERSION
xcode: latest
ios_signing:
distribution_type: app_store
bundle_identifier: com.savefamily.app
groups:
- codemagic-production
- codemagic-common-vars
triggering:
events:
- tag
scripts:
- name: Setup project (melos bootstrap)
script: sh ./codemagic_scripts/project_setup.sh
- name: Analyze + test (bloqueante)
script: sh ./codemagic_scripts/analyze_and_test.sh
- name: iOS signing + CocoaPods
script: sh ./codemagic_scripts/ios_signing_cocoapods.sh
- name: Print versions
script: sh ./codemagic_scripts/print_versions.sh ios production
- name: Build iOS app (APP_MODE=legacy)
script: sh ./codemagic_scripts/flutter_build.sh ios production legacy
- name: Mark success
script: touch ~/SUCCESS
artifacts:
- apps/mobile_app/build/ios/ipa/*.ipa
publishing:
app_store_connect:
auth: integration
submit_to_app_store: true
release_type: MANUAL
cancel_previous_submissions: true
copyright: 2026 SaveFamily
scripts:
- name: Report status
script: sh ./codemagic_scripts/build_status.sh ios production

Some files were not shown because too many files have changed in this diff Show More