fix(auth): end the identity-provider session on sign-out and force re-authentication on sign-in
Ordinary Sign out cleared only the QQQ session; the provider session left in the browser silently signed the same person back in (prompt=login alone was already observed reusing retained SSO on real Authentik). Sign out now runs the provider end-session flow when the server supports it, and every sign-in also sends max_age=0. Stored-session restore after sign-out was checked: not a cause. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3b15183f52
commit
b87171cafc
7 files changed
+91
-15
No files matched your search
@@ -3,6 +3,7 @@
|
|||||||
## 1.0.4 (versionCode 6)
|
## 1.0.4 (versionCode 6)
|
||||||
|
|
||||||
- Loading the server's metadata no longer fails with "Error loading QInstance: timeout" on a handheld. The metadata response is about 1.7 MB and takes the server 4 to 8 seconds to build, and the default 10 second read timeout expired. That one call now allows 60 seconds; every other call keeps its short timeout. A timeout now reads "Timed out after 60.0s loading the server's metadata from <server>/qqq/v1/metaData".
|
- Loading the server's metadata no longer fails with "Error loading QInstance: timeout" on a handheld. The metadata response is about 1.7 MB and takes the server 4 to 8 seconds to build, and the default 10 second read timeout expired. That one call now allows 60 seconds; every other call keeps its short timeout. A timeout now reads "Timed out after 60.0s loading the server's metadata from <server>/qqq/v1/metaData".
|
||||||
|
- Sign-out now also ends the identity-provider session in the browser when the server supports it (the same flow as "Sign out of company", now behind the single **Sign out** action), and every browser sign-in sends `max_age=0` next to `prompt=login`. Before, ordinary Sign out cleared only the QQQ session, so the provider session left in the browser silently signed the same person straight back in on the next sign-in. Servers without provider logout still get local plus QQQ sign-out. Stored-session restore after sign-out was checked and is not a cause (test added).
|
||||||
- Includes 1.0.3 (versionCode 5): string widget icons and summary-only help content from current QQQ core are accepted. (PR 24)
|
- Includes 1.0.3 (versionCode 5): string widget icons and summary-only help content from current QQQ core are accepted. (PR 24)
|
||||||
|
|
||||||
## 1.0.2 (versionCode 4), candidate
|
## 1.0.2 (versionCode 4), candidate
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ The fresh-login prompt is a request to the provider, not cryptographic proof of
|
|||||||
|
|
||||||
## Optional company sign-out (implementation awaiting qualification)
|
## Optional company sign-out (implementation awaiting qualification)
|
||||||
|
|
||||||
Ordinary **Sign out** continues to end only the QQQ session. The scanner also exposes a separate **Sign out of company** action in Account when the server advertises API1.10 or later within major1, feature `nativeSessionLogout`, and OAuth2 metadata `nativeSessionLogout` with flow `OIDC_RP_INITIATED_LOGOUT` and `postLogoutRedirectUris`. Other library hosts opt into that separate action with `LocalCompanySignOutEnabled`; it is disabled by default. No automatic provider-logout policy is selected. The confirmation explains that company sign-out may affect other applications in the browser.
|
Since 1.0.4, **Sign out** in Account ends the QQQ session and, when the server advertises API1.10 or later within major1, feature `nativeSessionLogout`, and OAuth2 metadata `nativeSessionLogout` with flow `OIDC_RP_INITIATED_LOGOUT` and `postLogoutRedirectUris`, also ends the provider session in the browser (formerly the separate **Sign out of company** action; without that support Sign out stays QQQ-only). Other library hosts opt into provider sign-out with `LocalCompanySignOutEnabled`; it is disabled by default. No automatic provider-logout policy is selected. The confirmation explains that company sign-out may affect other applications in the browser.
|
||||||
|
|
||||||
Register the installed package's exact callback separately as a provider **post-logout** redirect, even when it matches the existing login callback. The client sends `{"nativeLogout":{"postLogoutRedirectUri":"<registered callback>"}}` to the existing `POST /qqq/v1/logout`. Its optional `nativeLogoutData` contains `issuer`, `endSessionEndpoint`, `postLogoutRedirectUri` and sensitive `idTokenHint`. Old sessions or unsupported servers may legitimately omit this data. The ordinary logout request remains unchanged.
|
Register the installed package's exact callback separately as a provider **post-logout** redirect, even when it matches the existing login callback. The client sends `{"nativeLogout":{"postLogoutRedirectUri":"<registered callback>"}}` to the existing `POST /qqq/v1/logout`. Its optional `nativeLogoutData` contains `issuer`, `endSessionEndpoint`, `postLogoutRedirectUri` and sensitive `idTokenHint`. Old sessions or unsupported servers may legitimately omit this data. The ordinary logout request remains unchanged.
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -132,7 +132,9 @@ class QNavigator(initialController: NavHostController?, val qViewModel: QViewMod
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Route sign-out through explicit process-exit confirmation so pending work is not silently abandoned.
|
** Route sign-out through explicit process-exit confirmation so pending work is not silently abandoned.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
fun requestSignOut() = requestLeave("Sign out?", "Sign out", alwaysConfirm = true) { qViewModel.requestLogout() }
|
fun requestSignOut(providerSignOut: com.kingsrook.qqq.frontend.android.mobileapp.ui.authentication.OAuth2LogoutBinding? = null) =
|
||||||
|
if (providerSignOut != null) requestCompanySignOut(providerSignOut)
|
||||||
|
else requestLeave("Sign out?", "Sign out", alwaysConfirm = true) { qViewModel.requestLogout() }
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** The same guarded leave/cancel policy applies to explicitly enabled provider sign-out.
|
** The same guarded leave/cancel policy applies to explicitly enabled provider sign-out.
|
||||||
|
|||||||
+9
-1
@@ -43,6 +43,13 @@ import kotlin.coroutines.resumeWithException
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
val LocalOAuth2Discovery = staticCompositionLocalOf<suspend (OAuth2LoginBinding) -> AuthorizationServiceConfiguration> { ::discoverOAuth2 }
|
val LocalOAuth2Discovery = staticCompositionLocalOf<suspend (OAuth2LoginBinding) -> AuthorizationServiceConfiguration> { ::discoverOAuth2 }
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Sent with every interactive sign-in next to prompt=login. OIDC max_age=0 independently requires the provider to
|
||||||
|
** re-authenticate, so a provider session left in the browser cannot silently sign the next person in; the real
|
||||||
|
** Authentik test showed prompt=login alone reusing retained SSO (docs/AUTHENTICATION.md).
|
||||||
|
*******************************************************************************/
|
||||||
|
val OAUTH2_FORCE_REAUTH_PARAMETERS: Map<String, String> = mapOf("max_age" to "0")
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Browser authorization is standard AppAuth; API1.10 exchanges the code and validates the retained original nonce.
|
** Browser authorization is standard AppAuth; API1.10 exchanges the code and validates the retained original nonce.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@@ -93,7 +100,8 @@ fun QOAuth2Login(viewModel: QViewModel, metadata: OAuth2AuthenticationMetaData,
|
|||||||
val request = AuthorizationRequest.Builder(
|
val request = AuthorizationRequest.Builder(
|
||||||
configuration, binding.clientId, ResponseTypeValues.CODE,
|
configuration, binding.clientId, ResponseTypeValues.CODE,
|
||||||
Uri.parse(binding.redirectUri)
|
Uri.parse(binding.redirectUri)
|
||||||
).setScope(binding.scopes).setPrompt(binding.prompt).setResponseMode("query").build()
|
).setScope(binding.scopes).setPrompt(binding.prompt).setAdditionalParameters(OAUTH2_FORCE_REAUTH_PARAMETERS)
|
||||||
|
.setResponseMode("query").build()
|
||||||
check(request.codeVerifierChallengeMethod == "S256")
|
check(request.codeVerifierChallengeMethod == "S256")
|
||||||
if (viewModel.beginOAuth2Browser(
|
if (viewModel.beginOAuth2Browser(
|
||||||
generation, binding, requireNotNull(request.state),
|
generation, binding, requireNotNull(request.state),
|
||||||
|
|||||||
+20
-8
@@ -24,6 +24,7 @@ import com.kingsrook.qqq.frontend.android.mobileapp.viewmodel.LoadState
|
|||||||
import com.kingsrook.qqq.frontend.android.mobileapp.viewmodel.QViewModel
|
import com.kingsrook.qqq.frontend.android.mobileapp.viewmodel.QViewModel
|
||||||
import com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.OAuth2AuthenticationMetaData
|
import com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.OAuth2AuthenticationMetaData
|
||||||
import com.kingsrook.qqq.frontend.android.mobileapp.ui.authentication.LocalCompanySignOutEnabled
|
import com.kingsrook.qqq.frontend.android.mobileapp.ui.authentication.LocalCompanySignOutEnabled
|
||||||
|
import com.kingsrook.qqq.frontend.android.mobileapp.ui.authentication.OAuth2LogoutBinding
|
||||||
import com.kingsrook.qqq.frontend.android.mobileapp.ui.authentication.oauth2LogoutBinding
|
import com.kingsrook.qqq.frontend.android.mobileapp.ui.authentication.oauth2LogoutBinding
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@@ -34,10 +35,20 @@ val LocalOpenDiagnostics = compositionLocalOf<(() -> Unit)?> { null }
|
|||||||
val LocalBusinessNavigationEnabled = compositionLocalOf { true }
|
val LocalBusinessNavigationEnabled = compositionLocalOf { true }
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Expose account actions while keeping optional company sign-out separate from ordinary local sign-out.
|
** Expose account actions; sign-out also ends the provider session where the host and server support it.
|
||||||
|
*******************************************************************************/
|
||||||
|
/*******************************************************************************
|
||||||
|
** Sign-out ends the identity-provider session too whenever the host enables it and the server supports it;
|
||||||
|
** otherwise a session left in the browser silently signs the next sign-in back in. Null means local-only sign-out.
|
||||||
|
*******************************************************************************/
|
||||||
|
fun providerSignOutBinding(binding: OAuth2LogoutBinding?, serverSupportsProviderLogout: Boolean): OAuth2LogoutBinding? =
|
||||||
|
if (serverSupportsProviderLogout) binding else null
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Resolve the provider sign-out binding for the current server, or null when it is disabled or unsupported.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AccountDialog(model: QViewModel, dismiss: () -> Unit) {
|
fun rememberProviderSignOutBinding(model: QViewModel): OAuth2LogoutBinding? {
|
||||||
val companyLogout = if (LocalCompanySignOutEnabled.current)
|
val companyLogout = if (LocalCompanySignOutEnabled.current)
|
||||||
((model.authenticationMetaDataLoadState as? LoadState.Success)?.value as? OAuth2AuthenticationMetaData)?.let {
|
((model.authenticationMetaDataLoadState as? LoadState.Success)?.value as? OAuth2AuthenticationMetaData)?.let {
|
||||||
oauth2LogoutBinding(it, model.environment.baseUrl, LocalContext.current.packageName)
|
oauth2LogoutBinding(it, model.environment.baseUrl, LocalContext.current.packageName)
|
||||||
@@ -53,6 +64,12 @@ internal fun AccountDialog(model: QViewModel, dismiss: () -> Unit) {
|
|||||||
supportsCompanyLogout = false
|
supportsCompanyLogout = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return providerSignOutBinding(companyLogout, supportsCompanyLogout)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun AccountDialog(model: QViewModel, dismiss: () -> Unit) {
|
||||||
|
val providerSignOut = rememberProviderSignOutBinding(model)
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = dismiss, title = { Text("Account", Modifier.testTag("accountDialog.title")) },
|
onDismissRequest = dismiss, title = { Text("Account", Modifier.testTag("accountDialog.title")) },
|
||||||
text = {
|
text = {
|
||||||
@@ -60,13 +77,8 @@ internal fun AccountDialog(model: QViewModel, dismiss: () -> Unit) {
|
|||||||
Text(if (model.isAuthenticated) "Current User: ${model.sessionUserFullName}" else "Not currently logged in")
|
Text(if (model.isAuthenticated) "Current User: ${model.sessionUserFullName}" else "Not currently logged in")
|
||||||
TextButton(enabled = model.isAuthenticated && !model.isLogoutRequested, onClick = {
|
TextButton(enabled = model.isAuthenticated && !model.isLogoutRequested, onClick = {
|
||||||
dismiss()
|
dismiss()
|
||||||
model.qNavigator.requestSignOut()
|
model.qNavigator.requestSignOut(providerSignOut)
|
||||||
}) { Text("Sign out") }
|
}) { Text("Sign out") }
|
||||||
if (companyLogout != null && supportsCompanyLogout)
|
|
||||||
TextButton(enabled = model.isAuthenticated && !model.isLogoutRequested, onClick = {
|
|
||||||
dismiss()
|
|
||||||
model.qNavigator.requestCompanySignOut(companyLogout)
|
|
||||||
}) { Text("Sign out of company") }
|
|
||||||
}
|
}
|
||||||
}, confirmButton = { TextButton(onClick = dismiss) { Text("Close") } })
|
}, confirmButton = { TextButton(onClick = dismiss) { Text("Close") } })
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-3
@@ -13,7 +13,9 @@ import com.kingsrook.qqq.frontend.android.core.model.metadata.authentication.OAu
|
|||||||
import com.kingsrook.qqq.frontend.android.core.model.processes.ProcessStepResult
|
import com.kingsrook.qqq.frontend.android.core.model.processes.ProcessStepResult
|
||||||
import com.kingsrook.qqq.frontend.android.mobileapp.container.QAppContainer
|
import com.kingsrook.qqq.frontend.android.mobileapp.container.QAppContainer
|
||||||
import com.kingsrook.qqq.frontend.android.mobileapp.data.MockPreferencesDataStore
|
import com.kingsrook.qqq.frontend.android.mobileapp.data.MockPreferencesDataStore
|
||||||
|
import com.kingsrook.qqq.frontend.android.mobileapp.ui.authentication.OAUTH2_FORCE_REAUTH_PARAMETERS
|
||||||
import com.kingsrook.qqq.frontend.android.mobileapp.ui.authentication.OAuth2LoginBinding
|
import com.kingsrook.qqq.frontend.android.mobileapp.ui.authentication.OAuth2LoginBinding
|
||||||
|
import com.kingsrook.qqq.frontend.android.mobileapp.ui.horseshoe.providerSignOutBinding
|
||||||
import com.kingsrook.qqq.frontend.android.mobileapp.ui.authentication.OAuth2LogoutBinding
|
import com.kingsrook.qqq.frontend.android.mobileapp.ui.authentication.OAuth2LogoutBinding
|
||||||
import com.kingsrook.qqq.frontend.android.mobileapp.ui.authentication.OAuth2LogoutPhase
|
import com.kingsrook.qqq.frontend.android.mobileapp.ui.authentication.OAuth2LogoutPhase
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -145,12 +147,60 @@ class QViewModelLogoutTest {
|
|||||||
assertNull(json.decodeFromString<ManageSessionResponse>("""{"uuid":"s","values":{"user":{"name":" "}}}""").userName)
|
assertNull(json.decodeFromString<ManageSessionResponse>("""{"uuid":"s","values":{"user":{"name":" "}}}""").userName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** After sign-out (ordinary or provider) a restarted app finds no stored session and does not resume one.
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
fun restartAfterSignOutDoesNotRestoreTheSession() = runTest()
|
||||||
|
{
|
||||||
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
||||||
|
try {
|
||||||
|
for (provider in listOf(false, true)) {
|
||||||
|
val store = MockPreferencesDataStore()
|
||||||
|
val repository = LogoutRepository()
|
||||||
|
val model = signedInModel(repository, store)
|
||||||
|
assertEquals("session-1", store.preferences[com.kingsrook.qqq.frontend.android.mobileapp.data.SESSION_UUID_KEY])
|
||||||
|
if (provider) assertNotNull(companyLogout(model))
|
||||||
|
else { model.requestLogout(); model.logOut().join() }
|
||||||
|
assertNull(store.preferences[com.kingsrook.qqq.frontend.android.mobileapp.data.SESSION_UUID_KEY]?.takeIf { it.isNotEmpty() })
|
||||||
|
val restartedRepository = LogoutRepository()
|
||||||
|
val restarted = QViewModel(restartedRepository, store)
|
||||||
|
advanceUntilIdle()
|
||||||
|
assertFalse(restarted.isAuthenticated)
|
||||||
|
assertEquals("", restarted.sessionUUID)
|
||||||
|
assertEquals(0, restartedRepository.metaDataCalls)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Sign-out reaches the identity provider whenever the server supports it, never silently local-only.
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
fun signOutUsesProviderLogoutWhenSupported() {
|
||||||
|
val binding = OAuth2LogoutBinding(loginBinding, REDIRECT)
|
||||||
|
assertEquals(binding, providerSignOutBinding(binding, true))
|
||||||
|
assertNull(providerSignOutBinding(binding, false))
|
||||||
|
assertNull(providerSignOutBinding(null, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Every interactive sign-in forces re-authentication, so a lingering provider session cannot sign the next user in.
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
fun authorizationRequestForcesReauthentication() {
|
||||||
|
assertEquals(mapOf("max_age" to "0"), OAUTH2_FORCE_REAUTH_PARAMETERS)
|
||||||
|
assertEquals("login", loginBinding.prompt)
|
||||||
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Complete a native browser login against the fake repository so sign-out tests start authenticated.
|
** Complete a native browser login against the fake repository so sign-out tests start authenticated.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private fun TestScope.signedInModel(repository: LogoutRepository): QViewModel {
|
private fun TestScope.signedInModel(repository: LogoutRepository, store: MockPreferencesDataStore = MockPreferencesDataStore()): QViewModel {
|
||||||
QAppContainer.availableEnvironments = listOf(Environment("Test", SERVER))
|
QAppContainer.availableEnvironments = listOf(Environment("Test", SERVER))
|
||||||
val model = QViewModel(repository, MockPreferencesDataStore())
|
val model = QViewModel(repository, store)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
val generation = requireNotNull(model.prepareOAuth2Login(loginBinding))
|
val generation = requireNotNull(model.prepareOAuth2Login(loginBinding))
|
||||||
assertTrue(model.beginOAuth2Browser(generation, loginBinding, STATE, NONCE, VERIFIER))
|
assertTrue(model.beginOAuth2Browser(generation, loginBinding, STATE, NONCE, VERIFIER))
|
||||||
@@ -182,6 +232,7 @@ class QViewModelLogoutTest {
|
|||||||
private class LogoutRepository : QQQRepository {
|
private class LogoutRepository : QQQRepository {
|
||||||
val calls = mutableListOf<String>()
|
val calls = mutableListOf<String>()
|
||||||
var activeSession = ""
|
var activeSession = ""
|
||||||
|
var metaDataCalls = 0
|
||||||
|
|
||||||
override suspend fun getAuthenticationMetaData(): BaseAuthenticationMetaData = OAuth2AuthenticationMetaData(
|
override suspend fun getAuthenticationMetaData(): BaseAuthenticationMetaData = OAuth2AuthenticationMetaData(
|
||||||
"oauth", values = OAuth2AuthenticationMetaData.Values(
|
"oauth", values = OAuth2AuthenticationMetaData.Values(
|
||||||
@@ -214,7 +265,7 @@ class QViewModelLogoutTest {
|
|||||||
|
|
||||||
override fun setEnvironment(environment: Environment) {}
|
override fun setEnvironment(environment: Environment) {}
|
||||||
override suspend fun manageSession(body: ManageSessionRequest): ManageSessionResponse = error("Not used")
|
override suspend fun manageSession(body: ManageSessionRequest): ManageSessionResponse = error("Not used")
|
||||||
override suspend fun getMetaData(frontendName: String, frontendVersion: String, applicationName: String, applicationVersion: String): QInstance = QInstance()
|
override suspend fun getMetaData(frontendName: String, frontendVersion: String, applicationName: String, applicationVersion: String): QInstance { metaDataCalls++; return QInstance() }
|
||||||
override suspend fun getProcessMetaData(processName: String): QProcessMetaData = error("Not used")
|
override suspend fun getProcessMetaData(processName: String): QProcessMetaData = error("Not used")
|
||||||
override suspend fun processInit(processName: String, values: Map<String, Any?>, recordsParam: String?, recordIds: String?, filterJSON: String?, stepTimeoutMillis: Int?): ProcessStepResult = error("Not used")
|
override suspend fun processInit(processName: String, values: Map<String, Any?>, recordsParam: String?, recordIds: String?, filterJSON: String?, stepTimeoutMillis: Int?): ProcessStepResult = error("Not used")
|
||||||
override suspend fun processStep(processName: String, processUUID: String, stepName: String, values: Map<String, Any?>, stepTimeoutMillis: Int?): ProcessStepResult = error("Not used")
|
override suspend fun processStep(processName: String, processUUID: String, stepName: String, values: Map<String, Any?>, stepTimeoutMillis: Int?): ProcessStepResult = error("Not used")
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import androidx.compose.material3.TextButton
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import com.kingsrook.qqq.frontend.android.mobileapp.ui.authentication.LocalCompanySignOutEnabled
|
import com.kingsrook.qqq.frontend.android.mobileapp.ui.authentication.LocalCompanySignOutEnabled
|
||||||
|
import com.kingsrook.qqq.frontend.android.mobileapp.ui.horseshoe.rememberProviderSignOutBinding
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
@@ -405,6 +406,7 @@ private fun DeviceGate(
|
|||||||
viewModel: QViewModel, api: DeviceApi, credentials: DeviceCredentials, onApproval: (Boolean) -> Unit,
|
viewModel: QViewModel, api: DeviceApi, credentials: DeviceCredentials, onApproval: (Boolean) -> Unit,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val providerSignOut = rememberProviderSignOutBinding(viewModel)
|
||||||
var attempt by remember { mutableIntStateOf(0) }
|
var attempt by remember { mutableIntStateOf(0) }
|
||||||
var status by remember(viewModel.sessionUUID) { mutableStateOf("CHECKING") }
|
var status by remember(viewModel.sessionUUID) { mutableStateOf("CHECKING") }
|
||||||
var error by remember { mutableStateOf("") }
|
var error by remember { mutableStateOf("") }
|
||||||
@@ -444,6 +446,6 @@ private fun DeviceGate(
|
|||||||
Text("Your administrator can edit this device’s name, short name and description in QQQ before approval.")
|
Text("Your administrator can edit this device’s name, short name and description in QQQ before approval.")
|
||||||
Button(enabled = status != "CHECKING", onClick = { attempt++ }) { Text("Check approval") }
|
Button(enabled = status != "CHECKING", onClick = { attempt++ }) { Text("Check approval") }
|
||||||
if (enrollmentConflict) Button(onClick = { credentials.restartPendingEnrollment(); attempt++ }) { Text("Start new registration") }
|
if (enrollmentConflict) Button(onClick = { credentials.restartPendingEnrollment(); attempt++ }) { Text("Start new registration") }
|
||||||
Button(onClick = { viewModel.qNavigator.requestSignOut() }) { Text("Sign out") }
|
Button(onClick = { viewModel.qNavigator.requestSignOut(providerSignOut) }) { Text("Sign out") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user