Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
9 changes: 8 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,11 @@
DITTO_APP_ID=""
DITTO_PLAYGROUND_TOKEN=""
DITTO_AUTH_URL=""
DITTO_WEBSOCKET_URL=""
DITTO_WEBSOCKET_URL=""

# Optional: offline-only license token from Ditto support.
# When set (non-empty after trimming whitespace), the quickstart app initializes
# in offline-only mode (peer-to-peer only, no Big Peer / cloud sync) and ignores
# the playground/auth/websocket variables above. Leave empty for the default
# Online Playground mode. Request a token by contacting support@ditto.com.
DITTO_OFFLINE_LICENSE_TOKEN=""
14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ Before working with any app:
- `DITTO_PLAYGROUND_TOKEN`
- `DITTO_AUTH_URL`
- `DITTO_WEBSOCKET_URL`
- Optional: `DITTO_OFFLINE_LICENSE_TOKEN` — when non-empty (after trim), the
app switches to offline-only mode and the playground/auth/websocket vars
above are not used.

## Identity-mode selection

Every quickstart reads `DITTO_OFFLINE_LICENSE_TOKEN` from `.env`. If the value
is non-empty after trimming whitespace, the app initializes Ditto in offline-only
mode using the SDK's offline identity (v4: `DittoIdentity.OfflinePlayground`;
v5: `DittoConfig.Connect.SmallPeersOnly`) and calls
`setOfflineOnlyLicenseToken` with the trimmed value. Otherwise it falls back to
the Online Playground identity. In v4 apps, `disableSyncWithV3()` and the DQL
strict-mode `ALTER SYSTEM` call are kept in both branches — they are
store-level and apply regardless of identity.

## Common Development Commands

Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ To obtain your Ditto identity and configure the quickstart apps with it, follow
- in a macOS Finder window, press `⇧⌘.` (SHIFT+CMD+period) to show hidden files.
1. Save your App ID, Online Playground Token, Auth URL, and WebSocket URL in the `.env` file.

## Offline-only mode (optional)

The quickstart apps can also run in offline-only mode, where peers sync directly
with each other over Bluetooth/LAN/etc. and do not connect to Ditto's cloud.
This requires an offline-only license token, which you can request by contacting
<support@ditto.com>.

To run in offline mode, set `DITTO_OFFLINE_LICENSE_TOKEN` in your `.env` file.
When this variable is non-empty, the app initializes in offline-only mode and
ignores the playground/auth/websocket variables. When it is empty or unset,
the app uses Online Playground as before.

Please see the app-specific README files for details on the tools necessary to
build and run them.

