Browse Source

Update README to include Google Play badge, increment version to 1.1, and implement Umami analytics integration. Add analytics repository and use cases, track page views and events in dashboard, wallet, and workers screens. Enhance UI with pull-to-refresh functionality and improve settings screen with developer information.

codeskraps 1 week ago
parent
commit
2289d29bed
20 changed files with 426 additions and 121 deletions
  1. 2 0
      README.md
  2. 11 2
      app/build.gradle.kts
  3. 15 0
      app/src/main/java/com/codeskraps/publicpool/MainApplication.kt
  4. 51 0
      app/src/main/java/com/codeskraps/publicpool/data/remote/UmamiAnalyticsDataSource.kt
  5. 21 0
      app/src/main/java/com/codeskraps/publicpool/data/repository/AnalyticsRepositoryImpl.kt
  6. 7 0
      app/src/main/java/com/codeskraps/publicpool/di/DataModule.kt
  7. 8 0
      app/src/main/java/com/codeskraps/publicpool/di/DomainModule.kt
  8. 29 3
      app/src/main/java/com/codeskraps/publicpool/di/PresentationModule.kt
  9. 7 0
      app/src/main/java/com/codeskraps/publicpool/domain/repository/AnalyticsRepository.kt
  10. 11 0
      app/src/main/java/com/codeskraps/publicpool/domain/usecase/InitializeAnalyticsUseCase.kt
  11. 11 0
      app/src/main/java/com/codeskraps/publicpool/domain/usecase/TrackEventUseCase.kt
  12. 11 0
      app/src/main/java/com/codeskraps/publicpool/domain/usecase/TrackPageViewUseCase.kt
  13. 37 30
      app/src/main/java/com/codeskraps/publicpool/presentation/dashboard/DashboardContent.kt
  14. 23 2
      app/src/main/java/com/codeskraps/publicpool/presentation/dashboard/DashboardScreenModel.kt
  15. 17 3
      app/src/main/java/com/codeskraps/publicpool/presentation/settings/SettingsContent.kt
  16. 45 37
      app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletScreen.kt
  17. 19 2
      app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletScreenModel.kt
  18. 64 35
      app/src/main/java/com/codeskraps/publicpool/presentation/workers/WorkersScreen.kt
  19. 35 7
      app/src/main/java/com/codeskraps/publicpool/presentation/workers/WorkersScreenModel.kt
  20. 2 0
      gradle/libs.versions.toml

+ 2 - 0
README.md

@@ -12,6 +12,8 @@ Monitor your cryptocurrency mining activity on Public Pool directly from your An
 
 
 ## Installation
 ## Installation
 
 
+[![Get it on Google Play](https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png)](https://play.google.com/store/apps/details?id=com.codeskraps.publicpool)
+
 ## Contributing
 ## Contributing
 
 
 Contributions are welcome! Please feel free to submit pull requests or open issues on the project repository:
 Contributions are welcome! Please feel free to submit pull requests or open issues on the project repository:

+ 11 - 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 = 1
-        versionName = "1.0"
+        versionCode = 2
+        versionName = "1.1"
 
 
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
         vectorDrawables {
         vectorDrawables {
@@ -54,6 +54,14 @@ android {
             excludes += "/META-INF/INDEX.LIST"
             excludes += "/META-INF/INDEX.LIST"
         }
         }
     }
     }
+    
+    applicationVariants.all {
+        val variant = this
+        variant.outputs.all {
+            val output = this as? com.android.build.gradle.internal.api.BaseVariantOutputImpl
+            output?.outputFileName = "PublicPool-${variant.name}-v${variant.versionName}-${variant.versionCode}.apk"
+        }
+    }
 }
 }
 
 
 dependencies {
 dependencies {
@@ -63,6 +71,7 @@ dependencies {
     implementation(libs.androidx.core.splashscreen)
     implementation(libs.androidx.core.splashscreen)
     implementation(libs.androidx.lifecycle.runtime.ktx)
     implementation(libs.androidx.lifecycle.runtime.ktx)
     implementation(libs.androidx.activity.compose)
     implementation(libs.androidx.activity.compose)
+    implementation(libs.androidx.webkit) // WebView for analytics
 
 
     // Compose
     // Compose
     implementation(platform(libs.androidx.compose.bom))
     implementation(platform(libs.androidx.compose.bom))

+ 15 - 0
app/src/main/java/com/codeskraps/publicpool/MainApplication.kt

@@ -5,12 +5,22 @@ 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.InitializeAnalyticsUseCase
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+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
 import org.koin.core.context.GlobalContext.startKoin
 import org.koin.core.context.GlobalContext.startKoin
 import org.koin.core.logger.Level
 import org.koin.core.logger.Level
+import org.koin.java.KoinJavaComponent.inject
 
 
 class MainApplication : Application() {
 class MainApplication : Application() {
+    
+    private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
+    private val initializeAnalyticsUseCase: InitializeAnalyticsUseCase by inject(InitializeAnalyticsUseCase::class.java)
+    
     override fun onCreate() {
     override fun onCreate() {
         super.onCreate()
         super.onCreate()
 
 
@@ -25,5 +35,10 @@ class MainApplication : Application() {
                 // Add presentationModule later for ViewModels
                 // Add presentationModule later for ViewModels
             )
             )
         }
         }
+        
+        // Initialize analytics
+        applicationScope.launch {
+            initializeAnalyticsUseCase()
+        }
     }
     }
 } 
 } 

