Skip to content

Commit e1b7128

Browse files
authored
feat: handle federated case for user ids in QR code (WPB-10530) (#4012)
1 parent 23f81a8 commit e1b7128

File tree

7 files changed

+145
-20
lines changed

7 files changed

+145
-20
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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.util
19+
20+
import android.net.Uri
21+
import androidx.core.net.toUri
22+
import com.wire.android.util.deeplink.UserLinkQRMapper
23+
import com.wire.kalium.logic.data.id.QualifiedID
24+
import org.junit.Assert.assertEquals
25+
import org.junit.Test
26+
27+
class UserLinkQRMapperTest {
28+
29+
@Test
30+
fun givenAUriFullQualifiedUrl_thenMapCorrectly() {
31+
val uri: Uri = "wire://user/domain/user-id".toUri()
32+
val result = UserLinkQRMapper.fromDeepLinkToQualifiedId(uri, "defaultDomain")
33+
34+
assertEquals(UserLinkQRMapper.UserLinkQRResult.Success(QualifiedID("user-id", "domain")), result)
35+
}
36+
37+
@Test
38+
fun givenAUriWrongFormat_thenMapToError() {
39+
val uri: Uri = "wire://user".toUri()
40+
val result = UserLinkQRMapper.fromDeepLinkToQualifiedId(uri, "defaultDomain")
41+
42+
assertEquals(UserLinkQRMapper.UserLinkQRResult.Failure, result)
43+
}
44+
45+
@Test
46+
fun givenAUriFullUnqualified_thenMapCorrectlyWithDefaultDomain() {
47+
val uri: Uri = "wire://user/user-id".toUri()
48+
val result = UserLinkQRMapper.fromDeepLinkToQualifiedId(uri, "defaultDomain")
49+
50+
assertEquals(UserLinkQRMapper.UserLinkQRResult.Success(QualifiedID("user-id", "defaultDomain")), result)
51+
}
52+
53+
@Test
54+
fun givenAUriQualifiedButNotSupportedFormat_thenMapCorrectlyWithDefaultDomain() {
55+
val uri: Uri = "wire://user/USER-ID@domain".toUri()
56+
val result = UserLinkQRMapper.fromDeepLinkToQualifiedId(uri, "defaultDomain")
57+
58+
assertEquals(UserLinkQRMapper.UserLinkQRResult.Success(QualifiedID("user-id", "domain")), result)
59+
}
60+
61+
@Test
62+
fun givenAUriWithUpperCaseId_thenMapCorrectlyWithLowercaseId() {
63+
val uri: Uri = "wire://user/uSer-Id@domain".toUri()
64+
val result = UserLinkQRMapper.fromDeepLinkToQualifiedId(uri, "defaultDomain")
65+
66+
assertEquals(UserLinkQRMapper.UserLinkQRResult.Success(QualifiedID("user-id", "domain")), result)
67+
}
68+
}

app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,8 @@ fun PreviewSelfQRCodeContent() {
280280
SelfQRCodeState(
281281
userId = UserId("userId", "wire.com"),
282282
handle = "userid",
283-
userProfileLink = "https://account.wire.com/user-profile/?id=aaaaaaa-222-3333-4444-55555555"
283+
userProfileLink = "wire://user/wire.com/aaaaaaa-222-3333-4444-55555555",
284+
userAccountProfileLink = "https://account.wire.com/user-profile/?id=aaaaaaa-222-3333-4444-55555555@wire.com"
284285
),
285286
{ "".toUri() },
286287
{ }

app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class SelfQRCodeViewModel @Inject constructor(
111111

112112
private fun generateSelfUserUrls(accountsUrl: String): SelfQRCodeState =
113113
selfQRCodeState.copy(
114-
userAccountProfileLink = String.format(BASE_USER_PROFILE_URL, accountsUrl, selfUserId.value),
114+
userAccountProfileLink = String.format(BASE_USER_PROFILE_URL, accountsUrl, selfUserId),
115115
userProfileLink = String.format(DIRECT_BASE_USER_PROFILE_URL, selfUserId.domain, selfUserId.value)
116116
)
117117

app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -145,23 +145,14 @@ class DeepLinkProcessor @Inject constructor(
145145
}
146146
}
147147

148-
private fun getConnectingUserProfile(uri: Uri, switchedAccount: Boolean, accountInfo: AccountInfo.Valid): DeepLinkResult {
149-
// todo. handle with domain case, before lastPathSegment. format of deeplink wire://user/domain/user-id
150-
return uri.lastPathSegment?.toDefaultQualifiedId(accountInfo.userId.domain)?.let {
151-
DeepLinkResult.OpenOtherUserProfile(it, switchedAccount)
152-
} ?: return DeepLinkResult.Unknown
153-
}
154-
155148
/**
156-
* Converts the string to a [QualifiedID] with the current user domain or default, to preserve retro compatibility.
157-
* When implementing Milestone 2 this should be replaced with a new qualifiedIdMapper, implementing wire://user/domain/user-id
158-
*
159-
* - new mapper should follow "domain/user-id" parsing.
149+
* Format of deeplink to parse: wire://user/domain/user-id
160150
*/
161-
private fun String.toDefaultQualifiedId(currentUserDomain: String?): QualifiedID {
162-
val domain = currentUserDomain ?: "wire.com"
163-
// TODO. This lowercase is important, since web/iOS is sending/handling this as uppercase!!
164-
return QualifiedID(this.lowercase(), domain)
151+
private fun getConnectingUserProfile(uri: Uri, switchedAccount: Boolean, accountInfo: AccountInfo.Valid): DeepLinkResult {
152+
return when (val result = UserLinkQRMapper.fromDeepLinkToQualifiedId(uri, accountInfo.userId.domain)) {
153+
is UserLinkQRMapper.UserLinkQRResult.Failure -> DeepLinkResult.Unknown
154+
is UserLinkQRMapper.UserLinkQRResult.Success -> DeepLinkResult.OpenOtherUserProfile(result.qualifiedUserId, switchedAccount)
155+
}
165156
}
166157

167158
private suspend fun switchAccountIfNeeded(uri: Uri, accountInfo: AccountInfo.Valid): SwitchAccountStatus =
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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.util.deeplink
19+
20+
import android.net.Uri
21+
import com.wire.kalium.logic.data.id.QualifiedID
22+
import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl
23+
24+
object UserLinkQRMapper {
25+
26+
val qualifiedIdMapper = QualifiedIdMapperImpl(null)
27+
28+
fun fromDeepLinkToQualifiedId(uri: Uri, defaultDomain: String): UserLinkQRResult {
29+
val segments = uri.pathSegments
30+
return when (segments.size) {
31+
1 -> {
32+
val userId = qualifiedIdMapper.fromStringToQualifiedID(segments.last())
33+
val sanitizedId = userId.value.toDefaultQualifiedId(
34+
userDomain = userId.domain.takeIf {
35+
it.isNotBlank()
36+
} ?: defaultDomain
37+
)
38+
UserLinkQRResult.Success(sanitizedId)
39+
}
40+
41+
2 -> {
42+
val domain = segments.first()
43+
val userId = segments.last()
44+
UserLinkQRResult.Success(userId.toDefaultQualifiedId(domain))
45+
}
46+
47+
else -> {
48+
UserLinkQRResult.Failure
49+
}
50+
}
51+
}
52+
53+
/**
54+
* Converts the string to a [QualifiedID] with the current user domain or default.
55+
* IMPORTANT! This also handles the special case where iOS is sending the ID in uppercase.
56+
*/
57+
private fun String.toDefaultQualifiedId(userDomain: String): QualifiedID {
58+
return QualifiedID(this.lowercase(), userDomain)
59+
}
60+
61+
sealed class UserLinkQRResult {
62+
data class Success(val qualifiedUserId: QualifiedID) : UserLinkQRResult()
63+
data object Failure : UserLinkQRResult()
64+
}
65+
}

app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class SelfQRCodeViewModelTest {
3939
)
4040

4141
assertEquals(
42-
expected = "${ServerConfig.STAGING.accounts}/user-profile/?id=${TestUser.SELF_USER.id.value}",
42+
expected = "${ServerConfig.STAGING.accounts}/user-profile/?id=${TestUser.SELF_USER.id}",
4343
actual = viewModel.selfQRCodeState.userAccountProfileLink,
4444
)
4545
}

app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ class DeepLinkProcessorTest {
305305
val conversationResult = deepLinkProcessor(arrangement.uri)
306306
assertInstanceOf(DeepLinkResult.OpenOtherUserProfile::class.java, conversationResult)
307307
assertEquals(
308-
DeepLinkResult.OpenOtherUserProfile(UserId("other_user", "domain"), false),
308+
DeepLinkResult.OpenOtherUserProfile(UserId("other_user", "other_domain"), false),
309309
conversationResult
310310
)
311311
}
@@ -387,7 +387,7 @@ class DeepLinkProcessorTest {
387387

388388
fun withOtherUserProfileQRDeepLink(userIdToOpen: UserId = OTHER_USER_ID, userId: UserId = CURRENT_USER_ID) = apply {
389389
coEvery { uri.host } returns DeepLinkProcessor.OPEN_USER_PROFILE_DEEPLINK_HOST
390-
coEvery { uri.lastPathSegment } returns userIdToOpen.value
390+
coEvery { uri.pathSegments } returns listOf(userIdToOpen.domain, userIdToOpen.value)
391391
coEvery { uri.getQueryParameter(DeepLinkProcessor.USER_TO_USE_QUERY_PARAM) } returns userId.toString()
392392
}
393393

0 commit comments

Comments
 (0)