Expand Down
8 changes: 8 additions & 0 deletions android-java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,11 @@ ditto = "5.0.0"
```

To use a newer version of the SDK, change the version number on this line.

## Offline-only mode (optional)

Set `DITTO_OFFLINE_LICENSE_TOKEN` in the repo-root `.env` to run this
app in offline-only mode (peer-to-peer only, no cloud sync). When the
token is non-empty, the playground/auth/websocket vars are not used.
Request a token from <support@ditto.com>. See the top-level
[README](../README.md#offline-only-mode-optional) for full details.
9 changes: 9 additions & 0 deletions android-java/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ androidComponents {
"Ditto Auth URL"
)
)

it.buildConfigFields.put(
"DITTO_OFFLINE_LICENSE_TOKEN",
BuildConfigField(
"String",
"\"${envValue(prop, "DITTO_OFFLINE_LICENSE_TOKEN")}\"",
"Optional offline-only license token; when non-empty, app runs in offline mode"
)
)
Comment thread
bplattenburg marked this conversation as resolved.
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ object DittoHelper {
return DittoFactory.create(config)
}

@JvmStatic
fun createOfflineDitto(appId: String, offlineLicenseToken: String): Ditto {
val config = DittoConfig(
databaseId = appId,
connect = DittoConfig.Connect.SmallPeersOnly(privateKey = null)
)
val ditto = DittoFactory.create(config)
ditto.setOfflineOnlyLicenseToken(offlineLicenseToken)
return ditto
}

@JvmStatic
fun setupAuth(ditto: Ditto, token: String) {
ditto.auth?.let { auth ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.dittotasks;

/**
* Identity-mode selection based on env vars.
*
* Non-empty {@code DITTO_OFFLINE_LICENSE_TOKEN} (after trim) selects {@link #OFFLINE};
* otherwise the app uses {@link #ONLINE_PLAYGROUND}.
*/
public enum DittoMode {
ONLINE_PLAYGROUND,
OFFLINE;

public static DittoMode select(String offlineLicenseToken) {
if (offlineLicenseToken == null) {
return ONLINE_PLAYGROUND;
}
return offlineLicenseToken.trim().isEmpty() ? ONLINE_PLAYGROUND : OFFLINE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public class MainActivity extends ComponentActivity {
private final String dittoAppId = BuildConfig.DITTO_APP_ID;
private final String dittoPlaygroundToken = BuildConfig.DITTO_PLAYGROUND_TOKEN;
private final String dittoAuthUrl = BuildConfig.DITTO_AUTH_URL;
private final String dittoOfflineLicenseToken = BuildConfig.DITTO_OFFLINE_LICENSE_TOKEN.trim();
private final DittoMode dittoMode = DittoMode.select(dittoOfflineLicenseToken);

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
Expand Down Expand Up @@ -110,18 +112,24 @@ void initDitto() {
Log.d("DittoInit", "Skipping permissions during instrumentation test");
}

Log.d("DittoInit", "Starting Ditto SDK initialization...");
Log.d("DittoInit", "Starting Ditto SDK initialization in mode: " + dittoMode);
try {
// Create Ditto with server connection
// Create Ditto in the selected mode.
// https://docs.ditto.live/sdk/latest/install-guides/java#integrating-and-initializing
Log.d("DittoInit", "Creating Ditto instance...");
ditto = DittoHelper.createDitto(dittoAppId, dittoAuthUrl);
if (dittoMode == DittoMode.OFFLINE) {
ditto = DittoHelper.createOfflineDitto(dittoAppId, dittoOfflineLicenseToken);
} else {
ditto = DittoHelper.createDitto(dittoAppId, dittoAuthUrl);
}
Log.d("DittoInit", "Ditto instance created successfully");

// Set up authentication handler (must be set before sync.start())
Log.d("DittoInit", "Setting up authentication...");
DittoHelper.setupAuth(ditto, dittoPlaygroundToken);
Log.d("DittoInit", "Authentication configured");
if (dittoMode == DittoMode.ONLINE_PLAYGROUND) {
// Set up authentication handler (must be set before sync.start())
Log.d("DittoInit", "Setting up authentication...");
DittoHelper.setupAuth(ditto, dittoPlaygroundToken);
Log.d("DittoInit", "Authentication configured");
}

// register subscription
// https://docs.ditto.live/sdk/latest/sync/syncing-data#creating-subscriptions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.example.dittotasks;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class DittoModeTest {
@Test
public void nullTokenSelectsOnline() {
assertEquals(DittoMode.ONLINE_PLAYGROUND, DittoMode.select(null));
}

@Test
public void emptyTokenSelectsOnline() {
assertEquals(DittoMode.ONLINE_PLAYGROUND, DittoMode.select(""));
}

@Test
public void whitespaceOnlyTokenSelectsOnline() {
assertEquals(DittoMode.ONLINE_PLAYGROUND, DittoMode.select(" \t\n "));
}

@Test
public void nonEmptyTokenSelectsOffline() {
assertEquals(DittoMode.OFFLINE, DittoMode.select("any-real-license-token"));
}
}
22 changes: 14 additions & 8 deletions android-kotlin/QuickStartTasks/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,28 @@ plugins {
fun loadEnvProperties(): Properties {
val properties = Properties()
val envFile = rootProject.file("../../.env")

if (envFile.exists()) {
FileInputStream(envFile).use { properties.load(it) }
} else {
val requiredEnvVars = listOf(
"DITTO_APP_ID",
"DITTO_PLAYGROUND_TOKEN",
"DITTO_AUTH_URL",
"DITTO_APP_ID",
"DITTO_PLAYGROUND_TOKEN",
"DITTO_AUTH_URL",
"DITTO_WEBSOCKET_URL"
)

for (envVar in requiredEnvVars) {
val value = System.getenv(envVar)
val value = System.getenv(envVar)
?: throw RuntimeException("Required environment variable $envVar not found")
properties[envVar] = value
}
}
// Offline mode is opt-in; treat missing var as empty so the
// BuildConfig field always exists.
if (!properties.containsKey("DITTO_OFFLINE_LICENSE_TOKEN")) {
properties["DITTO_OFFLINE_LICENSE_TOKEN"] = System.getenv("DITTO_OFFLINE_LICENSE_TOKEN") ?: ""
}
Comment thread
bplattenburg marked this conversation as resolved.
return properties
}

Expand All @@ -39,13 +44,14 @@ androidComponents {
"DITTO_PLAYGROUND_TOKEN" to "Ditto playground token",
"DITTO_AUTH_URL" to "Ditto authentication URL",
"DITTO_WEBSOCKET_URL" to "Ditto websocket URL",
"DITTO_OFFLINE_LICENSE_TOKEN" to "Optional offline-only license token; when non-empty, app runs in offline mode",
"TEST_DOCUMENT_TITLE" to "Test document title for BrowserStack verification"
)

buildConfigFields.forEach { (key, description) ->
it.buildConfigFields.put(
key,
BuildConfigField("String", "\"${prop[key]}\"", description)
BuildConfigField("String", "\"${prop[key] ?: ""}\"", description)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package live.ditto.quickstart.tasks

/**
* Identity-mode selection based on env vars.
*
* Non-empty `DITTO_OFFLINE_LICENSE_TOKEN` (after trim) selects [OFFLINE];
* otherwise the app uses [ONLINE_PLAYGROUND].
*/
enum class DittoMode {
ONLINE_PLAYGROUND,
OFFLINE;

companion object {
fun select(offlineLicenseToken: String?): DittoMode =
if (offlineLicenseToken.isNullOrBlank()) ONLINE_PLAYGROUND else OFFLINE
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import live.ditto.Ditto
import live.ditto.DittoIdentity
import live.ditto.android.DefaultAndroidDittoDependencies
import live.ditto.quickstart.tasks.DittoHandler.Companion.ditto
import live.ditto.quickstart.tasks.DittoMode

class TasksApplication : Application() {

Expand Down Expand Up @@ -48,25 +49,37 @@ class TasksApplication : Application() {
val token = BuildConfig.DITTO_PLAYGROUND_TOKEN
val authUrl = BuildConfig.DITTO_AUTH_URL
val webSocketURL = BuildConfig.DITTO_WEBSOCKET_URL
val offlineLicenseToken = BuildConfig.DITTO_OFFLINE_LICENSE_TOKEN.trim()

val enableDittoCloudSync = false

/*
* Setup Ditto Identity
* https://docs.ditto.live/sdk/latest/install-guides/kotlin#integrating-and-initializing
*/
val identity = DittoIdentity.OnlinePlayground(
dependencies = androidDependencies,
appId = appId,
token = token,
customAuthUrl = authUrl,
enableDittoCloudSync = enableDittoCloudSync // This is required to be set to false to use the correct URLs
)
val mode = DittoMode.select(offlineLicenseToken)
val identity = when (mode) {
DittoMode.OFFLINE -> DittoIdentity.OfflinePlayground(
dependencies = androidDependencies,
appId = appId
)
DittoMode.ONLINE_PLAYGROUND -> DittoIdentity.OnlinePlayground(
dependencies = androidDependencies,
appId = appId,
token = token,
customAuthUrl = authUrl,
enableDittoCloudSync = enableDittoCloudSync // This is required to be set to false to use the correct URLs
)
}

ditto = Ditto(androidDependencies, identity)
ditto.updateTransportConfig { config ->
// Set the Ditto Websocket URL
config.connect.websocketUrls.add(webSocketURL)
if (mode == DittoMode.OFFLINE) {
ditto.setOfflineOnlyLicenseToken(offlineLicenseToken)
} else {
ditto.updateTransportConfig { config ->
// Set the Ditto Websocket URL (online mode only)
config.connect.websocketUrls.add(webSocketURL)
}
}

ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package live.ditto.quickstart.tasks

import org.junit.Assert.assertEquals
import org.junit.Test

class DittoModeTest {
@Test
fun nullTokenSelectsOnline() {
assertEquals(DittoMode.ONLINE_PLAYGROUND, DittoMode.select(null))
}

@Test
fun emptyTokenSelectsOnline() {
assertEquals(DittoMode.ONLINE_PLAYGROUND, DittoMode.select(""))
}

@Test
fun whitespaceOnlyTokenSelectsOnline() {
assertEquals(DittoMode.ONLINE_PLAYGROUND, DittoMode.select(" \t\n "))
}

@Test
fun nonEmptyTokenSelectsOffline() {
assertEquals(DittoMode.OFFLINE, DittoMode.select("any-real-license-token"))
}
}
8 changes: 8 additions & 0 deletions android-kotlin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,11 @@ It is implemented in
associated view model is in
`app/src/main/java/live/ditto/quickstart/tasks/list/edit/EditScreenViewModel.kt`,
and this contains the associated code for manipulating data in the Ditto store.

## Offline-only mode (optional)

Set `DITTO_OFFLINE_LICENSE_TOKEN` in the repo-root `.env` to run this
app in offline-only mode (peer-to-peer only, no cloud sync). When the
token is non-empty, the playground/auth/websocket vars are not used.
Request a token from <support@ditto.com>. See the top-level
[README](../README.md#offline-only-mode-optional) for full details.
Loading
Loading