@_nazmiforreal/flutter-ota 0.1.25 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/flutter-ota-linux-x64 +0 -0
- package/bin/flutter-ota.js +12 -11
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +46 -13
- package/dart-src/packages/cli-tools/lib/src/backend.dart +6 -6
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +2 -0
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +7 -7
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +4 -1
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/config.dart +5 -5
- package/dart-src/packages/cli-tools/lib/src/operations.dart +2 -2
- package/dart-src/packages/cli-tools/pubspec.yaml +8 -21
- package/dart-src/packages/cli-tools/test/backends_test.dart +3 -3
- package/dart-src/packages/cli-tools/test/cli_test.dart +1 -1
- package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +4 -1
- package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/pocketbase_smoke_test.dart +1 -1
- package/dart-src/root/README.md +394 -0
- package/dart-src/root/analysis_options.yaml +21 -0
- package/dart-src/root/android/build.gradle +116 -0
- package/dart-src/root/android/consumer-rules.pro +23 -0
- package/dart-src/root/android/settings.gradle +1 -0
- package/dart-src/root/android/src/main/AndroidManifest.xml +21 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/ApplyResult.kt +52 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/BlacklistStore.kt +154 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/BootDiagnosticStore.kt +150 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/CrashGuard.kt +271 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/FlutterPatcherApplication.kt +201 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/FlutterPatcherAutoInitProvider.kt +93 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/FlutterPatcherPlugin.kt +245 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/LoaderHook.kt +298 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatchManager.kt +1579 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatcherConfig.kt +173 -0
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/SignatureVerifier.kt +177 -0
- package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/CrashGuardTest.kt +61 -0
- package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/LoaderHookTest.kt +25 -0
- package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/PatchManagerTest.kt +401 -0
- package/dart-src/root/android/src/test/kotlin/com/flutter_patcher/flutter_patcher/PatcherConfigTest.kt +30 -0
- package/dart-src/root/bin/pack.dart +182 -0
- package/dart-src/root/lib/flutter_ota_kit.dart +1170 -0
- package/dart-src/root/lib/src/app_version_resolver.dart +36 -0
- package/dart-src/root/lib/src/aws_update_source.dart +97 -0
- package/dart-src/root/lib/src/blacklist.dart +54 -0
- package/dart-src/root/lib/src/boot_diagnostic.dart +138 -0
- package/dart-src/root/lib/src/cloudflare_update_source.dart +97 -0
- package/dart-src/root/lib/src/flutter_ota_app.dart +69 -0
- package/dart-src/root/lib/src/io.dart +48 -0
- package/dart-src/root/lib/src/io_stub.dart +19 -0
- package/dart-src/root/lib/src/ota_progress_overlay.dart +987 -0
- package/dart-src/root/lib/src/pack/pack.dart +434 -0
- package/dart-src/root/lib/src/patch_info.dart +12 -0
- package/dart-src/root/lib/src/patcher_channel.dart +160 -0
- package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_database.dart +1 -1
- package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_s3_client.dart +1 -1
- package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_storage.dart +1 -1
- package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_storage_profile.dart +1 -1
- package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/with_cloudfront_signed_url.dart +1 -1
- package/dart-src/root/lib/src/pkg/client/flutter_ota_kit_client.dart +1 -0
- package/dart-src/root/lib/src/pkg/client/src/server_update_source.dart +46 -0
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/cloudflare_worker_database.dart +1 -1
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_build_where.dart +1 -1
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_bundle_mapper.dart +3 -3
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_database.dart +1 -1
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_database_plugin.dart +2 -2
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_s3_client.dart +1 -1
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_storage.dart +1 -1
- package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_storage_profile.dart +1 -1
- package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/status.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/flutter_ota_kit_plugin_core.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_blob_database_plugin.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/filter_compatible_app_versions.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/get_update_info.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/request_update_bundle_state.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/resolve_update_info_from_bundles.dart +1 -1
- package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/types.dart +2 -2
- package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_bundle_mapper.dart +2 -2
- package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_database.dart +26 -15
- package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_storage.dart +1 -1
- package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_bundle_mapper.dart +2 -2
- package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_database.dart +2 -2
- package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_get_update_info.dart +2 -2
- package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_storage.dart +1 -1
- package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_types.dart +1 -1
- package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_bundle_mapper.dart +2 -2
- package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_database.dart +2 -2
- package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_edge_function_database.dart +1 -1
- package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_edge_function_storage.dart +1 -1
- package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_storage.dart +1 -1
- package/dart-src/root/lib/src/pocketbase_update_source.dart +107 -0
- package/dart-src/root/lib/src/postgres_update_source.dart +95 -0
- package/dart-src/root/lib/src/shared_update_check.dart +131 -0
- package/dart-src/root/lib/src/supabase_update_source.dart +93 -0
- package/dart-src/root/pubspec.yaml +54 -0
- package/package.json +2 -1
- package/scripts/postinstall.js +17 -13
- package/scripts/vendor.sh +22 -7
- package/dart-src/packages/core/CHANGELOG.md +0 -21
- package/dart-src/packages/core/README.md +0 -23
- package/dart-src/packages/core/example/lib/main.dart +0 -69
- package/dart-src/packages/core/pubspec.yaml +0 -13
- package/dart-src/packages/core/test/rollout_test.dart +0 -135
- package/dart-src/packages/core/test/semver_test.dart +0 -55
- package/dart-src/packages/core/test/semver_vectors.json +0 -1
- package/dart-src/packages/core/test/types_test.dart +0 -131
- package/dart-src/packages/core/test/uuid_test.dart +0 -40
- package/dart-src/packages/core/tool/dbg.dart +0 -7
- package/dart-src/plugins/aws/.gitkeep +0 -0
- package/dart-src/plugins/aws/CHANGELOG.md +0 -23
- package/dart-src/plugins/aws/LICENSE +0 -21
- package/dart-src/plugins/aws/README.md +0 -28
- package/dart-src/plugins/aws/example/lib/main.dart +0 -60
- package/dart-src/plugins/aws/pubspec.yaml +0 -30
- package/dart-src/plugins/aws/test/aws_database_test.dart +0 -161
- package/dart-src/plugins/aws/test/aws_storage_test.dart +0 -73
- package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +0 -78
- package/dart-src/plugins/cloudflare/.gitkeep +0 -0
- package/dart-src/plugins/cloudflare/CHANGELOG.md +0 -23
- package/dart-src/plugins/cloudflare/LICENSE +0 -21
- package/dart-src/plugins/cloudflare/README.md +0 -25
- package/dart-src/plugins/cloudflare/example/lib/main.dart +0 -62
- package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +0 -16
- package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +0 -8
- package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +0 -38
- package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +0 -11
- package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +0 -24
- package/dart-src/plugins/cloudflare/pubspec.yaml +0 -28
- package/dart-src/plugins/cloudflare/sql/bundles.sql +0 -42
- package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +0 -126
- package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +0 -106
- package/dart-src/plugins/cloudflare/test/d1_database_test.dart +0 -200
- package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +0 -261
- package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +0 -78
- package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +0 -74
- package/dart-src/plugins/plugin-core/CHANGELOG.md +0 -20
- package/dart-src/plugins/plugin-core/LICENSE +0 -21
- package/dart-src/plugins/plugin-core/README.md +0 -21
- package/dart-src/plugins/plugin-core/example/lib/main.dart +0 -61
- package/dart-src/plugins/plugin-core/pubspec.yaml +0 -23
- package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +0 -170
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +0 -74
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +0 -345
- package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +0 -843
- package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +0 -298
- package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +0 -195
- package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +0 -56
- package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +0 -1589
- package/dart-src/plugins/plugin-core/test/helpers_test.dart +0 -283
- package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +0 -202
- package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +0 -86
- package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +0 -88
- package/dart-src/plugins/pocketbase/CHANGELOG.md +0 -23
- package/dart-src/plugins/pocketbase/LICENSE +0 -21
- package/dart-src/plugins/pocketbase/README.md +0 -54
- package/dart-src/plugins/pocketbase/example/lib/main.dart +0 -62
- package/dart-src/plugins/pocketbase/pubspec.yaml +0 -26
- package/dart-src/plugins/pocketbase/test/pb_uri_test.dart +0 -12
- package/dart-src/plugins/postgres/.gitkeep +0 -0
- package/dart-src/plugins/postgres/CHANGELOG.md +0 -23
- package/dart-src/plugins/postgres/LICENSE +0 -21
- package/dart-src/plugins/postgres/README.md +0 -29
- package/dart-src/plugins/postgres/example/lib/main.dart +0 -67
- package/dart-src/plugins/postgres/pubspec.yaml +0 -26
- package/dart-src/plugins/postgres/sql/bundles.sql +0 -45
- package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +0 -21
- package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +0 -171
- package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +0 -126
- package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +0 -125
- package/dart-src/plugins/postgres/sql/hash_user_id.sql +0 -30
- package/dart-src/plugins/postgres/sql/is_device_eligible.sql +0 -237
- package/dart-src/plugins/postgres/sql/prepareSql.ts +0 -11
- package/dart-src/plugins/postgres/test/mock_postgres_client.dart +0 -320
- package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +0 -139
- package/dart-src/plugins/postgres/test/postgres_database_test.dart +0 -210
- package/dart-src/plugins/postgres/test/postgres_storage_runtime_test.dart +0 -60
- package/dart-src/plugins/supabase/CHANGELOG.md +0 -24
- package/dart-src/plugins/supabase/LICENSE +0 -21
- package/dart-src/plugins/supabase/README.md +0 -53
- package/dart-src/plugins/supabase/example/lib/main.dart +0 -63
- package/dart-src/plugins/supabase/pubspec.yaml +0 -27
- package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +0 -44
- package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +0 -968
- package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +0 -232
- package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +0 -137
- package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +0 -206
- package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +0 -186
- package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +0 -505
- package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +0 -50
- package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +0 -20
- package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +0 -75
- package/dart-src/plugins/supabase/test/mock_supabase_client.dart +0 -654
- package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +0 -115
- package/dart-src/plugins/supabase/test/supabase_database_test.dart +0 -198
- package/dart-src/plugins/supabase/test/supabase_storage_test.dart +0 -168
- /package/dart-src/{packages/core → root}/LICENSE +0 -0
- /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/flutter_ota_kit_aws.dart +0 -0
- /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_cloudfront_client.dart +0 -0
- /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_cloudfront_signer.dart +0 -0
- /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_config.dart +0 -0
- /package/dart-src/{plugins/aws/lib → root/lib/src/pkg/aws}/src/aws_ssm_client.dart +0 -0
- /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/flutter_ota_kit_cloudflare.dart +0 -0
- /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_client.dart +0 -0
- /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/d1_config.dart +0 -0
- /package/dart-src/{plugins/cloudflare/lib → root/lib/src/pkg/cloudflare}/src/r2_config.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/flutter_ota_kit_core.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/app_update_info.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/bundle.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/bundle_artifacts.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/bundle_patch_artifact.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/changed_asset.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/get_bundles_args.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/metadata.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/patch_info.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/platform.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/rollout.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/semver.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/semver_range.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/semver_version.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/strategy.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/update_bundle_params.dart +0 -0
- /package/dart-src/{packages/core/lib → root/lib/src/pkg/core}/src/uuid.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/asset_storage_layout.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_helpers.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_storage_layout.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_unit_of_work.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/bundle_unit_of_work_store.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/calculate_pagination.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/compression_format.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/content_addressed_assets.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_database_plugin.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_storage_key_builder.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/create_storage_plugin.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/generate_min_bundle_id.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/is_object.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/legacy_asset_storage_layout.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/paginate_bundles.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/parse_storage_uri.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/query_bundles.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/storage_helpers.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/storage_profile.dart +0 -0
- /package/dart-src/{plugins/plugin-core/lib → root/lib/src/pkg/plugin_core}/src/uuidv7.dart +0 -0
- /package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/flutter_ota_kit_pocketbase.dart +0 -0
- /package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_client.dart +0 -0
- /package/dart-src/{plugins/pocketbase/lib → root/lib/src/pkg/pocketbase}/src/pocketbase_config.dart +0 -0
- /package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/flutter_ota_kit_postgres.dart +0 -0
- /package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_client.dart +0 -0
- /package/dart-src/{plugins/postgres/lib → root/lib/src/pkg/postgres}/src/postgres_config.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/edge.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/flutter_ota_kit_supabase.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/error_message.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_client_adapter.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_client_http.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_config.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/supabase_signed_url_batcher.dart +0 -0
- /package/dart-src/{plugins/supabase/lib → root/lib/src/pkg/supabase}/src/types.dart +0 -0
|
@@ -0,0 +1,1170 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
|
|
3
|
+
import 'package:crypto/crypto.dart' as crypto;
|
|
4
|
+
import 'package:flutter/foundation.dart';
|
|
5
|
+
import 'package:flutter/services.dart' show EventChannel;
|
|
6
|
+
import 'package:flutter/widgets.dart';
|
|
7
|
+
|
|
8
|
+
import 'src/blacklist.dart';
|
|
9
|
+
import 'src/boot_diagnostic.dart';
|
|
10
|
+
import 'src/io_stub.dart' if (dart.library.io) 'src/io.dart' as platform_io;
|
|
11
|
+
import 'src/patch_info.dart';
|
|
12
|
+
import 'src/patcher_channel.dart';
|
|
13
|
+
import 'src/ota_progress_overlay.dart' show OtaOverlayManager;
|
|
14
|
+
import 'src/flutter_ota_app.dart' show FlutterPatcherShowUpdateUiBinding;
|
|
15
|
+
|
|
16
|
+
import 'package:flutter_ota_kit/src/pkg/client/flutter_ota_kit_client.dart'
|
|
17
|
+
show ServerUpdateResult;
|
|
18
|
+
|
|
19
|
+
import 'src/supabase_update_source.dart'
|
|
20
|
+
show SupabaseUpdateConfig, SupabaseUpdateSource;
|
|
21
|
+
import 'src/postgres_update_source.dart'
|
|
22
|
+
show PostgresUpdateConfig, PostgresUpdateSource;
|
|
23
|
+
import 'src/cloudflare_update_source.dart'
|
|
24
|
+
show CloudflareUpdateConfig, CloudflareUpdateSource;
|
|
25
|
+
import 'src/aws_update_source.dart' show AwsUpdateConfig, AwsUpdateSource;
|
|
26
|
+
import 'src/pocketbase_update_source.dart'
|
|
27
|
+
show PocketBaseUpdateConfig, PocketBaseUpdateSource;
|
|
28
|
+
|
|
29
|
+
import 'package:flutter_ota_kit/src/pkg/core/flutter_ota_kit_core.dart'
|
|
30
|
+
show Platform, UpdateStrategy, AppUpdateStatus;
|
|
31
|
+
|
|
32
|
+
export 'src/blacklist.dart';
|
|
33
|
+
export 'src/boot_diagnostic.dart';
|
|
34
|
+
export 'src/patch_info.dart';
|
|
35
|
+
export 'src/shared_update_check.dart' show performSharedUpdateCheck;
|
|
36
|
+
export 'src/supabase_update_source.dart'
|
|
37
|
+
show SupabaseUpdateConfig, SupabaseUpdateSource;
|
|
38
|
+
export 'src/postgres_update_source.dart'
|
|
39
|
+
show PostgresUpdateConfig, PostgresUpdateSource;
|
|
40
|
+
export 'src/cloudflare_update_source.dart'
|
|
41
|
+
show CloudflareUpdateConfig, CloudflareUpdateSource;
|
|
42
|
+
export 'src/aws_update_source.dart' show AwsUpdateConfig, AwsUpdateSource;
|
|
43
|
+
export 'src/pocketbase_update_source.dart'
|
|
44
|
+
show PocketBaseUpdateConfig, PocketBaseUpdateSource;
|
|
45
|
+
export 'src/flutter_ota_app.dart' show FlutterOtaApp;
|
|
46
|
+
export 'src/ota_progress_overlay.dart'
|
|
47
|
+
show
|
|
48
|
+
OtaOverlayManager,
|
|
49
|
+
OtaOverlayState,
|
|
50
|
+
OtaProgressOverlay,
|
|
51
|
+
OtaOverlayHandle,
|
|
52
|
+
LogLine;
|
|
53
|
+
|
|
54
|
+
export 'package:flutter_ota_kit/src/pkg/client/flutter_ota_kit_client.dart';
|
|
55
|
+
export 'package:flutter_ota_kit/src/pkg/core/flutter_ota_kit_core.dart'
|
|
56
|
+
show Platform, UpdateStrategy, AppUpdateStatus;
|
|
57
|
+
|
|
58
|
+
/// Outcome of [FlutterPatcher.rollbackToPrevious].
|
|
59
|
+
enum RollbackOutcome {
|
|
60
|
+
/// Successfully rolled back to a previous patch.
|
|
61
|
+
success,
|
|
62
|
+
|
|
63
|
+
/// All history entries were blacklisted or history empty; fell back to base APK.
|
|
64
|
+
fallbackToBase,
|
|
65
|
+
|
|
66
|
+
/// A history entry was found but skipped due to blacklist; tried next.
|
|
67
|
+
skippedBlacklisted,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Android-only Flutter hot-update entrypoint.
|
|
71
|
+
///
|
|
72
|
+
/// `flutter_ota_kit` installs a patch payload and loads it on the next cold
|
|
73
|
+
/// start. The payload can be a legacy `libapp.so` or a v2 `patch.zip` that
|
|
74
|
+
/// contains `libapp.so` plus explicitly selected Flutter assets.
|
|
75
|
+
///
|
|
76
|
+
/// Main APIs:
|
|
77
|
+
///
|
|
78
|
+
/// - [init]: configure native startup, crash protection, and first-frame boot
|
|
79
|
+
/// verification.
|
|
80
|
+
/// - [checkUpdate]: optional helper for the built-in minimal update protocol.
|
|
81
|
+
/// - [applyPatch]: download, verify, and install a payload URL.
|
|
82
|
+
/// - [applyPatchBytes]: install an already downloaded payload byte array.
|
|
83
|
+
/// - [applyUpdate]: apply a [ServerUpdateResult] and auto-restart if forced.
|
|
84
|
+
/// - [restart]: restart the whole App process (used to apply forced updates).
|
|
85
|
+
/// - [rollback]: delete the current patch.
|
|
86
|
+
///
|
|
87
|
+
/// Patches are never swapped into the current process. A successful apply takes
|
|
88
|
+
/// effect only after the next cold start. A non-forced update waits for that
|
|
89
|
+
/// next cold start; a forced update ([ServerUpdateResult.shouldForceUpdate])
|
|
90
|
+
/// is applied immediately via [applyUpdate] -> [restart].
|
|
91
|
+
///
|
|
92
|
+
/// {@category Architecture}
|
|
93
|
+
/// {@category API-reference}
|
|
94
|
+
/// {@category Crash-protection}
|
|
95
|
+
class FlutterPatcher {
|
|
96
|
+
FlutterPatcher._();
|
|
97
|
+
|
|
98
|
+
static bool _inited = false;
|
|
99
|
+
static SupabaseUpdateConfig? _supabaseConfig;
|
|
100
|
+
static PostgresUpdateConfig? _postgresConfig;
|
|
101
|
+
static CloudflareUpdateConfig? _cloudflareConfig;
|
|
102
|
+
static AwsUpdateConfig? _awsConfig;
|
|
103
|
+
static PocketBaseUpdateConfig? _pocketbaseConfig;
|
|
104
|
+
static bool _bootReported = false;
|
|
105
|
+
static bool _bootErrorReported = false;
|
|
106
|
+
static bool _nonAndroidWarned = false;
|
|
107
|
+
|
|
108
|
+
/// Whether Dart boot-error hooks can still report failures to native crash
|
|
109
|
+
/// protection. The first frame clears native boot state immediately; this
|
|
110
|
+
/// flag keeps Dart-side blank-screen detection alive for [init]'s
|
|
111
|
+
/// `verifyAfter` window.
|
|
112
|
+
static bool _dartHookActive = true;
|
|
113
|
+
|
|
114
|
+
/// When `true` (default), a forced update shows the built-in progress overlay
|
|
115
|
+
/// (terminal-style dot spinner + determinate bar + server OTA message). Set to
|
|
116
|
+
/// `false` to disable it (forced updates still apply, just without UI). Can
|
|
117
|
+
/// also be toggled via [FlutterOtaApp.showUpdateUi] or [navigatorKey].
|
|
118
|
+
static bool showUpdateUi = true;
|
|
119
|
+
|
|
120
|
+
/// Optional navigator key. When set on your [MaterialApp.navigatorKey], the
|
|
121
|
+
/// forced-update overlay can be shown even without wrapping the app in
|
|
122
|
+
/// [FlutterOtaApp]. The [FlutterOtaApp] wrapper is the zero-code path.
|
|
123
|
+
static final navigatorKey = GlobalKey<NavigatorState>();
|
|
124
|
+
|
|
125
|
+
// Wire the widget-level flag to [showUpdateUi] so [FlutterOtaApp] can toggle it,
|
|
126
|
+
// and let the overlay manager fall back to the consumer-provided navigator key.
|
|
127
|
+
static void _wireOverlay() {
|
|
128
|
+
FlutterPatcherShowUpdateUiBinding.applyShowUpdateUi = (value) =>
|
|
129
|
+
showUpdateUi = value;
|
|
130
|
+
OtaOverlayManager.instance.setResolver(
|
|
131
|
+
() => navigatorKey.currentState?.overlay,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
static bool _notAndroidGuard(String method) {
|
|
136
|
+
if (platform_io.isAndroid) return false;
|
|
137
|
+
if (!_nonAndroidWarned) {
|
|
138
|
+
_nonAndroidWarned = true;
|
|
139
|
+
debugPrint(
|
|
140
|
+
'[FlutterPatcher] WARNING: $method called on ${platform_io.operatingSystem}. '
|
|
141
|
+
'This plugin only supports Android; all calls are no-ops. '
|
|
142
|
+
'See README > What Can Be Patched?',
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static const EventChannel _eventChannel = EventChannel(
|
|
149
|
+
'flutter_ota_kit/events',
|
|
150
|
+
);
|
|
151
|
+
static Stream<PatchApplyProgress>? _progressStream;
|
|
152
|
+
|
|
153
|
+
/// Broadcast progress stream for [applyPatch].
|
|
154
|
+
///
|
|
155
|
+
/// Subscribe before calling [applyPatch] to receive `downloading`,
|
|
156
|
+
/// `verifying`, and `finalizing` events. Non-Android platforms return an
|
|
157
|
+
/// empty stream.
|
|
158
|
+
static Stream<PatchApplyProgress> get applyProgress {
|
|
159
|
+
if (_notAndroidGuard('applyProgress')) return const Stream.empty();
|
|
160
|
+
return _progressStream ??= _eventChannel.receiveBroadcastStream().map(
|
|
161
|
+
(raw) => PatchApplyProgress.fromNative(raw),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Initializes patch configuration and crash protection.
|
|
166
|
+
///
|
|
167
|
+
/// Call this in `main()` before `runApp()`. The method is idempotent.
|
|
168
|
+
///
|
|
169
|
+
/// [publicKeyBase64] is an optional X.509 SubjectPublicKeyInfo Ed25519 public
|
|
170
|
+
/// key in base64. Empty disables signature verification. If [PatchInfo.md5]
|
|
171
|
+
/// is empty, signature verification is skipped as well because the signed
|
|
172
|
+
/// message is the md5 hex string.
|
|
173
|
+
///
|
|
174
|
+
/// [maxCrashCount] defaults to `1` (fail-fast). Once a loaded patch causes an
|
|
175
|
+
/// early boot failure, the SDK rolls it back and blacklists the same payload.
|
|
176
|
+
///
|
|
177
|
+
/// [strictSignature] rejects signed patches on Android API < 33, where the
|
|
178
|
+
/// platform Ed25519 implementation is unavailable.
|
|
179
|
+
///
|
|
180
|
+
/// [loaderFieldCandidates] and [loaderFallbackHeuristic] are advanced Flutter
|
|
181
|
+
/// embedding compatibility controls. Keep defaults unless adapting a new
|
|
182
|
+
/// Flutter version.
|
|
183
|
+
///
|
|
184
|
+
/// [verifyAfter] is the post-first-frame Dart error watch window.
|
|
185
|
+
///
|
|
186
|
+
/// [autoApplyUpdates] wires zero-click updates: right after boot protection is
|
|
187
|
+
/// initialized, [checkAndApplyUpdates] runs in the background. A forced update
|
|
188
|
+
/// is downloaded, staged, and the process is restarted automatically — no user
|
|
189
|
+
/// tap required. Non-forced updates are staged for the next normal cold start.
|
|
190
|
+
/// The backend must be configured (e.g. via [configureSupabase]) *before*
|
|
191
|
+
/// calling [init] for this to take effect.
|
|
192
|
+
///
|
|
193
|
+
/// [maxPatchHistory] maximum number of previous patches to keep for rollback
|
|
194
|
+
/// (default 4). Set to 0 to disable history.
|
|
195
|
+
///
|
|
196
|
+
/// [maxAssetHistory] maximum number of asset archives to keep (default 4).
|
|
197
|
+
/// Assets are deduplicated; only patches that actually change assets create
|
|
198
|
+
/// a new asset archive.
|
|
199
|
+
static Future<void> init({
|
|
200
|
+
String publicKeyBase64 = '',
|
|
201
|
+
int maxCrashCount = 1,
|
|
202
|
+
bool strictSignature = true,
|
|
203
|
+
List<String> loaderFieldCandidates = const ['flutterLoader'],
|
|
204
|
+
bool loaderFallbackHeuristic = false,
|
|
205
|
+
Duration verifyAfter = const Duration(seconds: 5),
|
|
206
|
+
bool autoApplyUpdates = false,
|
|
207
|
+
int maxPatchHistory = 4,
|
|
208
|
+
int maxAssetHistory = 4,
|
|
209
|
+
}) async {
|
|
210
|
+
if (_notAndroidGuard('init')) return;
|
|
211
|
+
if (_inited) return;
|
|
212
|
+
_inited = true;
|
|
213
|
+
_verifyAfter = verifyAfter;
|
|
214
|
+
_wireOverlay();
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
await PatcherChannel.markBooting();
|
|
218
|
+
} catch (e, s) {
|
|
219
|
+
_log('markBooting failed: $e', s);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_installBootErrorCatchers();
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
await PatcherChannel.saveConfig(
|
|
226
|
+
publicKeyBase64: publicKeyBase64,
|
|
227
|
+
maxCrashCount: maxCrashCount,
|
|
228
|
+
strictSignature: strictSignature,
|
|
229
|
+
loaderFieldCandidates: loaderFieldCandidates,
|
|
230
|
+
loaderFallbackHeuristic: loaderFallbackHeuristic,
|
|
231
|
+
maxPatchHistory: maxPatchHistory,
|
|
232
|
+
maxAssetHistory: maxAssetHistory,
|
|
233
|
+
);
|
|
234
|
+
} catch (e, s) {
|
|
235
|
+
_log('saveConfig failed: $e', s);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
_BootVerifier.start();
|
|
239
|
+
|
|
240
|
+
// Auto-configure a backend from build-time environment variables
|
|
241
|
+
// (`--dart-define` / `.env`). No-op when a backend is already configured
|
|
242
|
+
// (e.g. via the generated `flutter_ota_kit_setup.dart` or an explicit
|
|
243
|
+
// `configureX`). Environment wins over the `.flutter_ota_kit/` project config.
|
|
244
|
+
_autoConfigureFromEnv();
|
|
245
|
+
|
|
246
|
+
if (autoApplyUpdates) {
|
|
247
|
+
// Fire-and-forget so a forced update applies + restarts without blocking
|
|
248
|
+
// the first frame. Swallowed failures never crash boot.
|
|
249
|
+
unawaited(checkAndApplyUpdates());
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// Auto-configure a backend from build-time environment variables
|
|
254
|
+
/// (`--dart-define` / `.env` via `--dart-define-from-file`), when no backend
|
|
255
|
+
/// was configured explicitly (e.g. via `configureSupabase` or the generated
|
|
256
|
+
/// `flutter_ota_kit_setup.dart`). This is the second supported config path
|
|
257
|
+
/// alongside the `.flutter_ota_kit/` project config; both honor the same
|
|
258
|
+
/// precedence — environment overrides any file-based default.
|
|
259
|
+
static void _autoConfigureFromEnv() {
|
|
260
|
+
if (_supabaseConfig != null ||
|
|
261
|
+
_postgresConfig != null ||
|
|
262
|
+
_cloudflareConfig != null ||
|
|
263
|
+
_awsConfig != null ||
|
|
264
|
+
_pocketbaseConfig != null) {
|
|
265
|
+
return; // already configured (e.g. generated setup ran first)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Optional explicit backend picker. When the user has multiple backends in
|
|
269
|
+
// their .env (e.g. switching from supabase to pocketbase), this var
|
|
270
|
+
// disambiguates which one wins. Without it, first-match-wins order is:
|
|
271
|
+
// supabase → postgres → cloudflare → aws → pocketbase.
|
|
272
|
+
const backendHint = String.fromEnvironment(
|
|
273
|
+
'FLUTTER_OTA_BACKEND',
|
|
274
|
+
defaultValue: '',
|
|
275
|
+
);
|
|
276
|
+
bool isMatching(String name) {
|
|
277
|
+
if (backendHint.isEmpty) return true;
|
|
278
|
+
return backendHint.toLowerCase() == name;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const supabaseUrl = String.fromEnvironment(
|
|
282
|
+
'SUPABASE_URL',
|
|
283
|
+
defaultValue: '',
|
|
284
|
+
);
|
|
285
|
+
if (supabaseUrl.isNotEmpty && isMatching('supabase')) {
|
|
286
|
+
_supabaseConfig = SupabaseUpdateConfig(
|
|
287
|
+
supabaseUrl: supabaseUrl,
|
|
288
|
+
anonKey: const String.fromEnvironment(
|
|
289
|
+
'SUPABASE_ANON_KEY',
|
|
290
|
+
defaultValue: '',
|
|
291
|
+
),
|
|
292
|
+
bucket: const String.fromEnvironment(
|
|
293
|
+
'SUPABASE_BUCKET',
|
|
294
|
+
defaultValue: 'bundles',
|
|
295
|
+
),
|
|
296
|
+
channel: const String.fromEnvironment(
|
|
297
|
+
'CHANNEL',
|
|
298
|
+
defaultValue: 'production',
|
|
299
|
+
),
|
|
300
|
+
platform: Platform.android,
|
|
301
|
+
updateStrategy: UpdateStrategy.appVersion,
|
|
302
|
+
appVersion: const String.fromEnvironment(
|
|
303
|
+
'APP_VERSION',
|
|
304
|
+
defaultValue: '1.0.0',
|
|
305
|
+
),
|
|
306
|
+
sdkVersion: const String.fromEnvironment(
|
|
307
|
+
'SDK_VERSION',
|
|
308
|
+
defaultValue: '1.0.0',
|
|
309
|
+
),
|
|
310
|
+
);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const pgHost = String.fromEnvironment('POSTGRES_HOST', defaultValue: '');
|
|
315
|
+
const pgDatabaseUrl = String.fromEnvironment(
|
|
316
|
+
'DATABASE_URL',
|
|
317
|
+
defaultValue: '',
|
|
318
|
+
);
|
|
319
|
+
if ((pgHost.isNotEmpty || pgDatabaseUrl.isNotEmpty) &&
|
|
320
|
+
isMatching('postgres')) {
|
|
321
|
+
_postgresConfig = PostgresUpdateConfig(
|
|
322
|
+
host: pgHost,
|
|
323
|
+
port:
|
|
324
|
+
int.tryParse(
|
|
325
|
+
const String.fromEnvironment(
|
|
326
|
+
'POSTGRES_PORT',
|
|
327
|
+
defaultValue: '5432',
|
|
328
|
+
),
|
|
329
|
+
) ??
|
|
330
|
+
5432,
|
|
331
|
+
database: const String.fromEnvironment(
|
|
332
|
+
'POSTGRES_DB',
|
|
333
|
+
defaultValue: 'postgres',
|
|
334
|
+
),
|
|
335
|
+
username: const String.fromEnvironment(
|
|
336
|
+
'POSTGRES_USER',
|
|
337
|
+
defaultValue: '',
|
|
338
|
+
),
|
|
339
|
+
password: const String.fromEnvironment(
|
|
340
|
+
'POSTGRES_PASSWORD',
|
|
341
|
+
defaultValue: '',
|
|
342
|
+
),
|
|
343
|
+
servingBaseUrl: const String.fromEnvironment(
|
|
344
|
+
'POSTGRES_SERVING_BASE_URL',
|
|
345
|
+
defaultValue: '',
|
|
346
|
+
),
|
|
347
|
+
channel: const String.fromEnvironment(
|
|
348
|
+
'CHANNEL',
|
|
349
|
+
defaultValue: 'production',
|
|
350
|
+
),
|
|
351
|
+
platform: Platform.android,
|
|
352
|
+
updateStrategy: UpdateStrategy.appVersion,
|
|
353
|
+
appVersion: const String.fromEnvironment(
|
|
354
|
+
'APP_VERSION',
|
|
355
|
+
defaultValue: '1.0.0',
|
|
356
|
+
),
|
|
357
|
+
);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const cfAccount = String.fromEnvironment(
|
|
362
|
+
'CLOUDFLARE_ACCOUNT_ID',
|
|
363
|
+
defaultValue: '',
|
|
364
|
+
);
|
|
365
|
+
if (cfAccount.isNotEmpty && isMatching('cloudflare')) {
|
|
366
|
+
_cloudflareConfig = CloudflareUpdateConfig(
|
|
367
|
+
accountId: cfAccount,
|
|
368
|
+
databaseId: const String.fromEnvironment(
|
|
369
|
+
'CLOUDFLARE_D1_DATABASE_ID',
|
|
370
|
+
defaultValue: '',
|
|
371
|
+
),
|
|
372
|
+
cloudflareApiToken: const String.fromEnvironment(
|
|
373
|
+
'CLOUDFLARE_API_TOKEN',
|
|
374
|
+
defaultValue: '',
|
|
375
|
+
),
|
|
376
|
+
bucketName: const String.fromEnvironment(
|
|
377
|
+
'R2_BUCKET',
|
|
378
|
+
defaultValue: 'bundles',
|
|
379
|
+
),
|
|
380
|
+
accessKeyId: const String.fromEnvironment(
|
|
381
|
+
'R2_ACCESS_KEY_ID',
|
|
382
|
+
defaultValue: '',
|
|
383
|
+
),
|
|
384
|
+
secretAccessKey: const String.fromEnvironment(
|
|
385
|
+
'R2_SECRET_ACCESS_KEY',
|
|
386
|
+
defaultValue: '',
|
|
387
|
+
),
|
|
388
|
+
basePath: const String.fromEnvironment(
|
|
389
|
+
'R2_BASE_PATH',
|
|
390
|
+
defaultValue: '',
|
|
391
|
+
),
|
|
392
|
+
channel: const String.fromEnvironment(
|
|
393
|
+
'CHANNEL',
|
|
394
|
+
defaultValue: 'production',
|
|
395
|
+
),
|
|
396
|
+
platform: Platform.android,
|
|
397
|
+
updateStrategy: UpdateStrategy.appVersion,
|
|
398
|
+
appVersion: const String.fromEnvironment(
|
|
399
|
+
'APP_VERSION',
|
|
400
|
+
defaultValue: '1.0.0',
|
|
401
|
+
),
|
|
402
|
+
);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const awsBucket = String.fromEnvironment('AWS_BUCKET', defaultValue: '');
|
|
407
|
+
if (awsBucket.isNotEmpty && isMatching('aws')) {
|
|
408
|
+
_awsConfig = AwsUpdateConfig(
|
|
409
|
+
bucketName: awsBucket,
|
|
410
|
+
region: const String.fromEnvironment('AWS_REGION', defaultValue: ''),
|
|
411
|
+
accessKeyId: const String.fromEnvironment(
|
|
412
|
+
'AWS_ACCESS_KEY_ID',
|
|
413
|
+
defaultValue: '',
|
|
414
|
+
),
|
|
415
|
+
secretAccessKey: const String.fromEnvironment(
|
|
416
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
417
|
+
defaultValue: '',
|
|
418
|
+
),
|
|
419
|
+
basePath: const String.fromEnvironment(
|
|
420
|
+
'AWS_BASE_PATH',
|
|
421
|
+
defaultValue: '',
|
|
422
|
+
),
|
|
423
|
+
endpoint: const String.fromEnvironment(
|
|
424
|
+
'AWS_ENDPOINT',
|
|
425
|
+
defaultValue: '',
|
|
426
|
+
),
|
|
427
|
+
channel: const String.fromEnvironment(
|
|
428
|
+
'CHANNEL',
|
|
429
|
+
defaultValue: 'production',
|
|
430
|
+
),
|
|
431
|
+
platform: Platform.android,
|
|
432
|
+
updateStrategy: UpdateStrategy.appVersion,
|
|
433
|
+
appVersion: const String.fromEnvironment(
|
|
434
|
+
'APP_VERSION',
|
|
435
|
+
defaultValue: '1.0.0',
|
|
436
|
+
),
|
|
437
|
+
);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const pbUrl = String.fromEnvironment('POCKETBASE_URL', defaultValue: '');
|
|
442
|
+
if (pbUrl.isNotEmpty && isMatching('pocketbase')) {
|
|
443
|
+
_pocketbaseConfig = PocketBaseUpdateConfig(
|
|
444
|
+
url: pbUrl,
|
|
445
|
+
adminEmail: const String.fromEnvironment(
|
|
446
|
+
'POCKETBASE_ADMIN_EMAIL',
|
|
447
|
+
defaultValue: '',
|
|
448
|
+
),
|
|
449
|
+
adminPassword: const String.fromEnvironment(
|
|
450
|
+
'POCKETBASE_ADMIN_PASSWORD',
|
|
451
|
+
defaultValue: '',
|
|
452
|
+
),
|
|
453
|
+
bundlesCollection: const String.fromEnvironment(
|
|
454
|
+
'POCKETBASE_BUNDLES_COLLECTION',
|
|
455
|
+
defaultValue: 'bundles',
|
|
456
|
+
),
|
|
457
|
+
bundlesBucket: const String.fromEnvironment(
|
|
458
|
+
'POCKETBASE_BUCKET',
|
|
459
|
+
defaultValue: 'bundles',
|
|
460
|
+
),
|
|
461
|
+
channel: const String.fromEnvironment(
|
|
462
|
+
'CHANNEL',
|
|
463
|
+
defaultValue: 'production',
|
|
464
|
+
),
|
|
465
|
+
platform: Platform.android,
|
|
466
|
+
updateStrategy: UpdateStrategy.appVersion,
|
|
467
|
+
appVersion: const String.fromEnvironment(
|
|
468
|
+
'APP_VERSION',
|
|
469
|
+
defaultValue: '1.0.0',
|
|
470
|
+
),
|
|
471
|
+
);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/// Marks the current patched boot as successful and clears native crash
|
|
477
|
+
/// protection state.
|
|
478
|
+
///
|
|
479
|
+
/// Usually called automatically by [init] after the first frame.
|
|
480
|
+
static Future<void> reportBootSuccess() async {
|
|
481
|
+
if (_notAndroidGuard('reportBootSuccess')) return;
|
|
482
|
+
if (_bootReported) return;
|
|
483
|
+
_bootReported = true;
|
|
484
|
+
try {
|
|
485
|
+
await PatcherChannel.reportBootSuccess();
|
|
486
|
+
} catch (e, s) {
|
|
487
|
+
_log('reportBootSuccess failed: $e', s);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/// Optional HTTP update checker for the built-in minimal JSON protocol.
|
|
492
|
+
///
|
|
493
|
+
/// Production apps can skip this method, parse their own update response, and
|
|
494
|
+
/// construct [PatchInfo] directly before calling [applyPatch].
|
|
495
|
+
///
|
|
496
|
+
/// The returned `patchUrl` is a payload URL: `libapp.so` for lib-only patches
|
|
497
|
+
/// or `patch.zip` for asset patches.
|
|
498
|
+
static Future<PatchCheckResult> checkUpdate(
|
|
499
|
+
String url, {
|
|
500
|
+
Map<String, String>? headers,
|
|
501
|
+
Duration timeout = const Duration(seconds: 10),
|
|
502
|
+
}) async {
|
|
503
|
+
if (_notAndroidGuard('checkUpdate')) {
|
|
504
|
+
return PatchCheckResult.none();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
final decoded = await platform_io.getJson(
|
|
509
|
+
url,
|
|
510
|
+
headers: headers,
|
|
511
|
+
timeout: timeout,
|
|
512
|
+
);
|
|
513
|
+
return PatchCheckResult.fromJson(decoded);
|
|
514
|
+
} on PatcherException {
|
|
515
|
+
rethrow;
|
|
516
|
+
} catch (e, s) {
|
|
517
|
+
_log('checkUpdate failed: $e', s);
|
|
518
|
+
throw PatcherException(e.toString());
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/// Configures the hosted **Supabase** update source.
|
|
523
|
+
///
|
|
524
|
+
/// Talks to the Supabase project directly over its REST API (PostgREST
|
|
525
|
+
/// `bundles` table + Storage signed URLs) — no separate server process is
|
|
526
|
+
/// required, because Supabase *is* the backend.
|
|
527
|
+
///
|
|
528
|
+
/// ```dart
|
|
529
|
+
/// FlutterPatcher.configureSupabase(SupabaseUpdateConfig(
|
|
530
|
+
/// supabaseUrl: 'https://<ref>.supabase.co',
|
|
531
|
+
/// anonKey: '<anon>', // public anon key (RLS-protected reads)
|
|
532
|
+
/// bucket: 'bundles',
|
|
533
|
+
/// channel: 'production',
|
|
534
|
+
/// platform: Platform.android,
|
|
535
|
+
/// updateStrategy: UpdateStrategy.appVersion,
|
|
536
|
+
/// appVersion: '1.0.0',
|
|
537
|
+
/// ));
|
|
538
|
+
/// ```
|
|
539
|
+
static void configureSupabase(SupabaseUpdateConfig config) {
|
|
540
|
+
_supabaseConfig = config;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/// Configures the **Postgres** update source (direct, no server).
|
|
544
|
+
///
|
|
545
|
+
/// Talks to a Postgres database for bundle metadata and the bytea-backed
|
|
546
|
+
/// storage plugin for artifacts. For HTTP downloads, provide [PostgresUpdateConfig.servingBaseUrl]
|
|
547
|
+
/// (a proxy in front of the `flutter_ota_kit_storage` table) or use a PostgREST
|
|
548
|
+
/// layer. See `flutter_ota_kit_postgres` for the backend plugin used by the CLI.
|
|
549
|
+
static void configurePostgres(PostgresUpdateConfig config) {
|
|
550
|
+
_postgresConfig = config;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/// Configures the **Cloudflare** update source (direct, no server).
|
|
554
|
+
///
|
|
555
|
+
/// Uses Cloudflare D1 for bundle metadata and R2 (S3-compatible) for artifact
|
|
556
|
+
/// storage. Presigned R2 download URLs are resolved on the device.
|
|
557
|
+
static void configureCloudflare(CloudflareUpdateConfig config) {
|
|
558
|
+
_cloudflareConfig = config;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/// Configures the **AWS S3** update source (direct, no server).
|
|
562
|
+
///
|
|
563
|
+
/// Uses an S3-backed blob database for bundle metadata and S3 (presigned URLs,
|
|
564
|
+
/// optionally fronted by CloudFront) for artifact storage.
|
|
565
|
+
static void configureAws(AwsUpdateConfig config) {
|
|
566
|
+
_awsConfig = config;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/// Configures the **PocketBase** update source (self-hosted, single binary).
|
|
570
|
+
///
|
|
571
|
+
/// Talks to a PocketBase instance directly over its REST API. PocketBase is a
|
|
572
|
+
/// single-binary Go backend with SQLite + auth + file storage + an admin UI
|
|
573
|
+
/// (~15MB). The schema is installed via `flutter_ota_kit pocketbase serve`.
|
|
574
|
+
///
|
|
575
|
+
/// PocketBase is the recommended option for small teams that want a
|
|
576
|
+
/// Supabase-like experience without a cloud account — single binary, no
|
|
577
|
+
/// Docker, no external database.
|
|
578
|
+
static void configurePocketBase(PocketBaseUpdateConfig config) {
|
|
579
|
+
_pocketbaseConfig = config;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/// Performs an update check against the configured backend (Supabase,
|
|
583
|
+
/// Postgres, Cloudflare, AWS, or PocketBase).
|
|
584
|
+
///
|
|
585
|
+
/// Returns a [ServerUpdateResult]; when [ServerUpdateResult.hasUpdate] is
|
|
586
|
+
/// true, install it with `FlutterPatcher.applyPatch(result.patch!)`.
|
|
587
|
+
static Future<ServerUpdateResult> checkForUpdate({
|
|
588
|
+
Duration timeout = const Duration(seconds: 10),
|
|
589
|
+
}) async {
|
|
590
|
+
final currentBundleId = await currentVersion;
|
|
591
|
+
|
|
592
|
+
final supabaseConfig = _supabaseConfig;
|
|
593
|
+
if (supabaseConfig != null) {
|
|
594
|
+
return SupabaseUpdateSource().check(
|
|
595
|
+
supabaseConfig,
|
|
596
|
+
currentBundleId: currentBundleId,
|
|
597
|
+
timeout: timeout,
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
final postgresConfig = _postgresConfig;
|
|
601
|
+
if (postgresConfig != null) {
|
|
602
|
+
return PostgresUpdateSource().check(
|
|
603
|
+
postgresConfig,
|
|
604
|
+
currentBundleId: currentBundleId,
|
|
605
|
+
timeout: timeout,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
final cloudflareConfig = _cloudflareConfig;
|
|
609
|
+
if (cloudflareConfig != null) {
|
|
610
|
+
return CloudflareUpdateSource().check(
|
|
611
|
+
cloudflareConfig,
|
|
612
|
+
currentBundleId: currentBundleId,
|
|
613
|
+
timeout: timeout,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
final awsConfig = _awsConfig;
|
|
617
|
+
if (awsConfig != null) {
|
|
618
|
+
return AwsUpdateSource().check(
|
|
619
|
+
awsConfig,
|
|
620
|
+
currentBundleId: currentBundleId,
|
|
621
|
+
timeout: timeout,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
final pocketbaseConfig = _pocketbaseConfig;
|
|
625
|
+
if (pocketbaseConfig != null) {
|
|
626
|
+
return PocketBaseUpdateSource().check(
|
|
627
|
+
pocketbaseConfig,
|
|
628
|
+
currentBundleId: currentBundleId,
|
|
629
|
+
timeout: timeout,
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
throw PatcherException(
|
|
633
|
+
'No update source configured. Call FlutterPatcher.configureSupabase(...), '
|
|
634
|
+
'configurePostgres(...), configureCloudflare(...), configureAws(...), or '
|
|
635
|
+
'configurePocketBase(...) first.',
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/// Downloads, verifies, and installs [patchInfo]'s payload.
|
|
640
|
+
///
|
|
641
|
+
/// The payload can be either a complete `libapp.so` or a v2 `patch.zip`.
|
|
642
|
+
/// Native code detects ZIP payloads automatically.
|
|
643
|
+
///
|
|
644
|
+
/// Flow:
|
|
645
|
+
///
|
|
646
|
+
/// 1. Download with retry.
|
|
647
|
+
/// 2. Verify payload md5/signature when provided.
|
|
648
|
+
/// 3. Install legacy lib-only payload, or parse and install v2 package.
|
|
649
|
+
/// 4. Commit patch files transactionally for the next cold start.
|
|
650
|
+
///
|
|
651
|
+
/// Returns a [PatchApplyResult]. `ok=true` means the patch is installed and
|
|
652
|
+
/// will take effect on the next cold start.
|
|
653
|
+
static Future<PatchApplyResult> applyPatch(
|
|
654
|
+
PatchInfo patchInfo, {
|
|
655
|
+
void Function(PatchApplyProgress)? onProgress,
|
|
656
|
+
}) async {
|
|
657
|
+
if (_notAndroidGuard('applyPatch')) {
|
|
658
|
+
return PatchApplyResult.failure(
|
|
659
|
+
PatchApplyError.unknown,
|
|
660
|
+
'not supported on ${platform_io.operatingSystem}',
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
StreamSubscription<PatchApplyProgress>? sub;
|
|
664
|
+
if (onProgress != null) {
|
|
665
|
+
sub = applyProgress.listen(onProgress);
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
final native = await PatcherChannel.applyPatch(patchInfo.toJson());
|
|
669
|
+
return PatchApplyResult.fromNative(native);
|
|
670
|
+
} catch (e, s) {
|
|
671
|
+
_log('applyPatch failed: $e', s);
|
|
672
|
+
return PatchApplyResult.failure(PatchApplyError.unknown, e.toString());
|
|
673
|
+
} finally {
|
|
674
|
+
await sub?.cancel();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
static String? _cachedStagingDir;
|
|
679
|
+
|
|
680
|
+
/// Installs an already downloaded patch payload.
|
|
681
|
+
///
|
|
682
|
+
/// Useful for bundled example patches, custom downloaders, or isolate-based
|
|
683
|
+
/// loading. The bytes can be either `libapp.so` or `patch.zip`.
|
|
684
|
+
///
|
|
685
|
+
/// This helper writes bytes to native cache, computes md5, then reuses
|
|
686
|
+
/// [applyPatch] through a `file://` URL.
|
|
687
|
+
static Future<PatchApplyResult> applyPatchBytes(
|
|
688
|
+
Uint8List bytes, {
|
|
689
|
+
required String version,
|
|
690
|
+
String signature = '',
|
|
691
|
+
int? targetVersionCode,
|
|
692
|
+
void Function(PatchApplyProgress)? onProgress,
|
|
693
|
+
}) async {
|
|
694
|
+
if (_notAndroidGuard('applyPatchBytes')) {
|
|
695
|
+
return PatchApplyResult.failure(
|
|
696
|
+
PatchApplyError.unknown,
|
|
697
|
+
'not supported on ${platform_io.operatingSystem}',
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
final dir = _cachedStagingDir ??= (await PatcherChannel.cacheDir()) ?? '';
|
|
701
|
+
if (dir.isEmpty) {
|
|
702
|
+
return PatchApplyResult.failure(
|
|
703
|
+
PatchApplyError.ioError,
|
|
704
|
+
'native cacheDir unavailable',
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
String? stagedPath;
|
|
708
|
+
try {
|
|
709
|
+
stagedPath = await platform_io.stagePatchBytes(dir, bytes);
|
|
710
|
+
final md5Hex = crypto.md5.convert(bytes).toString();
|
|
711
|
+
return await applyPatch(
|
|
712
|
+
PatchInfo(
|
|
713
|
+
version: version,
|
|
714
|
+
patchUrl: 'file://$stagedPath',
|
|
715
|
+
md5: md5Hex,
|
|
716
|
+
signature: signature,
|
|
717
|
+
targetVersionCode: targetVersionCode,
|
|
718
|
+
),
|
|
719
|
+
onProgress: onProgress,
|
|
720
|
+
);
|
|
721
|
+
} catch (e, s) {
|
|
722
|
+
_log('applyPatchBytes failed: $e', s);
|
|
723
|
+
return PatchApplyResult.failure(PatchApplyError.unknown, e.toString());
|
|
724
|
+
} finally {
|
|
725
|
+
try {
|
|
726
|
+
if (stagedPath != null) {
|
|
727
|
+
await platform_io.deleteFileIfExists(stagedPath);
|
|
728
|
+
}
|
|
729
|
+
} catch (_) {
|
|
730
|
+
// Staging cleanup must not block the apply result.
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/// Deletes the current patch. The next cold start uses the APK built-in
|
|
736
|
+
/// version. Manual rollback does not blacklist the patch.
|
|
737
|
+
static Future<void> rollback() async {
|
|
738
|
+
if (_notAndroidGuard('rollback')) return;
|
|
739
|
+
try {
|
|
740
|
+
await PatcherChannel.rollback();
|
|
741
|
+
} catch (e, s) {
|
|
742
|
+
_log('rollback failed: $e', s);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/// Rolls back to the immediately previous patch in local history.
|
|
747
|
+
///
|
|
748
|
+
/// Scans the local patch history (newest first) and restores the first
|
|
749
|
+
/// non-blacklisted entry. Skips entries that are in the local blacklist
|
|
750
|
+
/// (e.g., patches that previously crashed on boot).
|
|
751
|
+
///
|
|
752
|
+
/// Returns:
|
|
753
|
+
/// - [RollbackOutcome.success]: Rolled back to a previous patch.
|
|
754
|
+
/// - [RollbackOutcome.fallbackToBase]: No valid history; deleted current patch.
|
|
755
|
+
/// - [RollbackOutcome.skippedBlacklisted]: Skipped blacklisted entries, rolled back to earlier.
|
|
756
|
+
///
|
|
757
|
+
/// Call [restart] after a successful rollback to activate the previous patch.
|
|
758
|
+
static Future<RollbackOutcome> rollbackToPrevious() async {
|
|
759
|
+
if (_notAndroidGuard('rollbackToPrevious')) return RollbackOutcome.fallbackToBase;
|
|
760
|
+
try {
|
|
761
|
+
final outcomeName = await PatcherChannel.rollbackToPrevious();
|
|
762
|
+
return RollbackOutcome.values.byName(outcomeName);
|
|
763
|
+
} catch (e, s) {
|
|
764
|
+
_log('rollbackToPrevious failed: $e', s);
|
|
765
|
+
return RollbackOutcome.fallbackToBase;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/// Immediately restarts the whole App process.
|
|
770
|
+
///
|
|
771
|
+
/// A staged patch only loads on a cold start, so after a forced update the
|
|
772
|
+
/// new code won't run until the process restarts. Call this to apply the
|
|
773
|
+
/// forced update right away instead of waiting for the user to relaunch.
|
|
774
|
+
///
|
|
775
|
+
/// On non-Android this is a no-op. Failures are swallowed and logged — the
|
|
776
|
+
/// usual fallback is a "please restart" prompt rather than a crash.
|
|
777
|
+
static Future<void> restart() async {
|
|
778
|
+
if (_notAndroidGuard('restart')) return;
|
|
779
|
+
try {
|
|
780
|
+
await PatcherChannel.restartApp();
|
|
781
|
+
} catch (e, s) {
|
|
782
|
+
_log('restart failed: $e', s);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/// Stage an update from a [ServerUpdateResult] and auto-restart when forced.
|
|
787
|
+
///
|
|
788
|
+
/// Equivalent to `applyPatch(result.patch)`, then `restart()` **only if**
|
|
789
|
+
/// [ServerUpdateResult.shouldForceUpdate] is true. Non-forced updates are
|
|
790
|
+
/// staged and take effect on the next normal cold start (no restart).
|
|
791
|
+
///
|
|
792
|
+
/// When [showUpdateUi] is on (default) and the update is forced, a built-in
|
|
793
|
+
/// progress overlay (dot spinner + determinate bar + the server OTA message)
|
|
794
|
+
/// is shown automatically during the download/verify/install — the consuming
|
|
795
|
+
/// app writes no UI. Disable it with [showUpdateUi] = `false` or by wrapping
|
|
796
|
+
/// the app in [FlutterOtaApp(showUpdateUi: false)].
|
|
797
|
+
///
|
|
798
|
+
/// A developer-supplied [onProgress] callback, if provided, is still invoked
|
|
799
|
+
/// alongside the overlay.
|
|
800
|
+
///
|
|
801
|
+
/// Returns the [PatchApplyResult] from [applyPatch]. When there is nothing to
|
|
802
|
+
/// apply (`result.patch == null`) it returns a no-op failure result.
|
|
803
|
+
static Future<PatchApplyResult> applyUpdate(
|
|
804
|
+
ServerUpdateResult result, {
|
|
805
|
+
void Function(PatchApplyProgress)? onProgress,
|
|
806
|
+
}) async {
|
|
807
|
+
if (!result.hasUpdate || result.patch == null) {
|
|
808
|
+
_log('applyUpdate: no patch to apply');
|
|
809
|
+
return PatchApplyResult.failure(
|
|
810
|
+
PatchApplyError.unknown,
|
|
811
|
+
'no update available',
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
final showOverlay = showUpdateUi && result.shouldForceUpdate;
|
|
816
|
+
final fromVersion = await currentVersion;
|
|
817
|
+
final patchHash = result.patch!.md5;
|
|
818
|
+
final shortHash =
|
|
819
|
+
patchHash.isNotEmpty ? patchHash.substring(0, patchHash.length.clamp(0, 8)) : null;
|
|
820
|
+
|
|
821
|
+
// Extract channel/platform from whichever config is active
|
|
822
|
+
String? channel;
|
|
823
|
+
String? platform;
|
|
824
|
+
if (_supabaseConfig != null) {
|
|
825
|
+
channel = _supabaseConfig!.channel;
|
|
826
|
+
platform = _supabaseConfig!.platform.name;
|
|
827
|
+
} else if (_postgresConfig != null) {
|
|
828
|
+
channel = _postgresConfig!.channel;
|
|
829
|
+
platform = _postgresConfig!.platform.name;
|
|
830
|
+
} else if (_cloudflareConfig != null) {
|
|
831
|
+
channel = _cloudflareConfig!.channel;
|
|
832
|
+
platform = _cloudflareConfig!.platform.name;
|
|
833
|
+
} else if (_awsConfig != null) {
|
|
834
|
+
channel = _awsConfig!.channel;
|
|
835
|
+
platform = _awsConfig!.platform.name;
|
|
836
|
+
} else if (_pocketbaseConfig != null) {
|
|
837
|
+
channel = _pocketbaseConfig!.channel;
|
|
838
|
+
platform = _pocketbaseConfig!.platform.name;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
final handle = showOverlay
|
|
842
|
+
? OtaOverlayManager.instance.begin(
|
|
843
|
+
message: result.message,
|
|
844
|
+
targetVersion: result.id,
|
|
845
|
+
currentVersion: fromVersion,
|
|
846
|
+
bundleHash: shortHash,
|
|
847
|
+
gitCommit: result.gitCommitHash,
|
|
848
|
+
channel: channel,
|
|
849
|
+
platform: platform,
|
|
850
|
+
)
|
|
851
|
+
: null;
|
|
852
|
+
|
|
853
|
+
final applied = await applyPatch(
|
|
854
|
+
result.patch!,
|
|
855
|
+
onProgress: (p) {
|
|
856
|
+
handle?.update(p);
|
|
857
|
+
onProgress?.call(p);
|
|
858
|
+
},
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
if (!applied.ok) {
|
|
862
|
+
// Only blacklist on *deterministic* failures — a bad payload that will
|
|
863
|
+
// fail identically no matter how many times we retry. Transient failures
|
|
864
|
+
// (network drop, timeout, low disk, unclassified) must NOT be blacklisted:
|
|
865
|
+
// doing so would permanently pin the device off a perfectly good patch
|
|
866
|
+
// after a single flaky download, and it would never be retried. Those are
|
|
867
|
+
// simply retried on the next `checkAndApplyUpdates`.
|
|
868
|
+
final error = applied.error;
|
|
869
|
+
const deterministic = {
|
|
870
|
+
PatchApplyError.md5Mismatch,
|
|
871
|
+
PatchApplyError.signatureInvalid,
|
|
872
|
+
PatchApplyError.assetPackageInvalid,
|
|
873
|
+
PatchApplyError.unsupportedAbi,
|
|
874
|
+
PatchApplyError.invalidArgs,
|
|
875
|
+
};
|
|
876
|
+
final patchVersion = result.patch!.version;
|
|
877
|
+
final patchMd5 = result.patch!.md5;
|
|
878
|
+
if (error != null &&
|
|
879
|
+
deterministic.contains(error) &&
|
|
880
|
+
patchVersion.isNotEmpty) {
|
|
881
|
+
_log('applyUpdate: patch failed (${error.name}), blacklisting $patchVersion');
|
|
882
|
+
try {
|
|
883
|
+
await PatcherChannel.reportApplyFailure(
|
|
884
|
+
version: patchVersion,
|
|
885
|
+
md5: patchMd5,
|
|
886
|
+
reason: error.name,
|
|
887
|
+
);
|
|
888
|
+
} catch (e, s) {
|
|
889
|
+
_log('applyUpdate: failed to blacklist $patchVersion: $e', s);
|
|
890
|
+
}
|
|
891
|
+
} else {
|
|
892
|
+
_log('applyUpdate: patch failed (${error?.name}); transient, not '
|
|
893
|
+
'blacklisting (will retry next check)');
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (applied.ok && result.shouldForceUpdate) {
|
|
898
|
+
handle?.end();
|
|
899
|
+
// Wait for overlay to finish its minimum display time + success dwell
|
|
900
|
+
await Future.delayed(const Duration(seconds: 3));
|
|
901
|
+
await restart();
|
|
902
|
+
} else {
|
|
903
|
+
handle?.end(hasError: !applied.ok, errorText: applied.message);
|
|
904
|
+
}
|
|
905
|
+
return applied;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/// Checks for an update and applies it, auto-restarting when forced.
|
|
909
|
+
///
|
|
910
|
+
/// Call this once after [init] (or pass `autoApplyUpdates: true` to [init])
|
|
911
|
+
/// to implement "forced updates apply and restart with zero user taps".
|
|
912
|
+
///
|
|
913
|
+
/// The update is skipped when the returned patch version already equals the
|
|
914
|
+
/// currently installed patch version ([currentVersion]). This prevents
|
|
915
|
+
/// re-applying — and therefore re-restarting — on every launch once the
|
|
916
|
+
/// device is already on the latest bundle.
|
|
917
|
+
///
|
|
918
|
+
/// Failures are swallowed and logged so a broken backend can never crash boot.
|
|
919
|
+
static Future<PatchApplyResult?> checkAndApplyUpdates({
|
|
920
|
+
void Function(PatchApplyProgress)? onProgress,
|
|
921
|
+
}) async {
|
|
922
|
+
if (_notAndroidGuard('checkAndApplyUpdates')) return null;
|
|
923
|
+
try {
|
|
924
|
+
final result = await checkForUpdate();
|
|
925
|
+
|
|
926
|
+
// Server signaled rollback (current bundle disabled) -> roll back locally
|
|
927
|
+
if (result.status == AppUpdateStatus.rollback) {
|
|
928
|
+
_log('Server signaled rollback for ${result.id}, rolling back locally');
|
|
929
|
+
final outcome = await rollbackToPrevious();
|
|
930
|
+
if (outcome != RollbackOutcome.fallbackToBase) {
|
|
931
|
+
// Show toast on forced-update overlay if visible
|
|
932
|
+
if (showUpdateUi) {
|
|
933
|
+
OtaOverlayManager.instance.showRollbackToast(
|
|
934
|
+
message: 'Update reverted — using previous version',
|
|
935
|
+
previousVersion: outcome == RollbackOutcome.success ? 'previous' : 'base',
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
await restart();
|
|
939
|
+
}
|
|
940
|
+
return null;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (!result.hasUpdate || result.patch == null) {
|
|
944
|
+
_log('checkAndApplyUpdates: no update');
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
final current = await currentVersion;
|
|
948
|
+
if (current != null && current == result.patch!.version) {
|
|
949
|
+
_log('checkAndApplyUpdates: already on ${result.patch!.version}, skip');
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
// Skip if this version was previously blacklisted after a failed apply.
|
|
953
|
+
final patchVersion = result.patch!.version;
|
|
954
|
+
final patchMd5 = result.patch!.md5;
|
|
955
|
+
try {
|
|
956
|
+
if (await PatcherChannel.isVersionBlacklisted(patchVersion, md5: patchMd5)) {
|
|
957
|
+
_log('checkAndApplyUpdates: $patchVersion is blacklisted, skip');
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
} catch (e, s) {
|
|
961
|
+
_log('checkAndApplyUpdates: blacklist check failed: $e', s);
|
|
962
|
+
}
|
|
963
|
+
return await applyUpdate(result, onProgress: onProgress);
|
|
964
|
+
} catch (e, s) {
|
|
965
|
+
_log('checkAndApplyUpdates failed: $e', s);
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/// Current host APK versionCode. Returns null on failure/non-Android.
|
|
971
|
+
static Future<int?> get appVersionCode async {
|
|
972
|
+
if (_notAndroidGuard('appVersionCode')) return null;
|
|
973
|
+
try {
|
|
974
|
+
return await PatcherChannel.appVersionCode();
|
|
975
|
+
} catch (_) {
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/// Current device ABI. Useful when your backend routes patches per ABI.
|
|
981
|
+
static Future<String> get deviceAbi async {
|
|
982
|
+
if (_notAndroidGuard('deviceAbi')) return '';
|
|
983
|
+
try {
|
|
984
|
+
return await PatcherChannel.deviceAbi() ?? '';
|
|
985
|
+
} catch (_) {
|
|
986
|
+
return '';
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/// Patch version currently installed on disk.
|
|
991
|
+
///
|
|
992
|
+
/// A successful `applyPatch` updates this immediately, but the patch is only
|
|
993
|
+
/// loaded by Flutter after the next cold start. Returns null when no patch is
|
|
994
|
+
/// installed.
|
|
995
|
+
static Future<String?> get currentVersion async {
|
|
996
|
+
if (_notAndroidGuard('currentVersion')) return null;
|
|
997
|
+
try {
|
|
998
|
+
final v = await PatcherChannel.currentVersion();
|
|
999
|
+
if (v == null || v.isEmpty) return null;
|
|
1000
|
+
return v;
|
|
1001
|
+
} catch (_) {
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/// Local bad-patch blacklist, ordered from oldest to newest.
|
|
1007
|
+
///
|
|
1008
|
+
/// Automatic blacklist triggers include early boot failures, cold-start md5
|
|
1009
|
+
/// mismatches, and cold-start signature failures. For asset patches, the md5
|
|
1010
|
+
/// is the `patch.zip` payload md5.
|
|
1011
|
+
static Future<List<BlacklistEntry>> get blacklist async {
|
|
1012
|
+
if (_notAndroidGuard('blacklist')) return const [];
|
|
1013
|
+
try {
|
|
1014
|
+
final raw = await PatcherChannel.blacklist();
|
|
1015
|
+
if (raw == null) return const [];
|
|
1016
|
+
return raw
|
|
1017
|
+
.whereType<Map>()
|
|
1018
|
+
.map((m) => BlacklistEntry.fromNative(m))
|
|
1019
|
+
.toList(growable: false);
|
|
1020
|
+
} catch (e, s) {
|
|
1021
|
+
_log('blacklist failed: $e', s);
|
|
1022
|
+
return const [];
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/// Clears the local bad-patch blacklist.
|
|
1027
|
+
///
|
|
1028
|
+
/// Intended for tests, local debugging, or deliberate operational recovery.
|
|
1029
|
+
static Future<void> clearBlacklist() async {
|
|
1030
|
+
if (_notAndroidGuard('clearBlacklist')) return;
|
|
1031
|
+
try {
|
|
1032
|
+
await PatcherChannel.clearBlacklist();
|
|
1033
|
+
} catch (e, s) {
|
|
1034
|
+
_log('clearBlacklist failed: $e', s);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/// Checks whether a patch version is in the local blacklist.
|
|
1039
|
+
///
|
|
1040
|
+
/// When [md5] is provided, the match uses both version and md5. Without md5,
|
|
1041
|
+
/// any entry with the same version is considered a match.
|
|
1042
|
+
static Future<bool> isVersionBlacklisted(String version, {String md5 = ''}) async {
|
|
1043
|
+
if (_notAndroidGuard('isVersionBlacklisted')) return false;
|
|
1044
|
+
try {
|
|
1045
|
+
return await PatcherChannel.isVersionBlacklisted(version, md5: md5);
|
|
1046
|
+
} catch (e, s) {
|
|
1047
|
+
_log('isVersionBlacklisted failed: $e', s);
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/// Last cold-start patch loading diagnostic.
|
|
1053
|
+
///
|
|
1054
|
+
/// `applyPatch` reports install-time failures. This getter reports what
|
|
1055
|
+
/// happened on the next cold start: version mismatch, md5/signature failure,
|
|
1056
|
+
/// crash rollback, loader hook failure, or successful patch load.
|
|
1057
|
+
static Future<PatchBootDiagnostic?> get lastBootDiagnostic async {
|
|
1058
|
+
if (_notAndroidGuard('lastBootDiagnostic')) return null;
|
|
1059
|
+
try {
|
|
1060
|
+
final raw = await PatcherChannel.lastBootDiagnostic();
|
|
1061
|
+
if (raw == null) return null;
|
|
1062
|
+
return PatchBootDiagnostic.fromNative(raw);
|
|
1063
|
+
} catch (e, s) {
|
|
1064
|
+
_log('lastBootDiagnostic failed: $e', s);
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
static Duration _verifyAfter = const Duration(seconds: 5);
|
|
1070
|
+
|
|
1071
|
+
static void _installBootErrorCatchers() {
|
|
1072
|
+
final priorPlatformHandler = PlatformDispatcher.instance.onError;
|
|
1073
|
+
PlatformDispatcher.instance.onError = (error, stack) {
|
|
1074
|
+
_maybeReportBootError(error, stack);
|
|
1075
|
+
return priorPlatformHandler?.call(error, stack) ?? false;
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
final priorFlutterHandler = FlutterError.onError;
|
|
1079
|
+
FlutterError.onError = (details) {
|
|
1080
|
+
_maybeReportBootError(details.exception, details.stack);
|
|
1081
|
+
(priorFlutterHandler ?? FlutterError.presentError).call(details);
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
static void _maybeReportBootError(Object error, StackTrace? stack) {
|
|
1086
|
+
if (!_dartHookActive) return;
|
|
1087
|
+
if (_bootErrorReported) return;
|
|
1088
|
+
_bootErrorReported = true;
|
|
1089
|
+
final msg = error.toString();
|
|
1090
|
+
_log('boot-phase Dart error captured: $msg', stack);
|
|
1091
|
+
PatcherChannel.reportDartBootError(msg).catchError((e) {
|
|
1092
|
+
_log('reportDartBootError channel call failed: $e');
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
static void _log(String msg, [StackTrace? stack]) {
|
|
1097
|
+
if (kDebugMode) {
|
|
1098
|
+
debugPrint('[FlutterPatcher] $msg');
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/// Plugin-wide exception. Wraps network/parsing errors from [FlutterPatcher.checkUpdate].
|
|
1104
|
+
class PatcherException implements Exception {
|
|
1105
|
+
final String message;
|
|
1106
|
+
PatcherException(this.message);
|
|
1107
|
+
@override
|
|
1108
|
+
String toString() => 'PatcherException: $message';
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
class _BootVerifier with WidgetsBindingObserver {
|
|
1112
|
+
static _BootVerifier? _instance;
|
|
1113
|
+
|
|
1114
|
+
Duration _foregroundElapsed = Duration.zero;
|
|
1115
|
+
DateTime? _resumedAt;
|
|
1116
|
+
Timer? _timer;
|
|
1117
|
+
bool _windowClosed = false;
|
|
1118
|
+
|
|
1119
|
+
static void start() {
|
|
1120
|
+
if (_instance != null) return;
|
|
1121
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
1122
|
+
_instance ??= _BootVerifier().._begin();
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
void _begin() {
|
|
1127
|
+
FlutterPatcher.reportBootSuccess();
|
|
1128
|
+
|
|
1129
|
+
final binding = WidgetsBinding.instance;
|
|
1130
|
+
binding.addObserver(this);
|
|
1131
|
+
final state = binding.lifecycleState;
|
|
1132
|
+
if (state == null || state == AppLifecycleState.resumed) {
|
|
1133
|
+
_resumedAt = DateTime.now();
|
|
1134
|
+
_scheduleCheck();
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
@override
|
|
1139
|
+
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
1140
|
+
if (_windowClosed) return;
|
|
1141
|
+
if (state == AppLifecycleState.resumed) {
|
|
1142
|
+
_resumedAt = DateTime.now();
|
|
1143
|
+
_scheduleCheck();
|
|
1144
|
+
} else {
|
|
1145
|
+
if (_resumedAt != null) {
|
|
1146
|
+
_foregroundElapsed += DateTime.now().difference(_resumedAt!);
|
|
1147
|
+
_resumedAt = null;
|
|
1148
|
+
}
|
|
1149
|
+
_timer?.cancel();
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
void _scheduleCheck() {
|
|
1154
|
+
final remaining = FlutterPatcher._verifyAfter - _foregroundElapsed;
|
|
1155
|
+
_timer?.cancel();
|
|
1156
|
+
if (remaining <= Duration.zero) {
|
|
1157
|
+
_closeHookWindow();
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
_timer = Timer(remaining, _closeHookWindow);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
void _closeHookWindow() {
|
|
1164
|
+
if (_windowClosed) return;
|
|
1165
|
+
_windowClosed = true;
|
|
1166
|
+
WidgetsBinding.instance.removeObserver(this);
|
|
1167
|
+
_timer?.cancel();
|
|
1168
|
+
FlutterPatcher._dartHookActive = false;
|
|
1169
|
+
}
|
|
1170
|
+
}
|