Skip to content

Commit ddc9363

Browse files
committed
fix: sso flow fails after using a deep link [WPB-23906] (#4628)
(cherry picked from commit 735e9a6)
1 parent d87dd3f commit ddc9363

9 files changed

Lines changed: 106 additions & 159 deletions

File tree

app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,9 @@ import androidx.compose.ui.Modifier
2626
import androidx.hilt.navigation.compose.hiltViewModel
2727
import com.ramcosta.composedestinations.DestinationsNavHost
2828
import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination
29-
import com.ramcosta.composedestinations.generated.app.destinations.LoginScreenDestination
3029
import com.ramcosta.composedestinations.generated.app.destinations.NewLoginPasswordScreenDestination
31-
import com.ramcosta.composedestinations.generated.app.destinations.NewLoginScreenDestination
3230
import com.ramcosta.composedestinations.generated.app.destinations.NewLoginVerificationCodeScreenDestination
33-
import com.ramcosta.composedestinations.generated.app.navgraphs.LoginGraph
3431
import com.ramcosta.composedestinations.generated.app.navgraphs.NewConversationGraph
35-
import com.ramcosta.composedestinations.generated.app.navgraphs.NewLoginGraph
3632
import com.ramcosta.composedestinations.generated.app.navgraphs.PersonalToTeamMigrationGraph
3733
import com.ramcosta.composedestinations.generated.app.navgraphs.WireRootGraph
3834
import com.ramcosta.composedestinations.generated.app.navtype.groupConversationDetailsNavBackArgsNavType
@@ -49,8 +45,6 @@ import com.ramcosta.composedestinations.scope.resultRecipient
4945
import com.ramcosta.composedestinations.spec.Direction
5046
import com.wire.android.feature.sketch.model.DrawingCanvasNavBackArgs
5147
import com.wire.android.ui.authentication.login.email.LoginEmailViewModel
52-
import com.wire.android.ui.authentication.login.sso.SSOUrlConfigHolder
53-
import com.wire.android.ui.authentication.login.sso.SSOUrlConfigHolderImpl
5448
import com.wire.android.ui.home.conversations.ConversationScreen
5549
import com.wire.android.ui.home.newconversation.NewConversationViewModel
5650
import com.wire.android.ui.userprofile.teammigration.TeamMigrationViewModel
@@ -75,14 +69,6 @@ fun MainNavHost(
7569
// 👇 To make Navigator available to all destinations as a non-navigation parameter
7670
dependency(navigator)
7771

78-
// Always provide a default SSO holder at root scope so destinations can resolve it
79-
// even when navigated directly without going through the expected nested graph route.
80-
val rootEntry = remember(navBackStackEntry) {
81-
navController.getBackStackEntry(WireRootGraph.route)
82-
}
83-
val rootSSOHolder: SSOUrlConfigHolder = SSOUrlConfigHolderImpl(rootEntry.savedStateHandle)
84-
dependency(rootSSOHolder)
85-
8672
// 👇 To make LoginTypeSelector available to all destinations as a non-navigation parameter if provided
8773
if (loginTypeSelector != null) dependency(loginTypeSelector)
8874

@@ -102,35 +88,6 @@ fun MainNavHost(
10288
dependency(hiltViewModel<LoginEmailViewModel>(loginPasswordEntry))
10389
}
10490

105-
// 👇 To tie SSOUrlConfigHolder to nested LoginNavGraph, making it shared between all screens that belong to it
106-
navGraph(LoginGraph) {
107-
val parentEntry = remember(navBackStackEntry) {
108-
navController.getBackStackEntry(LoginGraph.route)
109-
}
110-
val holder: SSOUrlConfigHolder = SSOUrlConfigHolderImpl(parentEntry.savedStateHandle)
111-
dependency(holder)
112-
}
113-
114-
// 👇 To tie SSOUrlConfigHolder to nested NewLoginNavGraph, making it shared between all screens that belong to it
115-
navGraph(NewLoginGraph) {
116-
val parentEntry = remember(navBackStackEntry) {
117-
navController.getBackStackEntry(NewLoginGraph.route)
118-
}
119-
val holder: SSOUrlConfigHolder = SSOUrlConfigHolderImpl(parentEntry.savedStateHandle)
120-
dependency(holder)
121-
}
122-
123-
// Some flows navigate directly to screen destinations instead of the nav graph route.
124-
// Provide the dependency at destination scope as a safe fallback.
125-
destination(LoginScreenDestination) {
126-
val holder: SSOUrlConfigHolder = SSOUrlConfigHolderImpl(navBackStackEntry.savedStateHandle)
127-
dependency(holder)
128-
}
129-
destination(NewLoginScreenDestination) {
130-
val holder: SSOUrlConfigHolder = SSOUrlConfigHolderImpl(navBackStackEntry.savedStateHandle)
131-
dependency(holder)
132-
}
133-
13491
// 👇 To tie TeamMigrationViewModel to PersonalToTeamMigrationNavGraph, making it shared between all screens that belong to it
13592
navGraph(PersonalToTeamMigrationGraph) {
13693
val parentEntry = remember(navBackStackEntry) {

app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
package com.wire.android.ui.authentication.login
2020

21-
import com.wire.android.navigation.annotation.app.WireLoginDestination
2221
import androidx.annotation.StringRes
2322
import androidx.compose.animation.AnimatedContent
2423
import androidx.compose.animation.togetherWith
@@ -46,18 +45,21 @@ import androidx.compose.ui.platform.LocalFocusManager
4645
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
4746
import androidx.compose.ui.res.stringResource
4847
import androidx.hilt.navigation.compose.hiltViewModel
48+
import com.ramcosta.composedestinations.generated.app.destinations.E2EIEnrollmentScreenDestination
49+
import com.ramcosta.composedestinations.generated.app.destinations.HomeScreenDestination
50+
import com.ramcosta.composedestinations.generated.app.destinations.InitialSyncScreenDestination
51+
import com.ramcosta.composedestinations.generated.app.destinations.RemoveDeviceScreenDestination
4952
import com.wire.android.R
5053
import com.wire.android.navigation.BackStackMode
5154
import com.wire.android.navigation.NavigationCommand
5255
import com.wire.android.navigation.Navigator
56+
import com.wire.android.navigation.annotation.app.WireLoginDestination
5357
import com.wire.android.navigation.style.TransitionAnimationType
5458
import com.wire.android.ui.authentication.create.common.ServerTitle
5559
import com.wire.android.ui.authentication.login.email.LoginEmailScreen
5660
import com.wire.android.ui.authentication.login.email.LoginEmailVerificationCodeScreen
5761
import com.wire.android.ui.authentication.login.email.LoginEmailViewModel
5862
import com.wire.android.ui.authentication.login.sso.LoginSSOScreen
59-
import com.wire.android.ui.authentication.login.sso.SSOUrlConfigHolder
60-
import com.wire.android.ui.authentication.login.sso.SSOUrlConfigHolderPreview
6163
import com.wire.android.ui.common.TabItem
6264
import com.wire.android.ui.common.WireTabRow
6365
import com.wire.android.ui.common.calculateCurrentTab
@@ -68,10 +70,6 @@ import com.wire.android.ui.common.scaffold.WireScaffold
6870
import com.wire.android.ui.common.topappbar.NavigationIconType
6971
import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar
7072
import com.wire.android.ui.common.visbility.rememberVisibilityState
71-
import com.ramcosta.composedestinations.generated.app.destinations.E2EIEnrollmentScreenDestination
72-
import com.ramcosta.composedestinations.generated.app.destinations.HomeScreenDestination
73-
import com.ramcosta.composedestinations.generated.app.destinations.InitialSyncScreenDestination
74-
import com.ramcosta.composedestinations.generated.app.destinations.RemoveDeviceScreenDestination
7573
import com.wire.android.ui.theme.WireTheme
7674
import com.wire.android.ui.theme.wireDimensions
7775
import com.wire.android.ui.theme.wireTypography
@@ -88,7 +86,6 @@ import kotlinx.coroutines.launch
8886
fun LoginScreen(
8987
navigator: Navigator,
9088
loginNavArgs: LoginNavArgs,
91-
ssoUrlConfigHolder: SSOUrlConfigHolder,
9289
loginEmailViewModel: LoginEmailViewModel = hiltViewModel()
9390
) {
9491

@@ -110,7 +107,7 @@ fun LoginScreen(
110107
},
111108
loginEmailViewModel = loginEmailViewModel,
112109
ssoLoginResult = loginNavArgs.ssoLoginResult,
113-
ssoUrlConfigHolder = ssoUrlConfigHolder,
110+
// ssoUrlConfigHolder = ssoUrlConfigHolder,
114111
)
115112
}
116113

@@ -121,7 +118,6 @@ private fun LoginContent(
121118
onRemoveDeviceNeeded: () -> Unit,
122119
loginEmailViewModel: LoginEmailViewModel,
123120
ssoLoginResult: DeepLinkResult.SSOLogin?,
124-
ssoUrlConfigHolder: SSOUrlConfigHolder,
125121
) {
126122
Column(modifier = Modifier.fillMaxSize()) {
127123
/*
@@ -144,7 +140,7 @@ private fun LoginContent(
144140
onRemoveDeviceNeeded = onRemoveDeviceNeeded,
145141
loginEmailViewModel = loginEmailViewModel,
146142
ssoLoginResult = ssoLoginResult,
147-
ssoUrlConfigHolder = ssoUrlConfigHolder
143+
// ssoUrlConfigHolder = ssoUrlConfigHolder
148144
)
149145
}
150146
}
@@ -159,7 +155,6 @@ private fun MainLoginContent(
159155
onRemoveDeviceNeeded: () -> Unit,
160156
loginEmailViewModel: LoginEmailViewModel,
161157
ssoLoginResult: DeepLinkResult.SSOLogin?,
162-
ssoUrlConfigHolder: SSOUrlConfigHolder,
163158
) {
164159

165160
val scope = rememberCoroutineScope()
@@ -228,7 +223,12 @@ private fun MainLoginContent(
228223
) { pageIndex ->
229224
when (LoginTabItem.values()[pageIndex]) {
230225
LoginTabItem.EMAIL -> LoginEmailScreen(onSuccess, onRemoveDeviceNeeded, loginEmailViewModel, scrollState)
231-
LoginTabItem.SSO -> LoginSSOScreen(onSuccess, onRemoveDeviceNeeded, ssoLoginResult, ssoUrlConfigHolder)
226+
LoginTabItem.SSO -> LoginSSOScreen(
227+
onSuccess,
228+
onRemoveDeviceNeeded,
229+
ssoLoginResult,
230+
// ssoUrlConfigHolder
231+
)
232232
}
233233
}
234234
if (!pagerState.isScrollInProgress && focusedTabIndex != pagerState.currentPage) {
@@ -245,6 +245,7 @@ private fun MainLoginContent(
245245
enum class LoginTabItem(@StringRes val titleResId: Int) : TabItem {
246246
EMAIL(R.string.login_tab_email),
247247
SSO(R.string.login_tab_sso);
248+
248249
override val title: UIText = UIText.StringResource(titleResId)
249250
}
250251

@@ -258,7 +259,7 @@ private fun PreviewLoginScreen() = WireTheme {
258259
onRemoveDeviceNeeded = {},
259260
loginEmailViewModel = hiltViewModel(),
260261
ssoLoginResult = null,
261-
ssoUrlConfigHolder = SSOUrlConfigHolderPreview,
262+
// ssoUrlConfigHolder = SSOUrlConfigHolderPreview,
262263
)
263264
}
264265
}

app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,16 @@ fun LoginSSOScreen(
6464
onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit,
6565
onRemoveDeviceNeeded: () -> Unit,
6666
ssoLoginResult: DeepLinkResult.SSOLogin?,
67-
ssoUrlConfigHolder: SSOUrlConfigHolder,
6867
loginSSOViewModel: LoginSSOViewModel = hiltViewModel(),
6968
scrollState: ScrollState = rememberScrollState()
7069
) {
7170
val scope = rememberCoroutineScope()
7271
val context = LocalContext.current
7372

7473
LaunchedEffect(ssoLoginResult) {
75-
loginSSOViewModel.handleSSOResult(ssoLoginResult, ssoUrlConfigHolder.get()?.serverConfig)
74+
loginSSOViewModel.handleSSOResult(
75+
ssoLoginResult,
76+
)
7677
}
7778
LoginSSOContent(
7879
scrollState = scrollState,
@@ -85,12 +86,11 @@ fun LoginSSOScreen(
8586
},
8687
onLoginButtonClick = loginSSOViewModel::login,
8788
onCustomServerDialogDismiss = loginSSOViewModel::onCustomServerDialogDismiss,
88-
onCustomServerDialogConfirm = loginSSOViewModel::onCustomServerDialogConfirm
89+
onCustomServerDialogConfirm = loginSSOViewModel::onCustomServerDialogConfirm
8990
)
9091

9192
LaunchedEffect(loginSSOViewModel) {
9293
loginSSOViewModel.openWebUrl.onEach { (url, serverConfig) ->
93-
ssoUrlConfigHolder.set(SSOUrlConfig(serverConfig))
9494
CustomTabsHelper.launchUrl(context, url)
9595
}.launchIn(scope)
9696
}

app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ class LoginSSOViewModel(
156156
ssoCode = defaultSSOCode,
157157
onAuthScopeFailure = { updateSSOFlowState(it.toLoginError()) },
158158
onSSOInitiateFailure = { updateSSOFlowState(it.toLoginSSOError()) },
159-
onSuccess = { requestUrl, _ -> openWebUrl(requestUrl, state.serverLinks) }
159+
onSuccess = { requestUrl -> openWebUrl(requestUrl, state.serverLinks) }
160160
)
161161
}
162162
}
@@ -208,7 +208,7 @@ class LoginSSOViewModel(
208208
ssoCode = ssoTextState.text.toString(),
209209
onAuthScopeFailure = { updateSSOFlowState(it.toLoginError()) },
210210
onSSOInitiateFailure = { updateSSOFlowState(it.toLoginSSOError()) },
211-
onSuccess = { requestUrl, _ -> openWebUrl(requestUrl, serverConfig) }
211+
onSuccess = { requestUrl -> openWebUrl(requestUrl, serverConfig) }
212212
)
213213
}
214214
}
@@ -218,14 +218,12 @@ class LoginSSOViewModel(
218218
fun establishSSOSession(
219219
cookie: String,
220220
serverConfigId: String,
221-
serverConfig: ServerConfig.Links? = null,
222221
) {
223222
updateSSOFlowState(LoginState.Loading)
224223
viewModelScope.launch {
225224
ssoExtension.establishSSOSession(
226225
cookie = cookie,
227226
serverConfigId = serverConfigId,
228-
serverConfig = serverConfig ?: this@LoginSSOViewModel.serverConfig,
229227
onAuthScopeFailure = { updateSSOFlowState(it.toLoginError()) },
230228
onSSOLoginFailure = { updateSSOFlowState(it.toLoginError()) },
231229
onAddAuthenticatedUserFailure = { updateSSOFlowState(it.toLoginError()) },
@@ -247,10 +245,15 @@ class LoginSSOViewModel(
247245
}
248246
}
249247

250-
fun handleSSOResult(ssoLoginResult: DeepLinkResult.SSOLogin?, serverConfig: ServerConfig.Links? = null) {
248+
fun handleSSOResult(
249+
ssoLoginResult: DeepLinkResult.SSOLogin?,
250+
) {
251251
when (ssoLoginResult) {
252252
is DeepLinkResult.SSOLogin.Success -> {
253-
establishSSOSession(ssoLoginResult.cookie, ssoLoginResult.serverConfigId, serverConfig)
253+
establishSSOSession(
254+
ssoLoginResult.cookie,
255+
ssoLoginResult.serverConfigId,
256+
)
254257
}
255258

256259
is DeepLinkResult.SSOLogin.Failure ->

app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelExtension.kt

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818
package com.wire.android.ui.authentication.login.sso
1919

20+
import com.wire.android.appLogger
2021
import com.wire.kalium.logic.CoreLogic
2122
import com.wire.kalium.logic.configuration.server.ServerConfig
2223
import com.wire.kalium.logic.data.user.UserId
@@ -52,13 +53,13 @@ class LoginSSOViewModelExtension(
5253
ssoCode: String,
5354
onAuthScopeFailure: (AutoVersionAuthScopeUseCase.Result.Failure) -> Unit,
5455
onSSOInitiateFailure: (SSOInitiateLoginResult.Failure) -> Unit,
55-
onSuccess: suspend (redirectUrl: String, serverConfig: ServerConfig.Links) -> Unit,
56+
onSuccess: suspend (redirectUrl: String) -> Unit,
5657
) {
5758
withAuthenticationScope(serverConfig, onAuthScopeFailure) { authScope ->
5859
authScope.ssoLoginScope.initiate(SSOInitiateLoginUseCase.Param.WithRedirect(ssoCode)).let { result ->
5960
when (result) {
6061
is SSOInitiateLoginResult.Failure -> onSSOInitiateFailure(result)
61-
is SSOInitiateLoginResult.Success -> onSuccess(result.requestUrl, serverConfig)
62+
is SSOInitiateLoginResult.Success -> onSuccess(result.requestUrl)
6263
}
6364
}
6465
}
@@ -84,34 +85,44 @@ class LoginSSOViewModelExtension(
8485
suspend fun establishSSOSession(
8586
cookie: String,
8687
serverConfigId: String,
87-
serverConfig: ServerConfig.Links,
8888
onAuthScopeFailure: (AutoVersionAuthScopeUseCase.Result.Failure) -> Unit,
8989
onSSOLoginFailure: (SSOLoginSessionResult.Failure) -> Unit,
9090
onAddAuthenticatedUserFailure: (AddAuthenticatedUserUseCase.Result.Failure) -> Unit,
9191
onSuccess: suspend (UserId) -> Unit,
9292
) {
93-
withAuthenticationScope(serverConfig, onAuthScopeFailure) { authScope ->
94-
authScope.ssoLoginScope.getLoginSession(cookie).let { ssoLoginResult ->
95-
when (ssoLoginResult) {
96-
is SSOLoginSessionResult.Failure -> onSSOLoginFailure(ssoLoginResult)
97-
is SSOLoginSessionResult.Success -> {
98-
addAuthenticatedUser(
99-
authTokens = ssoLoginResult.accountTokens,
100-
ssoId = ssoLoginResult.ssoId,
101-
serverConfigId = serverConfigId,
102-
proxyCredentials = ssoLoginResult.proxyCredentials,
103-
managedBy = ssoLoginResult.managedBy,
104-
isPersistentWebSocketEnabled = defaultWebSocketEnabledByDefault,
105-
replace = false
106-
).let { authenticatedUserResult ->
107-
when (authenticatedUserResult) {
108-
is AddAuthenticatedUserUseCase.Result.Failure -> onAddAuthenticatedUserFailure(authenticatedUserResult)
109-
is AddAuthenticatedUserUseCase.Result.Success -> onSuccess(authenticatedUserResult.userId)
110-
}
111-
}
112-
}
113-
}
93+
val authScope = when (val result = coreLogic.authenticationScopeForConfigId(serverConfigId)) {
94+
is AutoVersionAuthScopeUseCase.Result.Success -> {
95+
appLogger.i("SSO: Resolved auth scope from serverConfigId=$serverConfigId")
96+
result.authenticationScope
97+
}
98+
is AutoVersionAuthScopeUseCase.Result.Failure -> {
99+
appLogger.e("SSO: Failed to resolve auth scope for serverConfigId=$serverConfigId")
100+
onAuthScopeFailure(result)
101+
return
102+
}
103+
}
104+
105+
val ssoLoginSuccess = when (val ssoLoginResult = authScope.ssoLoginScope.getLoginSession(cookie)) {
106+
is SSOLoginSessionResult.Failure -> {
107+
onSSOLoginFailure(ssoLoginResult)
108+
return
114109
}
110+
is SSOLoginSessionResult.Success -> ssoLoginResult
111+
}
112+
113+
val authenticatedUserResult = addAuthenticatedUser(
114+
authTokens = ssoLoginSuccess.accountTokens,
115+
ssoId = ssoLoginSuccess.ssoId,
116+
serverConfigId = serverConfigId,
117+
proxyCredentials = ssoLoginSuccess.proxyCredentials,
118+
managedBy = ssoLoginSuccess.managedBy,
119+
isPersistentWebSocketEnabled = defaultWebSocketEnabledByDefault,
120+
replace = false
121+
)
122+
123+
when (authenticatedUserResult) {
124+
is AddAuthenticatedUserUseCase.Result.Failure -> onAddAuthenticatedUserFailure(authenticatedUserResult)
125+
is AddAuthenticatedUserUseCase.Result.Success -> onSuccess(authenticatedUserResult.userId)
115126
}
116127
}
117128
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2025 Wire Swiss GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see http://www.gnu.org/licenses/.
17+
*/
18+
package com.wire.android.ui.authentication.login.sso
19+
20+
import kotlinx.serialization.Serializable
21+
22+
@Serializable
23+
data class SSOUrlConfig(val userIdentifier: String = "")

0 commit comments

Comments
 (0)