Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Wire
* Copyright (C) 2025 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.util

import android.net.Uri
import androidx.core.net.toUri
import com.wire.android.util.deeplink.UserLinkQRMapper
import com.wire.kalium.logic.data.id.QualifiedID
import org.junit.Assert.assertEquals
import org.junit.Test

class UserLinkQRMapperTest {

@Test
fun givenAUriFullQualifiedUrl_thenMapCorrectly() {
val uri: Uri = "wire://user/domain/user-id".toUri()
val result = UserLinkQRMapper.fromDeepLinkToQualifiedId(uri, "defaultDomain")

assertEquals(UserLinkQRMapper.UserLinkQRResult.Success(QualifiedID("user-id", "domain")), result)
}

@Test
fun givenAUriWrongFormat_thenMapToError() {
val uri: Uri = "wire://user".toUri()
val result = UserLinkQRMapper.fromDeepLinkToQualifiedId(uri, "defaultDomain")

assertEquals(UserLinkQRMapper.UserLinkQRResult.Failure, result)
}

@Test
fun givenAUriFullUnqualified_thenMapCorrectlyWithDefaultDomain() {
val uri: Uri = "wire://user/user-id".toUri()
val result = UserLinkQRMapper.fromDeepLinkToQualifiedId(uri, "defaultDomain")

assertEquals(UserLinkQRMapper.UserLinkQRResult.Success(QualifiedID("user-id", "defaultDomain")), result)
}

@Test
fun givenAUriQualifiedButNotSupportedFormat_thenMapCorrectlyWithDefaultDomain() {
val uri: Uri = "wire://user/USER-ID@domain".toUri()
val result = UserLinkQRMapper.fromDeepLinkToQualifiedId(uri, "defaultDomain")

assertEquals(UserLinkQRMapper.UserLinkQRResult.Success(QualifiedID("user-id", "domain")), result)
}

@Test
fun givenAUriWithUpperCaseId_thenMapCorrectlyWithLowercaseId() {
val uri: Uri = "wire://user/uSer-Id@domain".toUri()
val result = UserLinkQRMapper.fromDeepLinkToQualifiedId(uri, "defaultDomain")

assertEquals(UserLinkQRMapper.UserLinkQRResult.Success(QualifiedID("user-id", "domain")), result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ fun PreviewSelfQRCodeContent() {
SelfQRCodeState(
userId = UserId("userId", "wire.com"),
handle = "userid",
userProfileLink = "https://account.wire.com/user-profile/?id=aaaaaaa-222-3333-4444-55555555"
userProfileLink = "wire://user/wire.com/aaaaaaa-222-3333-4444-55555555",
userAccountProfileLink = "https://account.wire.com/user-profile/?id=aaaaaaa-222-3333-4444-55555555@wire.com"
),
{ "".toUri() },
{ }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class SelfQRCodeViewModel @Inject constructor(

private fun generateSelfUserUrls(accountsUrl: String): SelfQRCodeState =
selfQRCodeState.copy(
userAccountProfileLink = String.format(BASE_USER_PROFILE_URL, accountsUrl, selfUserId.value),
userAccountProfileLink = String.format(BASE_USER_PROFILE_URL, accountsUrl, selfUserId),
userProfileLink = String.format(DIRECT_BASE_USER_PROFILE_URL, selfUserId.domain, selfUserId.value)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,23 +145,14 @@ class DeepLinkProcessor @Inject constructor(
}
}

private fun getConnectingUserProfile(uri: Uri, switchedAccount: Boolean, accountInfo: AccountInfo.Valid): DeepLinkResult {
// todo. handle with domain case, before lastPathSegment. format of deeplink wire://user/domain/user-id
return uri.lastPathSegment?.toDefaultQualifiedId(accountInfo.userId.domain)?.let {
DeepLinkResult.OpenOtherUserProfile(it, switchedAccount)
} ?: return DeepLinkResult.Unknown
}

/**
* Converts the string to a [QualifiedID] with the current user domain or default, to preserve retro compatibility.
* When implementing Milestone 2 this should be replaced with a new qualifiedIdMapper, implementing wire://user/domain/user-id
*
* - new mapper should follow "domain/user-id" parsing.
* Format of deeplink to parse: wire://user/domain/user-id
*/
private fun String.toDefaultQualifiedId(currentUserDomain: String?): QualifiedID {
val domain = currentUserDomain ?: "wire.com"
// TODO. This lowercase is important, since web/iOS is sending/handling this as uppercase!!
return QualifiedID(this.lowercase(), domain)
private fun getConnectingUserProfile(uri: Uri, switchedAccount: Boolean, accountInfo: AccountInfo.Valid): DeepLinkResult {
return when (val result = UserLinkQRMapper.fromDeepLinkToQualifiedId(uri, accountInfo.userId.domain)) {
is UserLinkQRMapper.UserLinkQRResult.Failure -> DeepLinkResult.Unknown
is UserLinkQRMapper.UserLinkQRResult.Success -> DeepLinkResult.OpenOtherUserProfile(result.qualifiedUserId, switchedAccount)
}
}

private suspend fun switchAccountIfNeeded(uri: Uri, accountInfo: AccountInfo.Valid): SwitchAccountStatus =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Wire
* Copyright (C) 2025 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.util.deeplink

import android.net.Uri
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl

object UserLinkQRMapper {

val qualifiedIdMapper = QualifiedIdMapperImpl(null)

fun fromDeepLinkToQualifiedId(uri: Uri, defaultDomain: String): UserLinkQRResult {
val segments = uri.pathSegments
return when (segments.size) {
1 -> {
val userId = qualifiedIdMapper.fromStringToQualifiedID(segments.last())
val sanitizedId = userId.value.toDefaultQualifiedId(

Check warning on line 33 in app/src/main/kotlin/com/wire/android/util/deeplink/UserLinkQRMapper.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/util/deeplink/UserLinkQRMapper.kt#L32-L33

Added lines #L32 - L33 were not covered by tests
userDomain = userId.domain.takeIf {
it.isNotBlank()
} ?: defaultDomain

Check warning on line 36 in app/src/main/kotlin/com/wire/android/util/deeplink/UserLinkQRMapper.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/util/deeplink/UserLinkQRMapper.kt#L35-L36

Added lines #L35 - L36 were not covered by tests
)
UserLinkQRResult.Success(sanitizedId)

Check warning on line 38 in app/src/main/kotlin/com/wire/android/util/deeplink/UserLinkQRMapper.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/util/deeplink/UserLinkQRMapper.kt#L38

Added line #L38 was not covered by tests
}

2 -> {
val domain = segments.first()
val userId = segments.last()
UserLinkQRResult.Success(userId.toDefaultQualifiedId(domain))
}

else -> {
UserLinkQRResult.Failure

Check warning on line 48 in app/src/main/kotlin/com/wire/android/util/deeplink/UserLinkQRMapper.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/util/deeplink/UserLinkQRMapper.kt#L48

Added line #L48 was not covered by tests
}
}
}

/**
* Converts the string to a [QualifiedID] with the current user domain or default.
* IMPORTANT! This also handles the special case where iOS is sending the ID in uppercase.
*/
private fun String.toDefaultQualifiedId(userDomain: String): QualifiedID {
return QualifiedID(this.lowercase(), userDomain)
}

sealed class UserLinkQRResult {
data class Success(val qualifiedUserId: QualifiedID) : UserLinkQRResult()
data object Failure : UserLinkQRResult()

Check warning on line 63 in app/src/main/kotlin/com/wire/android/util/deeplink/UserLinkQRMapper.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/util/deeplink/UserLinkQRMapper.kt#L63

Added line #L63 was not covered by tests
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class SelfQRCodeViewModelTest {
)

assertEquals(
expected = "${ServerConfig.STAGING.accounts}/user-profile/?id=${TestUser.SELF_USER.id.value}",
expected = "${ServerConfig.STAGING.accounts}/user-profile/?id=${TestUser.SELF_USER.id}",
actual = viewModel.selfQRCodeState.userAccountProfileLink,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ class DeepLinkProcessorTest {
val conversationResult = deepLinkProcessor(arrangement.uri)
assertInstanceOf(DeepLinkResult.OpenOtherUserProfile::class.java, conversationResult)
assertEquals(
DeepLinkResult.OpenOtherUserProfile(UserId("other_user", "domain"), false),
DeepLinkResult.OpenOtherUserProfile(UserId("other_user", "other_domain"), false),
conversationResult
)
}
Expand Down Expand Up @@ -387,7 +387,7 @@ class DeepLinkProcessorTest {

fun withOtherUserProfileQRDeepLink(userIdToOpen: UserId = OTHER_USER_ID, userId: UserId = CURRENT_USER_ID) = apply {
coEvery { uri.host } returns DeepLinkProcessor.OPEN_USER_PROFILE_DEEPLINK_HOST
coEvery { uri.lastPathSegment } returns userIdToOpen.value
coEvery { uri.pathSegments } returns listOf(userIdToOpen.domain, userIdToOpen.value)
coEvery { uri.getQueryParameter(DeepLinkProcessor.USER_TO_USE_QUERY_PARAM) } returns userId.toString()
}

Expand Down
Loading