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
25 changes: 20 additions & 5 deletions Android/app/src/main/java/live/ditto/pos/DittoPOSApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,37 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import live.ditto.pos.core.domain.repository.DittoRepository
import live.ditto.ditto_wrapper.DittoManager
import live.ditto.pos.core.data.demo.DemoSeeder
import live.ditto.pos.core.data.repository.LocationsRepository
import live.ditto.pos.core.data.repository.OrdersRepository
import live.ditto.pos.core.data.repository.SaleItemsRepository
import javax.inject.Inject

@HiltAndroidApp
class DittoPOSApplication : Application() {

@Inject lateinit var dittoRepository: DittoRepository
@Inject lateinit var dittoManager: DittoManager

@Inject lateinit var locationsRepository: LocationsRepository

@Inject lateinit var ordersRepository: OrdersRepository

// Injected here even though we don't reference it directly — its init
// block sets up the location-id flow collector that drives the sale_items
// subscription on app start.
@Suppress("unused")
@Inject
lateinit var saleItemsRepository: SaleItemsRepository

private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

override fun onCreate() {
super.onCreate()
dittoRepository.startLocationsSubscription()
locationsRepository.startSubscription()
applicationScope.launch {
dittoRepository.seedAll()
dittoRepository.runEvictionIfDue()
DemoSeeder(dittoManager.requireDitto()).seedAll()
ordersRepository.runEvictionIfDue()
}
}
}
4 changes: 2 additions & 2 deletions Android/app/src/main/java/live/ditto/pos/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import dagger.hilt.android.AndroidEntryPoint
import live.ditto.pos.core.presentation.composables.screens.PosKdsApp
import live.ditto.pos.core.presentation.viewmodel.CoreViewModel
import live.ditto.pos.core.presentation.viewmodel.MainViewModel

