Skip to content

Commit a24639c

Browse files
committed
feat(nomad profiles): login and log out intent
1 parent 7903610 commit a24639c

File tree

12 files changed

+450
-26
lines changed

12 files changed

+450
-26
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,13 @@
318318
<receiver
319319
android:name=".notification.broadcastreceivers.StopAudioMessageReceiver"
320320
android:exported="false" />
321+
<receiver
322+
android:name=".notification.broadcastreceivers.LogoutReceiver"
323+
android:exported="true">
324+
<intent-filter>
325+
<action android:name="com.wire.ACTION_LOGOUT" />
326+
</intent-filter>
327+
</receiver>
321328

322329
<service
323330
android:name=".services.WireFirebaseMessagingService"
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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.notification.broadcastreceivers
19+
20+
import android.content.Context
21+
import android.content.Intent
22+
import com.wire.android.appLogger
23+
import com.wire.android.di.KaliumCoreLogic
24+
import com.wire.android.feature.AccountSwitchUseCase
25+
import com.wire.android.feature.SwitchAccountParam
26+
import com.wire.android.ui.WireActivity
27+
import com.wire.kalium.logic.CoreLogic
28+
import com.wire.kalium.logic.data.logout.LogoutReason
29+
import com.wire.kalium.logic.feature.session.CurrentSessionResult
30+
import com.wire.kalium.logic.feature.session.CurrentSessionUseCase
31+
import com.wire.kalium.logic.feature.session.DeleteSessionUseCase
32+
import dagger.hilt.android.AndroidEntryPoint
33+
import javax.inject.Inject
34+
35+
@AndroidEntryPoint
36+
class LogoutReceiver : CoroutineReceiver() {
37+
38+
@Inject
39+
@KaliumCoreLogic
40+
lateinit var coreLogic: CoreLogic
41+
42+
@Inject
43+
lateinit var currentSession: CurrentSessionUseCase
44+
45+
@Inject
46+
lateinit var deleteSession: DeleteSessionUseCase
47+
48+
@Inject
49+
lateinit var accountSwitch: AccountSwitchUseCase
50+
51+
override suspend fun receive(context: Context, intent: Intent) {
52+
if (intent.action != ACTION_LOGOUT) return
53+
54+
appLogger.i("$TAG Received logout broadcast")
55+
56+
when (val session = currentSession()) {
57+
is CurrentSessionResult.Success -> {
58+
val userId = session.accountInfo.userId
59+
appLogger.i("$TAG Logging out user: ${userId.toLogString()}")
60+
coreLogic.getSessionScope(userId).logout(LogoutReason.SELF_HARD_LOGOUT, waitUntilCompletes = true)
61+
deleteSession(userId)
62+
accountSwitch(SwitchAccountParam.TryToSwitchToNextAccount)
63+
val wireActivityIntent = Intent(context, WireActivity::class.java).apply {
64+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
65+
}
66+
context.startActivity(wireActivityIntent)
67+
}
68+
69+
is CurrentSessionResult.Failure.SessionNotFound ->
70+
appLogger.i("$TAG No active session found, nothing to logout")
71+
72+
is CurrentSessionResult.Failure.Generic ->
73+
appLogger.e("$TAG Failed to get current session")
74+
}
75+
}
76+
77+
companion object {
78+
const val ACTION_LOGOUT = "com.wire.ACTION_LOGOUT"
79+
private const val TAG = "LogoutReceiver"
80+
}
81+
}

