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
10 changes: 5 additions & 5 deletions Android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ For support, please contact Ditto Support (<support@ditto.com>).
If you'd like to just view the app, it is available in the [Play store](https://play.google.com/store/apps/details?id=live.ditto.pos). No setup is required for this. If you'd like to build and run the app, see the instructions below.

### Building and Running the App in Android Studio
1. In your [Ditto portal](https://portal.ditto.live), create an app to generate an App ID and
playground token.
1. In your [Ditto portal](https://portal.ditto.live), create an app to generate a Database ID,
development token, and URL.
2. Clone this repo to a location on your machine, and open in Android Studio.
3. Create a `local.properties` file or if you already have one, open it.
4. In the `local.properties` file add the following entries (keep the quotes):
```
dittoOnlinePlaygroundAppId="replace-with-your-app-id"
dittoOnlinePlaygroundToken="replace-with-your-playground-token"
dittoWebsocketURL="replace-with-your-websocket-url"
dittoDatabaseId="replace-with-your-database-id"
dittoDevelopmentToken="replace-with-your-development-token"
dittoUrl="replace-with-your-url"
```
5. Hit the green play button to run the app

Expand Down
12 changes: 6 additions & 6 deletions Android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,20 @@ android {
// Load Ditto API keys
buildConfigField(
"String",
"DITTO_ONLINE_PLAYGROUND_APP_ID",
getLocalProperty("dittoOnlinePlaygroundAppId")
"DITTO_DATABASE_ID",
getLocalProperty("dittoDatabaseId")
)

buildConfigField(
"String",
"DITTO_ONLINE_PLAYGROUND_TOKEN",
getLocalProperty("dittoOnlinePlaygroundToken")
"DITTO_DEVELOPMENT_TOKEN",
getLocalProperty("dittoDevelopmentToken")
)

buildConfigField(
"String",
"DITTO_WEBSOCKET_URL",
getLocalProperty("dittoWebsocketURL")
"DITTO_URL",
getLocalProperty("dittoUrl")
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package live.ditto.pos.core.data

import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import com.ditto.kotlin.DittoQueryResult
import com.ditto.kotlin.DittoQueryResultItem
import com.ditto.kotlin.DittoStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.serialization.encodeToString
import live.ditto.DittoQueryResult
import live.ditto.DittoQueryResultItem
import live.ditto.DittoStore

// JSON encoding / decoding helpers for @Serializable models. Push values into
// DQL via `deserialize_json(:json)` and read them back from `jsonString()`.
Expand Down Expand Up @@ -41,23 +38,14 @@ inline fun <reified T> DittoQueryResult.decodeOrSkip(): List<T> =
/**
* Observe a DQL query as a Flow of decoded models, skipping any that fail to decode.
*
* Uses Ditto's `signalNext` overload of `registerObserver` for backpressure:
* `signalNext()` is called only after the value has been sent into the channel.
* With the default rendezvous channel that means Ditto won't deliver the next
* update until our consumer has actually taken this one.
* `store.observe` returns a cold Flow whose suspending `transform` provides
* natural backpressure: Ditto delivers the next result only after the previous
* `transform` completes. The query result is auto-closed once `transform`
* returns; each item's `dematerialize()` runs inside `decode()` via try/finally.
*/
inline fun <reified T> DittoStore.observeAsFlow(
query: String,
args: Map<String, Any> = emptyMap()
): Flow<List<T>> = callbackFlow {
val observer = registerObserver(query, args) { result, signalNext ->
// Decode first; signalNext only after the consumer has the decoded
// payload so Ditto's next delivery is gated on us having actually
// finished this one.
// .use { } closes the result handle after decoding; each item's
// dematerialize() is run inside decode() via try/finally.
result.use { trySendBlocking(it.decodeOrSkip<T>()) }
signalNext()
}
awaitClose { observer.close() }
): Flow<List<T>> = observe(query, args) { result ->
result.decodeOrSkip<T>()
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package live.ditto.pos.core.data.demo
// — idempotent and peer-safe, so every device can run this on launch and
// the network converges on a single copy.

import live.ditto.Ditto
import com.ditto.kotlin.Ditto
import live.ditto.pos.core.data.SaleItem
import live.ditto.pos.core.data.dittoJsonString
import live.ditto.pos.core.data.locations.Location
Expand Down Expand Up @@ -59,7 +59,7 @@ class DemoSeeder(private val ditto: Ditto) {
INITIAL DOCUMENTS ${placeholders.joinToString(", ")}
""".trimIndent(),
args
).use { }
)
} catch (error: Throwable) {
android.util.Log.w("DemoSeeder", "$label: ${error.message}")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package live.ditto.pos.core.data.repository

import com.ditto.kotlin.Ditto
import com.ditto.kotlin.DittoSyncSubscription
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import live.ditto.Ditto
import live.ditto.DittoSyncSubscription
import live.ditto.ditto_wrapper.DittoManager
import live.ditto.pos.core.data.demo.LocationSeed
import live.ditto.pos.core.data.locations.Location
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package live.ditto.pos.core.data.repository

import android.content.Context
import android.util.Log
import com.ditto.kotlin.Ditto
import com.ditto.kotlin.DittoSyncSubscription
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -15,8 +17,6 @@ import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.toLocalDateTime
import live.ditto.Ditto
import live.ditto.DittoSyncSubscription
import live.ditto.ditto_wrapper.DittoManager
import live.ditto.pos.core.data.dittoJsonString
import live.ditto.pos.core.data.observeAsFlow
Expand Down Expand Up @@ -75,7 +75,7 @@ class OrdersRepository @Inject constructor(
ON ID CONFLICT DO UPDATE_LOCAL_DIFF
""".trimIndent(),
mapOf("json" to order.dittoJsonString())
).use { }
)
}

suspend fun clearCart(order: Order) {
Expand All @@ -88,7 +88,7 @@ class OrdersRepository @Inject constructor(
WHERE _id.id = :id AND _id.locationId = :locationId
""".trimIndent(),
mapOf("id" to order.documentId.id, "locationId" to order.documentId.locationId)
).use { }
)
}

suspend fun reset(order: Order) {
Expand All @@ -113,7 +113,7 @@ class OrdersRepository @Inject constructor(
WHERE _id.id = :id AND _id.locationId = :locationId
""".trimIndent()
}
ditto.store.execute(query, baseArgs).use { }
ditto.store.execute(query, baseArgs)
}

suspend fun runEvictionIfDue() {
Expand All @@ -127,7 +127,7 @@ class OrdersRepository @Inject constructor(
ditto.store.execute(
"EVICT FROM ${Order.COLLECTION_NAME} WHERE createdAt <= :TTL",
mapOf("TTL" to ttl)
).use { }
)
prefs.edit().putLong(LAST_EVICTION_KEY, now).apply()
Log.i("Eviction", "evicted orders with createdAt <= $ttl")
} catch (error: Throwable) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package live.ditto.pos.core.data.repository

import com.ditto.kotlin.Ditto
import com.ditto.kotlin.DittoSyncSubscription
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
Expand All @@ -8,8 +10,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import live.ditto.Ditto
import live.ditto.DittoSyncSubscription
import live.ditto.ditto_wrapper.DittoManager
import live.ditto.pos.core.data.SaleItem
import live.ditto.pos.core.data.observeAsFlow
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package live.ditto.pos.core.domain.usecase.ditto

import live.ditto.Ditto
import com.ditto.kotlin.Ditto
import live.ditto.ditto_wrapper.DittoManager
import javax.inject.Inject

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package live.ditto.pos.core.presentation.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ditto.kotlin.Ditto
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import live.ditto.Ditto
import live.ditto.pos.core.data.repository.LocationsRepository
import live.ditto.pos.core.domain.usecase.AppConfigurationStateUseCase
import live.ditto.pos.core.domain.usecase.AppConfigurationStateUseCase.AppConfigurationState
Expand Down
30 changes: 15 additions & 15 deletions Android/app/src/main/java/live/ditto/pos/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,34 @@ internal object AppModule {
@Singleton
fun provideDittoManager(
@ApplicationContext context: Context,
@DittoOnlinePlaygroundAppId onlinePlaygroundAppId: String,
@DittoOnlinePlaygroundAppToken dittoOnlinePlaygroundAppToken: String,
@DittoWebsocketURL dittoWebsocketURL: String
@DittoDatabaseId databaseId: String,
@DittoDevelopmentToken developmentToken: String,
@DittoUrl url: String
): DittoManager {
return DittoManager(
context = context,
dittoOnlinePlaygroundAppId = onlinePlaygroundAppId,
dittoOnlinePlaygroundToken = dittoOnlinePlaygroundAppToken,
dittoWebsocketURL = dittoWebsocketURL
dittoDatabaseId = databaseId,
dittoDevelopmentToken = developmentToken,
dittoUrl = url
)
}

@DittoOnlinePlaygroundAppId
@DittoDatabaseId
@Provides
fun provideDittoOnlinePlaygroundAppId(): String {
return BuildConfig.DITTO_ONLINE_PLAYGROUND_APP_ID
fun provideDittoDatabaseId(): String {
return BuildConfig.DITTO_DATABASE_ID
}

@DittoOnlinePlaygroundAppToken
@DittoDevelopmentToken
@Provides
fun provideDittoOnlinePlaygroundAppToken(): String {
return BuildConfig.DITTO_ONLINE_PLAYGROUND_TOKEN
fun provideDittoDevelopmentToken(): String {
return BuildConfig.DITTO_DEVELOPMENT_TOKEN
}

@DittoWebsocketURL
@DittoUrl
@Provides
fun provideDittoWebsocketURL(): String {
return BuildConfig.DITTO_WEBSOCKET_URL
fun provideDittoUrl(): String {
return BuildConfig.DITTO_URL
}

@Provides
Expand Down
6 changes: 3 additions & 3 deletions Android/app/src/main/java/live/ditto/pos/di/Qualifiers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import javax.inject.Qualifier

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DittoOnlinePlaygroundAppId
annotation class DittoDatabaseId

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DittoOnlinePlaygroundAppToken
annotation class DittoDevelopmentToken

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DittoWebsocketURL
annotation class DittoUrl
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,60 @@ package live.ditto.ditto_wrapper

import android.content.Context
import android.util.Log
import com.ditto.kotlin.Ditto
import com.ditto.kotlin.DittoAuthenticationProvider
import com.ditto.kotlin.DittoConfig
import com.ditto.kotlin.DittoFactory
import com.ditto.kotlin.DittoLogLevel
import com.ditto.kotlin.DittoLogger
import com.ditto.kotlin.transports.DittoSyncPermissions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import live.ditto.Ditto
import live.ditto.DittoIdentity
import live.ditto.DittoLogLevel
import live.ditto.DittoLogger
import live.ditto.android.DefaultAndroidDittoDependencies
import live.ditto.transports.DittoSyncPermissions

private val TAG = DittoManager::class.java.name

class DittoManager(
val context: Context,
dittoOnlinePlaygroundAppId: String,
dittoOnlinePlaygroundToken: String,
dittoWebsocketURL: String
private val dittoDatabaseId: String,
private val dittoDevelopmentToken: String,
private val dittoUrl: String
) {
private val ditto: Ditto? by lazy {
try {
val androidDependencies = DefaultAndroidDittoDependencies(context)
val identity = DittoIdentity.OnlinePlayground(
dependencies = androidDependencies,
appId = dittoOnlinePlaygroundAppId,
token = dittoOnlinePlaygroundToken,
enableDittoCloudSync = false
DittoLogger.minimumLogLevel = DittoLogLevel.Debug

// Configure → Initialize → Authenticate → Sync. The server URL is
// the portal's "Connect via SDK" URL. Strict mode defaults to off,
// giving DQL map/object CRDT semantics.
// https://docs.ditto.live/sdk/latest/ditto-config
val config = DittoConfig(
databaseId = dittoDatabaseId,
connect = DittoConfig.Connect.Server(dittoUrl)
)
DittoLogger.minimumLogLevel = DittoLogLevel.DEBUG
Ditto(androidDependencies, identity).apply {
disableSyncWithV3()

updateTransportConfig { config ->
config.connect.websocketUrls.add(dittoWebsocketURL)
DittoFactory.create(config).apply {
// Authenticate before sync starts: provide a fresh token
// whenever the current one is missing or near expiry.
auth?.expirationHandler = { authDitto, _ ->
try {
authDitto.auth?.login(
token = dittoDevelopmentToken,
provider = DittoAuthenticationProvider.development()
)
} catch (e: Throwable) {
Log.e(TAG, "Authentication failed: ${e.message}")
}
}

CoroutineScope(Dispatchers.IO).launch {
try {
store.execute(query = "ALTER SYSTEM SET DQL_STRICT_MODE = false")
startSync()
sync.start()
} catch (e: Throwable) {
Log.e(TAG, "Failed to execute DQL mode query: ${e.message}")
Log.e(TAG, "Failed to start sync: ${e.message}")
}
}
}

} catch (e: Exception) {
Log.e(TAG, e.message.orEmpty())
null
Expand All @@ -59,7 +68,7 @@ class DittoManager(
val value = locationId.toUIntOrNull() ?: return
val ditto = requireDitto()

ditto.stopSync()
ditto.sync.stop()
ditto.updateTransportConfig { config ->
// Isolate the peer-to-peer mesh to devices at this location.
// https://docs.ditto.live/sdk/latest/sync/creating-sync-groups
Expand All @@ -69,7 +78,7 @@ class DittoManager(
// https://docs.ditto.live/sdk/latest/deployment/setting-routing-hints
config.global.routingHint = value
}
ditto.startSync()
ditto.sync.start()
}

fun requireDitto(): Ditto {
Expand All @@ -85,4 +94,4 @@ class DittoManager(
}
}

class DittoNotCreatedException : Throwable("Ditto cannot be null")
class DittoNotCreatedException : Throwable("Ditto cannot be null")
Loading