+ 51 - 0
app/src/main/java/com/codeskraps/publicpool/data/remote/UmamiAnalyticsDataSource.kt

@@ -0,0 +1,51 @@
+package com.codeskraps.publicpool.data.remote
+
+import android.content.Context
+import android.webkit.WebView
+import androidx.webkit.WebViewClientCompat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class UmamiAnalyticsDataSource(private val context: Context) {
+    
+    private val webView: WebView by lazy {
+        WebView(context).apply {
+            settings.javaScriptEnabled = true
+            webViewClient = UmamiWebViewClient()
+            loadUrl("about:blank")
+        }
+    }
+    
+    private val umamiScript = """
+        <script defer src="https://umami.codeskraps.com/script.js" data-website-id="b3e6309f-9724-48e5-a1c6-11757de3fe83"></script>
+    """.trimIndent()
+    
+    suspend fun initialize() = withContext(Dispatchers.Main) {
+        webView.loadDataWithBaseURL(
+            "https://umami.codeskraps.com",
+            "<html><head>$umamiScript</head><body></body></html>",
+            "text/html",
+            "UTF-8",
+            null
+        )
+    }
+    
+    suspend fun trackPageView(pageName: String) = withContext(Dispatchers.Main) {
+        webView.evaluateJavascript(
+            "umami.trackView('$pageName')",
+            null
+        )
+    }
+    
+    suspend fun trackEvent(eventName: String, eventData: Map<String, String> = emptyMap()) = withContext(Dispatchers.Main) {
+        val dataJson = eventData.entries.joinToString(",") { 
+            "\"${it.key}\": \"${it.value}\"" 
+        }
+        webView.evaluateJavascript(
+            "umami.trackEvent('$eventName', {$dataJson})",
+            null
+        )
+    }
+    
+    private class UmamiWebViewClient : WebViewClientCompat()
+} 

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

@@ -0,0 +1,21 @@
+package com.codeskraps.publicpool.data.repository
+
+import com.codeskraps.publicpool.data.remote.UmamiAnalyticsDataSource
+import com.codeskraps.publicpool.domain.repository.AnalyticsRepository
+
+class AnalyticsRepositoryImpl(
+    private val analyticsDataSource: UmamiAnalyticsDataSource
+) : AnalyticsRepository {
+    
+    override suspend fun initialize() {
+        analyticsDataSource.initialize()
+    }
+    
+    override suspend fun trackPageView(pageName: String) {
+        analyticsDataSource.trackPageView(pageName)
+    }
+    
+    override suspend fun trackEvent(eventName: String, eventData: Map<String, String>) {
+        analyticsDataSource.trackEvent(eventName, eventData)
+    }
+} 

+ 7 - 0
app/src/main/java/com/codeskraps/publicpool/di/DataModule.kt

@@ -2,7 +2,10 @@ 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.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.PublicPoolRepository
 import com.codeskraps.publicpool.domain.repository.PublicPoolRepository
 import io.ktor.client.* // Ktor client
 import io.ktor.client.* // Ktor client
 import io.ktor.client.engine.android.* // Ktor Android engine
 import io.ktor.client.engine.android.* // Ktor Android engine
@@ -41,4 +44,8 @@ val dataModule = module {
 
 
     // Repository Implementation
     // Repository Implementation
     single<PublicPoolRepository> { PublicPoolRepositoryImpl(apiService = get(), dataStore = get()) }
     single<PublicPoolRepository> { PublicPoolRepositoryImpl(apiService = get(), dataStore = get()) }
+
+    // Analytics
+    single { UmamiAnalyticsDataSource(get()) }
+    single<AnalyticsRepository> { AnalyticsRepositoryImpl(get()) }
 } 
 } 

+ 8 - 0
app/src/main/java/com/codeskraps/publicpool/di/DomainModule.kt

@@ -1,6 +1,9 @@
 package com.codeskraps.publicpool.di
 package com.codeskraps.publicpool.di
 
 
 import com.codeskraps.publicpool.domain.usecase.*
 import com.codeskraps.publicpool.domain.usecase.*
