Skip to content

Commit 782e8c0

Browse files
committed
feat(profile): Improve proxy settings and network handling
This commit enhances the proxy configuration UI and ensures proxy settings are applied correctly during application startup and runtime. - **feat(profile)**: Added a password visibility toggle for proxy authentication. - **feat(profile)**: Improved proxy settings validation, including port range checking (1-65535) and UI feedback for invalid inputs. - **feat(profile)**: Added descriptive text for "None" and "System" proxy types and updated the "Save" button to a `FilledTonalButton` with validation logic. - **fix(network)**: Moved proxy initialization to a synchronous `runBlocking` block within `networkModule` to ensure settings are applied before the first network request. - **fix(network)**: Refactored `GitHubClientProvider` to use a thread-safe volatile client that updates reactively when proxy configurations change. - **fix(data)**: Hardened `ProxyRepositoryImpl` with better validation when loading saved configurations and ensured persistence occurs before in-memory application. - **fix(android)**: Reset the default `java.net.Authenticator` in `HttpClientFactory` to prevent credential leakage across client instances. - **refactor**: Cleaned up `MainViewModel` and `SharedModules` by removing redundant proxy initialization logic.
1 parent 8da1532 commit 782e8c0

12 files changed

Lines changed: 179 additions & 81 deletions

File tree

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
77
import kotlinx.coroutines.flow.asStateFlow
88
import kotlinx.coroutines.flow.update
99
import kotlinx.coroutines.launch
10-
import kotlinx.coroutines.flow.first
11-
import zed.rainxch.core.data.network.ProxyManager
12-
import zed.rainxch.core.domain.model.ProxyConfig
1310
import zed.rainxch.core.domain.repository.AuthenticationState
1411
import zed.rainxch.core.domain.repository.InstalledAppsRepository
15-
import zed.rainxch.core.domain.repository.ProxyRepository
1612
import zed.rainxch.core.domain.repository.RateLimitRepository
1713
import zed.rainxch.core.domain.repository.ThemesRepository
1814
import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase
@@ -22,8 +18,7 @@ class MainViewModel(
2218
private val installedAppsRepository: InstalledAppsRepository,
2319
private val authenticationState: AuthenticationState,
2420
private val rateLimitRepository: RateLimitRepository,
25-
private val syncUseCase: SyncInstalledAppsUseCase,
26-
private val proxyRepository: ProxyRepository
21+
private val syncUseCase: SyncInstalledAppsUseCase
2722
) : ViewModel() {
2823

2924
private val _state = MutableStateFlow(MainState())
@@ -99,26 +94,6 @@ class MainViewModel(
9994
installedAppsRepository.checkAllForUpdates()
10095
}
10196
}
102-
103-
viewModelScope.launch(Dispatchers.IO) {
104-
val savedConfig = proxyRepository.getProxyConfig().first()
105-
when (savedConfig) {
106-
is ProxyConfig.None -> ProxyManager.setNoProxy()
107-
is ProxyConfig.System -> ProxyManager.setSystemProxy()
108-
is ProxyConfig.Http -> ProxyManager.setHttpProxy(
109-
host = savedConfig.host,
110-
port = savedConfig.port,
111-
username = savedConfig.username,
112-
password = savedConfig.password
113-
)
114-
is ProxyConfig.Socks -> ProxyManager.setSocksProxy(
115-
host = savedConfig.host,
116-
port = savedConfig.port,
117-
username = savedConfig.username,
118-
password = savedConfig.password
119-
)
120-
}
121-
}
12297
}
12398

12499
fun onAction(action: MainAction) {

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ val mainModule: Module = module {
1515
installedAppsRepository = get(),
1616
rateLimitRepository = get(),
1717
syncUseCase = get(),
18-
authenticationState = get(),
19-
proxyRepository = get()
18+
authenticationState = get()
2019
)
2120
}
2221
}

core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import java.net.PasswordAuthentication
1010
import java.net.ProxySelector
1111

1212
actual fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient {
13+
java.net.Authenticator.setDefault(null)
14+
1315
return HttpClient(OkHttp) {
1416
engine {
1517
when (proxyConfig) {

core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import io.ktor.client.HttpClient
44
import kotlinx.coroutines.CoroutineScope
55
import kotlinx.coroutines.Dispatchers
66
import kotlinx.coroutines.SupervisorJob
7+
import kotlinx.coroutines.flow.first
8+
import kotlinx.coroutines.runBlocking
79
import org.koin.dsl.module
810
import zed.rainxch.core.data.data_source.TokenStore
911
import zed.rainxch.core.data.data_source.impl.DefaultTokenStore
@@ -26,6 +28,7 @@ import zed.rainxch.core.data.repository.ThemesRepositoryImpl
2628
import zed.rainxch.core.domain.getPlatform
2729
import zed.rainxch.core.domain.logging.GitHubStoreLogger
2830
import zed.rainxch.core.domain.model.Platform
31+
import zed.rainxch.core.domain.model.ProxyConfig
2932
import zed.rainxch.core.domain.repository.AuthenticationState
3033
import zed.rainxch.core.domain.repository.FavouritesRepository
3134
import zed.rainxch.core.domain.repository.InstalledAppsRepository
@@ -104,6 +107,29 @@ val coreModule = module {
104107

105108
val networkModule = module {
106109
single<GitHubClientProvider> {
110+
// Load saved proxy config SYNCHRONOUSLY before creating the client provider
111+
// so the very first HTTP client uses the correct proxy. This is critical for
112+
// users in regions where direct GitHub access is blocked (e.g. China).
113+
runBlocking {
114+
val config = get<ProxyRepository>().getProxyConfig().first()
115+
when (config) {
116+
is ProxyConfig.None -> ProxyManager.setNoProxy()
117+
is ProxyConfig.System -> ProxyManager.setSystemProxy()
118+
is ProxyConfig.Http -> ProxyManager.setHttpProxy(
119+
host = config.host,
120+
port = config.port,
121+
username = config.username,
122+
password = config.password
123+
)
124+
is ProxyConfig.Socks -> ProxyManager.setSocksProxy(
125+
host = config.host,
126+
port = config.port,
127+
username = config.username,
128+
password = config.password
129+
)
130+
}
131+
}
132+
107133
GitHubClientProvider(
108134
tokenStore = get(),
109135
rateLimitRepository = get(),

core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@ import kotlinx.coroutines.CoroutineScope
55
import kotlinx.coroutines.Dispatchers
66
import kotlinx.coroutines.SupervisorJob
77
import kotlinx.coroutines.cancel
8-
import kotlinx.coroutines.flow.Flow
9-
import kotlinx.coroutines.flow.MutableStateFlow
10-
import kotlinx.coroutines.flow.SharingStarted
118
import kotlinx.coroutines.flow.StateFlow
129
import kotlinx.coroutines.flow.distinctUntilChanged
13-
import kotlinx.coroutines.flow.map
14-
import kotlinx.coroutines.flow.stateIn
10+
import kotlinx.coroutines.flow.drop
11+
import kotlinx.coroutines.flow.launchIn
12+
import kotlinx.coroutines.flow.onEach
1513
import kotlinx.coroutines.sync.Mutex
1614
import kotlinx.coroutines.sync.withLock
1715
import zed.rainxch.core.data.data_source.TokenStore
@@ -24,37 +22,36 @@ class GitHubClientProvider(
2422
proxyConfigFlow: StateFlow<ProxyConfig>
2523
) {
2624
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
27-
private var currentClient: HttpClient? = null
2825
private val mutex = Mutex()
2926

30-
private val _client: StateFlow<HttpClient> = proxyConfigFlow
31-
.map { proxyConfig ->
32-
mutex.withLock {
33-
currentClient?.close()
34-
val newClient = createGitHubHttpClient(
35-
tokenStore = tokenStore,
36-
rateLimitRepository = rateLimitRepository,
37-
proxyConfig = proxyConfig
38-
)
39-
currentClient = newClient
40-
newClient
27+
@Volatile
28+
private var currentClient: HttpClient = createGitHubHttpClient(
29+
tokenStore = tokenStore,
30+
rateLimitRepository = rateLimitRepository,
31+
proxyConfig = proxyConfigFlow.value
32+
)
33+
34+
init {
35+
proxyConfigFlow
36+
.drop(1)
37+
.distinctUntilChanged()
38+
.onEach { proxyConfig ->
39+
mutex.withLock {
40+
currentClient.close()
41+
currentClient = createGitHubHttpClient(
42+
tokenStore = tokenStore,
43+
rateLimitRepository = rateLimitRepository,
44+
proxyConfig = proxyConfig
45+
)
46+
}
4147
}
42-
}
43-
.stateIn(
44-
scope = scope,
45-
started = SharingStarted.Eagerly,
46-
initialValue = createGitHubHttpClient(
47-
tokenStore = tokenStore,
48-
rateLimitRepository = rateLimitRepository,
49-
proxyConfig = proxyConfigFlow.value
50-
).also { currentClient = it }
51-
)
48+
.launchIn(scope)
49+
}
5250

53-
/** Get the current HttpClient (always up to date with proxy settings) */
54-
val client: HttpClient get() = _client.value
51+
val client: HttpClient get() = currentClient
5552

5653
fun close() {
57-
currentClient?.close()
54+
currentClient.close()
5855
scope.cancel()
5956
}
6057
}

core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,41 @@ class ProxyRepositoryImpl(
2525
return preferences.data.map { prefs ->
2626
when (prefs[proxyTypeKey]) {
2727
"system" -> ProxyConfig.System
28-
"http" -> ProxyConfig.Http(
29-
host = prefs[proxyHostKey] ?: "",
30-
port = prefs[proxyPortKey] ?: 8080,
31-
username = prefs[proxyUsernameKey],
32-
password = prefs[proxyPasswordKey]
33-
)
34-
"socks" -> ProxyConfig.Socks(
35-
host = prefs[proxyHostKey] ?: "",
36-
port = prefs[proxyPortKey] ?: 1080,
37-
username = prefs[proxyUsernameKey],
38-
password = prefs[proxyPasswordKey]
39-
)
28+
"http" -> {
29+
val host = prefs[proxyHostKey]?.takeIf { it.isNotBlank() }
30+
val port = prefs[proxyPortKey]?.takeIf { it in 1..65535 }
31+
if (host != null && port != null) {
32+
ProxyConfig.Http(
33+
host = host,
34+
port = port,
35+
username = prefs[proxyUsernameKey],
36+
password = prefs[proxyPasswordKey]
37+
)
38+
} else {
39+
ProxyConfig.None
40+
}
41+
}
42+
"socks" -> {
43+
val host = prefs[proxyHostKey]?.takeIf { it.isNotBlank() }
44+
val port = prefs[proxyPortKey]?.takeIf { it in 1..65535 }
45+
if (host != null && port != null) {
46+
ProxyConfig.Socks(
47+
host = host,
48+
port = port,
49+
username = prefs[proxyUsernameKey],
50+
password = prefs[proxyPasswordKey]
51+
)
52+
} else {
53+
ProxyConfig.None
54+
}
55+
}
4056
else -> ProxyConfig.None
4157
}
4258
}
4359
}
4460

4561
override suspend fun setProxyConfig(config: ProxyConfig) {
46-
applyToProxyManager(config)
62+
// Persist first so config survives crashes, then apply in-memory
4763
preferences.edit { prefs ->
4864
when (config) {
4965
is ProxyConfig.None -> {
@@ -92,6 +108,7 @@ class ProxyRepositoryImpl(
92108
}
93109
}
94110
}
111+
applyToProxyManager(config)
95112
}
96113

97114
private fun applyToProxyManager(config: ProxyConfig) {

feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ sealed interface ProfileAction {
1818
data class OnProxyPortChanged(val port: String) : ProfileAction
1919
data class OnProxyUsernameChanged(val username: String) : ProfileAction
2020
data class OnProxyPasswordChanged(val password: String) : ProfileAction
21+
data object OnProxyPasswordVisibilityToggle : ProfileAction
2122
data object OnProxySave : ProfileAction
2223
}

feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileEvent.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ sealed interface ProfileEvent {
44
data object OnLogoutSuccessful : ProfileEvent
55
data class OnLogoutError(val message: String) : ProfileEvent
66
data object OnProxySaved : ProfileEvent
7+
data class OnProxySaveError(val message: String) : ProfileEvent
78
}

feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ fun ProfileRoot(
7474
snackbarState.showSnackbar(getString(Res.string.proxy_saved))
7575
}
7676
}
77+
78+
is ProfileEvent.OnProxySaveError -> {
79+
coroutineScope.launch {
80+
snackbarState.showSnackbar(event.message)
81+
}
82+
}
7783
}
7884
}
7985

feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ data class ProfileState(
1919
val proxyPort: String = "",
2020
val proxyUsername: String = "",
2121
val proxyPassword: String = "",
22+
val isProxyPasswordVisible: Boolean = false,
2223
)
2324

2425
enum class ProxyType {

0 commit comments

Comments
 (0)