Skip to content

Commit c69c883

Browse files
committed
feat: add shared auth flow for iOS handoff
1 parent 724a88c commit c69c883

58 files changed

Lines changed: 3097 additions & 824 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ dependencies {
201201
implementationWithCoverage(projects.features.sketch)
202202
implementationWithCoverage(projects.features.meetings)
203203
implementationWithCoverage(projects.features.sync)
204+
implementationWithCoverage(projects.shared.auth)
204205

205206
// Anonymous Analytics
206207
val flavors = getFlavorsSettings()

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class LoginEmailViewModel constructor(
8080
private val dispatchers: DispatcherProvider,
8181
defaultServerConfig: ServerConfig.Links,
8282
@DefaultWebSocketEnabledByDefault private val defaultWebSocketEnabledByDefault: Boolean,
83+
private val sharedAuthLoginEmailAdapter: SharedAuthLoginEmailAdapter = LegacySharedAuthLoginEmailAdapter,
8384
) : LoginViewModel(
8485
loginNavArgs,
8586
clientScopeProviderFactory,
@@ -160,6 +161,7 @@ class LoginEmailViewModel constructor(
160161
null
161162
}
162163
}
164+
if (tryLoginWithSharedAuth(usernameAllowed)) return@launch
163165
// first, cancel and revert any previous login if it's still running, just to be sure
164166
revertLogin()
165167
// then, start a new login job
@@ -172,6 +174,30 @@ class LoginEmailViewModel constructor(
172174
}
173175
}
174176

