diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 000000000..b987073ac --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.29.3" +} \ No newline at end of file diff --git a/.github/browserstack-devices.yml b/.github/browserstack-devices.yml index cf5a53643..9b13f4584 100644 --- a/.github/browserstack-devices.yml +++ b/.github/browserstack-devices.yml @@ -43,7 +43,7 @@ flutter: - "Google Pixel 6-12.0" # Android 12 support ios: devices: - - "iPhone 13-15" # iOS 15 support + - "iPhone 13-16" # iOS 16 support (min deployment target is 15.6) - "iPhone 14-16" # iOS 16 current - "iPhone 12-17" # iOS 17 latest diff --git a/.gitignore b/.gitignore index 9bff26155..4e5401a23 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ packages/ Pods/ target/ xcuserdata + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/flutter_app/README.md b/flutter_app/README.md index b67691eca..3322c9948 100644 --- a/flutter_app/README.md +++ b/flutter_app/README.md @@ -7,7 +7,7 @@ This is a basic task application that demonstrates how to use Ditto's peer-to-pe ## Prerequisites - Dart SDK installed -- Flutter SDK installed (tested on 3.24) +- Flutter SDK installed (tested on 3.29) - Java Virtual Machine (JVM) 11 installed - Git command line installed (Windows requirement) - XCode installed (for iOS development) @@ -15,6 +15,14 @@ This is a basic task application that demonstrates how to use Ditto's peer-to-pe - Android SDK installed - IDE of choice (Android Studio, VS Code, etc) +### MacOS Development +This has been tested with XCode 26.2 on MacOS 26.6 with Flutter 3.29.3. + +### Windows Development +To build the Windows version of this Flutter app requires Visual Studio 2022 specifically be +installed and configured with C++ and cmake installed from the Visual Studio Installer. This has +been tested with Flutter version 3.29.3 on Windows. + ## Getting Started ### 1. Clone the Repository @@ -78,7 +86,7 @@ Please choose one (or "q" to quit): > If you are going to use a physical iPhone, you will need to update the Team under Signing & Capabilities in XCode. You can open the ios/Runner.xcodeproj file in XCode and then set your team from the Runner Target -> Signing & Capabilities tab. > -- Ensure that cocoapods is up to date +- Ensure that cocoapods is up to date (or you can use Homebrew with `brew install cocoapods`) ```bash gem install cocoapods diff --git a/flutter_app/integration_test/app_test.dart b/flutter_app/integration_test/app_test.dart index ae42476ad..4adacedf6 100644 --- a/flutter_app/integration_test/app_test.dart +++ b/flutter_app/integration_test/app_test.dart @@ -17,21 +17,15 @@ void main() { (WidgetTester tester) async { // Initialize app await app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); + // Allow up to 10 seconds for Ditto to initialise and the first sync + // exchange to complete. pumpAndSettle returns as soon as the UI is + // idle, so on fast devices this resolves much sooner. + await tester.pumpAndSettle(const Duration(seconds: 10)); - // Tap "OK" button if Bluetooth permission dialog appears (iOS) - final okButton = find.text('OK'); - if (okButton.evaluate().isNotEmpty) { - await tester.tap(okButton); - await tester.pumpAndSettle(const Duration(seconds: 2)); - } - - // Tap "Allow" button if local network permission dialog appears (iOS) - final allowButton = find.text('Allow'); - if (allowButton.evaluate().isNotEmpty) { - await tester.tap(allowButton); - await tester.pumpAndSettle(const Duration(seconds: 2)); - } + // NOTE: iOS system permission dialogs (Bluetooth, Local Network) are + // native UIAlertControllers and cannot be found or tapped via Flutter's + // widget-tree finders. They are handled at the XCTest layer in + // ios/RunnerTests/RunnerTests.m via addUIInterruptionMonitor. // Verify app title is present expect(find.text('Ditto Tasks'), findsOneWidget); @@ -45,11 +39,9 @@ void main() { // Verify clear button is present expect(find.byIcon(Icons.clear), findsOneWidget); - // Wait for sync to complete - await Future.delayed(const Duration(seconds: 5)); - await tester.pumpAndSettle(); - - // Look for the test document that should be synced from Ditto cloud + // Look for the test document that should be synced from Ditto cloud. + // The playground can accumulate many documents from previous CI runs, + // so we poll rather than waiting a fixed amount of time. const testTitle = String.fromEnvironment('TASK_TO_FIND'); if (testTitle.isEmpty) { @@ -57,9 +49,23 @@ void main() { 'Build with: --dart-define=TASK_TO_FIND='); } - expect(find.text(testTitle), findsOneWidget, + // Poll every 500 ms for up to 45 seconds to give the cloud sync + // enough time to deliver and write all documents to the local store. + const syncTimeout = Duration(seconds: 45); + final deadline = DateTime.now().add(syncTimeout); + bool taskFound = false; + + while (DateTime.now().isBefore(deadline)) { + await tester.pump(const Duration(milliseconds: 500)); + if (find.text(testTitle).evaluate().isNotEmpty) { + taskFound = true; + break; + } + } + + expect(taskFound, isTrue, reason: - 'Should find test document with title: $testTitle synced from Ditto cloud'); + 'Should find test document with title: $testTitle synced from Ditto cloud within ${syncTimeout.inSeconds}s'); }); }); } diff --git a/flutter_app/ios/Podfile.lock b/flutter_app/ios/Podfile.lock index 5c8331588..38ef90e37 100644 --- a/flutter_app/ios/Podfile.lock +++ b/flutter_app/ios/Podfile.lock @@ -1,8 +1,8 @@ PODS: - - ditto_live (4.13.1): - - DittoFlutter (= 4.13.1) + - ditto_live (5.0.0-rc.1): + - DittoFlutter (= 5.0.0-rc.1) - Flutter - - DittoFlutter (4.13.1) + - DittoFlutter (5.0.0-rc.1) - Flutter (1.0.0) - integration_test (0.0.1): - Flutter @@ -36,12 +36,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" SPEC CHECKSUMS: - ditto_live: 93459c7d7c067ba16d4104925ea80c54dd13bf67 - DittoFlutter: 26e21d5665e9bcc11660c4eceb1ec66b8ba64667 + ditto_live: 8536ab4af437fb23e15fdbef9c59bfa6abb95fc5 + DittoFlutter: 2d2269478637f4e8bda53def335bd1a728d5ddf0 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d PODFILE CHECKSUM: 1959d098c91d8a792531a723c4a9d7e9f6a01e38 diff --git a/flutter_app/ios/Runner.xcodeproj/project.pbxproj b/flutter_app/ios/Runner.xcodeproj/project.pbxproj index 8ebdad35c..1345e7d89 100644 --- a/flutter_app/ios/Runner.xcodeproj/project.pbxproj +++ b/flutter_app/ios/Runner.xcodeproj/project.pbxproj @@ -494,6 +494,7 @@ DEVELOPMENT_TEAM = 3T2VMFZPPQ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -685,6 +686,7 @@ DEVELOPMENT_TEAM = 3T2VMFZPPQ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -710,6 +712,7 @@ DEVELOPMENT_TEAM = 3T2VMFZPPQ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/flutter_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1728d1042..15cada483 100644 --- a/flutter_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/flutter_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -51,7 +51,7 @@ Uses WiFi to connect and sync with nearby devices. NSBonjourServices - _http-alt._tcp. + _http-alt._tcp. + + + UIBackgroundModes + + bluetooth-central + bluetooth-peripheral diff --git a/flutter_app/ios/RunnerTests/RunnerTests.m b/flutter_app/ios/RunnerTests/RunnerTests.m index 73977793b..37c04433a 100644 --- a/flutter_app/ios/RunnerTests/RunnerTests.m +++ b/flutter_app/ios/RunnerTests/RunnerTests.m @@ -1,3 +1,46 @@ @import XCTest; - @import integration_test; - INTEGRATION_TEST_IOS_RUNNER(RunnerTests) +@import integration_test; + +// This file replaces the one-liner INTEGRATION_TEST_IOS_RUNNER macro so we +// can add a UIInterruptionMonitor. Without it, native iOS system permission +// dialogs (Bluetooth, Local Network, etc.) block the test and can never be +// dismissed by Flutter's find.text() which only searches Flutter widgets. +// +// The interruption monitor fires whenever a springboard-level alert appears +// during the test and automatically taps the first "accept" button it finds. + +@interface RunnerTests : XCTestCase +@end + +@implementation RunnerTests + ++ (XCTestSuite *)defaultTestSuite { + return [IntegrationTestIosRunner defaultTestSuiteForIntegrationTestRunner:self]; +} + +- (void)setUp { + [super setUp]; + + // Automatically accept system permission dialogs so that Ditto's + // Bluetooth and Local Network transports can initialise during tests. + // "OK" – Bluetooth usage alert + // "Allow" / "Allow While Using App" – Local Network usage alert + [self addUIInterruptionMonitorWithDescription:@"System Permission Alert" + handler:^BOOL(XCUIElement *alert) { + NSArray *acceptLabels = @[ + @"OK", + @"Allow", + @"Allow While Using App" + ]; + for (NSString *label in acceptLabels) { + XCUIElement *button = alert.buttons[label]; + if (button.exists) { + [button tap]; + return YES; + } + } + return NO; + }]; +} + +@end diff --git a/flutter_app/lib/main.dart b/flutter_app/lib/main.dart index 4f2b1b8dd..d6c07023c 100644 --- a/flutter_app/lib/main.dart +++ b/flutter_app/lib/main.dart @@ -29,7 +29,8 @@ class _DittoExampleState extends State { dotenv.env['DITTO_APP_ID'] ?? (throw Exception("env not found")); final token = dotenv.env['DITTO_PLAYGROUND_TOKEN'] ?? (throw Exception("env not found")); - final authUrl = dotenv.env['DITTO_AUTH_URL']; + final authUrl = + dotenv.env['DITTO_AUTH_URL'] ?? (throw Exception("env not found")); final websocketUrl = dotenv.env['DITTO_WEBSOCKET_URL'] ?? (throw Exception("env not found")); @@ -70,26 +71,31 @@ class _DittoExampleState extends State { await Ditto.init(); - final identity = OnlinePlaygroundIdentity( - appID: appID, - token: token, - enableDittoCloudSync: - false, // This is required to be set to false to use the correct URLs - customAuthUrl: authUrl); - - final ditto = await Ditto.open(identity: identity); - - ditto.updateTransportConfig((config) { - // Note: this will not enable peer-to-peer sync on the web platform - config.setAllPeerToPeerEnabled(true); - config.connect.webSocketUrls.add(websocketUrl); + DittoLogger.isEnabled = true; + DittoLogger.minimumLogLevel = LogLevel.debug; + + //new configuration - https://docs.ditto.live/sdk/latest/ditto-config + final config = DittoConfig( + databaseID: appID, connect: DittoConfigConnectServer(url: authUrl)); + final ditto = await Ditto.open(config); + await ditto.auth.setExpirationHandler((ditto, timeUntilExpiration) async { + final authResult = await ditto.auth + .login(token: token, provider: Authenticator.developmentProvider); + if (authResult.exception != null) { + throw authResult.exception!; + } }); - // Disable DQL strict mode - // https://docs.ditto.live/dql/strict-mode - await ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false"); + // Register the tasks subscription before starting sync so it is + // included in the very first sync exchange with the cloud. + // Without this, the subscription is registered later when the + // DqlBuilder widget builds, causing a 5–10 second delay on the + // first sync cycle. + ditto.sync.registerSubscription( + "SELECT * FROM tasks WHERE deleted = false", + ); - ditto.startSync(); + ditto.sync.start(); if (mounted) { setState(() => _ditto = ditto); @@ -167,19 +173,20 @@ class _DittoExampleState extends State { Widget get _syncTile => SwitchListTile( title: const Text("Sync Active"), - value: _ditto!.isSyncActive, + value: _ditto!.sync.isActive, onChanged: (value) { if (value) { - setState(() => _ditto!.startSync()); + setState(() => _ditto!.sync.start()); } else { - setState(() => _ditto!.stopSync()); + setState(() => _ditto!.sync.stop()); } }, ); + //TODO review to see if we want to add in the order by title asc back in by making the dql builder use two queries. Widget get _tasksList => DqlBuilder( ditto: _ditto!, - query: "SELECT * FROM tasks WHERE deleted = false ORDER BY title ASC", + query: "SELECT * FROM tasks WHERE deleted = false", builder: (context, result) { final tasks = result.items.map((r) => r.value).map(Task.fromJson); return ListView( diff --git a/flutter_app/macos/Podfile.lock b/flutter_app/macos/Podfile.lock index ffbf53676..fd3dc80e0 100644 --- a/flutter_app/macos/Podfile.lock +++ b/flutter_app/macos/Podfile.lock @@ -1,8 +1,8 @@ PODS: - - ditto_live (4.13.1): - - DittoFlutter (= 4.13.1) + - ditto_live (5.0.0-rc.1): + - DittoFlutter (= 5.0.0-rc.1) - FlutterMacOS - - DittoFlutter (4.13.1) + - DittoFlutter (5.0.0-rc.1) - FlutterMacOS (1.0.0) - path_provider_foundation (0.0.1): - Flutter @@ -26,10 +26,10 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin SPEC CHECKSUMS: - ditto_live: a46b3eba63227c95adffe6094d3153a74c060d0c - DittoFlutter: 26e21d5665e9bcc11660c4eceb1ec66b8ba64667 + ditto_live: 5e30d1209a9f21d05167aa6da499e3b5ceccb1be + DittoFlutter: 2d2269478637f4e8bda53def335bd1a728d5ddf0 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82 diff --git a/flutter_app/macos/Runner/DebugProfile.entitlements b/flutter_app/macos/Runner/DebugProfile.entitlements index dddb8a30c..c946719a1 100644 --- a/flutter_app/macos/Runner/DebugProfile.entitlements +++ b/flutter_app/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.network.client + diff --git a/flutter_app/pubspec.lock b/flutter_app/pubspec.lock index 563838639..e8ec28627 100644 --- a/flutter_app/pubspec.lock +++ b/flutter_app/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.12.0" boolean_selector: dependency: transitive description: @@ -69,26 +69,26 @@ packages: dependency: "direct main" description: name: ditto_live - sha256: f095d52ec464e0c50323a4c37717a4e4727c00c035be8e11c0120b5bac927103 + sha256: "5e022a2687dc1d38c8e9173cedca536734db7da6daef296f0cb5987e44256c43" url: "https://pub.dev" source: hosted - version: "4.13.1" + version: "5.0.0-rc.1" equatable: dependency: "direct main" description: name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.8" fake_async: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: dependency: transitive description: @@ -114,10 +114,10 @@ packages: dependency: "direct main" description: name: flutter_dotenv - sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b + sha256: d4130c4a43e0b13fefc593bc3961f2cb46e30cb79e253d4a526b1b5d24ae1ce4 url: "https://pub.dev" source: hosted - version: "5.2.1" + version: "6.0.0" flutter_driver: dependency: transitive description: flutter @@ -179,10 +179,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: @@ -299,18 +299,18 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 url: "https://pub.dev" source: hosted - version: "11.4.0" + version: "12.0.1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "13.0.1" permission_handler_apple: dependency: transitive description: @@ -448,10 +448,10 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "14.3.1" web: dependency: transitive description: @@ -464,10 +464,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.0.4" xdg_directories: dependency: transitive description: diff --git a/flutter_app/pubspec.yaml b/flutter_app/pubspec.yaml index 0f2d5011e..68d7624c3 100644 --- a/flutter_app/pubspec.yaml +++ b/flutter_app/pubspec.yaml @@ -32,15 +32,15 @@ dependencies: flutter: sdk: flutter - ditto_live: 4.13.1 + ditto_live: 5.0.0-rc.1 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 equatable: ^2.0.5 - permission_handler: ^11.3.1 + permission_handler: ^12.0.1 json_annotation: ^4.9.0 - flutter_dotenv: ^5.1.0 + flutter_dotenv: ^6.0.0 dev_dependencies: flutter_test: diff --git a/flutter_app/test/widget_test.dart b/flutter_app/test/widget_test.dart index 6b21c2c5d..da5f0cb77 100644 --- a/flutter_app/test/widget_test.dart +++ b/flutter_app/test/widget_test.dart @@ -14,12 +14,16 @@ import 'package:flutter_quickstart/main.dart'; void main() { setUpAll(() async { // Initialize dotenv for testing - dotenv.testLoad(fileInput: ''' -DITTO_APP_ID=test_app_id -DITTO_PLAYGROUND_TOKEN=test_playground_token -DITTO_AUTH_URL=https://auth.example.com -DITTO_WEBSOCKET_URL=wss://websocket.example.com -'''); + await dotenv.load( + fileName: '.env', + mergeWith: { + 'DITTO_APP_ID': 'test_app_id', + 'DITTO_PLAYGROUND_TOKEN': 'test_playground_token', + 'DITTO_AUTH_URL': 'https://auth.example.com', + 'DITTO_WEBSOCKET_URL': 'wss://websocket.example.com', + }, + isOptional: true, + ); }); testWidgets('Counter increments smoke test', (WidgetTester tester) async { diff --git a/flutter_app/windows/CMakeLists.txt b/flutter_app/windows/CMakeLists.txt index 6c87c1c44..8943541fe 100644 --- a/flutter_app/windows/CMakeLists.txt +++ b/flutter_app/windows/CMakeLists.txt @@ -39,9 +39,9 @@ add_definitions(-DUNICODE -D_UNICODE) # of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) - target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /W5 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) - target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=1") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() @@ -64,7 +64,7 @@ include(flutter/generated_plugins.cmake) # so that building and running from within Visual Studio will work. set(BUILD_BUNDLE_DIR "$") # Make the "install" step default, as it's required to run. -set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 2) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif()