fix(android): unblock first device test — auth contrast + dynamic base URL
Two regressions surfaced on the first real device run.
1. Contrast on ServerUrl + Login screens: both wrapped content in a
bare Box(fillMaxSize), no Surface. The obsidian background never
painted (rendered against the system root view's default),
LocalContentColor cascade fell through to Material's default
contentColor — the screens rendered as near-invisible dark text
on dark grey. Wrap both in Surface(color = background, contentColor
= onBackground) so the bg paints AND the M3 contentColor pipeline
flows correctly through OutlinedTextField labels / cursor /
placeholders + the Button content tint.
2. The bigger bug: NetworkModule.provideRetrofit read
authStore.baseUrl.value ONCE at Retrofit creation. AuthStore loads
from Room async, so at injection time the value was still the
localhost:8080 placeholder. Result: even after the user typed
their real server URL on the ServerUrl screen, every API call
kept hitting localhost:8080 ("Failed to connect to
localhost/127.0.0.1:8080" on the login attempt). The pre-fix
NetworkModule comment even acknowledged it — *"Server-URL
changes require an app relaunch"*.
Fix: per-request rewrite. New BaseUrlInterceptor reads the live
AuthStore.baseUrl.value on every request and rewrites
scheme/host/port of the outgoing URL. Retrofit now keeps a
placeholder baseUrl ("http://placeholder.invalid/") solely to
satisfy its parser; the actual target host is dynamic. Order in
OkHttp chain: BaseUrl first → Auth → logging, so the cookie
interceptor sees the final URL.
New:
- api/BaseUrlInterceptor.kt — per-request scheme/host/port rewrite
from AuthStore.baseUrl. Falls through to the original request
when the stored URL is unparseable.
Modified:
- api/NetworkModule.kt — adds BaseUrlInterceptor to the OkHttp
chain. Drops the AuthStore dependency from provideRetrofit;
swaps baseUrl for the placeholder.
- auth/ui/ServerUrlScreen.kt — Box → Surface wrap.
- auth/ui/LoginScreen.kt — Box → Surface wrap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package com.fabledsword.minstrel.api
|
||||
|
||||
import com.fabledsword.minstrel.auth.AuthStore
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Rewrites every outgoing request's scheme/host/port to match the
|
||||
* current [AuthStore.baseUrl]. Retrofit's `.baseUrl(...)` is read
|
||||
* once at Retrofit creation, but AuthStore.baseUrl loads from Room
|
||||
* asynchronously — so at injection time the Retrofit instance is
|
||||
* frozen pointing at the [AuthStore.DEFAULT_BASE_URL] placeholder.
|
||||
*
|
||||
* This interceptor closes the gap: Retrofit can stay built with the
|
||||
* placeholder forever, and every actual request gets retargeted at
|
||||
* the live AuthStore value. Lets the user change server URL in
|
||||
* Settings without an app relaunch (Phase 11 wiring).
|
||||
*
|
||||
* No-op when the stored base URL is unparseable (falls through to
|
||||
* whatever Retrofit had) — that case shows up as a transport-level
|
||||
* error in the caller's normal error path.
|
||||
*/
|
||||
@Singleton
|
||||
class BaseUrlInterceptor @Inject constructor(
|
||||
private val authStore: AuthStore,
|
||||
) : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val original = chain.request()
|
||||
val baseUrl = authStore.baseUrl.value.toHttpUrlOrNull()
|
||||
?: return chain.proceed(original)
|
||||
val rewritten: HttpUrl = original.url.newBuilder()
|
||||
.scheme(baseUrl.scheme)
|
||||
.host(baseUrl.host)
|
||||
.port(baseUrl.port)
|
||||
.build()
|
||||
return chain.proceed(original.newBuilder().url(rewritten).build())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.fabledsword.minstrel.api
|
||||
|
||||
import com.fabledsword.minstrel.BuildConfig
|
||||
import com.fabledsword.minstrel.auth.AuthStore
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -44,10 +43,15 @@ object NetworkModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttp(
|
||||
baseUrl: BaseUrlInterceptor,
|
||||
auth: AuthCookieInterceptor,
|
||||
logging: HttpLoggingInterceptor,
|
||||
): OkHttpClient =
|
||||
OkHttpClient.Builder()
|
||||
// BaseUrlInterceptor first — it rewrites scheme/host/port
|
||||
// to match the live AuthStore.baseUrl on every request.
|
||||
// Auth then sees the final URL.
|
||||
.addInterceptor(baseUrl)
|
||||
.addInterceptor(auth)
|
||||
.addInterceptor(logging)
|
||||
.connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
@@ -59,13 +63,14 @@ object NetworkModule {
|
||||
fun provideRetrofit(
|
||||
okHttp: OkHttpClient,
|
||||
json: Json,
|
||||
authStore: AuthStore,
|
||||
): Retrofit =
|
||||
Retrofit.Builder()
|
||||
// Base URL is read once on Retrofit creation. Server-URL
|
||||
// changes (Settings screen) require an app relaunch; the
|
||||
// Settings flow that handles that lands in M8 phase 11.
|
||||
.baseUrl(authStore.baseUrl.value.trimEnd('/') + "/")
|
||||
// Placeholder base URL — BaseUrlInterceptor rewrites every
|
||||
// outgoing request to the live AuthStore value, so this
|
||||
// string only has to satisfy Retrofit's parser and is
|
||||
// never reached as an actual host. This lets the user
|
||||
// change server URL in Settings without an app relaunch.
|
||||
.baseUrl("http://placeholder.invalid/")
|
||||
.client(okHttp)
|
||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.fabledsword.minstrel.auth.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -13,6 +12,7 @@ import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -45,7 +45,11 @@ fun LoginScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
) {
|
||||
LoginForm(
|
||||
state = state,
|
||||
onUsernameChange = viewModel::setUsername,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.fabledsword.minstrel.auth.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -11,6 +10,7 @@ import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -101,7 +101,11 @@ fun ServerUrlScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
) {
|
||||
ServerUrlForm(
|
||||
state = state,
|
||||
onUrlChange = viewModel::setUrl,
|
||||
|
||||
Reference in New Issue
Block a user