diff --git a/Android/README.md b/Android/README.md index f9ec233..3f33970 100644 --- a/Android/README.md +++ b/Android/README.md @@ -12,15 +12,15 @@ For support, please contact Ditto Support (). 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 diff --git a/Android/app/build.gradle.kts b/Android/app/build.gradle.kts index c7b1ed8..baadec4 100644 --- a/Android/app/build.gradle.kts +++ b/Android/app/build.gradle.kts @@ -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") ) } diff --git a/Android/app/src/main/java/live/ditto/pos/core/data/DittoCodableExt.kt b/Android/app/src/main/java/live/ditto/pos/core/data/DittoCodableExt.kt index 8947341..ddc20cd 100644 --- a/Android/app/src/main/java/live/ditto/pos/core/data/DittoCodableExt.kt +++ b/Android/app/src/main/java/live/ditto/pos/core/data/DittoCodableExt.kt @@ -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()`. @@ -41,23 +38,14 @@ inline fun DittoQueryResult.decodeOrSkip(): List = /** * 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 DittoStore.observeAsFlow( query: String, args: Map = emptyMap() -): Flow> = 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()) } - signalNext() - } - awaitClose { observer.close() } +): Flow> = observe(query, args) { result -> + result.decodeOrSkip() } diff --git a/Android/app/src/main/java/live/ditto/pos/core/data/demo/DemoSeeder.kt b/Android/app/src/main/java/live/ditto/pos/core/data/demo/DemoSeeder.kt index e3a8a6d..e225f3c 100644 --- a/Android/app/src/main/java/live/ditto/pos/core/data/demo/DemoSeeder.kt +++ b/Android/app/src/main/java/live/ditto/pos/core/data/demo/DemoSeeder.kt @@ -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 @@ -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}") } diff --git a/Android/app/src/main/java/live/ditto/pos/core/data/repository/LocationsRepository.kt b/Android/app/src/main/java/live/ditto/pos/core/data/repository/LocationsRepository.kt index 28f17c6..3bdcbfe 100644 --- a/Android/app/src/main/java/live/ditto/pos/core/data/repository/LocationsRepository.kt +++ b/Android/app/src/main/java/live/ditto/pos/core/data/repository/LocationsRepository.kt @@ -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 diff --git a/Android/app/src/main/java/live/ditto/pos/core/data/repository/OrdersRepository.kt b/Android/app/src/main/java/live/ditto/pos/core/data/repository/OrdersRepository.kt index 1cd36f1..0ad8b5e 100644 --- a/Android/app/src/main/java/live/ditto/pos/core/data/repository/OrdersRepository.kt +++ b/Android/app/src/main/java/live/ditto/pos/core/data/repository/OrdersRepository.kt @@ -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 @@ -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 @@ -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) { @@ -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) { @@ -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() { @@ -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) { diff --git a/Android/app/src/main/java/live/ditto/pos/core/data/repository/SaleItemsRepository.kt b/Android/app/src/main/java/live/ditto/pos/core/data/repository/SaleItemsRepository.kt index cc2f375..8f0d14d 100644 --- a/Android/app/src/main/java/live/ditto/pos/core/data/repository/SaleItemsRepository.kt +++ b/Android/app/src/main/java/live/ditto/pos/core/data/repository/SaleItemsRepository.kt @@ -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 @@ -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 diff --git a/Android/app/src/main/java/live/ditto/pos/core/domain/usecase/ditto/GetDittoInstanceUseCase.kt b/Android/app/src/main/java/live/ditto/pos/core/domain/usecase/ditto/GetDittoInstanceUseCase.kt index 73c24fa..66df467 100644 --- a/Android/app/src/main/java/live/ditto/pos/core/domain/usecase/ditto/GetDittoInstanceUseCase.kt +++ b/Android/app/src/main/java/live/ditto/pos/core/domain/usecase/ditto/GetDittoInstanceUseCase.kt @@ -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 diff --git a/Android/app/src/main/java/live/ditto/pos/core/presentation/viewmodel/MainViewModel.kt b/Android/app/src/main/java/live/ditto/pos/core/presentation/viewmodel/MainViewModel.kt index f0635b9..65ae227 100644 --- a/Android/app/src/main/java/live/ditto/pos/core/presentation/viewmodel/MainViewModel.kt +++ b/Android/app/src/main/java/live/ditto/pos/core/presentation/viewmodel/MainViewModel.kt @@ -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 diff --git a/Android/app/src/main/java/live/ditto/pos/di/AppModule.kt b/Android/app/src/main/java/live/ditto/pos/di/AppModule.kt index a64858b..a74d9b4 100644 --- a/Android/app/src/main/java/live/ditto/pos/di/AppModule.kt +++ b/Android/app/src/main/java/live/ditto/pos/di/AppModule.kt @@ -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 diff --git a/Android/app/src/main/java/live/ditto/pos/di/Qualifiers.kt b/Android/app/src/main/java/live/ditto/pos/di/Qualifiers.kt index e5f4246..a1655b5 100644 --- a/Android/app/src/main/java/live/ditto/pos/di/Qualifiers.kt +++ b/Android/app/src/main/java/live/ditto/pos/di/Qualifiers.kt @@ -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 diff --git a/Android/ditto-wrapper/src/main/java/live/ditto/ditto_wrapper/DittoManager.kt b/Android/ditto-wrapper/src/main/java/live/ditto/ditto_wrapper/DittoManager.kt index 3304efb..e13e603 100644 --- a/Android/ditto-wrapper/src/main/java/live/ditto/ditto_wrapper/DittoManager.kt +++ b/Android/ditto-wrapper/src/main/java/live/ditto/ditto_wrapper/DittoManager.kt @@ -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 @@ -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 @@ -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 { @@ -85,4 +94,4 @@ class DittoManager( } } -class DittoNotCreatedException : Throwable("Ditto cannot be null") \ No newline at end of file +class DittoNotCreatedException : Throwable("Ditto cannot be null") diff --git a/Android/gradle/libs.versions.toml b/Android/gradle/libs.versions.toml index 9a3a0bc..b0fdd85 100644 --- a/Android/gradle/libs.versions.toml +++ b/Android/gradle/libs.versions.toml @@ -4,8 +4,8 @@ agp = "8.9.3" composeBom = "2025.06.00" coreKtx = "1.16.0" datastorePreferences = "1.1.7" -ditto = "4.14.3" -ditto-tools = "5.0.2" +ditto = "5.0.2" +ditto-tools = "6.0.1-rc.1" espressoCore = "3.6.1" hiltAndroid = "2.56.1" hiltNavigationCompose = "1.2.0" @@ -40,7 +40,7 @@ androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtim androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" } androidx-material-icons-extended-android = { module = "androidx.compose.material:material-icons-extended-android", version.ref = "materialIconsExtendedAndroid" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } -ditto = { module = "live.ditto:ditto", version.ref = "ditto" } +ditto = { module = "com.ditto:ditto-kotlin", version.ref = "ditto" } ditto-tools = { module = "live.ditto:ditto-tools-android", version.ref = "ditto-tools" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hiltAndroid" } diff --git a/iOS/.env.template b/iOS/.env.template index 1aa7139..b86992a 100644 --- a/iOS/.env.template +++ b/iOS/.env.template @@ -1,3 +1,3 @@ -DITTO_APP_ID= -DITTO_PLAYGROUND_TOKEN= -DITTO_WEBSOCKET_URL= +DITTO_DATABASE_ID= +DITTO_DEVELOPMENT_TOKEN= +DITTO_URL= diff --git a/iOS/DittoPOS.xcodeproj/project.pbxproj b/iOS/DittoPOS.xcodeproj/project.pbxproj index 92b656b..6427261 100644 --- a/iOS/DittoPOS.xcodeproj/project.pbxproj +++ b/iOS/DittoPOS.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 07AE54C62C81812600E1FCBD /* DittoObjC in Frameworks */ = {isa = PBXBuildFile; productRef = 07AE54C52C81812600E1FCBD /* DittoObjC */; }; 07AE54C82C81812600E1FCBD /* DittoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 07AE54C72C81812600E1FCBD /* DittoSwift */; }; 07AE54CB2C81814400E1FCBD /* DittoAllToolsMenu in Frameworks */ = {isa = PBXBuildFile; productRef = 07AE54CA2C81814400E1FCBD /* DittoAllToolsMenu */; }; @@ -124,7 +123,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 07AE54C62C81812600E1FCBD /* DittoObjC in Frameworks */, 07AE54C82C81812600E1FCBD /* DittoSwift in Frameworks */, 07AE54CB2C81814400E1FCBD /* DittoAllToolsMenu in Frameworks */, ); @@ -308,7 +306,6 @@ ); name = DittoPOS; packageProductDependencies = ( - 07AE54C52C81812600E1FCBD /* DittoObjC */, 07AE54C72C81812600E1FCBD /* DittoSwift */, 07AE54CA2C81814400E1FCBD /* DittoAllToolsMenu */, ); @@ -641,13 +638,13 @@ MARKETING_VERSION = 2.0.0; PRODUCT_BUNDLE_IDENTIFIER = "live.ditto.skyservice-pos.crew"; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,3"; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -680,13 +677,13 @@ MARKETING_VERSION = 2.0.0; PRODUCT_BUNDLE_IDENTIFIER = "live.ditto.skyservice-pos.crew"; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,3"; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -706,15 +703,14 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; STRING_CATALOG_GENERATE_SYMBOLS = NO; - SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,3"; + TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DittoPOS.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/DittoPOS"; - TVOS_DEPLOYMENT_TARGET = 16.0; }; name = Debug; }; @@ -734,14 +730,13 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; STRING_CATALOG_GENERATE_SYMBOLS = NO; - SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,3"; + TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DittoPOS.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/DittoPOS"; - TVOS_DEPLOYMENT_TARGET = 16.0; }; name = Release; }; @@ -782,26 +777,21 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/getditto/DittoSwiftPackage"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 4.14.3; + kind = upToNextMinorVersion; + minimumVersion = 5.0.2; }; }; 07AE54C92C81814400E1FCBD /* XCRemoteSwiftPackageReference "DittoSwiftTools" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/getditto/DittoSwiftTools"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 9.0.1; + kind = exactVersion; + version = "10.0.0-rc.1"; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 07AE54C52C81812600E1FCBD /* DittoObjC */ = { - isa = XCSwiftPackageProductDependency; - package = 07AE54C42C81812600E1FCBD /* XCRemoteSwiftPackageReference "DittoSwiftPackage" */; - productName = DittoObjC; - }; 07AE54C72C81812600E1FCBD /* DittoSwift */ = { isa = XCSwiftPackageProductDependency; package = 07AE54C42C81812600E1FCBD /* XCRemoteSwiftPackageReference "DittoSwiftPackage" */; diff --git a/iOS/DittoPOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/iOS/DittoPOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9271848..5d84c62 100644 --- a/iOS/DittoPOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/iOS/DittoPOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,22 +1,13 @@ { "originHash" : "813f35598a8fa9c931a22f18a3f20117f2e42931ac2373c546c2e0f4df7e2d65", "pins" : [ - { - "identity" : "dittopresenceviewercore", - "kind" : "remoteSourceControl", - "location" : "https://github.com/getditto/DittoPresenceViewerCore.git", - "state" : { - "revision" : "4e8a4f131c21926194ac1dfad2333d9a6aef368a", - "version" : "2.1.2" - } - }, { "identity" : "dittoswiftpackage", "kind" : "remoteSourceControl", "location" : "https://github.com/getditto/DittoSwiftPackage", "state" : { - "revision" : "61b203cffc6a85c1c0d0987d1833d62eca7c549c", - "version" : "4.14.3" + "revision" : "1b18de5b8ea4c9148531af0a23407d0ef687b78d", + "version" : "5.0.2" } }, { @@ -24,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getditto/DittoSwiftTools", "state" : { - "revision" : "a9ed218a3e4ec29f53e690fa332086a94a3fe395", - "version" : "9.0.1" + "revision" : "66b1b8bc38d2c97d9882332d1be7115363a201db", + "version" : "10.0.0-rc.1" } }, { diff --git a/iOS/DittoPOS/Data/DittoManager.swift b/iOS/DittoPOS/Data/DittoManager.swift index d7f6f4d..2bdd6ce 100644 --- a/iOS/DittoPOS/Data/DittoManager.swift +++ b/iOS/DittoPOS/Data/DittoManager.swift @@ -7,6 +7,7 @@ import Combine import DittoSwift +import Foundation /// Owns the `Ditto` instance and starts sync. Holds transport-level /// configuration including the sync group and routing hint, both of which @@ -17,42 +18,52 @@ final class DittoManager: ObservableObject { let ditto: Ditto private init() { - #if os(tvOS) - let directory: FileManager.SearchPathDirectory = .cachesDirectory - #else let directory: FileManager.SearchPathDirectory = .documentDirectory - #endif let persistenceDirURL = try? FileManager() .url(for: directory, in: .userDomainMask, appropriateFor: nil, create: true) .appendingPathComponent("ditto-pos-demo") - ditto = Ditto(identity: .onlinePlayground( - appID: Env.DITTO_APP_ID, - token: Env.DITTO_PLAYGROUND_TOKEN, - enableDittoCloudSync: false - ), persistenceDirectory: persistenceDirURL) - - ditto.updateTransportConfig { transportConfig in - transportConfig.connect.webSocketURLs.insert(Env.DITTO_WEBSOCKET_URL) + guard let serverURL = URL(string: Env.DITTO_URL) else { + fatalError("Invalid DITTO_URL: \"\(Env.DITTO_URL)\"") } + DittoLogger.minimumLogLevel = .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 + let config = DittoConfig( + databaseID: Env.DITTO_DATABASE_ID, + connect: .server(url: serverURL), + persistenceDirectory: persistenceDirURL + ) + do { - try ditto.disableSyncWithV3() + ditto = try Ditto.openSync(config: config) } catch { - print("ERROR: disableSyncWithV3() failed: \(error)") + fatalError("Failed to open Ditto: \(error)") + } + + // Authenticate before sync starts: provide a fresh token whenever the + // current one is missing or near expiry. + ditto.auth?.expirationHandler = { ditto, _ in + ditto.auth?.login(token: Env.DITTO_DEVELOPMENT_TOKEN, provider: .development) { _, error in + if let error { + print("ERROR: Ditto auth login failed: \(error)") + } + } } Task { - do { - // strict mode off lets DQL use map/object CRDT semantics - try await ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = false") - let isPreview: Bool = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" - if !isPreview { + let isPreview: Bool = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" + if !isPreview { + do { try ditto.sync.start() + } catch { + print("ERROR: starting sync failed: \(error)") } - } catch { - print("ERROR: starting sync failed: \(error)") } } } diff --git a/iOS/DittoPOS/Data/Repositories/OrdersRepository.swift b/iOS/DittoPOS/Data/Repositories/OrdersRepository.swift index cd83be3..6f55f20 100644 --- a/iOS/DittoPOS/Data/Repositories/OrdersRepository.swift +++ b/iOS/DittoPOS/Data/Repositories/OrdersRepository.swift @@ -7,6 +7,7 @@ import Combine import DittoSwift +import Foundation /// Owns the `pos_orders` collection: registers the per-location subscription, /// observes the synced documents, runs DQL mutations, and gates startup diff --git a/iOS/DittoPOS/KDS/KDSOrderView.swift b/iOS/DittoPOS/KDS/KDSOrderView.swift index f7f2ee2..850fee5 100644 --- a/iOS/DittoPOS/KDS/KDSOrderView.swift +++ b/iOS/DittoPOS/KDS/KDSOrderView.swift @@ -73,16 +73,6 @@ struct KDSOrderView: View { .frame(height: 35) .frame(maxWidth: .infinity) .background(viewModel.statusColor) - #if os(tvOS) - Spacer() - Button(action: { - viewModel.incrementOrderStatus() - }, label: { - Text("clear \(viewModel.statusTitle)") - .font(.caption) - }) - .padding(.horizontal) - #endif } .padding(4) .onTapGesture { diff --git a/iOS/DittoPOS/KDS/KDSOrdersGridView.swift b/iOS/DittoPOS/KDS/KDSOrdersGridView.swift index 23e3956..5004658 100644 --- a/iOS/DittoPOS/KDS/KDSOrdersGridView.swift +++ b/iOS/DittoPOS/KDS/KDSOrdersGridView.swift @@ -11,11 +11,7 @@ struct KDSOrdersGridView: View { @Environment(\.colorScheme) private var colorScheme @StateObject private var viewModel: KDSViewModel let ordersRepository: OrdersRepository - #if os(tvOS) - @State var columns = [GridItem(.adaptive(minimum: 300), alignment: .top)] - #else @State var columns = [GridItem(.adaptive(minimum: 172), alignment: .top)] - #endif init(ordersRepository: OrdersRepository) { self.ordersRepository = ordersRepository diff --git a/iOS/DittoPOS/Locations/LocationsView.swift b/iOS/DittoPOS/Locations/LocationsView.swift index 05c4f9c..3ad1f82 100644 --- a/iOS/DittoPOS/Locations/LocationsView.swift +++ b/iOS/DittoPOS/Locations/LocationsView.swift @@ -31,8 +31,6 @@ struct LocationsView: View { Spacer() } .navigationBarTitle("Locations") - #if !os(tvOS) .navigationBarTitleDisplayMode(.inline) - #endif } } diff --git a/iOS/DittoPOS/MainView.swift b/iOS/DittoPOS/MainView.swift index 8a110c5..5e321ae 100644 --- a/iOS/DittoPOS/MainView.swift +++ b/iOS/DittoPOS/MainView.swift @@ -62,9 +62,7 @@ struct MainView: View { } } .navigationTitle(viewModel.mainTitle) - #if !os(tvOS) .navigationBarTitleDisplayMode(.inline) - #endif .navigationViewStyle(StackNavigationViewStyle()) .onAppear { viewModel.ensureLocationSelected() } } diff --git a/iOS/DittoPOS/POS/POSGridView.swift b/iOS/DittoPOS/POS/POSGridView.swift index 728abac..0478105 100644 --- a/iOS/DittoPOS/POS/POSGridView.swift +++ b/iOS/DittoPOS/POS/POSGridView.swift @@ -15,24 +15,6 @@ struct POSGridView: View { var body: some View { NavigationView { ScrollView(showsIndicators: false) { - #if os(tvOS) - LazyVGrid(columns: [GridItem(), GridItem(), GridItem()], spacing: 10) { - ForEach(viewModel.saleItems, id: \.self) { item in - Button(action: { - viewModel.addOrderItem(item) - }, label: { - VStack { - Image(ImageNameMapping.assetName(for: item.imageName)) - .resizable() - .scaledToFit() - .frame(width: 200, height: 200) - Text(item.name) - .font(.body) - } - }) - } - } - #else LazyVGrid(columns: layoutViewModel.gridColumns(for: horizontalSizeClass)) { ForEach(viewModel.saleItems, id: \.self) { item in let side = layoutViewModel.itemSide(for: horizontalSizeClass) @@ -44,7 +26,6 @@ struct POSGridView: View { } } .padding(.vertical, 16) - #endif } } } diff --git a/iOS/DittoPOS/POS/POSOrderView.swift b/iOS/DittoPOS/POS/POSOrderView.swift index dec4042..4bab149 100644 --- a/iOS/DittoPOS/POS/POSOrderView.swift +++ b/iOS/DittoPOS/POS/POSOrderView.swift @@ -30,11 +30,9 @@ struct POSOrderView: View { withAnimation { proxy.scrollTo(last, anchor: .top) } } } - #if !os(tvOS) .onRotate { _ in withAnimation { scrollToBottom(proxy: proxy) } } - #endif } } } diff --git a/iOS/DittoPOS/POS/POSView.swift b/iOS/DittoPOS/POS/POSView.swift index 4b2346e..10235de 100644 --- a/iOS/DittoPOS/POS/POSView.swift +++ b/iOS/DittoPOS/POS/POSView.swift @@ -17,11 +17,7 @@ import SwiftUI func updateWidths() { menuViewWidth = .screenWidth * 0.56 - #if os(tvOS) - orderViewWidth = .screenWidth * 0.30 - #else orderViewWidth = .screenWidth * 0.40 - #endif } /// Sale-item tile side length, sized to the current horizontal size class. @@ -65,13 +61,11 @@ struct POSView: View { } .environmentObject(viewModel) .environmentObject(layoutViewModel) - #if !os(tvOS) .onRotate { orient in guard orient.isLandscape || orient.isPortrait else { return } DispatchQueue.main.async { layoutViewModel.updateWidths() } } - #endif } } diff --git a/iOS/DittoPOS/POS/POSViewModel.swift b/iOS/DittoPOS/POS/POSViewModel.swift index b511bc7..05d2d83 100644 --- a/iOS/DittoPOS/POS/POSViewModel.swift +++ b/iOS/DittoPOS/POS/POSViewModel.swift @@ -39,21 +39,15 @@ import SwiftUI /// when the device is in a transitional orientation — in that case the /// view should skip the scroll. func scrollToBottomConfig() -> (id: String, delay: TimeInterval)? { - #if !os(tvOS) let orientation = UIDevice.current.orientation guard orientation.isLandscape || orientation.isPortrait else { return nil } - #endif guard let last = orderItems.last?.id else { return nil } - #if os(tvOS) - return (last, 0.5) - #else let needsDelay = UIScreen.isPortrait && UIDevice.current.orientation.isLandscape && orderItems.count > 4 return (last, needsDelay ? 0.5 : 0.0) - #endif } private let ordersRepository: OrdersRepository diff --git a/iOS/DittoPOS/Utilities/Utils.swift b/iOS/DittoPOS/Utilities/Utils.swift index 209bd29..3e1e8fc 100644 --- a/iOS/DittoPOS/Utilities/Utils.swift +++ b/iOS/DittoPOS/Utilities/Utils.swift @@ -35,7 +35,6 @@ extension Color { static let gray5 = Color("systemGray5", bundle: .main) } -#if !os(tvOS) extension View { func onRotate(perform action: @escaping (UIDeviceOrientation) -> Void) -> some View { self.modifier(DeviceRotationViewModifier(action: action)) @@ -83,7 +82,6 @@ extension UIDeviceOrientation: CustomStringConvertible { } } } -#endif // https://www.swiftbysundell.com/articles/reducers-in-swift/ extension Sequence {