Skip to content
Open
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
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.
40 changes: 33 additions & 7 deletions android-java/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,33 @@ fun loadEnvProperties(): Properties {
if (envFile.exists()) {
FileInputStream(envFile).use { properties.load(it) }
} else {
val requiredEnvVars = listOf(
// Read every var we know about from the process env. DITTO_APP_ID is
// always required; the playground/auth pair is only required when
// DITTO_OFFLINE_LICENSE_TOKEN is unset.
val knownEnvVars = listOf(
"DITTO_APP_ID",
"DITTO_PLAYGROUND_TOKEN",
"DITTO_AUTH_URL"
"DITTO_AUTH_URL",
"DITTO_OFFLINE_LICENSE_TOKEN"
)

for (envVar in requiredEnvVars) {
val value = System.getenv(envVar)
?: throw RuntimeException("Required environment variable $envVar not found")
properties[envVar] = value
for (envVar in knownEnvVars) {
System.getenv(envVar)?.let { properties[envVar] = it }
}
val appId = properties["DITTO_APP_ID"] as String?
if (appId.isNullOrBlank()) {
throw RuntimeException("Required environment variable DITTO_APP_ID not found")
}
val offlineToken = (properties["DITTO_OFFLINE_LICENSE_TOKEN"] as String? ?: "").trim()
if (offlineToken.isEmpty()) {
for (envVar in listOf("DITTO_PLAYGROUND_TOKEN", "DITTO_AUTH_URL")) {
val value = properties[envVar] as String?
if (value.isNullOrBlank()) {
throw RuntimeException(
"Required environment variable $envVar not found " +
"(set DITTO_OFFLINE_LICENSE_TOKEN to use offline mode instead)"
)
}
}
}
}
return properties
Expand Down Expand Up @@ -67,6 +84,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"));
}
}
49 changes: 36 additions & 13 deletions android-kotlin/QuickStartTasks/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,44 @@ 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_WEBSOCKET_URL"
// Read every var we know about from the process env. DITTO_APP_ID is
// always required; the playground/auth/websocket trio is only required
// when DITTO_OFFLINE_LICENSE_TOKEN is unset.
val knownEnvVars = listOf(
"DITTO_APP_ID",
"DITTO_PLAYGROUND_TOKEN",
"DITTO_AUTH_URL",
"DITTO_WEBSOCKET_URL",
"DITTO_OFFLINE_LICENSE_TOKEN"
)

for (envVar in requiredEnvVars) {
val value = System.getenv(envVar)
?: throw RuntimeException("Required environment variable $envVar not found")
properties[envVar] = value
for (envVar in knownEnvVars) {
System.getenv(envVar)?.let { properties[envVar] = it }
}
val appId = properties["DITTO_APP_ID"] as String?
if (appId.isNullOrBlank()) {
throw RuntimeException("Required environment variable DITTO_APP_ID not found")
}
val offlineToken = (properties["DITTO_OFFLINE_LICENSE_TOKEN"] as String? ?: "").trim()
if (offlineToken.isEmpty()) {
for (envVar in listOf("DITTO_PLAYGROUND_TOKEN", "DITTO_AUTH_URL", "DITTO_WEBSOCKET_URL")) {
val value = properties[envVar] as String?
if (value.isNullOrBlank()) {
throw RuntimeException(
"Required environment variable $envVar not found " +
"(set DITTO_OFFLINE_LICENSE_TOKEN to use offline mode instead)"
)
}
}
}
}
// 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 +61,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
Loading
Loading