app/src/main/kotlin/com/wire/android/ui/WireActivity.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ class WireActivity : AppCompatActivity() {
385385
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
386386
.collectLatest { (intent, savedInstanceState) ->
387387
currentKeyboardController?.hide()
388-
handleDeepLink(currentNavigator, intent, savedInstanceState)
388+
handleDeepLinkOrIntent(currentNavigator, intent, savedInstanceState)
389389
}
390390
}
391391
}
@@ -730,7 +730,7 @@ class WireActivity : AppCompatActivity() {
730730
/*
731731
* This method is responsible for handling deep links from given intent
732732
*/
733-
private fun handleDeepLink(
733+
private suspend fun handleDeepLinkOrIntent(
734734
navigator: Navigator,
735735
intent: Intent?,
736736
savedInstanceState: Bundle? = null
@@ -742,19 +742,23 @@ class WireActivity : AppCompatActivity() {
742742
}
743743
val originalIntent = savedInstanceState.getOriginalIntent()
744744
if (intent == null
745-
|| intent.action == Intent.ACTION_MAIN // This is the case when the app is opened from launcher so no deep link to handle
745+
|| intent.action == Intent.ACTION_MAIN // The app is opened from launcher so no deep link to handle, only start intents if any
746746
|| intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0
747747
|| originalIntent == intent // This is the case when the activity is recreated and already handled
748748
|| intent.getBooleanExtra(HANDLED_DEEPLINK_FLAG, false)
749749
) {
750-
if (navigator.isEmptyWelcomeStartDestination()) {
751-
// no deep link to handle so if "welcome empty start" screen then switch "start" screen to login by navigating to it
752-
navigator.navigate(NavigationCommand(NewLoginScreenDestination(), BackStackMode.CLEAR_WHOLE))
750+
val handled = viewModel.handleIntentsThatAreNotDeepLinks(intent)
751+
if (!handled && navigator.isEmptyWelcomeStartDestination()) {
752+
// nothing to handle so if "welcome empty start" screen then switch "start" screen to login by navigating to it
753+
navigate(NavigationCommand(NewLoginScreenDestination(), BackStackMode.CLEAR_WHOLE))
753754
}
754755
return
755756
} else {
756-
viewModel.handleDeepLink(intent)
757-
intent.putExtra(HANDLED_DEEPLINK_FLAG, true)
757+
val handled = viewModel.handleIntentsThatAreNotDeepLinks(intent)
758+
if (!handled) {
759+
viewModel.handleDeepLink(intent)
760+
intent.putExtra(HANDLED_DEEPLINK_FLAG, true)
761+
}
758762
}
759763
}
760764

app/src/main/kotlin/com/wire/android/ui/WireActivityActionsHandler.kt

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package com.wire.android.ui
2020
import android.content.Context
2121
import android.widget.Toast
2222
import androidx.compose.runtime.Composable
23+
import androidx.compose.runtime.rememberCoroutineScope
2324
import androidx.compose.ui.platform.LocalContext
2425
import com.ramcosta.composedestinations.utils.destination
2526
import com.wire.android.R
@@ -28,23 +29,29 @@ import com.wire.android.navigation.LoginTypeSelector
2829
import com.wire.android.navigation.NavigationCommand
2930
import com.wire.android.navigation.Navigator
3031
import com.wire.android.ui.authentication.login.PreFilledUserIdentifierType
32+
import com.wire.android.ui.authentication.login.SSOCodeAutoLogin
3133
import com.wire.android.ui.common.HandleActions
3234
import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination
3335
import com.ramcosta.composedestinations.generated.app.destinations.HomeScreenDestination
3436
import com.ramcosta.composedestinations.generated.app.destinations.ImportMediaScreenDestination
3537
import com.ramcosta.composedestinations.generated.app.destinations.LoginScreenDestination
3638
import com.ramcosta.composedestinations.generated.app.destinations.NewLoginScreenDestination
3739
import com.ramcosta.composedestinations.generated.app.destinations.OtherUserProfileScreenDestination
40+
import com.wire.android.ui.authentication.login.LoginPasswordPath
41+
import kotlinx.coroutines.CoroutineScope
3842
import kotlinx.coroutines.flow.Flow
43+
import kotlinx.coroutines.launch
3944

4045
@Composable
4146
internal fun HandleViewActions(actions: Flow<WireActivityViewAction>, navigator: Navigator, loginTypeSelector: LoginTypeSelector) {
4247

4348
val context = LocalContext.current
49+
val coroutineScope = rememberCoroutineScope()
4450
HandleActions(actions) { action ->
4551
when (action) {
4652
is OnAuthorizationNeeded -> onAuthorizationNeeded(context, navigator)
4753
is OnMigrationLogin -> onMigration(navigator, loginTypeSelector, action)
54+
is OnAutomaticLogin -> onAutomaticLogin(coroutineScope, navigator, loginTypeSelector, action)
4855
is OnOpenUserProfile -> openUserProfile(action, navigator)
4956
is OnSSOLogin -> openSsoLogin(navigator, action)
5057
is OnShowImportMediaScreen -> openImportMediaScreen(navigator)
@@ -90,9 +97,16 @@ private fun openSsoLogin(navigator: Navigator, action: OnSSOLogin) {
9097
NavigationCommand(
9198
when (navigator.navController.currentBackStackEntry?.destination()?.baseRoute) {
9299
// if SSO login started from new login screen then go back to the new login flow
93-
NewLoginScreenDestination.baseRoute -> NewLoginScreenDestination(
94-
ssoLoginResult = action.result
95-
)
100+
NewLoginScreenDestination.baseRoute -> {
101+
val existingLoginPasswordPath = navigator.navController.currentBackStackEntry
102+
?.savedStateHandle
103+
?.let { NewLoginScreenDestination.argsFrom(it) }
104+
?.loginPasswordPath
105+
NewLoginScreenDestination(
106+
ssoLoginResult = action.result,
107+
loginPasswordPath = existingLoginPasswordPath,
108+
)
109+
}
96110

97111
else -> LoginScreenDestination(
98112
ssoLoginResult = action.result
@@ -145,6 +159,47 @@ private fun onMigration(
145159
)
146160
}
147161

162+
private fun onAutomaticLogin(
163+
coroutineScope: CoroutineScope,
164+
navigator: Navigator,
165+
loginTypeSelector: LoginTypeSelector,
166+
action: OnAutomaticLogin
167+
) {
168+
// Auto-apply backend config AND navigate with SSO code pre-filled
169+
val serverLinks = action.serverLinks
170+
val ssoCode = action.ssoCode
171+
val ssoCodeAutoLogin = ssoCode?.let { SSOCodeAutoLogin(ssoCode = it, autoInitiateLogin = true) }
172+
173+
coroutineScope.launch {
174+
val useNewLogin = loginTypeSelector.canUseNewLogin(serverLinks)
175+
176+
val destination = when (useNewLogin) {
177+
true -> {
178+
NewLoginScreenDestination(
179+
loginPasswordPath = LoginPasswordPath(serverLinks),
180+
ssoCodeAutoLogin = ssoCodeAutoLogin
181+
)
182+
}
183+
false -> {
184+
LoginScreenDestination(
185+
loginPasswordPath = LoginPasswordPath(serverLinks),
186+
ssoCodeAutoLogin = ssoCodeAutoLogin
187+
)
188+
}
189+
}
190+
191+
navigator.navigate(
192+
NavigationCommand(
193+
destination = destination,
194+
backStackMode = when (navigator.shouldReplaceWelcomeLoginStartDestination()) {
195+
true -> BackStackMode.CLEAR_WHOLE
196+
else -> BackStackMode.UPDATE_EXISTED
197+
}
198+
)
199+
)
200+
}
201+
}
202+
148203
private fun onAuthorizationNeeded(context: Context, navigator: Navigator) {
149204
if (navigator.isEmptyWelcomeStartDestination()) {
150205
// log in needed so if "welcome empty start" screen then switch "start" screen to login by navigating to it

app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import com.wire.android.util.deeplink.LoginType
5757
import com.wire.android.util.dispatchers.DispatcherProvider
5858
import com.wire.android.util.ui.UIText
5959
import com.wire.android.emm.ManagedConfigurationsManager
60+
import com.wire.android.util.lifecycle.IntentsProcessor
6061
import com.wire.android.workmanager.worker.cancelPeriodicPersistentWebsocketCheckWorker
6162
import com.wire.android.workmanager.worker.enqueuePeriodicPersistentWebsocketCheckWorker
6263
import com.wire.kalium.logic.CoreLogic
@@ -117,6 +118,7 @@ class WireActivityViewModel @Inject constructor(
117118
private val doesValidSessionExist: Lazy<DoesValidSessionExistUseCase>,
118119
private val getServerConfigUseCase: Lazy<GetServerConfigUseCase>,
119120
private val deepLinkProcessor: Lazy<DeepLinkProcessor>,
121+
private val intentsProcessor: Lazy<IntentsProcessor>,
120122
private val observeSessions: Lazy<ObserveSessionsUseCase>,
121123
private val accountSwitch: Lazy<AccountSwitchUseCase>,
122124
private val servicesManager: Lazy<ServicesManager>,
@@ -339,6 +341,16 @@ class WireActivityViewModel @Inject constructor(
339341
}
340342
}
341343

344+
// Returns whether an intent was handled, or if there was nothing to do
345+
fun handleIntentsThatAreNotDeepLinks(intent: Intent?): Boolean {
346+
val result = intentsProcessor.get().invoke(intent)
347+
if (result != null) {
348+
onAutomaticLoginParameters(result.backendConfig, result.ssoCode)
349+
return true
350+
}
351+
return false
352+
}
353+
342354
@Suppress("ComplexMethod")
343355
fun handleDeepLink(intent: Intent?) {
344356
viewModelScope.launch(dispatchers.io()) {
@@ -369,6 +381,21 @@ class WireActivityViewModel @Inject constructor(
369381
}
370382
}
371383

384+
private fun onAutomaticLoginParameters(backendConfigUrl: String?, ssoCode: String?) {
385+
viewModelScope.launch(dispatchers.io()) {
386+
// Load backend config
387+
val serverLinks = backendConfigUrl?.let { loadServerConfig(it) }
388+
389+
val backendConfigLoadFailed = backendConfigUrl != null && serverLinks == null
390+
val nothingProvided = backendConfigUrl == null && ssoCode == null
391+
if (backendConfigLoadFailed || nothingProvided) {
392+
sendAction(OnUnknownDeepLink)
393+
} else {
394+
sendAction(OnAutomaticLogin(serverLinks, ssoCode))
395+
}
396+
}
397+
}
398+
372399
fun dismissCustomBackendDialog() {
373400
globalAppState = globalAppState.copy(customBackendDialog = null)
374401
}
@@ -661,6 +688,7 @@ internal data object OnShowImportMediaScreen : WireActivityViewAction
661688
internal data object OnAuthorizationNeeded : WireActivityViewAction
662689
internal data object OnUnknownDeepLink : WireActivityViewAction
663690
internal data class OnMigrationLogin(val result: DeepLinkResult.MigrationLogin) : WireActivityViewAction
691+
internal data class OnAutomaticLogin(val serverLinks: ServerConfig.Links?, val ssoCode: String?) : WireActivityViewAction
664692
internal data class OnOpenUserProfile(val result: DeepLinkResult.OpenOtherUserProfile) : WireActivityViewAction
665693
internal data class OnSSOLogin(val result: DeepLinkResult.SSOLogin) : WireActivityViewAction
666694
internal data class ShowToast(val messageResId: Int) : WireActivityViewAction

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ data class LoginNavArgs(
2626
val userHandle: PreFilledUserIdentifierType.PreFilled? = null,
2727
val ssoLoginResult: DeepLinkResult.SSOLogin? = null,
2828
val loginPasswordPath: LoginPasswordPath? = null,
29+
val ssoCodeAutoLogin: SSOCodeAutoLogin? = null,
30+
)
31+
32+
@Serializable
33+
data class SSOCodeAutoLogin(
34+
val ssoCode: String,
35+
val autoInitiateLogin: Boolean = true
2936
)
3037

3138
@Serializable

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ fun LoginScreen(
107107
},
108108
loginEmailViewModel = loginEmailViewModel,
109109
ssoLoginResult = loginNavArgs.ssoLoginResult,
110-
// ssoUrlConfigHolder = ssoUrlConfigHolder,
110+
ssoCodeAutoLogin = loginNavArgs.ssoCodeAutoLogin
111111
)
112112
}
113113

@@ -118,6 +118,7 @@ private fun LoginContent(
118118
onRemoveDeviceNeeded: () -> Unit,
119119
loginEmailViewModel: LoginEmailViewModel,
120120
ssoLoginResult: DeepLinkResult.SSOLogin?,
121+
ssoCodeAutoLogin: SSOCodeAutoLogin?,
121122
) {
122123
Column(modifier = Modifier.fillMaxSize()) {
123124
/*
@@ -140,7 +141,7 @@ private fun LoginContent(
140141
onRemoveDeviceNeeded = onRemoveDeviceNeeded,
141142
loginEmailViewModel = loginEmailViewModel,
142143
ssoLoginResult = ssoLoginResult,
143-
// ssoUrlConfigHolder = ssoUrlConfigHolder
144+
ssoCodeAutoLogin = ssoCodeAutoLogin
144145
)
145146
}
146147
}
@@ -155,11 +156,17 @@ private fun MainLoginContent(
155156
onRemoveDeviceNeeded: () -> Unit,
156157
loginEmailViewModel: LoginEmailViewModel,
157158
ssoLoginResult: DeepLinkResult.SSOLogin?,
159+
ssoCodeAutoLogin: SSOCodeAutoLogin?,
158160
) {
159161

160162
val scope = rememberCoroutineScope()
161163
val scrollState = rememberScrollState()
162-
val initialPageIndex = if (ssoLoginResult == null) LoginTabItem.EMAIL.ordinal else LoginTabItem.SSO.ordinal
164+
// Show SSO tab if we have either ssoLoginResult or ssoCodeAutoLogin
165+
val initialPageIndex = if (ssoLoginResult != null || ssoCodeAutoLogin != null) {
166+
LoginTabItem.SSO.ordinal
167+
} else {
168+
LoginTabItem.EMAIL.ordinal
169+
}
163170
val pagerState = rememberPagerState(
164171
initialPage = initialPageIndex,
165172
pageCount = { LoginTabItem.values().size }
@@ -227,7 +234,7 @@ private fun MainLoginContent(
227234
onSuccess,
228235
onRemoveDeviceNeeded,
229236
ssoLoginResult,
230-
// ssoUrlConfigHolder
237+
ssoCodeAutoLogin
231238
)
232239
}
233240
}
@@ -259,7 +266,7 @@ private fun PreviewLoginScreen() = WireTheme {
259266
onRemoveDeviceNeeded = {},
260267
loginEmailViewModel = hiltViewModel(),
261268
ssoLoginResult = null,
262-
// ssoUrlConfigHolder = SSOUrlConfigHolderPreview,
269+
ssoCodeAutoLogin = null,
263270
)
264271
}
265272
}

0 commit comments

Comments
 (0)