177+
private suspend fun tryLoginWithSharedAuth(usernameAllowed: Boolean): Boolean =
178+
sharedAuthLoginEmailAdapter.tryLogin(
179+
request = SharedAuthLoginEmailRequest(
180+
userIdentifier = userIdentifierTextState.text.toString(),
181+
password = passwordTextState.text.toString(),
182+
secondFactorVerificationCode = secondFactorVerificationCodeTextState.text.toString(),
183+
usernameAllowed = usernameAllowed,
184+
serverConfig = serverConfig,
185+
),
186+
callbacks = object : SharedAuthLoginEmailCallbacks {
187+
override suspend fun updateFlowState(flowState: LoginState) {
188+
updateEmailFlowState(flowState)
189+
}
190+
191+
override suspend fun updateSecondFactorState(update: (VerificationCodeState) -> VerificationCodeState) {
192+
secondFactorVerificationCodeState = update(secondFactorVerificationCodeState)
193+
}
194+
195+
override suspend fun startResendCodeTimer() {
196+
this@LoginEmailViewModel.startResendCodeTimer()
197+
}
198+
}
199+
)
200+
175201
@Suppress("LongMethod")
176202
private fun startLoginJob(usernameAllowed: Boolean): Job {
177203
return viewModelScope.launch {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2026 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+
19+
package com.wire.android.ui.authentication.login.email
20+
21+
import com.wire.android.ui.authentication.login.LoginState
22+
import com.wire.android.ui.authentication.verificationcode.VerificationCodeState
23+
import com.wire.kalium.logic.configuration.server.ServerConfig
24+
25+
/**
26+
* Android-side boundary for replacing the email/password login step with shared/auth.
27+
*
28+
* A future implementation should adapt shared/auth state and effects to the existing Android UI contract.
29+
* Android keeps text fields, resources, navigation, proxy form rendering and screen lifecycle ownership.
30+
*/
31+
fun interface SharedAuthLoginEmailAdapter {
32+
suspend fun tryLogin(
33+
request: SharedAuthLoginEmailRequest,
34+
callbacks: SharedAuthLoginEmailCallbacks,
35+
): Boolean
36+
}
37+
38+
data class SharedAuthLoginEmailRequest(
39+
val userIdentifier: String,
40+
val password: String,
41+
val secondFactorVerificationCode: String,
42+
val usernameAllowed: Boolean,
43+
val serverConfig: ServerConfig.Links,
44+
)
45+
46+
interface SharedAuthLoginEmailCallbacks {
47+
suspend fun updateFlowState(flowState: LoginState)
48+
suspend fun updateSecondFactorState(update: (VerificationCodeState) -> VerificationCodeState)
49+
suspend fun startResendCodeTimer()
50+
}
51+
52+
object LegacySharedAuthLoginEmailAdapter : SharedAuthLoginEmailAdapter {
53+
override suspend fun tryLogin(
54+
request: SharedAuthLoginEmailRequest,
55+
callbacks: SharedAuthLoginEmailCallbacks,
56+
): Boolean = false
57+
}

app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModel.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class NewLoginViewModel(
7878
defaultServerConfig: ServerConfig.Links,
7979
defaultSSOCodeConfig: String,
8080
private val recoverableLogoutExceptionDetector: NewLoginRecoverableLogoutExceptionDetector,
81+
private val sharedAuthNewLoginAdapter: SharedAuthNewLoginAdapter = LegacySharedAuthNewLoginAdapter,
8182
) : ActionsViewModel<NewLoginAction>() {
8283

8384
private val preFilledUserIdentifier: PreFilledUserIdentifierType = loginNavArgs.userHandle ?: PreFilledUserIdentifierType.None
@@ -146,6 +147,7 @@ class NewLoginViewModel(
146147
viewModelScope.launch(dispatchers.io()) {
147148
updateLoginFlowState(NewLoginFlowState.Loading)
148149
val sanitizedInput = userIdentifierTextState.text.trim().toString()
150+
if (tryStartSharedAuthLogin(sanitizedInput)) return@launch
149151
when (validateEmailOrSSOCode(sanitizedInput)) {
150152
ValidateEmailOrSSOCodeUseCase.Result.InvalidInput -> {
151153
updateLoginFlowState(NewLoginFlowState.Error.TextFieldError.InvalidValue)
@@ -163,6 +165,43 @@ class NewLoginViewModel(
163165
}
164166
}
165167

168+
private suspend fun tryStartSharedAuthLogin(userIdentifier: String): Boolean =
169+
sharedAuthNewLoginAdapter.tryStartLogin(
170+
request = SharedAuthNewLoginRequest(
171+
userIdentifier = userIdentifier,
172+
serverConfig = serverConfig,
173+
customServerConfig = loginNavArgs.loginPasswordPath?.customServerConfig,
174+
),
175+
callbacks = object : SharedAuthNewLoginCallbacks {
176+
override suspend fun showInvalidInput() {
177+
updateLoginFlowState(NewLoginFlowState.Error.TextFieldError.InvalidValue)
178+
}
179+
180+
override suspend fun showGenericError(failure: CoreFailure) {
181+
updateLoginFlowState(NewLoginFlowState.Error.DialogError.GenericError(failure))
182+
}
183+
184+
override suspend fun showCustomServerDialog(serverLinks: ServerConfig.Links) {
185+
updateLoginFlowState(NewLoginFlowState.CustomConfigDialog(serverLinks))
186+
}
187+
188+
override suspend fun openEmailPassword(userIdentifier: String, loginPasswordPath: LoginPasswordPath) {
189+
sendAction(NewLoginAction.EmailPassword(userIdentifier, loginPasswordPath))
190+
updateLoginFlowState(NewLoginFlowState.Default)
191+
}
192+
193+
override suspend fun openSso(url: String, config: SSOUrlConfig) {
194+
sendAction(NewLoginAction.SSO(url, config))
195+
updateLoginFlowState(NewLoginFlowState.Default)
196+
}
197+
198+
override suspend fun openEnterpriseLoginNotSupported(userIdentifier: String) {
199+
sendAction(NewLoginAction.EnterpriseLoginNotSupported(userIdentifier))
200+
updateLoginFlowState(NewLoginFlowState.Default)
201+
}
202+
}
203+
)
204+
166205
@VisibleForTesting
167206
internal suspend fun getEnterpriseLoginFlow(email: String) = withContext(dispatchers.io()) {
168207
ssoExtension.withAuthenticationScope(
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2026 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+
19+
package com.wire.android.ui.newauthentication.login
20+
21+
import com.wire.android.ui.authentication.login.LoginPasswordPath
22+
import com.wire.android.ui.authentication.login.sso.SSOUrlConfig
23+
import com.wire.kalium.common.error.CoreFailure
24+
import com.wire.kalium.logic.configuration.server.ServerConfig
25+
26+
/**
27+
* Android-side boundary for replacing the new login identifier step with shared/auth.
28+
*
29+
* The production shared/auth implementation should map shared state/effects into these Android callbacks.
30+
* Android keeps Compose text fields, navigation, dialogs, custom tabs and resources in app.
31+
*/
32+
fun interface SharedAuthNewLoginAdapter {
33+
suspend fun tryStartLogin(
34+
request: SharedAuthNewLoginRequest,
35+
callbacks: SharedAuthNewLoginCallbacks,
36+
): Boolean
37+
}
38+
39+
data class SharedAuthNewLoginRequest(
40+
val userIdentifier: String,
41+
val serverConfig: ServerConfig.Links,
42+
val customServerConfig: ServerConfig.Links?,
43+
)
44+
45+
interface SharedAuthNewLoginCallbacks {
46+
suspend fun showInvalidInput()
47+
suspend fun showGenericError(failure: CoreFailure)
48+
suspend fun showCustomServerDialog(serverLinks: ServerConfig.Links)
49+
suspend fun openEmailPassword(userIdentifier: String, loginPasswordPath: LoginPasswordPath)
50+
suspend fun openSso(url: String, config: SSOUrlConfig)
51+
suspend fun openEnterpriseLoginNotSupported(userIdentifier: String)
52+
}
53+
54+
object LegacySharedAuthNewLoginAdapter : SharedAuthNewLoginAdapter {
55+
override suspend fun tryStartLogin(
56+
request: SharedAuthNewLoginRequest,
57+
callbacks: SharedAuthNewLoginCallbacks,
58+
): Boolean = false
59+
}

shared/auth/build.gradle.kts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
plugins {
2+
id(libs.plugins.wire.kmp.library.get().pluginId)
3+
alias(libs.plugins.metro)
4+
}
5+
6+
kotlin {
7+
android {
8+
namespace = "com.wire.shared.auth"
9+
}
10+
11+
sourceSets {
12+
val commonMain by getting {
13+
dependencies {
14+
api(libs.coroutines.core)
15+
}
16+
}
17+
18+
val commonTest by getting {
19+
dependencies {
20+
implementation(kotlin("test"))
21+
implementation(libs.coroutines.test)
22+
}
23+
}
24+
}
25+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2026 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.shared.auth
19+
20+
data class AuthLoginSuccessPayload(
21+
val userIdValue: String,
22+
val userIdDomain: String?,
23+
val accessTokenValue: String,
24+
val accessTokenType: String,
25+
val accessTokenExpiresInSeconds: Int?,
26+
val refreshTokenValue: String,
27+
val refreshTokenCookieName: String = REFRESH_TOKEN_COOKIE_NAME,
28+
val refreshTokenCookieDomain: String?,
29+
val refreshTokenCookiePath: String = REFRESH_TOKEN_COOKIE_PATH,
30+
val refreshTokenCookieSecure: Boolean = true,
31+
val refreshTokenCookieHttpOnly: Boolean = true,
32+
val email: String?,
33+
val password: String?,
34+
val secondFactorCode: String?,
35+
val initialSyncCompleted: Boolean,
36+
val isE2EIRequired: Boolean,
37+
val clientId: String?,
38+
) {
39+
companion object {
40+
const val REFRESH_TOKEN_COOKIE_NAME = "zuid"
41+
const val REFRESH_TOKEN_COOKIE_PATH = "/"
42+
}
43+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2026 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.shared.auth
19+
20+
import kotlinx.coroutines.CoroutineScope
21+
import kotlinx.coroutines.Job
22+
23+
internal fun CoroutineScope.cancelScope() {
24+
coroutineContext[Job]?.cancel()
25+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2026 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.shared.auth
19+
20+
data object NoEffect
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2026 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.shared.auth
19+
20+
import com.wire.shared.auth.login.model.LoginServerLinks
21+
22+
data class SharedAuthConfig(
23+
val defaultServerLinks: LoginServerLinks,
24+
val isThereActiveSession: Boolean = false,
25+
val maxAccountsReached: Boolean = false,
26+
val nomadAccountBlocksLogin: Boolean = false,
27+
val isAccountCreationAllowed: Boolean = true,
28+
val useNewRegistration: Boolean = true,
29+
)

0 commit comments

Comments
 (0)