Browse Source

Increment version to 1.2.1, enhance Umami analytics integration by adding user identification functionality, and refactor related components. Update MainApplication to initialize analytics and identify users on startup. Improve analytics repository and use cases to support user identification. Adjust UI components in Dashboard and Settings screens to track page views when visible. Refactor dependency injection for better organization.

Carles Sentis 20 hours ago
parent
commit
6b25fafe1b
25 changed files with 304 additions and 123 deletions
  1. 2 2
      app/build.gradle.kts
  2. 0 1
      app/src/main/java/com/codeskraps/publicpool/MainActivity.kt
  3. 17 1
      app/src/main/java/com/codeskraps/publicpool/MainApplication.kt
  4. 142 7
      app/src/main/java/com/codeskraps/publicpool/data/remote/UmamiAnalyticsDataSource.kt
  5. 4 0
      app/src/main/java/com/codeskraps/publicpool/data/repository/AnalyticsRepositoryImpl.kt
  6. 4 5
      app/src/main/java/com/codeskraps/publicpool/di/AppModule.kt
  7. 25 10
      app/src/main/java/com/codeskraps/publicpool/di/DataModule.kt
  8. 22 12
      app/src/main/java/com/codeskraps/publicpool/di/DomainModule.kt
  9. 5 31
      app/src/main/java/com/codeskraps/publicpool/di/PresentationModule.kt
  10. 1 0
      app/src/main/java/com/codeskraps/publicpool/domain/repository/AnalyticsRepository.kt
  11. 11 0
      app/src/main/java/com/codeskraps/publicpool/domain/usecase/IdentifyUserUseCase.kt
  12. 7 8
      app/src/main/java/com/codeskraps/publicpool/presentation/dashboard/DashboardContent.kt
  13. 1 0
      app/src/main/java/com/codeskraps/publicpool/presentation/dashboard/DashboardMvi.kt
  14. 8 8
      app/src/main/java/com/codeskraps/publicpool/presentation/dashboard/DashboardScreenModel.kt
  15. 2 2
      app/src/main/java/com/codeskraps/publicpool/presentation/navigation/BottomTabs.kt
  16. 5 0
      app/src/main/java/com/codeskraps/publicpool/presentation/settings/SettingsContent.kt
  17. 1 0
      app/src/main/java/com/codeskraps/publicpool/presentation/settings/SettingsMvi.kt
  18. 17 1
      app/src/main/java/com/codeskraps/publicpool/presentation/settings/SettingsScreenModel.kt
  19. 10 22
      app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletContent.kt
  20. 2 1
      app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletMvi.kt
  21. 6 6
      app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletScreenModel.kt
  22. 1 0
      app/src/main/java/com/codeskraps/publicpool/presentation/workers/WorkersMvi.kt
  23. 5 0
      app/src/main/java/com/codeskraps/publicpool/presentation/workers/WorkersScreen.kt
  24. 5 5
      app/src/main/java/com/codeskraps/publicpool/presentation/workers/WorkersScreenModel.kt
  25. 1 1
      gradle/libs.versions.toml

+ 2 - 2
app/build.gradle.kts

@@ -15,8 +15,8 @@ android {
         applicationId = "com.codeskraps.publicpool"
         applicationId = "com.codeskraps.publicpool"
         minSdk = 26
         minSdk = 26
         targetSdk = 35
         targetSdk = 35
-        versionCode = 3
-        versionName = "1.2"
+        versionCode = 4
+        versionName = "1.2.1"
 
 
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
         vectorDrawables {
         vectorDrawables {

+ 0 - 1
app/src/main/java/com/codeskraps/publicpool/MainActivity.kt

@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.HorizontalDivider
 import androidx.compose.material3.HorizontalDivider
 import androidx.compose.material3.Icon
 import androidx.compose.material3.Icon

+ 17 - 1
app/src/main/java/com/codeskraps/publicpool/MainApplication.kt

@@ -5,10 +5,14 @@ import com.codeskraps.publicpool.di.appModule
 import com.codeskraps.publicpool.di.dataModule
 import com.codeskraps.publicpool.di.dataModule
 import com.codeskraps.publicpool.di.domainModule
 import com.codeskraps.publicpool.di.domainModule
 import com.codeskraps.publicpool.di.presentationModule
 import com.codeskraps.publicpool.di.presentationModule
+import com.codeskraps.publicpool.domain.usecase.GetWalletAddressUseCase
+import com.codeskraps.publicpool.domain.usecase.IdentifyUserUseCase
 import com.codeskraps.publicpool.domain.usecase.InitializeAnalyticsUseCase
 import com.codeskraps.publicpool.domain.usecase.InitializeAnalyticsUseCase
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import org.koin.android.ext.koin.androidContext
 import org.koin.android.ext.koin.androidContext
 import org.koin.android.ext.koin.androidLogger
 import org.koin.android.ext.koin.androidLogger
@@ -20,6 +24,8 @@ class MainApplication : Application() {
     
     
     private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
     private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
     private val initializeAnalyticsUseCase: InitializeAnalyticsUseCase by inject(InitializeAnalyticsUseCase::class.java)
     private val initializeAnalyticsUseCase: InitializeAnalyticsUseCase by inject(InitializeAnalyticsUseCase::class.java)
+    private val getWalletAddressUseCase: GetWalletAddressUseCase by inject(GetWalletAddressUseCase::class.java)
+    private val identifyUserUseCase: IdentifyUserUseCase by inject(IdentifyUserUseCase::class.java)
     
     
     override fun onCreate() {
     override fun onCreate() {
         super.onCreate()
         super.onCreate()
@@ -36,9 +42,19 @@ class MainApplication : Application() {
             )
             )
         }
         }
         
         
-        // Initialize analytics
+        // Initialize analytics and identify user
         applicationScope.launch {
         applicationScope.launch {
+            // First initialize analytics
             initializeAnalyticsUseCase()
             initializeAnalyticsUseCase()
+            
+            // Then retrieve wallet address and identify user if available
+            getWalletAddressUseCase()
+                .catch { /* Ignore errors during startup */ }
+                .firstOrNull()
+                ?.let { walletAddress ->
+                    // Identify user with their wallet address
+                    identifyUserUseCase(walletAddress)
+                }
         }
         }
     }
     }
 } 
 } 