+import com.codeskraps.publicpool.domain.usecase.InitializeAnalyticsUseCase
+import com.codeskraps.publicpool.domain.usecase.TrackEventUseCase
+import com.codeskraps.publicpool.domain.usecase.TrackPageViewUseCase
 import org.koin.dsl.module
 import org.koin.dsl.module
 
 
 val domainModule = module {
 val domainModule = module {
@@ -13,4 +16,9 @@ val domainModule = module {
     factory { CalculateTwoHourAverageUseCase() }
     factory { CalculateTwoHourAverageUseCase() }
     factory { GetBlockchainWalletInfoUseCase(repository = get()) }
     factory { GetBlockchainWalletInfoUseCase(repository = get()) }
     factory { GetBtcPriceUseCase(repository = get()) }
     factory { GetBtcPriceUseCase(repository = get()) }
+    
+    // Analytics
+    factory { InitializeAnalyticsUseCase(get()) }
+    factory { TrackPageViewUseCase(get()) }
+    factory { TrackEventUseCase(get()) }
 } 
 } 

+ 29 - 3
app/src/main/java/com/codeskraps/publicpool/di/PresentationModule.kt

@@ -8,8 +8,34 @@ import org.koin.dsl.module
 
 
 val presentationModule = module {
 val presentationModule = module {
     // Voyager ScreenModels (similar to ViewModels)
     // Voyager ScreenModels (similar to ViewModels)
-    factory { DashboardScreenModel(get(), get(), get(), get(), get(), get()) } // Add 6th `get()` for AppReadinessState
+    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 { SettingsScreenModel(get(), get()) } // Inject use cases
-    factory { WorkersScreenModel(get(), get()) } // Provide WorkersScreenModel
-    factory { WalletScreenModel(get(), get(), get()) } // Provide WalletScreenModel (add 3rd get)
+    factory {
+        WorkersScreenModel(
+            getWalletAddressUseCase = get(),
+            getClientInfoUseCase = get(),
+            trackPageViewUseCase = get(),
+            trackEventUseCase = get()
+        )
+    }
+    factory {
+        WalletScreenModel(
+            getWalletAddressUseCase = get(),
+            getBlockchainWalletInfoUseCase = get(),
+            getBtcPriceUseCase = get(),
+            trackPageViewUseCase = get(),
+            trackEventUseCase = get()
+        )
+    }
 } 
 } 

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

@@ -0,0 +1,7 @@
+package com.codeskraps.publicpool.domain.repository
+
+interface AnalyticsRepository {
+    suspend fun initialize()
+    suspend fun trackPageView(pageName: String)
+    suspend fun trackEvent(eventName: String, eventData: Map<String, String> = emptyMap())
+} 

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

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

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

@@ -0,0 +1,11 @@
+package com.codeskraps.publicpool.domain.usecase
+
+import com.codeskraps.publicpool.domain.repository.AnalyticsRepository
+
+class TrackEventUseCase(
+    private val analyticsRepository: AnalyticsRepository
+) {
+    suspend operator fun invoke(eventName: String, eventData: Map<String, String> = emptyMap()) {
+        analyticsRepository.trackEvent(eventName, eventData)
+    }
+} 

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

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

+ 37 - 30
app/src/main/java/com/codeskraps/publicpool/presentation/dashboard/DashboardContent.kt

@@ -1,6 +1,7 @@
 package com.codeskraps.publicpool.presentation.dashboard
 package com.codeskraps.publicpool.presentation.dashboard
 
 
 import android.util.Log
 import android.util.Log
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Column
@@ -18,6 +19,7 @@ import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Info
 import androidx.compose.material.icons.filled.Info
 import androidx.compose.material.icons.filled.Settings
 import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.AlertDialog
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.material3.Icon
@@ -28,6 +30,7 @@ import androidx.compose.material3.SnackbarDuration
 import androidx.compose.material3.SnackbarHost
 import androidx.compose.material3.SnackbarHost
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.Text
 import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
 import androidx.compose.material3.TopAppBar
 import androidx.compose.material3.TopAppBar
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
@@ -44,9 +47,7 @@ import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.compose.ui.viewinterop.AndroidView
 import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.LocalNavigator
-import cafe.adriel.voyager.navigator.Navigator
 import cafe.adriel.voyager.navigator.currentOrThrow
 import cafe.adriel.voyager.navigator.currentOrThrow
-import cafe.adriel.voyager.core.screen.Screen
 import com.anychart.APIlib
 import com.anychart.APIlib
 import com.anychart.AnyChart
 import com.anychart.AnyChart
 import com.anychart.AnyChartView
 import com.anychart.AnyChartView
@@ -60,17 +61,16 @@ import com.codeskraps.publicpool.R
 import com.codeskraps.publicpool.domain.model.ChartDataPoint
 import com.codeskraps.publicpool.domain.model.ChartDataPoint
 import com.codeskraps.publicpool.presentation.common.AppCard
 import com.codeskraps.publicpool.presentation.common.AppCard
 import com.codeskraps.publicpool.presentation.navigation.SettingsScreen
 import com.codeskraps.publicpool.presentation.navigation.SettingsScreen
+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 com.codeskraps.publicpool.presentation.navigation.getParentOrSelf
-import androidx.compose.foundation.clickable
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Button
-import androidx.compose.material3.TextButton
+import androidx.compose.material3.pulltorefresh.PullToRefreshBox
+import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
 
 
 @OptIn(ExperimentalMaterial3Api::class)
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 @Composable
@@ -80,7 +80,7 @@ fun DashboardContent(screenModel: DashboardScreenModel) {
     // Get the current navigator, which might be inside a tab
     // Get the current navigator, which might be inside a tab
     val navigator = LocalNavigator.currentOrThrow
     val navigator = LocalNavigator.currentOrThrow
     val snackbarHostState = remember { SnackbarHostState() }
     val snackbarHostState = remember { SnackbarHostState() }
-
+    
     // Handle effects (navigation, snackbars)
     // Handle effects (navigation, snackbars)
     LaunchedEffect(key1 = screenModel.effect) {
     LaunchedEffect(key1 = screenModel.effect) {
         screenModel.effect.collectLatest { effect ->
         screenModel.effect.collectLatest { effect ->
@@ -121,39 +121,46 @@ fun DashboardContent(screenModel: DashboardScreenModel) {
             )
             )
         }
         }
     ) { paddingValues ->
     ) { paddingValues ->
-        Column(
+        PullToRefreshBox(
+            isRefreshing = state.isLoading,
+            onRefresh = { screenModel.handleEvent(DashboardEvent.RefreshData) },
             modifier = Modifier
             modifier = Modifier
                 .fillMaxSize()
                 .fillMaxSize()
                 .padding(paddingValues)
                 .padding(paddingValues)
-                .padding(horizontal = 16.dp) // Add horizontal padding
-                .verticalScroll(rememberScrollState()) // Make column scrollable
         ) {
         ) {
-            // Add padding between TopAppBar and first card row
-            Spacer(modifier = Modifier.height(16.dp))
+            Column(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .padding(horizontal = 16.dp) // Add horizontal padding
+                    .verticalScroll(rememberScrollState()) // Make column scrollable
+            ) {
+                // Add padding between TopAppBar and first card row
+                Spacer(modifier = Modifier.height(16.dp))
 
 
-            // Show message if no wallet address is set
-            if (!state.isWalletLoading && (state.walletAddress?.isBlank() != false)) {
-                AppCard(modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp)) {
-                    Text(
-                        text = stringResource(R.string.dashboard_info_set_wallet),
-                        modifier = Modifier.padding(16.dp),
-                        style = MaterialTheme.typography.bodyLarge
-                    )
+                // Show message if no wallet address is set
+                if (!state.isWalletLoading && (state.walletAddress?.isBlank() != false)) {
+                    AppCard(modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp)) {
+                        Text(
+                            text = stringResource(R.string.dashboard_info_set_wallet),
+                            modifier = Modifier.padding(16.dp),
+                            style = MaterialTheme.typography.bodyLarge
+                        )
+                    }
                 }
                 }
-            }
 
 
-            // Top Info Cards Row/Grid
-            TopInfoCards(state = state)
+                // Top Info Cards Row/Grid
+                TopInfoCards(state = state)
 
 
-            Spacer(modifier = Modifier.height(16.dp))
+                Spacer(modifier = Modifier.height(16.dp))
 
 
-            // Placeholder for Workers List (Add later if API provides worker data)
-            // WorkersSection(state = state)
+                // Placeholder for Workers List (Add later if API provides worker data)
+                // WorkersSection(state = state)
 
 
-            // Chart Section
-            ChartSection(state = state)
+                // Chart Section
+                ChartSection(state = state)
 
 
-            Spacer(modifier = Modifier.height(16.dp)) // Bottom padding
+                Spacer(modifier = Modifier.height(16.dp)) // Bottom padding
+            }
         }
         }
     }
     }
 }
 }

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

