diff --git a/app/src/androidTest/kotlin/com/wire/android/util/UserLinkQRMapperTest.kt b/app/src/androidTest/kotlin/com/wire/android/util/UserLinkQRMapperTest.kt new file mode 100644 index 00000000000..22ef46ba33e --- /dev/null +++ b/app/src/androidTest/kotlin/com/wire/android/util/UserLinkQRMapperTest.kt @@ -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) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt index d8d511dea8b..7c370808955 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeScreen.kt @@ -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() }, { } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt index 1fcbe8409ec..ed285f49399 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModel.kt @@ -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) ) diff --git a/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt b/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt index 922a2e15d67..3e85d3a5ec2 100644 --- a/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt +++ b/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt @@ -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 = diff --git a/app/src/main/kotlin/com/wire/android/util/deeplink/UserLinkQRMapper.kt b/app/src/main/kotlin/com/wire/android/util/deeplink/UserLinkQRMapper.kt new file mode 100644 index 00000000000..753923bdf42 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/util/deeplink/UserLinkQRMapper.kt @@ -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( + userDomain = userId.domain.takeIf { + it.isNotBlank() + } ?: defaultDomain + ) + UserLinkQRResult.Success(sanitizedId) + } + + 2 -> { + val domain = segments.first() + val userId = segments.last() + UserLinkQRResult.Success(userId.toDefaultQualifiedId(domain)) + } + + else -> { + UserLinkQRResult.Failure + } + } + } + + /** + * 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() + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt index 9c391ae86f9..ebb5f9717b7 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/qr/SelfQRCodeViewModelTest.kt @@ -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, ) } diff --git a/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt b/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt index 10df70ceb35..960f57bb8cc 100644 --- a/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt +++ b/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt @@ -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 ) } @@ -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() }