Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .fvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"flutter": "3.29.3"
}
2 changes: 1 addition & 1 deletion .github/browserstack-devices.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ packages/
Pods/
target/
xcuserdata

# FVM Version Cache
.fvm/
12 changes: 10 additions & 2 deletions flutter_app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,22 @@ 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)
- Android Studio installed (for managing the Android SDK)
- 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
Expand Down Expand Up @@ -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
Expand Down
48 changes: 27 additions & 21 deletions flutter_app/integration_test/app_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -45,21 +39,33 @@ 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) {
throw Exception('TASK_TO_FIND environment variable must be set. '
'Build with: --dart-define=TASK_TO_FIND=<task_title>');
}

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');
});
});
}
16 changes: 8 additions & 8 deletions flutter_app/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions flutter_app/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
Expand Down
8 changes: 7 additions & 1 deletion flutter_app/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@
<string>Uses WiFi to connect and sync with nearby devices.</string>
<key>NSBonjourServices</key>
<array>
<string>_http-alt._tcp.</string>
<string>_http-alt._tcp.</string>
</array>
<!-- Required by the Ditto SDK for BLE sync to operate in the background -->
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
</array>
</dict>
</plist>
47 changes: 45 additions & 2 deletions flutter_app/ios/RunnerTests/RunnerTests.m
Original file line number Diff line number Diff line change
@@ -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<NSString *> *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
51 changes: 29 additions & 22 deletions flutter_app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ class _DittoExampleState extends State<DittoExample> {
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"));

Expand Down Expand Up @@ -70,26 +71,31 @@ class _DittoExampleState extends State<DittoExample> {

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);
Expand Down Expand Up @@ -167,19 +173,20 @@ class _DittoExampleState extends State<DittoExample> {

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(
Expand Down
12 changes: 6 additions & 6 deletions flutter_app/macos/Podfile.lock
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down
2 changes: 2 additions & 0 deletions flutter_app/macos/Runner/DebugProfile.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
Loading
Loading