@@ -8,6 +8,8 @@ import com.codeskraps.publicpool.domain.usecase.GetChartDataUseCase
 import com.codeskraps.publicpool.domain.usecase.GetClientInfoUseCase
 import com.codeskraps.publicpool.domain.usecase.GetClientInfoUseCase
 import com.codeskraps.publicpool.domain.usecase.GetNetworkInfoUseCase
 import com.codeskraps.publicpool.domain.usecase.GetNetworkInfoUseCase
 import com.codeskraps.publicpool.domain.usecase.GetWalletAddressUseCase
 import com.codeskraps.publicpool.domain.usecase.GetWalletAddressUseCase
+import com.codeskraps.publicpool.domain.usecase.TrackPageViewUseCase
+import com.codeskraps.publicpool.domain.usecase.TrackEventUseCase
 import com.codeskraps.publicpool.di.AppReadinessState
 import com.codeskraps.publicpool.di.AppReadinessState
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.channels.Channel
@@ -20,6 +22,8 @@ class DashboardScreenModel(
     private val getClientInfoUseCase: GetClientInfoUseCase,
     private val getClientInfoUseCase: GetClientInfoUseCase,
     private val getChartDataUseCase: GetChartDataUseCase,
     private val getChartDataUseCase: GetChartDataUseCase,
     private val calculateTwoHourAverageUseCase: CalculateTwoHourAverageUseCase,
     private val calculateTwoHourAverageUseCase: CalculateTwoHourAverageUseCase,
+    private val trackPageViewUseCase: TrackPageViewUseCase,
+    private val trackEventUseCase: TrackEventUseCase,
     private val appReadinessState: AppReadinessState
     private val appReadinessState: AppReadinessState
 ) : StateScreenModel<DashboardState>(DashboardState()) {
 ) : StateScreenModel<DashboardState>(DashboardState()) {
 
 
@@ -29,6 +33,11 @@ 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)
     }
     }