+ 142 - 7
app/src/main/java/com/codeskraps/publicpool/data/remote/UmamiAnalyticsDataSource.kt

@@ -1,12 +1,30 @@
 package com.codeskraps.publicpool.data.remote
 package com.codeskraps.publicpool.data.remote
 
 
+import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Context
 import android.webkit.WebView
 import android.webkit.WebView
 import androidx.webkit.WebViewClientCompat
 import androidx.webkit.WebViewClientCompat
+import com.codeskraps.publicpool.di.AppReadinessState
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
+import kotlinx.coroutines.suspendCancellableCoroutine
 
 
-class UmamiAnalyticsDataSource(private val context: Context) {
+data class UmamiConfig(
+    val scriptUrl: String,
+    val websiteId: String,
+    val baseUrl: String
+)
+
+@SuppressLint("SetJavaScriptEnabled")
+class UmamiAnalyticsDataSource(
+    private val context: Context,
+    private val appReadinessState: AppReadinessState,
+    private val config: UmamiConfig
+) {
+    
+    private var isInitialized = false
     
     
     private val webView: WebView by lazy {
     private val webView: WebView by lazy {
         WebView(context).apply {
         WebView(context).apply {
@@ -17,32 +35,149 @@ class UmamiAnalyticsDataSource(private val context: Context) {
     }
     }
     
     
     private val umamiScript = """
     private val umamiScript = """
-        <script defer src="https://umami.codeskraps.com/script.js" data-website-id="b3e6309f-9724-48e5-a1c6-11757de3fe83"></script>
+        <script defer src="${config.scriptUrl}" data-website-id="${config.websiteId}"></script>
     """.trimIndent()
     """.trimIndent()
     
     
+    @OptIn(ExperimentalCoroutinesApi::class)
     suspend fun initialize() = withContext(Dispatchers.Main) {
     suspend fun initialize() = withContext(Dispatchers.Main) {
+        if (isInitialized) return@withContext
+        
         webView.loadDataWithBaseURL(
         webView.loadDataWithBaseURL(
-            "https://umami.codeskraps.com",
+            config.baseUrl,
             "<html><head>$umamiScript</head><body></body></html>",
             "<html><head>$umamiScript</head><body></body></html>",
             "text/html",
             "text/html",
             "UTF-8",
             "UTF-8",
             null
             null
         )
         )
+        
+        // Check if Umami is ready by evaluating JavaScript
+        var attempts = 0
+        val maxAttempts = 10 // Maximum number of attempts
+        
+        while (attempts < maxAttempts) {
+            try {
+                val isUmamiReady = suspendCancellableCoroutine { continuation ->
+                    webView.evaluateJavascript(
+                        """
+                        (function() {
+                            return typeof umami !== 'undefined';
+                        })();
+                        """.trimIndent()
+                    ) { result ->
+                        continuation.resume(result.toBooleanStrictOrNull() ?: false, null)
+                    }
+                }
+                
+                if (isUmamiReady) {
+                    isInitialized = true
+                    break
+                }
+            } catch (e: Exception) {
+                // Log error if needed
+            }
+            
+            attempts++
+            delay(200) // Short delay between checks
+        }
+        
+        // Set app as ready regardless of Umami state
+        // This ensures the app doesn't hang if analytics fails
+        appReadinessState.setReady()
     }
     }
     
     
     suspend fun trackPageView(pageName: String) = withContext(Dispatchers.Main) {
     suspend fun trackPageView(pageName: String) = withContext(Dispatchers.Main) {
+        if (!isInitialized) return@withContext
+        
         webView.evaluateJavascript(
         webView.evaluateJavascript(
-            "umami.trackView('$pageName')",
+            """
+            (function() {
+                if (typeof umami === 'undefined') {
+                    console.error('Umami is not defined');
+                    return false;
+                }
+                
+                try {
+                    // Format the page name as a proper URL path
+                    const path = '$pageName'.startsWith('/') ? '$pageName' : '/$pageName';
+                    // Create a title from the page name (capitalize first letter, replace dashes with spaces)
+                    const title = '$pageName'
+                        .replace(/-/g, ' ')
+                        .replace(/\\b\\w/g, l => l.toUpperCase());
+                    
+                    umami.track({ 
+                        url: path, 
+                        title: title,
+                        website: '${config.websiteId}'
+                    });
+                    console.log('Page view tracked:', path, 'with title:', title);
+                    return true;
+                } catch (e) {
+                    console.error('Error tracking page view:', e);
+                    return false;
+                }
+            })();
+            """,
             null
             null
         )
         )
     }
     }
     
     
     suspend fun trackEvent(eventName: String, eventData: Map<String, String> = emptyMap()) = withContext(Dispatchers.Main) {
     suspend fun trackEvent(eventName: String, eventData: Map<String, String> = emptyMap()) = withContext(Dispatchers.Main) {
-        val dataJson = eventData.entries.joinToString(",") { 
-            "\"${it.key}\": \"${it.value}\"" 
+        if (!isInitialized) return@withContext
+        
+        webView.evaluateJavascript(
+            """
+            (function() {
+                if (typeof umami === 'undefined') {
+                    console.error('Umami is not defined');
+                    return false;
+                }
+                
+                try {
+                    const data = ${eventData.entries.joinToString(",", "{", "}") { 
+                        "\"${it.key}\": \"${it.value}\"" 
+                    }};
+                    umami.track('$eventName', data, '${config.websiteId}');
+                    console.log('Event tracked:', '$eventName', data);
+                    return true;
+                } catch (e) {
+                    console.error('Error tracking event:', e);
+                    return false;
+                }
+            })();
+            """,
+            null
+        )
+    }
+    
+    suspend fun identifyUser(walletAddress: String?) = withContext(Dispatchers.Main) {
+        if (!isInitialized || walletAddress.isNullOrBlank()) return@withContext
+        
+        // Anonymize the address by using only the first and last 4 characters
+        val addressLength = walletAddress.length
+        val anonymizedId = if (addressLength > 8) {
+            "${walletAddress.take(4)}...${walletAddress.takeLast(4)}"
+        } else {
+            walletAddress
         }
         }
+        
         webView.evaluateJavascript(
         webView.evaluateJavascript(
-            "umami.trackEvent('$eventName', {$dataJson})",
+            """
+            (function() {
+                if (typeof umami === 'undefined') {
+                    console.error('Umami is not defined');
+                    return false;
+                }
+                
+                try {
+                    umami.identify({ wallet_id: '$anonymizedId' }, '${config.websiteId}');
+                    console.log('User identified:', '$anonymizedId');
+                    return true;
+                } catch (e) {
+                    console.error('Error identifying user:', e);
+                    return false;
+                }
+            })();
+            """,
             null
             null
         )
         )
     }
     }

+ 4 - 0
app/src/main/java/com/codeskraps/publicpool/data/repository/AnalyticsRepositoryImpl.kt

@@ -18,4 +18,8 @@ class AnalyticsRepositoryImpl(
     override suspend fun trackEvent(eventName: String, eventData: Map<String, String>) {
     override suspend fun trackEvent(eventName: String, eventData: Map<String, String>) {
         analyticsDataSource.trackEvent(eventName, eventData)
         analyticsDataSource.trackEvent(eventName, eventData)
     }
     }
+    
+    override suspend fun identifyUser(walletAddress: String?) {
+        analyticsDataSource.identifyUser(walletAddress)
+    }
 } 
 } 

+ 4 - 5
app/src/main/java/com/codeskraps/publicpool/di/AppModule.kt

@@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.preferencesDataStore
 import androidx.datastore.preferences.preferencesDataStore
 import org.koin.android.ext.koin.androidContext
 import org.koin.android.ext.koin.androidContext
+import org.koin.core.module.dsl.singleOf
 import org.koin.dsl.module
 import org.koin.dsl.module
 
 
 // Define DataStore instance at the top level
 // Define DataStore instance at the top level
@@ -13,10 +14,8 @@ val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "pu
 
 
 val appModule = module {
 val appModule = module {
     // Provide DataStore instance
     // Provide DataStore instance
-    single<DataStore<Preferences>> {
-        androidContext().dataStore
-    }
-
+    single<DataStore<Preferences>> { androidContext().dataStore }
+    
     // Provide AppReadinessState as a singleton
     // Provide AppReadinessState as a singleton
-    single { AppReadinessState() }
+    singleOf(::AppReadinessState)
 } 
 } 

+ 25 - 10
app/src/main/java/com/codeskraps/publicpool/di/DataModule.kt

@@ -3,16 +3,22 @@ package com.codeskraps.publicpool.di
 import com.codeskraps.publicpool.data.remote.KtorApiService
 import com.codeskraps.publicpool.data.remote.KtorApiService
 import com.codeskraps.publicpool.data.remote.KtorApiServiceImpl
 import com.codeskraps.publicpool.data.remote.KtorApiServiceImpl
 import com.codeskraps.publicpool.data.remote.UmamiAnalyticsDataSource
 import com.codeskraps.publicpool.data.remote.UmamiAnalyticsDataSource
+import com.codeskraps.publicpool.data.remote.UmamiConfig
 import com.codeskraps.publicpool.data.repository.AnalyticsRepositoryImpl
 import com.codeskraps.publicpool.data.repository.AnalyticsRepositoryImpl
 import com.codeskraps.publicpool.data.repository.PublicPoolRepositoryImpl
 import com.codeskraps.publicpool.data.repository.PublicPoolRepositoryImpl
 import com.codeskraps.publicpool.domain.repository.AnalyticsRepository
 import com.codeskraps.publicpool.domain.repository.AnalyticsRepository
 import com.codeskraps.publicpool.domain.repository.PublicPoolRepository
 import com.codeskraps.publicpool.domain.repository.PublicPoolRepository
-import io.ktor.client.* // Ktor client
-import io.ktor.client.engine.android.* // Ktor Android engine
-import io.ktor.client.plugins.contentnegotiation.* // Ktor Content Negotiation
-import io.ktor.client.plugins.logging.* // Ktor Logging
-import io.ktor.serialization.kotlinx.json.* // Ktor Kotlinx Serialization
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.android.Android
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.client.plugins.logging.DEFAULT
+import io.ktor.client.plugins.logging.LogLevel
+import io.ktor.client.plugins.logging.Logger
+import io.ktor.client.plugins.logging.Logging
+import io.ktor.serialization.kotlinx.json.json
 import kotlinx.serialization.json.Json
 import kotlinx.serialization.json.Json
+import org.koin.core.module.dsl.singleOf
+import org.koin.dsl.bind
 import org.koin.dsl.module
 import org.koin.dsl.module
 
 
 val dataModule = module {
 val dataModule = module {
@@ -40,12 +46,21 @@ val dataModule = module {
     }
     }
 
 
     // API Service Implementation
     // API Service Implementation
-    single<KtorApiService> { KtorApiServiceImpl(client = get()) }
+    singleOf(::KtorApiServiceImpl) bind KtorApiService::class
 
 
     // Repository Implementation
     // Repository Implementation
-    single<PublicPoolRepository> { PublicPoolRepositoryImpl(apiService = get(), dataStore = get()) }
+    singleOf(::PublicPoolRepositoryImpl) bind PublicPoolRepository::class
 
 
-    // Analytics
-    single { UmamiAnalyticsDataSource(get()) }
-    single<AnalyticsRepository> { AnalyticsRepositoryImpl(get()) }
+    // Analytics Configuration
+    single { 
+        UmamiConfig(
+            scriptUrl = "https://umami.codeskraps.com/script.js",
+            websiteId = "b3e6309f-9724-48e5-a1c6-11757de3fe83",
+            baseUrl = "https://umami.codeskraps.com"
+        )
+    }
+
+    // Analytics Implementation
+    singleOf(::UmamiAnalyticsDataSource)
+    singleOf(::AnalyticsRepositoryImpl) bind AnalyticsRepository::class
 } 
 } 

+ 22 - 12
app/src/main/java/com/codeskraps/publicpool/di/DomainModule.kt

@@ -1,24 +1,34 @@
 package com.codeskraps.publicpool.di
 package com.codeskraps.publicpool.di
 
 
-import com.codeskraps.publicpool.domain.usecase.*
+import com.codeskraps.publicpool.domain.usecase.CalculateTwoHourAverageUseCase
+import com.codeskraps.publicpool.domain.usecase.GetBlockchainWalletInfoUseCase
+import com.codeskraps.publicpool.domain.usecase.GetBtcPriceUseCase
+import com.codeskraps.publicpool.domain.usecase.GetChartDataUseCase
+import com.codeskraps.publicpool.domain.usecase.GetClientInfoUseCase
+import com.codeskraps.publicpool.domain.usecase.GetNetworkInfoUseCase
+import com.codeskraps.publicpool.domain.usecase.GetWalletAddressUseCase
+import com.codeskraps.publicpool.domain.usecase.IdentifyUserUseCase
 import com.codeskraps.publicpool.domain.usecase.InitializeAnalyticsUseCase
 import com.codeskraps.publicpool.domain.usecase.InitializeAnalyticsUseCase
+import com.codeskraps.publicpool.domain.usecase.SaveWalletAddressUseCase
 import com.codeskraps.publicpool.domain.usecase.TrackEventUseCase
 import com.codeskraps.publicpool.domain.usecase.TrackEventUseCase
 import com.codeskraps.publicpool.domain.usecase.TrackPageViewUseCase
 import com.codeskraps.publicpool.domain.usecase.TrackPageViewUseCase
+import org.koin.core.module.dsl.factoryOf
 import org.koin.dsl.module
 import org.koin.dsl.module
 
 
 val domainModule = module {
 val domainModule = module {
     // Use Case providers
     // Use Case providers
-    factory { GetWalletAddressUseCase(repository = get()) }
-    factory { SaveWalletAddressUseCase(repository = get()) }
-    factory { GetNetworkInfoUseCase(repository = get()) }
-    factory { GetClientInfoUseCase(repository = get()) }
-    factory { GetChartDataUseCase(repository = get()) }
-    factory { CalculateTwoHourAverageUseCase() }
-    factory { GetBlockchainWalletInfoUseCase(repository = get()) }
-    factory { GetBtcPriceUseCase(repository = get()) }
+    factoryOf(::GetWalletAddressUseCase)
+    factoryOf(::SaveWalletAddressUseCase)
+    factoryOf(::GetNetworkInfoUseCase)
+    factoryOf(::GetClientInfoUseCase)
+    factoryOf(::GetChartDataUseCase)
+    factoryOf(::CalculateTwoHourAverageUseCase)
+    factoryOf(::GetBlockchainWalletInfoUseCase)
+    factoryOf(::GetBtcPriceUseCase)
     
     
     // Analytics
     // Analytics
-    factory { InitializeAnalyticsUseCase(get()) }
-    factory { TrackPageViewUseCase(get()) }
-    factory { TrackEventUseCase(get()) }
+    factoryOf(::InitializeAnalyticsUseCase)
+    factoryOf(::TrackPageViewUseCase)
+    factoryOf(::TrackEventUseCase)
+    factoryOf(::IdentifyUserUseCase)
 } 
 } 

+ 5 - 31
app/src/main/java/com/codeskraps/publicpool/di/PresentationModule.kt

@@ -4,38 +4,12 @@ import com.codeskraps.publicpool.presentation.dashboard.DashboardScreenModel
 import com.codeskraps.publicpool.presentation.settings.SettingsScreenModel
 import com.codeskraps.publicpool.presentation.settings.SettingsScreenModel
 import com.codeskraps.publicpool.presentation.wallet.WalletScreenModel // Import Wallet ScreenModel
 import com.codeskraps.publicpool.presentation.wallet.WalletScreenModel // Import Wallet ScreenModel
 import com.codeskraps.publicpool.presentation.workers.WorkersScreenModel // Import Worker ScreenModel
 import com.codeskraps.publicpool.presentation.workers.WorkersScreenModel // Import Worker ScreenModel
+import org.koin.core.module.dsl.factoryOf
 import org.koin.dsl.module
 import org.koin.dsl.module
 
 
 val presentationModule = module {
 val presentationModule = module {
-    // Voyager ScreenModels (similar to ViewModels)
-    factory {
-        DashboardScreenModel(
-            getWalletAddressUseCase = get(),
-            getNetworkInfoUseCase = get(),
-            getClientInfoUseCase = get(),
-            getChartDataUseCase = get(),
-            calculateTwoHourAverageUseCase = get(),
-            trackPageViewUseCase = get(),
-            trackEventUseCase = get(),
-            appReadinessState = get()
-        )
-    }
-    factory { SettingsScreenModel(get(), get()) } // Inject use cases
-    factory {
-        WorkersScreenModel(
-            getWalletAddressUseCase = get(),
-            getClientInfoUseCase = get(),
-            trackPageViewUseCase = get(),
-            trackEventUseCase = get()
-        )
-    }
-    factory {
-        WalletScreenModel(
-            getWalletAddressUseCase = get(),
-            getBlockchainWalletInfoUseCase = get(),
-            getBtcPriceUseCase = get(),
-            trackPageViewUseCase = get(),
-            trackEventUseCase = get()
-        )
-    }
+    factoryOf(::DashboardScreenModel)
+    factoryOf(::SettingsScreenModel)
+    factoryOf(::WorkersScreenModel)
+    factoryOf(::WalletScreenModel)
 } 
 } 

+ 1 - 0
app/src/main/java/com/codeskraps/publicpool/domain/repository/AnalyticsRepository.kt

@@ -4,4 +4,5 @@ interface AnalyticsRepository {
     suspend fun initialize()
     suspend fun initialize()
     suspend fun trackPageView(pageName: String)
     suspend fun trackPageView(pageName: String)
     suspend fun trackEvent(eventName: String, eventData: Map<String, String> = emptyMap())
     suspend fun trackEvent(eventName: String, eventData: Map<String, String> = emptyMap())
+    suspend fun identifyUser(walletAddress: String?)
 } 
 } 

+ 11 - 0
app/src/main/java/com/codeskraps/publicpool/domain/usecase/IdentifyUserUseCase.kt

@@ -0,0 +1,11 @@
+package com.codeskraps.publicpool.domain.usecase
+
+import com.codeskraps.publicpool.domain.repository.AnalyticsRepository
+
+class IdentifyUserUseCase(
+    private val analyticsRepository: AnalyticsRepository
+) {
+    suspend operator fun invoke(walletAddress: String?) {
+        analyticsRepository.identifyUser(walletAddress)
+    }
+} 

+ 7 - 8
app/src/main/java/com/codeskraps/publicpool/presentation/dashboard/DashboardContent.kt

@@ -32,19 +32,16 @@ import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.Text
 import androidx.compose.material3.Text
 import androidx.compose.material3.TextButton
 import androidx.compose.material3.TextButton
 import androidx.compose.material3.TopAppBar
 import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.pulltorefresh.PullToRefreshBox
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
@@ -68,12 +65,9 @@ import com.codeskraps.publicpool.presentation.navigation.getParentOrSelf
 import com.codeskraps.publicpool.ui.theme.PositiveGreen
 import com.codeskraps.publicpool.ui.theme.PositiveGreen
 import com.codeskraps.publicpool.util.formatHashRate
 import com.codeskraps.publicpool.util.formatHashRate
 import com.codeskraps.publicpool.util.formatLargeNumber
 import com.codeskraps.publicpool.util.formatLargeNumber
-import com.codeskraps.publicpool.util.formatLargeNumber
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.collectLatest
 import java.text.NumberFormat
 import java.text.NumberFormat
 import java.util.Locale
 import java.util.Locale
-import androidx.compose.material3.pulltorefresh.PullToRefreshBox
-import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
 
 
 @OptIn(ExperimentalMaterial3Api::class)
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 @Composable
@@ -84,6 +78,11 @@ fun DashboardContent(screenModel: DashboardScreenModel) {
     val navigator = LocalNavigator.currentOrThrow
     val navigator = LocalNavigator.currentOrThrow
     val snackbarHostState = remember { SnackbarHostState() }
     val snackbarHostState = remember { SnackbarHostState() }
     
     
+    // Track page view when screen becomes visible
+    LaunchedEffect(Unit) {
+        screenModel.handleEvent(DashboardEvent.OnScreenVisible)
+    }
+    
     // Handle effects (navigation, snackbars)
     // Handle effects (navigation, snackbars)
     LaunchedEffect(key1 = screenModel.effect) {
     LaunchedEffect(key1 = screenModel.effect) {
         screenModel.effect.collectLatest { effect ->
         screenModel.effect.collectLatest { effect ->
@@ -289,7 +288,7 @@ fun InfoCard(
                             fontWeight = FontWeight.Bold,
                             fontWeight = FontWeight.Bold,
                             color = MaterialTheme.colorScheme.onSurface
                             color = MaterialTheme.colorScheme.onSurface
                         )
                         )
-                        if (secondaryValue != null && secondaryValue.isNotEmpty()) {
+                        if (!secondaryValue.isNullOrEmpty()) {
                             Text(
                             Text(
                                 text = secondaryValue,
                                 text = secondaryValue,
                                 style = MaterialTheme.typography.bodySmall,
                                 style = MaterialTheme.typography.bodySmall,

+ 1 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/dashboard/DashboardMvi.kt

@@ -42,6 +42,7 @@ sealed interface DashboardEvent : UiEvent {
     data class NetworkInfoResult(val result: Result<NetworkInfo>) : DashboardEvent // Internal event
     data class NetworkInfoResult(val result: Result<NetworkInfo>) : DashboardEvent // Internal event
     data class ClientInfoResult(val result: Result<ClientInfo>) : DashboardEvent // Internal event
     data class ClientInfoResult(val result: Result<ClientInfo>) : DashboardEvent // Internal event
     data class ChartDataResult(val result: Result<List<ChartDataPoint>>) : DashboardEvent // Internal event
     data class ChartDataResult(val result: Result<List<ChartDataPoint>>) : DashboardEvent // Internal event
+    data object OnScreenVisible : DashboardEvent
 }
 }
 
 
 // --- Effects ---
 // --- Effects ---

+ 8 - 8
app/src/main/java/com/codeskraps/publicpool/presentation/dashboard/DashboardScreenModel.kt

@@ -33,11 +33,6 @@ class DashboardScreenModel(
     private var dataLoadingJob: Job? = null
     private var dataLoadingJob: Job? = null
 
 
     init {
     init {
-        // Track page view
-        screenModelScope.launch {
-            trackPageViewUseCase("Dashboard")
-        }
-        
         // Start loading data immediately
         // Start loading data immediately
         handleEvent(DashboardEvent.LoadData)
         handleEvent(DashboardEvent.LoadData)
     }
     }
@@ -59,6 +54,11 @@ class DashboardScreenModel(
                     trackEventUseCase("navigation", mapOf("to" to "settings", "from" to "dashboard"))
                     trackEventUseCase("navigation", mapOf("to" to "settings", "from" to "dashboard"))
                 }
                 }
             }
             }
+            DashboardEvent.OnScreenVisible -> {
+                screenModelScope.launch {
+                    trackPageViewUseCase("Dashboard")
+                }
+            }
             // Internal Events triggered by data loading flows/calls
             // Internal Events triggered by data loading flows/calls
             is DashboardEvent.WalletAddressLoaded -> processWalletAddress(event.address)
             is DashboardEvent.WalletAddressLoaded -> processWalletAddress(event.address)
             is DashboardEvent.NetworkInfoResult -> processNetworkInfoResult(event.result)
             is DashboardEvent.NetworkInfoResult -> processNetworkInfoResult(event.result)
@@ -89,7 +89,7 @@ class DashboardScreenModel(
         appReadinessState.setReady()
         appReadinessState.setReady()
 
 
         mutableState.update { it.copy(walletAddress = address, isWalletLoading = false) }
         mutableState.update { it.copy(walletAddress = address, isWalletLoading = false) }
-        if (address != null && address.isNotBlank()) {
+        if (!address.isNullOrBlank()) {
             // Wallet address available, fetch client-specific data
             // Wallet address available, fetch client-specific data
             fetchClientInfoAndChartData(address)
             fetchClientInfoAndChartData(address)
         } else {
         } else {
@@ -116,7 +116,7 @@ class DashboardScreenModel(
         fetchNetworkInfo()
         fetchNetworkInfo()
         state.value.walletAddress?.let { address ->
         state.value.walletAddress?.let { address ->
             if (address.isNotBlank()) {
             if (address.isNotBlank()) {
-                fetchClientInfoAndChartData(address, isRefresh = true)
+                fetchClientInfoAndChartData(address)
             }
             }
         }
         }
     }
     }
@@ -129,7 +129,7 @@ class DashboardScreenModel(
         }
         }
     }
     }
 
 
-    private fun fetchClientInfoAndChartData(address: String, isRefresh: Boolean = false) {
+    private fun fetchClientInfoAndChartData(address: String) {
         dataLoadingJob?.cancel() // Cancel previous loads before starting new ones
         dataLoadingJob?.cancel() // Cancel previous loads before starting new ones
         dataLoadingJob = screenModelScope.launch {
         dataLoadingJob = screenModelScope.launch {
             mutableState.update {
             mutableState.update {

+ 2 - 2
app/src/main/java/com/codeskraps/publicpool/presentation/navigation/BottomTabs.kt

@@ -10,7 +10,7 @@ import androidx.compose.ui.res.stringResource
 import cafe.adriel.voyager.navigator.tab.Tab
 import cafe.adriel.voyager.navigator.tab.Tab
 import cafe.adriel.voyager.navigator.tab.TabOptions
 import cafe.adriel.voyager.navigator.tab.TabOptions
 import com.codeskraps.publicpool.R
 import com.codeskraps.publicpool.R
-import com.codeskraps.publicpool.presentation.wallet.WalletScreen
+import com.codeskraps.publicpool.presentation.wallet.WalletContent
 import com.codeskraps.publicpool.presentation.workers.WorkersScreen
 import com.codeskraps.publicpool.presentation.workers.WorkersScreen
 
 
 internal data object DashboardTab : Tab {
 internal data object DashboardTab : Tab {
@@ -75,6 +75,6 @@ internal data object WalletTab : Tab {
 
 
     @Composable
     @Composable
     override fun Content() {
     override fun Content() {
-        WalletScreen.Content()
+        WalletContent.Content()
     }
     }
 } 
 } 

+ 5 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/settings/SettingsContent.kt

@@ -50,6 +50,11 @@ fun SettingsContent(screenModel: SettingsScreenModel) {
     val navigator = LocalNavigator.currentOrThrow // Get the navigator
     val navigator = LocalNavigator.currentOrThrow // Get the navigator
     val focusManager = LocalFocusManager.current // Focus manager to hide keyboard
     val focusManager = LocalFocusManager.current // Focus manager to hide keyboard
 
 
+    // Track page view when screen becomes visible
+    LaunchedEffect(Unit) {
+        screenModel.handleEvent(SettingsEvent.OnScreenVisible)
+    }
+
     // Resolve strings needed inside LaunchedEffect here
     // Resolve strings needed inside LaunchedEffect here
     val walletSavedMessage = stringResource(R.string.settings_toast_wallet_saved)
     val walletSavedMessage = stringResource(R.string.settings_toast_wallet_saved)
 
 

+ 1 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/settings/SettingsMvi.kt

@@ -15,6 +15,7 @@ sealed interface SettingsEvent : UiEvent {
     data class WalletAddressChanged(val address: String) : SettingsEvent
     data class WalletAddressChanged(val address: String) : SettingsEvent
     data object SaveWalletAddress : SettingsEvent
     data object SaveWalletAddress : SettingsEvent
     data object LoadWalletAddress : SettingsEvent // To trigger initial load
     data object LoadWalletAddress : SettingsEvent // To trigger initial load
+    data object OnScreenVisible : SettingsEvent
 }
 }
 
 
 // --- Effects ---
 // --- Effects ---

+ 17 - 1
app/src/main/java/com/codeskraps/publicpool/presentation/settings/SettingsScreenModel.kt

@@ -3,14 +3,18 @@ package com.codeskraps.publicpool.presentation.settings
 import cafe.adriel.voyager.core.model.StateScreenModel
 import cafe.adriel.voyager.core.model.StateScreenModel
 import cafe.adriel.voyager.core.model.screenModelScope
 import cafe.adriel.voyager.core.model.screenModelScope
 import com.codeskraps.publicpool.domain.usecase.GetWalletAddressUseCase
 import com.codeskraps.publicpool.domain.usecase.GetWalletAddressUseCase
+import com.codeskraps.publicpool.domain.usecase.IdentifyUserUseCase
 import com.codeskraps.publicpool.domain.usecase.SaveWalletAddressUseCase
 import com.codeskraps.publicpool.domain.usecase.SaveWalletAddressUseCase
+import com.codeskraps.publicpool.domain.usecase.TrackPageViewUseCase
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.*
 import kotlinx.coroutines.flow.*
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 
 
 class SettingsScreenModel(
 class SettingsScreenModel(
     private val getWalletAddressUseCase: GetWalletAddressUseCase,
     private val getWalletAddressUseCase: GetWalletAddressUseCase,
-    private val saveWalletAddressUseCase: SaveWalletAddressUseCase
+    private val saveWalletAddressUseCase: SaveWalletAddressUseCase,
+    private val identifyUserUseCase: IdentifyUserUseCase,
+    private val trackPageViewUseCase: TrackPageViewUseCase
 ) : StateScreenModel<SettingsState>(SettingsState()) { // Initialize with default state
 ) : StateScreenModel<SettingsState>(SettingsState()) { // Initialize with default state
 
 
     private val _effect = Channel<SettingsEffect>()
     private val _effect = Channel<SettingsEffect>()
@@ -29,6 +33,11 @@ class SettingsScreenModel(
             }
             }
             SettingsEvent.SaveWalletAddress -> saveWalletAddress()
             SettingsEvent.SaveWalletAddress -> saveWalletAddress()
             SettingsEvent.LoadWalletAddress -> loadWalletAddress()
             SettingsEvent.LoadWalletAddress -> loadWalletAddress()
+            SettingsEvent.OnScreenVisible -> {
+                screenModelScope.launch {
+                    trackPageViewUseCase("Settings")
+                }
+            }
         }
         }
     }
     }
 
 
@@ -48,6 +57,9 @@ class SettingsScreenModel(
                             isLoading = false
                             isLoading = false
                         )
                         )
                     }
                     }
+                    
+                    // Identify user with the loaded wallet address
+                    identifyUserUseCase(address)
                 }
                 }
         }
         }
     }
     }
@@ -58,6 +70,10 @@ class SettingsScreenModel(
                 // Use the current address from the state, explicitly allowing blank addresses
                 // Use the current address from the state, explicitly allowing blank addresses
                 val addressToSave = mutableState.value.walletAddress
                 val addressToSave = mutableState.value.walletAddress
                 saveWalletAddressUseCase(addressToSave)
                 saveWalletAddressUseCase(addressToSave)
+                
+                // Identify user with the newly saved wallet address
+                identifyUserUseCase(addressToSave)
+                
                 _effect.send(SettingsEffect.WalletAddressSaved)
                 _effect.send(SettingsEffect.WalletAddressSaved)
             } catch (e: Exception) {
             } catch (e: Exception) {
                 // Handle error saving address
                 // Handle error saving address

+ 10 - 22
app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletScreen.kt → app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletContent.kt

@@ -1,8 +1,5 @@
 package com.codeskraps.publicpool.presentation.wallet
 package com.codeskraps.publicpool.presentation.wallet
 
 
-import android.content.ClipData
-import android.content.ClipboardManager
-import android.content.Context
 import android.os.Parcelable
 import android.os.Parcelable
 import android.widget.Toast
 import android.widget.Toast
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement
@@ -25,7 +22,6 @@ import androidx.compose.material3.Scaffold
 import androidx.compose.material3.Text
 import androidx.compose.material3.Text
 import androidx.compose.material3.TopAppBar
 import androidx.compose.material3.TopAppBar
 import androidx.compose.material3.pulltorefresh.PullToRefreshBox
 import androidx.compose.material3.pulltorefresh.PullToRefreshBox
-import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.collectAsState
@@ -56,8 +52,8 @@ import java.util.Currency
 import java.util.Locale
 import java.util.Locale
 
 
 @Parcelize
 @Parcelize
-data object WalletScreen : Screen, Parcelable {
-    private fun readResolve(): Any = WalletScreen
+data object WalletContent : Screen, Parcelable {
+    private fun readResolve(): Any = WalletContent
 
 
     @OptIn(ExperimentalMaterial3Api::class)
     @OptIn(ExperimentalMaterial3Api::class)
     @Composable
     @Composable
@@ -66,6 +62,11 @@ data object WalletScreen : Screen, Parcelable {
         val state by screenModel.state.collectAsState()
         val state by screenModel.state.collectAsState()
         val context = LocalContext.current
         val context = LocalContext.current
 
 
+        // Track page view when screen becomes visible
+        LaunchedEffect(Unit) {
+            screenModel.handleEvent(WalletEvent.OnScreenVisible)
+        }
+
         LaunchedEffect(key1 = screenModel.effect) {
         LaunchedEffect(key1 = screenModel.effect) {
             screenModel.effect.collectLatest { effect ->
             screenModel.effect.collectLatest { effect ->
                 when (effect) {
                 when (effect) {
@@ -143,7 +144,6 @@ fun WalletDetailsContent(
     walletInfo: WalletInfo,
     walletInfo: WalletInfo,
     btcPrice: CryptoPrice?
     btcPrice: CryptoPrice?
 ) {
 ) {
-    val context = LocalContext.current
     val btcFormat = remember { "%.8f BTC" }
     val btcFormat = remember { "%.8f BTC" }
     val dateFormatter = remember { DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") }
     val dateFormatter = remember { DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") }
     val displayCurrency = stringResource(R.string.currency_usd)
     val displayCurrency = stringResource(R.string.currency_usd)
@@ -253,11 +253,11 @@ fun CurrentPriceCard(btcPrice: CryptoPrice?, currencyFormat: NumberFormat) {
 
 
 @Composable
 @Composable
 fun BalanceCard(
 fun BalanceCard(
+    modifier: Modifier = Modifier,
     label: String,
     label: String,
     valueBtc: String,
     valueBtc: String,
     valueFiat: String? = null,
     valueFiat: String? = null,
-    fiatCurrencyLabel: String? = null,
-    modifier: Modifier = Modifier
+    fiatCurrencyLabel: String? = null
 ) {
 ) {
     AppCard(modifier = modifier) {
     AppCard(modifier = modifier) {
         Column(
         Column(
@@ -319,16 +319,4 @@ fun TransactionItem(tx: WalletTransaction, dateFormatter: DateTimeFormatter) {
             )
             )
         }
         }
     }
     }
-}
-
-// Helper function to copy text to clipboard
-fun copyToClipboard(context: Context, text: String) {
-    val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
-    // Use context.getString() for non-composable contexts
-    val label = context.getString(R.string.wallet_clipboard_label)
-    val message = context.getString(R.string.wallet_toast_address_copied)
-
-    val clip = ClipData.newPlainText(label, text)
-    clipboard.setPrimaryClip(clip)
-    Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
-} 
+}

+ 2 - 1
app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletMvi.kt

@@ -1,7 +1,7 @@
 package com.codeskraps.publicpool.presentation.wallet
 package com.codeskraps.publicpool.presentation.wallet
 
 
-import com.codeskraps.publicpool.domain.model.WalletInfo
 import com.codeskraps.publicpool.domain.model.CryptoPrice
 import com.codeskraps.publicpool.domain.model.CryptoPrice
+import com.codeskraps.publicpool.domain.model.WalletInfo
 import com.codeskraps.publicpool.presentation.common.UiEffect
 import com.codeskraps.publicpool.presentation.common.UiEffect
 import com.codeskraps.publicpool.presentation.common.UiEvent
 import com.codeskraps.publicpool.presentation.common.UiEvent
 import com.codeskraps.publicpool.presentation.common.UiState
 import com.codeskraps.publicpool.presentation.common.UiState
@@ -25,6 +25,7 @@ sealed interface WalletEvent : UiEvent {
     data object LoadWalletDetails : WalletEvent
     data object LoadWalletDetails : WalletEvent
     data class WalletAddressLoaded(val address: String?) : WalletEvent // Internal
     data class WalletAddressLoaded(val address: String?) : WalletEvent // Internal
     data class PriceResult(val result: Result<CryptoPrice>) : WalletEvent // Internal for price
     data class PriceResult(val result: Result<CryptoPrice>) : WalletEvent // Internal for price
+    data object OnScreenVisible : WalletEvent
 }
 }
 
 
 // --- Effects ---
 // --- Effects ---

+ 6 - 6
app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletScreenModel.kt

@@ -24,11 +24,6 @@ class WalletScreenModel(
     val effect = _effect.receiveAsFlow()
     val effect = _effect.receiveAsFlow()
 
 
     init {
     init {
-        // Track page view
-        screenModelScope.launch {
-            trackPageViewUseCase("Wallet")
-        }
-        
         // Start loading wallet address immediately
         // Start loading wallet address immediately
         handleEvent(WalletEvent.LoadWalletDetails)
         handleEvent(WalletEvent.LoadWalletDetails)
     }
     }
@@ -44,6 +39,11 @@ class WalletScreenModel(
                     }
                     }
                 }
                 }
             }
             }
+            WalletEvent.OnScreenVisible -> {
+                screenModelScope.launch {
+                    trackPageViewUseCase("Wallet")
+                }
+            }
             is WalletEvent.WalletAddressLoaded -> processWalletAddress(event.address)
             is WalletEvent.WalletAddressLoaded -> processWalletAddress(event.address)
             is WalletEvent.PriceResult -> processPriceResult(event.result)
             is WalletEvent.PriceResult -> processPriceResult(event.result)
         }
         }
@@ -69,7 +69,7 @@ class WalletScreenModel(
 
 
     private fun processWalletAddress(address: String?) {
     private fun processWalletAddress(address: String?) {
         mutableState.update { it.copy(walletAddress = address, isWalletLoading = false) }
         mutableState.update { it.copy(walletAddress = address, isWalletLoading = false) }
-        if (address != null && address.isNotBlank()) {
+        if (!address.isNullOrBlank()) {
             fetchWalletDetails(address)
             fetchWalletDetails(address)
         } else {
         } else {
             // No wallet address, clear info and show message
             // No wallet address, clear info and show message

+ 1 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/workers/WorkersMvi.kt

@@ -19,6 +19,7 @@ data class WorkersState(
 sealed interface WorkersEvent : UiEvent {
 sealed interface WorkersEvent : UiEvent {
     data object LoadWorkers : WorkersEvent
     data object LoadWorkers : WorkersEvent
     data class WalletAddressLoaded(val address: String?) : WorkersEvent // Internal
     data class WalletAddressLoaded(val address: String?) : WorkersEvent // Internal
+    data object OnScreenVisible : WorkersEvent
 }
 }
 
 
 // --- Effects ---
 // --- Effects ---

+ 5 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/workers/WorkersScreen.kt

@@ -67,6 +67,11 @@ data object WorkersScreen : Screen, Parcelable {
         val state by screenModel.state.collectAsState()
         val state by screenModel.state.collectAsState()
         val context = LocalContext.current
         val context = LocalContext.current
 
 
+        // Track page view when screen becomes visible
+        LaunchedEffect(Unit) {
+            screenModel.handleEvent(WorkersEvent.OnScreenVisible)
+        }
+
         LaunchedEffect(key1 = screenModel.effect) {
         LaunchedEffect(key1 = screenModel.effect) {
             screenModel.effect.collectLatest { effect ->
             screenModel.effect.collectLatest { effect ->
                 when (effect) {
                 when (effect) {

+ 5 - 5
app/src/main/java/com/codeskraps/publicpool/presentation/workers/WorkersScreenModel.kt

@@ -24,11 +24,6 @@ class WorkersScreenModel(
     val effect = _effect.receiveAsFlow()
     val effect = _effect.receiveAsFlow()
 
 
     init {
     init {
-        // Track page view
-        screenModelScope.launch {
-            trackPageViewUseCase("Workers")
-        }
-        
         // Start loading wallet address immediately
         // Start loading wallet address immediately
         handleEvent(WorkersEvent.LoadWorkers)
         handleEvent(WorkersEvent.LoadWorkers)
     }
     }
@@ -44,6 +39,11 @@ class WorkersScreenModel(
                     }
                     }
                 }
                 }
             }
             }
+            WorkersEvent.OnScreenVisible -> {
+                screenModelScope.launch {
+                    trackPageViewUseCase("Workers")
+                }
+            }
             is WorkersEvent.WalletAddressLoaded -> processWalletAddress(event.address)
             is WorkersEvent.WalletAddressLoaded -> processWalletAddress(event.address)
         }
         }
     }
     }

+ 1 - 1
gradle/libs.versions.toml

@@ -13,7 +13,7 @@ coil = "2.6.0"
 logback = "1.3.14" # Or latest stable for Ktor logging
 logback = "1.3.14" # Or latest stable for Ktor logging
 kotlinxSerializationJson = "1.6.3" # Ensure compatibility
 kotlinxSerializationJson = "1.6.3" # Ensure compatibility
 voyager = "1.1.0-beta03" # Add Voyager version
 voyager = "1.1.0-beta03" # Add Voyager version
-webkit = "1.6.1" # WebView for Umami analytics
+webkit = "1.13.0" # WebView for Umami analytics
 # --- End Added Versions ---
 # --- End Added Versions ---
 coreKtx = "1.15.0"
 coreKtx = "1.15.0"
 junit = "4.13.2"
 junit = "4.13.2"