val LocalActivity = staticCompositionLocalOf<ComponentActivity> {
error("LocalActivity is not present")
Expand All @@ -18,7 +18,7 @@ val LocalActivity = staticCompositionLocalOf<ComponentActivity> {
@AndroidEntryPoint
class MainActivity : ComponentActivity() {

private val viewModel: CoreViewModel by viewModels<CoreViewModel>()
private val viewModel: MainViewModel by viewModels<MainViewModel>()

private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
viewModel.refreshDittoPermissions()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package live.ditto.pos.core.data.repository

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
import live.ditto.pos.core.data.observeAsFlow
import live.ditto.pos.settings.AppSettings
import javax.inject.Inject
import javax.inject.Singleton

/**
* Owns the `locations` collection and the active-location state.
* `setActiveLocation` is the only writer — it persists to [AppSettings] and
* applies [DittoManager]'s routing config. Other per-collection repositories
* subscribe to `locationIdFlow()` and re-register their own subscriptions
* when it emits. Mirrors iOS `LocationsRepository`.
*/
@Singleton
class LocationsRepository @Inject constructor(
private val dittoManager: DittoManager,
private val appSettings: AppSettings
) {
private val ditto: Ditto get() = dittoManager.requireDitto()
private var subscription: DittoSyncSubscription? = null
private val repoScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

init {
// Restore the persisted active location on first injection (mirrors
// iOS, where this happens synchronously in init). Routing config
// applies as part of `setActiveLocation`.
repoScope.launch {
val saved = appSettings.locationId()
if (saved.isNotEmpty()) {
setActiveLocation(saved)
}
}
}

fun startSubscription() {
if (subscription != null) return
subscription = ditto.sync.registerSubscription(
"SELECT * FROM ${Location.COLLECTION_NAME}"
)
}

/**
* Switch the active location. Persists, applies routing, and downstream
* repositories react via `locationIdFlow()`. Pass an empty string to
* clear the selection.
*/
suspend fun setActiveLocation(locationId: String) {
appSettings.setLocationId(locationId = locationId)
if (locationId.isNotEmpty()) {
dittoManager.setRoutingConfig(locationId)
}
}

/** Reactive stream of the active location id. Empty string when not set. */
fun locationIdFlow(): Flow<String> = appSettings.locationIdFlow()

fun observeAllLocations(): Flow<List<Location>> =
ditto.store
.observeAsFlow<Location>("SELECT * FROM ${Location.COLLECTION_NAME}")
.map { locations -> locations.filter { it.id in LocationSeed.demoLocationIds } }
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package live.ditto.pos.core.domain.repository
package live.ditto.pos.core.data.repository

import android.content.Context
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
Expand All @@ -16,111 +18,56 @@ import kotlinx.datetime.toLocalDateTime
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.demo.DemoSeeder
import live.ditto.pos.core.data.dittoJsonString
import live.ditto.pos.core.data.locations.Location
import live.ditto.pos.core.data.observeAsFlow
import live.ditto.pos.core.data.orders.Order
import live.ditto.pos.core.data.toDittoIsoString
import live.ditto.pos.settings.AppSettings
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class DittoRepository
@Inject
constructor(
class OrdersRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val dittoManager: DittoManager,
private val appSettings: AppSettings
private val locationsRepository: LocationsRepository
) {
private val ditto: Ditto get() = dittoManager.requireDitto()
private val activeSubs = mutableMapOf<String, DittoSyncSubscription>()
private var subscription: DittoSyncSubscription? = null
private val repoScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

init {
// Track the saved locationId and (re-)configure routing + subscriptions
// whenever it's set. Covers both the user picking a location and app
// re-launch with a value restored from DataStore.
appSettings.locationIdFlow()
// Pull the active location from LocationsRepository and re-register
// the subscription on every change. Mirrors iOS.
locationsRepository.locationIdFlow()
.filter { it.isNotEmpty() }
.onEach { setActiveLocation(it) }
.distinctUntilChanged()
.onEach { locationId -> setActiveLocation(locationId) }
.launchIn(repoScope)
}

fun requireDitto(): Ditto = ditto

fun refreshPermissions() {
ditto.refreshPermissions()
}

fun getMissingPermissions(): Array<String> = dittoManager.missingPermissions()

// ----- Subscriptions -----

fun startLocationsSubscription() {
registerSub(
key = "locations",
query = "SELECT * FROM ${Location.COLLECTION_NAME}"
)
}

fun setActiveLocation(locationId: String) {
dittoManager.setRoutingConfig(locationId)
registerSub(
key = "orders",
query = """
SELECT * FROM ${Order.COLLECTION_NAME}
WHERE _id.locationId = :locationId
AND createdAt > :TTL
""".trimIndent(),
args = mapOf("locationId" to locationId, "TTL" to startOfTodayIso())
)
registerSub(
key = "sale_items",
query = """
SELECT * FROM ${SaleItem.COLLECTION_NAME}
WHERE _id.locationId = :locationId
ORDER BY name
private fun setActiveLocation(locationId: String) {
subscription?.close()
subscription = ditto.sync.registerSubscription(
"""
SELECT * FROM ${Order.COLLECTION_NAME}
WHERE _id.locationId = :locationId
AND createdAt > :TTL
""".trimIndent(),
args = mapOf("locationId" to locationId)
mapOf("locationId" to locationId, "TTL" to startOfTodayIso())
)
}

private fun registerSub(key: String, query: String, args: Map<String, Any> = emptyMap()) {
activeSubs[key]?.close()
activeSubs[key] = ditto.sync.registerSubscription(query, args)
}

// ----- Observers -----

fun observeAllLocations(): Flow<List<Location>> =
ditto.store.observeAsFlow("SELECT * FROM ${Location.COLLECTION_NAME}")

fun observeLocationOrders(locationId: String): Flow<List<Order>> =
ditto.store.observeAsFlow(
query = """
SELECT * FROM ${Order.COLLECTION_NAME}
WHERE _id.locationId = :locationId
AND createdAt > :TTL
SELECT * FROM ${Order.COLLECTION_NAME}
WHERE _id.locationId = :locationId
AND createdAt > :TTL
""".trimIndent(),
args = mapOf("locationId" to locationId, "TTL" to startOfTodayIso())
)

fun observeLocationSaleItems(locationId: String): Flow<List<SaleItem>> =
ditto.store.observeAsFlow(
query = """
SELECT * FROM ${SaleItem.COLLECTION_NAME}
WHERE _id.locationId = :locationId
ORDER BY name
""".trimIndent(),
args = mapOf("locationId" to locationId)
)

// ----- Mutations -----

suspend fun upsertOrder(order: Order) {
suspend fun upsert(order: Order) {
ditto.store.execute(
"""
INSERT INTO ${Order.COLLECTION_NAME}
Expand All @@ -144,7 +91,7 @@ constructor(
).use { }
}

suspend fun resetOrder(order: Order) {
suspend fun reset(order: Order) {
val createdAtNow = Clock.System.now().toDittoIsoString()
val baseArgs = mapOf<String, Any>(
"id" to order.documentId.id,
Expand All @@ -169,12 +116,6 @@ constructor(
ditto.store.execute(query, baseArgs).use { }
}

// ----- Lifecycle -----

suspend fun seedAll() {
DemoSeeder(ditto).seedAll()
}

suspend fun runEvictionIfDue() {
val prefs = context.getSharedPreferences(EVICTION_PREFS, Context.MODE_PRIVATE)
val now = Clock.System.now().toEpochMilliseconds()
Expand All @@ -188,9 +129,9 @@ constructor(
mapOf("TTL" to ttl)
).use { }
prefs.edit().putLong(LAST_EVICTION_KEY, now).apply()
android.util.Log.i("Eviction", "evicted orders with createdAt <= $ttl")
Log.i("Eviction", "evicted orders with createdAt <= $ttl")
} catch (error: Throwable) {
android.util.Log.w("Eviction", error.message.orEmpty())
Log.w("Eviction", error.message.orEmpty())
}
}

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

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
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
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class SaleItemsRepository @Inject constructor(
private val dittoManager: DittoManager,
private val locationsRepository: LocationsRepository
) {
private val ditto: Ditto get() = dittoManager.requireDitto()
private var subscription: DittoSyncSubscription? = null
private val repoScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

init {
locationsRepository.locationIdFlow()
.filter { it.isNotEmpty() }
.distinctUntilChanged()
.onEach { locationId -> setActiveLocation(locationId) }
.launchIn(repoScope)
}

private fun setActiveLocation(locationId: String) {
subscription?.close()
subscription = ditto.sync.registerSubscription(
"""
SELECT * FROM ${SaleItem.COLLECTION_NAME}
WHERE _id.locationId = :locationId
ORDER BY name
""".trimIndent(),
mapOf("locationId" to locationId)
)
}

fun observeLocationSaleItems(locationId: String): Flow<List<SaleItem>> =
ditto.store.observeAsFlow(
query = """
SELECT * FROM ${SaleItem.COLLECTION_NAME}
WHERE _id.locationId = :locationId
ORDER BY name
""".trimIndent(),
args = mapOf("locationId" to locationId)
)
}

This file was deleted.

Loading