@@ -36,8 +45,20 @@ class DashboardScreenModel(
     fun handleEvent(event: DashboardEvent) {
     fun handleEvent(event: DashboardEvent) {
         when (event) {
         when (event) {
             DashboardEvent.LoadData -> loadInitialData()
             DashboardEvent.LoadData -> loadInitialData()
-            DashboardEvent.RefreshData -> refreshData()
-            DashboardEvent.GoToSettings -> sendEffect(DashboardEffect.NavigateToSettings)
+            DashboardEvent.RefreshData -> {
+                refreshData()
+                // Track refresh event
+                screenModelScope.launch {
+                    trackEventUseCase("dashboard_refresh", mapOf("action" to "pull_to_refresh"))
+                }
+            }
+            DashboardEvent.GoToSettings -> {
+                sendEffect(DashboardEffect.NavigateToSettings)
+                // Track settings navigation
+                screenModelScope.launch {
+                    trackEventUseCase("navigation", mapOf("to" to "settings", "from" to "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)

+ 17 - 3
app/src/main/java/com/codeskraps/publicpool/presentation/settings/SettingsContent.kt

@@ -141,13 +141,27 @@ fun SettingsContent(screenModel: SettingsScreenModel) {
                             style = MaterialTheme.typography.bodyMedium
                             style = MaterialTheme.typography.bodyMedium
                         )
                         )
                         
                         
+                        TextButton(
+                            onClick = {
+                                val intent = Intent(Intent.ACTION_VIEW)
+                                intent.data = "https://codeskraps.com".toUri()
+                                context.startActivity(intent)
+                            }
+                        ) {
+                            Text(
+                                text = "Developer: codeskraps.com",
+                                style = MaterialTheme.typography.bodyMedium,
+                                color = MaterialTheme.colorScheme.primary
+                            )
+                        }
+                        
                         Text(
                         Text(
-                            text = "Developer: codeskraps",
+                            text = "License: MIT",
                             style = MaterialTheme.typography.bodyMedium
                             style = MaterialTheme.typography.bodyMedium
                         )
                         )
                         
                         
                         Text(
                         Text(
-                            text = "License: MIT",
+                            text = "Analytics: Self-hosted Umami",
                             style = MaterialTheme.typography.bodyMedium
                             style = MaterialTheme.typography.bodyMedium
                         )
                         )
                         
                         
@@ -160,7 +174,7 @@ fun SettingsContent(screenModel: SettingsScreenModel) {
                             }
                             }
                         ) {
                         ) {
                             Text(
                             Text(
-                                text = "https://repo.codeskraps.com/codeskraps/PublicPoolAndroid",
+                                text = "Source Repository",
                                 style = MaterialTheme.typography.bodyMedium,
                                 style = MaterialTheme.typography.bodyMedium,
                                 color = MaterialTheme.colorScheme.primary,
                                 color = MaterialTheme.colorScheme.primary,
                                 textAlign = TextAlign.Center
                                 textAlign = TextAlign.Center

+ 45 - 37
app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletScreen.kt

@@ -24,6 +24,8 @@ import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Scaffold
 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.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
@@ -77,47 +79,53 @@ data object WalletScreen : Screen, Parcelable {
         Scaffold(
         Scaffold(
             topBar = { TopAppBar(title = { Text(stringResource(R.string.screen_title_wallet_details)) }) }
             topBar = { TopAppBar(title = { Text(stringResource(R.string.screen_title_wallet_details)) }) }
         ) { paddingValues ->
         ) { paddingValues ->
-            Box(
+            PullToRefreshBox(
+                isRefreshing = state.isWalletLoading || state.isLoading,
+                onRefresh = { screenModel.handleEvent(WalletEvent.LoadWalletDetails) },
                 modifier = Modifier
                 modifier = Modifier
                     .fillMaxSize()
                     .fillMaxSize()
                     .padding(paddingValues)
                     .padding(paddingValues)
             ) {
             ) {
-                when {
-                    state.isWalletLoading || state.isLoading -> {
-                        // Combined loading indicator
-                        CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
-                    }
-                    state.walletAddress.isNullOrBlank() -> {
-                        // Use stringResource for message
-                        Text(
-                            text = stringResource(R.string.wallet_error_not_set),
-                            modifier = Modifier.align(Alignment.Center).padding(16.dp),
-                            textAlign = TextAlign.Center
-                        )
-                    }
-                    state.errorMessage != null -> {
-                        // Use stringResource for generic error, keep specific from state
-                        Text(
-                            text = state.errorMessage ?: stringResource(R.string.error_unknown),
-                            modifier = Modifier.align(Alignment.Center).padding(16.dp),
-                            color = MaterialTheme.colorScheme.error,
-                            textAlign = TextAlign.Center
-                        )
-                    }
-                    state.walletInfo == null -> {
-                         // Use stringResource for message
-                         Text(
-                            text = stringResource(R.string.wallet_error_load_failed),
-                            modifier = Modifier.align(Alignment.Center).padding(16.dp),
-                            textAlign = TextAlign.Center
-                        )
-                    }
-                    else -> {
-                        // Display Wallet Info and Transactions
-                        WalletDetailsContent(
-                            walletInfo = state.walletInfo!!,
-                            btcPrice = state.btcPrice
-                        )
+                Box(
+                    modifier = Modifier.fillMaxSize()
+                ) {
+                    when {
+                        state.isWalletLoading || state.isLoading -> {
+                            // Combined loading indicator
+                            CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+                        }
+                        state.walletAddress.isNullOrBlank() -> {
+                            // Use stringResource for message
+                            Text(
+                                text = stringResource(R.string.wallet_error_not_set),
+                                modifier = Modifier.align(Alignment.Center).padding(16.dp),
+                                textAlign = TextAlign.Center
+                            )
+                        }
+                        state.errorMessage != null -> {
+                            // Use stringResource for generic error, keep specific from state
+                            Text(
+                                text = state.errorMessage ?: stringResource(R.string.error_unknown),
+                                modifier = Modifier.align(Alignment.Center).padding(16.dp),
+                                color = MaterialTheme.colorScheme.error,
+                                textAlign = TextAlign.Center
+                            )
+                        }
+                        state.walletInfo == null -> {
+                             // Use stringResource for message
+                             Text(
+                                text = stringResource(R.string.wallet_error_load_failed),
+                                modifier = Modifier.align(Alignment.Center).padding(16.dp),
+                                textAlign = TextAlign.Center
+                            )
+                        }
+                        else -> {
+                            // Display Wallet Info and Transactions
+                            WalletDetailsContent(
+                                walletInfo = state.walletInfo!!,
+                                btcPrice = state.btcPrice
+                            )
+                        }
                     }
                     }
                 }
                 }
             }
             }

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

@@ -5,6 +5,8 @@ import cafe.adriel.voyager.core.model.screenModelScope
 import com.codeskraps.publicpool.domain.usecase.GetBlockchainWalletInfoUseCase
 import com.codeskraps.publicpool.domain.usecase.GetBlockchainWalletInfoUseCase
 import com.codeskraps.publicpool.domain.usecase.GetWalletAddressUseCase
 import com.codeskraps.publicpool.domain.usecase.GetWalletAddressUseCase
 import com.codeskraps.publicpool.domain.usecase.GetBtcPriceUseCase
 import com.codeskraps.publicpool.domain.usecase.GetBtcPriceUseCase
+import com.codeskraps.publicpool.domain.usecase.TrackPageViewUseCase
+import com.codeskraps.publicpool.domain.usecase.TrackEventUseCase
 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
@@ -13,20 +15,35 @@ import android.util.Log
 class WalletScreenModel(
 class WalletScreenModel(
     private val getWalletAddressUseCase: GetWalletAddressUseCase,
     private val getWalletAddressUseCase: GetWalletAddressUseCase,
     private val getBlockchainWalletInfoUseCase: GetBlockchainWalletInfoUseCase,
     private val getBlockchainWalletInfoUseCase: GetBlockchainWalletInfoUseCase,
-    private val getBtcPriceUseCase: GetBtcPriceUseCase
+    private val getBtcPriceUseCase: GetBtcPriceUseCase,
+    private val trackPageViewUseCase: TrackPageViewUseCase,
+    private val trackEventUseCase: TrackEventUseCase
 ) : StateScreenModel<WalletState>(WalletState()) {
 ) : StateScreenModel<WalletState>(WalletState()) {
 
 
     private val _effect = Channel<WalletEffect>()
     private val _effect = Channel<WalletEffect>()
     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)
     }
     }
 
 
     fun handleEvent(event: WalletEvent) {
     fun handleEvent(event: WalletEvent) {
         when (event) {
         when (event) {
-            WalletEvent.LoadWalletDetails -> loadWalletAndDetails()
+            WalletEvent.LoadWalletDetails -> {
+                loadWalletAndDetails()
+                // Track refresh event when explicitly requested (not on initial load)
+                if (state.value.walletAddress != null) {
+                    screenModelScope.launch {
+                        trackEventUseCase("wallet_refresh", mapOf("action" to "pull_to_refresh"))
+                    }
+                }
+            }
             is WalletEvent.WalletAddressLoaded -> processWalletAddress(event.address)
             is WalletEvent.WalletAddressLoaded -> processWalletAddress(event.address)
             is WalletEvent.PriceResult -> processPriceResult(event.result)
             is WalletEvent.PriceResult -> processPriceResult(event.result)
         }
         }

+ 64 - 35
app/src/main/java/com/codeskraps/publicpool/presentation/workers/WorkersScreen.kt

@@ -17,8 +17,6 @@ import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.items
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.HorizontalDivider
 import androidx.compose.material3.HorizontalDivider
@@ -28,6 +26,7 @@ import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Scaffold
 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.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
@@ -43,6 +42,7 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
+import cafe.adriel.voyager.core.model.screenModelScope
 import cafe.adriel.voyager.core.screen.Screen
 import cafe.adriel.voyager.core.screen.Screen
 import cafe.adriel.voyager.koin.koinScreenModel
 import cafe.adriel.voyager.koin.koinScreenModel
 import com.codeskraps.publicpool.R
 import com.codeskraps.publicpool.R
@@ -53,6 +53,7 @@ import com.codeskraps.publicpool.util.formatDifficulty
 import com.codeskraps.publicpool.util.formatHashRate
 import com.codeskraps.publicpool.util.formatHashRate
 import com.codeskraps.publicpool.util.formatRelativeTime
 import com.codeskraps.publicpool.util.formatRelativeTime
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
 import kotlinx.parcelize.Parcelize
 import kotlinx.parcelize.Parcelize
 
 
 @Parcelize
 @Parcelize
@@ -79,43 +80,52 @@ data object WorkersScreen : Screen, Parcelable {
         Scaffold(
         Scaffold(
             topBar = { TopAppBar(title = { Text(stringResource(id = R.string.screen_title_workers)) }) }
             topBar = { TopAppBar(title = { Text(stringResource(id = R.string.screen_title_workers)) }) }
         ) { paddingValues ->
         ) { paddingValues ->
-            Box(
+            PullToRefreshBox(
+                isRefreshing = state.isLoading,
+                onRefresh = { screenModel.handleEvent(WorkersEvent.LoadWorkers) },
                 modifier = Modifier
                 modifier = Modifier
                     .fillMaxSize()
                     .fillMaxSize()
                     .padding(paddingValues)
                     .padding(paddingValues)
             ) {
             ) {
-                when {
-                    state.isLoading -> {
-                        Box(
-                            modifier = Modifier.fillMaxSize(),
-                            contentAlignment = Alignment.Center
-                        ) {
-                            CircularProgressIndicator()
+                Box(
+                    modifier = Modifier.fillMaxSize()
+                ) {
+                    when {
+                        state.isLoading -> {
+                            Box(
+                                modifier = Modifier.fillMaxSize(),
+                                contentAlignment = Alignment.Center
+                            ) {
+                                CircularProgressIndicator()
+                            }
                         }
                         }
-                    }
 
 
-                    state.errorMessage != null -> {
-                        Text(
-                            text = state.errorMessage ?: stringResource(R.string.error_unknown),
-                            modifier = Modifier.align(Alignment.Center)
-                        )
-                    }
+                        state.errorMessage != null -> {
+                            Text(
+                                text = state.errorMessage ?: stringResource(R.string.error_unknown),
+                                modifier = Modifier.align(Alignment.Center)
+                            )
+                        }
 
 
-                    state.groupedWorkers.isEmpty() && !state.isWalletLoading -> {
-                        Text(
-                            text = if (state.walletAddress.isNullOrBlank())
-                                stringResource(R.string.workers_no_wallet)
-                            else
-                                stringResource(R.string.workers_no_data),
-                            modifier = Modifier
-                                .align(Alignment.Center)
-                                .padding(16.dp),
-                            textAlign = TextAlign.Center
-                        )
-                    }
+                        state.groupedWorkers.isEmpty() && !state.isWalletLoading -> {
+                            Text(
+                                text = if (state.walletAddress.isNullOrBlank())
+                                    stringResource(R.string.workers_no_wallet)
+                                else
+                                    stringResource(R.string.workers_no_data),
+                                modifier = Modifier
+                                    .align(Alignment.Center)
+                                    .padding(16.dp),
+                                textAlign = TextAlign.Center
+                            )
+                        }
 
 
-                    else -> {
-                        WorkersList(groupedWorkers = state.groupedWorkers)
+                        else -> {
+                            WorkersList(
+                                groupedWorkers = state.groupedWorkers,
+                                screenModel = screenModel
+                            )
+                        }
                     }
                     }
                 }
                 }
             }
             }
@@ -124,20 +134,36 @@ data object WorkersScreen : Screen, Parcelable {
 }
 }
 
 
 @Composable
 @Composable
-fun WorkersList(groupedWorkers: Map<String, List<Worker>>) {
+fun WorkersList(
+    groupedWorkers: Map<String, List<Worker>>,
+    screenModel: WorkersScreenModel
+) {
     LazyColumn(
     LazyColumn(
         modifier = Modifier.fillMaxSize(),
         modifier = Modifier.fillMaxSize(),
         contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
         contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
     ) {
     ) {
         items(groupedWorkers.entries.toList(), key = { it.key }) { (workerName, sessions) ->
         items(groupedWorkers.entries.toList(), key = { it.key }) { (workerName, sessions) ->
-            WorkerGroupCard(workerName = workerName, sessions = sessions)
+            WorkerGroupCard(
+                workerName = workerName, 
+                sessions = sessions,
+                onExpandCollapse = { isExpanded ->
+                    // Track worker group expansion/collapse
+                    screenModel.screenModelScope.launch {
+                        screenModel.trackWorkerGroupToggle(workerName, isExpanded)
+                    }
+                }
+            )
             Spacer(modifier = Modifier.height(8.dp))
             Spacer(modifier = Modifier.height(8.dp))
         }
         }
     }
     }
 }
 }
 
 
 @Composable
 @Composable
-fun WorkerGroupCard(workerName: String, sessions: List<Worker>) {
+fun WorkerGroupCard(
+    workerName: String, 
+    sessions: List<Worker>,
+    onExpandCollapse: (Boolean) -> Unit = {}
+) {
     var expanded by remember { mutableStateOf(false) }
     var expanded by remember { mutableStateOf(false) }
 
 
     val totalHashRate = sessions.sumOf { it.hashRate ?: 0.0 }
     val totalHashRate = sessions.sumOf { it.hashRate ?: 0.0 }
@@ -155,7 +181,10 @@ fun WorkerGroupCard(workerName: String, sessions: List<Worker>) {
             Row(
             Row(
                 modifier = Modifier
                 modifier = Modifier
                     .fillMaxWidth()
                     .fillMaxWidth()
-                    .clickable { expanded = !expanded },
+                    .clickable { 
+                        expanded = !expanded
+                        onExpandCollapse(expanded)
+                    },
                 verticalAlignment = Alignment.CenterVertically
                 verticalAlignment = Alignment.CenterVertically
             ) {
             ) {
                 Icon(
                 Icon(

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

@@ -2,32 +2,48 @@ package com.codeskraps.publicpool.presentation.workers
 
 
 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.presentation.workers.WorkersEffect
-import com.codeskraps.publicpool.presentation.workers.WorkersEvent
-import com.codeskraps.publicpool.presentation.workers.WorkersState
-import com.codeskraps.publicpool.domain.model.Worker
 import com.codeskraps.publicpool.domain.usecase.GetClientInfoUseCase
 import com.codeskraps.publicpool.domain.usecase.GetClientInfoUseCase
 import com.codeskraps.publicpool.domain.usecase.GetWalletAddressUseCase
 import com.codeskraps.publicpool.domain.usecase.GetWalletAddressUseCase
+import com.codeskraps.publicpool.domain.usecase.TrackEventUseCase
+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.catch
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 
 
 class WorkersScreenModel(
 class WorkersScreenModel(
     private val getWalletAddressUseCase: GetWalletAddressUseCase,
     private val getWalletAddressUseCase: GetWalletAddressUseCase,
-    private val getClientInfoUseCase: GetClientInfoUseCase
+    private val getClientInfoUseCase: GetClientInfoUseCase,
+    private val trackPageViewUseCase: TrackPageViewUseCase,
+    private val trackEventUseCase: TrackEventUseCase
 ) : StateScreenModel<WorkersState>(WorkersState()) {
 ) : StateScreenModel<WorkersState>(WorkersState()) {
 
 
     private val _effect = Channel<WorkersEffect>()
     private val _effect = Channel<WorkersEffect>()
     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)
     }
     }
 
 
     fun handleEvent(event: WorkersEvent) {
     fun handleEvent(event: WorkersEvent) {
         when (event) {
         when (event) {
-            WorkersEvent.LoadWorkers -> loadWalletAndWorkers()
+            WorkersEvent.LoadWorkers -> {
+                loadWalletAndWorkers()
+                // Track refresh event when explicitly requested (not on initial load)
+                if (state.value.walletAddress != null) {
+                    screenModelScope.launch {
+                        trackEventUseCase("workers_refresh", mapOf("action" to "pull_to_refresh"))
+                    }
+                }
+            }
             is WorkersEvent.WalletAddressLoaded -> processWalletAddress(event.address)
             is WorkersEvent.WalletAddressLoaded -> processWalletAddress(event.address)
         }
         }
     }
     }
@@ -88,4 +104,16 @@ class WorkersScreenModel(
             _effect.send(effectToSend)
             _effect.send(effectToSend)
         }
         }
     }
     }
+
+    // Method to track worker group expansion/collapse
+    suspend fun trackWorkerGroupToggle(workerName: String, isExpanded: Boolean) {
+        val action = if (isExpanded) "expand" else "collapse"
+        trackEventUseCase(
+            "worker_group_toggle", 
+            mapOf(
+                "worker_name" to workerName,
+                "action" to action
+            )
+        )
+    }
 } 
 } 

+ 2 - 0
gradle/libs.versions.toml

@@ -13,6 +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
 # --- End Added Versions ---
 # --- End Added Versions ---
 coreKtx = "1.15.0"
 coreKtx = "1.15.0"
 junit = "4.13.2"
 junit = "4.13.2"
@@ -66,6 +67,7 @@ voyager-koin = { group = "cafe.adriel.voyager", name = "voyager-koin", version.r
 voyager-transitions = { group = "cafe.adriel.voyager", name = "voyager-transitions", version.ref = "voyager" }
 voyager-transitions = { group = "cafe.adriel.voyager", name = "voyager-transitions", version.ref = "voyager" }
 voyager-tab-navigator = { group = "cafe.adriel.voyager", name = "voyager-tab-navigator", version.ref = "voyager" } # Add TabNavigator
 voyager-tab-navigator = { group = "cafe.adriel.voyager", name = "voyager-tab-navigator", version.ref = "voyager" } # Add TabNavigator
 androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" }
 androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" }
+androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "webkit" }
 # --- End Added Libraries ---
 # --- End Added Libraries ---
 
 
 [plugins]
 [plugins]