From 80e1ac1e2186488572b2ab2437b8ec704606b3ae Mon Sep 17 00:00:00 2001 From: Radmir Date: Wed, 20 May 2026 11:54:12 +0500 Subject: [PATCH 1/5] Fix SolanaSignerTests build after solana nft metadata change Solana NFT support added an `nft` parameter to the TransactionLoadMetadata.solana case, but the SolanaSignerTests call sites were not updated, breaking the SignerTests build. Pass `nft: nil` for the three existing non-NFT transfer cases; the signed transaction output is unchanged. --- ios/Packages/Signer/Tests/SignerTests/SolanaSignerTests.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ios/Packages/Signer/Tests/SignerTests/SolanaSignerTests.swift b/ios/Packages/Signer/Tests/SignerTests/SolanaSignerTests.swift index 7f9993757c..ad0006d501 100644 --- a/ios/Packages/Signer/Tests/SignerTests/SolanaSignerTests.swift +++ b/ios/Packages/Signer/Tests/SignerTests/SolanaSignerTests.swift @@ -27,6 +27,7 @@ struct SolanaSignerTests { senderTokenAddress: nil, recipientTokenAddress: nil, tokenProgram: nil, + nft: nil, blockHash: "8ntZRPm8pbf4R4pTWsVnTdgqXA35yYXSz8TxUzwBhXEK", ), ) @@ -51,6 +52,7 @@ struct SolanaSignerTests { senderTokenAddress: "DVWPV7brSbPDkA7a3qdn6UJsVc3J3DyhQhjNaZeZqwzo", recipientTokenAddress: "8ntZRPm8pbf4R4pTWsVnTdgqXA35yYXSz8TxUzwBhXEK", tokenProgram: .token, + nft: nil, blockHash: "8ntZRPm8pbf4R4pTWsVnTdgqXA35yYXSz8TxUzwBhXEK", ), ) @@ -75,6 +77,7 @@ struct SolanaSignerTests { senderTokenAddress: "DVWPV7brSbPDkA7a3qdn6UJsVc3J3DyhQhjNaZeZqwzo", recipientTokenAddress: nil, tokenProgram: .token, + nft: nil, blockHash: "8ntZRPm8pbf4R4pTWsVnTdgqXA35yYXSz8TxUzwBhXEK", ), ) From 0facea7634e3d03b7a26d6d2a107df2ad8047bf7 Mon Sep 17 00:00:00 2001 From: Radmir Date: Wed, 20 May 2026 22:24:46 +0500 Subject: [PATCH 2/5] Replace custom URL parsing with Gemstone primitives Replace in-app URL/deeplink and WalletConnect parsing with the shared GemstonePrimitives/uniffi helpers and update related code and tests. Changes include: - Android: remove custom URI decoding and WalletConnect parsing helpers; use walletConnectDecodeUrl and deeplinkDecodeUrl in PendingNavigationCoordinator and WebDeepLinks; adjust intent handling and tests to mock uniffi static functions. - iOS: add GemstonePrimitives package and bridging extensions (DeepLink <-> Gemstone, WalletConnect mapping), URLParser, and unit tests; update Features and ViewModels to use gemUrl/deeplink builders; update Navigation and RootScene handling to use deeplink-based URLAction. - Primitives: simplify DeepLink enum and add URLAction.deeplink case; update test utilities. - UI: Asset sharing now builds and shares a deeplink URL via Gemstone. - Remove legacy parsing files and Android/iOS parser tests that are replaced by the new GemstonePrimitives implementation. - Update core submodule pointer. These changes centralize deeplink and WalletConnect logic into the Gemstone primitives, reduce duplicated parsing code, and align tests to mock the new uniffi-based functions. --- android/app/src/main/AndroidManifest.xml | 1 - .../android/PendingNavigationCoordinator.kt | 8 +- .../gemwallet/android/UriComponentDecoding.kt | 23 ---- .../gemwallet/android/WalletConnectLinks.kt | 59 --------- .../com/gemwallet/android/WebDeepLinks.kt | 43 +----- .../PendingNavigationCoordinatorTest.kt | 34 ++++- .../android/WalletConnectLinksTest.kt | 47 ------- .../com/gemwallet/android/WebDeepLinksTest.kt | 59 +++++---- .../details/components/AssetDetailsMenu.kt | 7 +- core | 2 +- ios/Features/Assets/Package.swift | 2 + .../ViewModels/AssetSceneViewModel.swift | 3 +- .../ViewModels/DeveloperViewModel.swift | 3 +- ios/Gem/Navigation/NavigationHandler.swift | 14 +- ios/Gem/ViewModels/RootSceneViewModel.swift | 3 +- .../Extensions/DeepLink+GemstoneSwift.swift | 28 ++++ ...WalletConnectLink+GemstonePrimitives.swift | 14 ++ .../Sources/URLParser.swift | 33 +++++ .../DeepLinkMock.swift | 22 +++ .../DeepLinkTests.swift | 27 ++++ .../URLParserTests.swift | 35 +++++ .../Primitives/Sources/DeepLink.swift | 73 +--------- .../Primitives/Sources/URLAction.swift | 9 +- .../Primitives/Sources/URLParser.swift | 97 -------------- .../TestKit/AssetId+PrimitivesTestKit.swift | 4 + .../PrimitivesTests/URLParserTests.swift | 125 ------------------ 26 files changed, 259 insertions(+), 516 deletions(-) delete mode 100644 android/app/src/main/kotlin/com/gemwallet/android/UriComponentDecoding.kt delete mode 100644 android/app/src/main/kotlin/com/gemwallet/android/WalletConnectLinks.kt delete mode 100644 android/app/src/test/kotlin/com/gemwallet/android/WalletConnectLinksTest.kt create mode 100644 ios/Packages/GemstonePrimitives/Sources/Extensions/DeepLink+GemstoneSwift.swift create mode 100644 ios/Packages/GemstonePrimitives/Sources/Extensions/WalletConnectLink+GemstonePrimitives.swift create mode 100644 ios/Packages/GemstonePrimitives/Sources/URLParser.swift create mode 100644 ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkMock.swift create mode 100644 ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkTests.swift create mode 100644 ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/URLParserTests.swift delete mode 100644 ios/Packages/Primitives/Sources/URLParser.swift delete mode 100644 ios/Packages/Primitives/Tests/PrimitivesTests/URLParserTests.swift diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ca33b220ab..a17d237777 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -63,7 +63,6 @@ - diff --git a/android/app/src/main/kotlin/com/gemwallet/android/PendingNavigationCoordinator.kt b/android/app/src/main/kotlin/com/gemwallet/android/PendingNavigationCoordinator.kt index 3af9298811..ac700b6300 100644 --- a/android/app/src/main/kotlin/com/gemwallet/android/PendingNavigationCoordinator.kt +++ b/android/app/src/main/kotlin/com/gemwallet/android/PendingNavigationCoordinator.kt @@ -7,6 +7,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import uniffi.gemstone.WalletConnectLink +import uniffi.gemstone.walletConnectDecodeUrl import javax.inject.Inject internal sealed interface PendingNavigation { @@ -37,11 +39,11 @@ class PendingNavigationCoordinator @Inject constructor( val pendingIntent = (_pendingNavigation.value as? PendingNavigation.RawIntent)?.intent ?: return val uri = pendingIntent.dataString - uri?.toWalletConnectLink()?.let { link -> + uri?.let(::walletConnectDecodeUrl)?.let { link -> when (link) { - is WalletConnectLink.Pairing -> walletConnect.onPairing(link.uri) + is WalletConnectLink.Connect -> walletConnect.onPairing(link.uri) WalletConnectLink.Request -> walletConnect.onRequest() - WalletConnectLink.Session -> Unit + is WalletConnectLink.Session -> Unit } replace(pendingIntent, replacement = null) return diff --git a/android/app/src/main/kotlin/com/gemwallet/android/UriComponentDecoding.kt b/android/app/src/main/kotlin/com/gemwallet/android/UriComponentDecoding.kt deleted file mode 100644 index b13f4d5a78..0000000000 --- a/android/app/src/main/kotlin/com/gemwallet/android/UriComponentDecoding.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.gemwallet.android - -import java.net.URLDecoder - -internal fun String.decodeUriComponent(): String? { - return runCatching { URLDecoder.decode(this, Charsets.UTF_8.name()) }.getOrNull() -} - -internal fun String?.decodeUriQueryParameters(): Map { - val rawQuery = this?.takeIf { it.isNotBlank() } ?: return emptyMap() - return rawQuery - .split("&") - .mapNotNull { pair -> - if (pair.isBlank()) return@mapNotNull null - val separator = pair.indexOf("=") - val rawKey = if (separator < 0) pair else pair.substring(0, separator) - val rawValue = if (separator < 0) "" else pair.substring(separator + 1) - val key = rawKey.decodeUriComponent() ?: return@mapNotNull null - val value = rawValue.decodeUriComponent() ?: return@mapNotNull null - key to value - } - .toMap() -} diff --git a/android/app/src/main/kotlin/com/gemwallet/android/WalletConnectLinks.kt b/android/app/src/main/kotlin/com/gemwallet/android/WalletConnectLinks.kt deleted file mode 100644 index 7c5546b3ec..0000000000 --- a/android/app/src/main/kotlin/com/gemwallet/android/WalletConnectLinks.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.gemwallet.android - -import java.net.URI - -internal sealed interface WalletConnectLink { - data class Pairing(val uri: String) : WalletConnectLink - data object Request : WalletConnectLink - data object Session : WalletConnectLink -} - -internal fun String.toWalletConnectLink(): WalletConnectLink? { - val uri = runCatching { URI(this) }.getOrNull() ?: return null - val queryParameters = uri.walletConnectQueryParameters() - return when { - WalletConnectScheme.equals(uri.scheme, ignoreCase = true) -> { - // Keep this parser classification-only; Reown validates direct wc: pairing payloads. - queryParameters.toWalletConnectCallback() ?: WalletConnectLink.Pairing(this) - } - GemScheme.equals(uri.scheme, ignoreCase = true) && WalletConnectHost.equals(uri.host, ignoreCase = true) -> { - toGemWalletConnectLink(queryParameters) - } - else -> null - } -} - -private fun toGemWalletConnectLink(queryParameters: Map): WalletConnectLink? { - return WalletConnectQuery.Uri.nonBlankValue(queryParameters)?.let(WalletConnectLink::Pairing) - ?: queryParameters.toWalletConnectCallback() -} - -private fun Map.toWalletConnectCallback(): WalletConnectLink? { - if (WalletConnectQuery.SessionTopic.nonBlankValue(this) != null) return WalletConnectLink.Session - return WalletConnectLink.Request.takeIf { WalletConnectQuery.RequestId.existsIn(this) } -} - -private fun URI.walletConnectQueryParameters(): Map { - val query = rawQuery ?: rawSchemeSpecificPart - ?.substringAfter('?', missingDelimiterValue = "") - ?.takeIf { it.isNotBlank() } - return query.decodeUriQueryParameters() -} - -private enum class WalletConnectQuery(val value: String) { - RequestId("requestId"), - SessionTopic("sessionTopic"), - Uri("uri"); - - fun nonBlankValue(queryParameters: Map): String? { - return queryParameters[value]?.takeIf { it.isNotBlank() } - } - - fun existsIn(queryParameters: Map): Boolean { - return value in queryParameters - } -} - -private const val WalletConnectScheme = "wc" -private const val GemScheme = "gem" -private const val WalletConnectHost = "wc" diff --git a/android/app/src/main/kotlin/com/gemwallet/android/WebDeepLinks.kt b/android/app/src/main/kotlin/com/gemwallet/android/WebDeepLinks.kt index a1c3194f97..ea7168f6ba 100644 --- a/android/app/src/main/kotlin/com/gemwallet/android/WebDeepLinks.kt +++ b/android/app/src/main/kotlin/com/gemwallet/android/WebDeepLinks.kt @@ -1,47 +1,16 @@ package com.gemwallet.android import androidx.navigation3.runtime.NavKey -import com.gemwallet.android.features.asset_select.presents.navigation.AssetsSearchRoute +import com.gemwallet.android.ext.toAssetId import com.gemwallet.android.ui.navigation.routes.AssetRoute import com.gemwallet.android.ui.navigation.routes.ReferralRoute -import com.wallet.core.primitives.AssetId -import com.wallet.core.primitives.Chain -import java.net.URI +import uniffi.gemstone.Deeplink +import uniffi.gemstone.deeplinkDecodeUrl internal fun String.toWebDeepLinkRoute(): NavKey? { - val uri = runCatching { URI(this) }.getOrNull() ?: return null - if (!WebDeepLinkScheme.equals(uri.scheme, ignoreCase = true)) return null - if (!WebDeepLinkHost.equals(uri.host, ignoreCase = true)) return null - - val segments = uri.pathSegments() - return when (segments.firstOrNull()) { - WebDeepLinkPathJoin -> ReferralRoute( - code = segments.elementAtOrNull(1) - ?: uri.rawQuery - .decodeUriQueryParameters()[WebDeepLinkCodeQuery] - ?.takeIf(String::isNotBlank), - ) - WebDeepLinkPathTokens -> segments.toTokenRoute() + return when (val deeplink = deeplinkDecodeUrl(this)) { + is Deeplink.Asset -> deeplink.assetId.toAssetId()?.let { AssetRoute(it) } + is Deeplink.Rewards -> ReferralRoute(code = deeplink.code?.takeIf(String::isNotBlank)) else -> null } } - -private fun List.toTokenRoute(): NavKey? { - if (size == 1) return AssetsSearchRoute - if (size !in 2..3) return null - val chain = Chain.entries.firstOrNull { it.string == this[1] } ?: return null - return AssetRoute(AssetId(chain = chain, tokenId = elementAtOrNull(2))) -} - -private fun URI.pathSegments(): List { - return rawPath - ?.split("/") - ?.mapNotNull { it.takeIf(String::isNotBlank)?.decodeUriComponent() } - .orEmpty() -} - -private const val WebDeepLinkScheme = "https" -private const val WebDeepLinkHost = "gemwallet.com" -private const val WebDeepLinkPathJoin = "join" -private const val WebDeepLinkPathTokens = "tokens" -private const val WebDeepLinkCodeQuery = "code" diff --git a/android/app/src/test/kotlin/com/gemwallet/android/PendingNavigationCoordinatorTest.kt b/android/app/src/test/kotlin/com/gemwallet/android/PendingNavigationCoordinatorTest.kt index 60ad7739fa..1d2be9d72f 100644 --- a/android/app/src/test/kotlin/com/gemwallet/android/PendingNavigationCoordinatorTest.kt +++ b/android/app/src/test/kotlin/com/gemwallet/android/PendingNavigationCoordinatorTest.kt @@ -7,16 +7,30 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Before import org.junit.Test +import uniffi.gemstone.Deeplink +import uniffi.gemstone.WalletConnectLink +import uniffi.gemstone.deeplinkDecodeUrl +import uniffi.gemstone.walletConnectDecodeUrl class PendingNavigationCoordinatorTest { private val notificationNavigation = mockk(relaxed = true) private val coordinator = PendingNavigationCoordinator(notificationNavigation) + @Before + fun setUp() = mockkStatic("uniffi.gemstone.GemstoneKt") + + @After + fun tearDown() = unmockkStatic("uniffi.gemstone.GemstoneKt") + @Test fun resolve_withoutPendingIntent_isNoOp() = runTest { coordinator.resolve(NoOpWalletConnect) @@ -27,18 +41,22 @@ class PendingNavigationCoordinatorTest { @Test fun resolve_walletConnectPairing_invokesPairingHandlerAndClears() = runTest { val handler = RecordingWalletConnect() - coordinator.setPendingIntentForTest(intent(uri = "wc:abc@2?relay-protocol=irn")) + val uri = "wc:abc@2?relay-protocol=irn" + every { walletConnectDecodeUrl(uri) } returns WalletConnectLink.Connect(uri) + coordinator.setPendingIntentForTest(intent(uri = uri)) coordinator.resolve(handler) - assertEquals(listOf("pairing:wc:abc@2?relay-protocol=irn"), handler.events) + assertEquals(listOf("pairing:$uri"), handler.events) assertNull("intent must be cleared after handing off to wallet connect", coordinator.pendingNavigation.value) } @Test fun resolve_walletConnectRequest_invokesRequestHandlerAndClears() = runTest { val handler = RecordingWalletConnect() - coordinator.setPendingIntentForTest(intent(uri = "gem://wc?requestId=42")) + val uri = "gem://wc?requestId=42" + every { walletConnectDecodeUrl(uri) } returns WalletConnectLink.Request + coordinator.setPendingIntentForTest(intent(uri = uri)) coordinator.resolve(handler) @@ -48,7 +66,10 @@ class PendingNavigationCoordinatorTest { @Test fun resolve_webDeepLink_storesRoute() = runTest { - coordinator.setPendingIntentForTest(intent(uri = "https://gemwallet.com/join/gemcoder")) + val uri = "https://gemwallet.com/join/gemcoder" + every { walletConnectDecodeUrl(uri) } returns null + every { deeplinkDecodeUrl(uri) } returns Deeplink.Rewards(code = "gemcoder") + coordinator.setPendingIntentForTest(intent(uri = uri)) coordinator.resolve(NoOpWalletConnect) @@ -58,7 +79,10 @@ class PendingNavigationCoordinatorTest { @Test fun resolve_unknownIntentWithoutNotificationPayload_clears() = runTest { - coordinator.setPendingIntentForTest(intent(uri = "https://example.com/unknown")) + val uri = "https://example.com/unknown" + every { walletConnectDecodeUrl(uri) } returns null + every { deeplinkDecodeUrl(uri) } returns null + coordinator.setPendingIntentForTest(intent(uri = uri)) coordinator.resolve(NoOpWalletConnect) diff --git a/android/app/src/test/kotlin/com/gemwallet/android/WalletConnectLinksTest.kt b/android/app/src/test/kotlin/com/gemwallet/android/WalletConnectLinksTest.kt deleted file mode 100644 index f1e0494a35..0000000000 --- a/android/app/src/test/kotlin/com/gemwallet/android/WalletConnectLinksTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.gemwallet.android - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Test - -class WalletConnectLinksTest { - - @Test - fun walletConnectLink_acceptsDirectPairingUri() { - val uri = "wc:abc@2?relay-protocol=irn&symKey=123" - - assertEquals(WalletConnectLink.Pairing(uri), uri.toWalletConnectLink()) - } - - @Test - fun walletConnectLink_acceptsDirectCallbackUri() { - assertEquals(WalletConnectLink.Request, "wc:abc@2?requestId".toWalletConnectLink()) - assertEquals(WalletConnectLink.Request, "wc:abc@2?requestId=123".toWalletConnectLink()) - } - - @Test - fun walletConnectLink_acceptsGemPairingUri() { - assertEquals( - WalletConnectLink.Pairing("wc:abc@2?relay-protocol=irn&symKey=123"), - "gem://wc?uri=wc%3Aabc%402%3Frelay-protocol%3Dirn%26symKey%3D123".toWalletConnectLink(), - ) - } - - @Test - fun walletConnectLink_acceptsGemCallbacks() { - assertEquals(WalletConnectLink.Request, "gem://wc?requestId".toWalletConnectLink()) - assertEquals(WalletConnectLink.Request, "gem://wc?requestId=123".toWalletConnectLink()) - assertEquals(WalletConnectLink.Session, "gem://wc?sessionTopic=topic".toWalletConnectLink()) - } - - @Test - fun walletConnectLink_rejectsEmptySessionCallback() { - assertNull("gem://wc?sessionTopic=".toWalletConnectLink()) - } - - @Test - fun walletConnectLink_rejectsNormalNavigationLinks() { - assertNull("gem://asset/solana".toWalletConnectLink()) - assertNull("https://gemwallet.com/join?code=abc123".toWalletConnectLink()) - } -} diff --git a/android/app/src/test/kotlin/com/gemwallet/android/WebDeepLinksTest.kt b/android/app/src/test/kotlin/com/gemwallet/android/WebDeepLinksTest.kt index 4a953276e6..45f86958d5 100644 --- a/android/app/src/test/kotlin/com/gemwallet/android/WebDeepLinksTest.kt +++ b/android/app/src/test/kotlin/com/gemwallet/android/WebDeepLinksTest.kt @@ -1,49 +1,50 @@ package com.gemwallet.android -import com.gemwallet.android.features.asset_select.presents.navigation.AssetsSearchRoute import com.gemwallet.android.testkit.mockAssetId import com.gemwallet.android.ui.navigation.routes.AssetRoute import com.gemwallet.android.ui.navigation.routes.ReferralRoute import com.wallet.core.primitives.Chain +import io.mockk.every +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Before import org.junit.Test +import uniffi.gemstone.Deeplink +import uniffi.gemstone.deeplinkDecodeUrl class WebDeepLinksTest { - @Test - fun webDeepLinkRoute_acceptsJoinLinks() { - assertEquals( - ReferralRoute(code = "gemcoder"), - "https://gemwallet.com/join/gemcoder".toWebDeepLinkRoute(), - ) - assertEquals( - ReferralRoute(code = "gemcoder"), - "https://gemwallet.com/join?code=gemcoder".toWebDeepLinkRoute(), - ) - assertEquals(ReferralRoute(), "https://gemwallet.com/join".toWebDeepLinkRoute()) - } + @Before + fun setUp() = mockkStatic("uniffi.gemstone.GemstoneKt") + + @After + fun tearDown() = unmockkStatic("uniffi.gemstone.GemstoneKt") @Test - fun webDeepLinkRoute_acceptsTokenLinks() { - assertEquals(AssetsSearchRoute, "https://gemwallet.com/tokens".toWebDeepLinkRoute()) - assertEquals( - AssetRoute(mockAssetId(Chain.Bitcoin)), - "https://gemwallet.com/tokens/bitcoin".toWebDeepLinkRoute(), - ) - assertEquals( - AssetRoute(mockAssetId(Chain.Solana, tokenId = "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN")), - "https://gemwallet.com/tokens/solana/JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN".toWebDeepLinkRoute(), - ) + fun webDeepLinkRoute_mapsSupportedDeeplinks() { + val tokenId = "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN" + every { deeplinkDecodeUrl(any()) } returns Deeplink.Asset(assetId = "bitcoin") + assertEquals(AssetRoute(mockAssetId(Chain.Bitcoin)), "https://gemwallet.com/tokens/bitcoin".toWebDeepLinkRoute()) + + every { deeplinkDecodeUrl(any()) } returns Deeplink.Asset(assetId = "solana_$tokenId") + assertEquals(AssetRoute(mockAssetId(Chain.Solana, tokenId)), "gem://tokens/solana/$tokenId".toWebDeepLinkRoute()) + + every { deeplinkDecodeUrl(any()) } returns Deeplink.Rewards(code = "gemcoder") + assertEquals(ReferralRoute(code = "gemcoder"), "https://gemwallet.com/join/gemcoder".toWebDeepLinkRoute()) + + every { deeplinkDecodeUrl(any()) } returns Deeplink.Rewards(code = null) + assertEquals(ReferralRoute(), "https://gemwallet.com/join".toWebDeepLinkRoute()) } @Test fun webDeepLinkRoute_rejectsUnsupportedLinks() { - assertNull("https://gemwallet.com/swap/bitcoin".toWebDeepLinkRoute()) - assertNull("https://gemwallet.com/en/tokens/bitcoin".toWebDeepLinkRoute()) - assertNull("https://example.com/tokens/bitcoin".toWebDeepLinkRoute()) - assertNull("gem://tokens/bitcoin".toWebDeepLinkRoute()) - assertNull("https://gemwallet.com/tokens/notachain".toWebDeepLinkRoute()) - assertNull("https://gemwallet.com/tokens/bitcoin/too/many".toWebDeepLinkRoute()) + every { deeplinkDecodeUrl(any()) } returns Deeplink.Perpetuals + assertNull("https://gemwallet.com/perpetuals".toWebDeepLinkRoute()) + + every { deeplinkDecodeUrl(any()) } returns null + assertNull("https://gemwallet.com/tokens".toWebDeepLinkRoute()) } } diff --git a/android/features/asset/presents/src/main/kotlin/com/gemwallet/android/features/asset/presents/details/components/AssetDetailsMenu.kt b/android/features/asset/presents/src/main/kotlin/com/gemwallet/android/features/asset/presents/details/components/AssetDetailsMenu.kt index ffc1fdecb7..0c0c89b4e8 100644 --- a/android/features/asset/presents/src/main/kotlin/com/gemwallet/android/features/asset/presents/details/components/AssetDetailsMenu.kt +++ b/android/features/asset/presents/src/main/kotlin/com/gemwallet/android/features/asset/presents/details/components/AssetDetailsMenu.kt @@ -25,8 +25,11 @@ import androidx.compose.ui.res.stringResource import com.gemwallet.android.ui.R import com.gemwallet.android.ui.open import com.gemwallet.android.features.asset.viewmodels.details.models.AssetInfoUIModel +import com.gemwallet.android.ext.toIdentifier import com.wallet.core.primitives.AssetId import kotlinx.coroutines.launch +import uniffi.gemstone.Deeplink +import uniffi.gemstone.deeplinkBuildUrl @Composable fun RowScope.AssetDetailsMenu( @@ -47,11 +50,13 @@ fun RowScope.AssetDetailsMenu( val onShare = fun () { val type = "text/plain" val subject = "${uiState.assetInfo.owner?.chain}\n${uiState.assetInfo.asset.symbol}" + val assetId = uiState.asset.id + val shareUrl = deeplinkBuildUrl(Deeplink.Asset(assetId = assetId.toIdentifier())) val intent = Intent(Intent.ACTION_SEND) intent.type = type intent.putExtra(Intent.EXTRA_SUBJECT, subject) - intent.putExtra(Intent.EXTRA_TEXT, uiState.assetInfo.owner?.address) + intent.putExtra(Intent.EXTRA_TEXT, shareUrl) context.startActivity(Intent.createChooser(intent, shareTitle)) } diff --git a/core b/core index 935dd0352f..024861b2c0 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 935dd0352f483e4c77a81af8ca93e08b42253919 +Subproject commit 024861b2c0bc578948b181d8e11a7fb861e9dee1 diff --git a/ios/Features/Assets/Package.swift b/ios/Features/Assets/Package.swift index 54eaa2076e..218628ae2d 100644 --- a/ios/Features/Assets/Package.swift +++ b/ios/Features/Assets/Package.swift @@ -25,6 +25,7 @@ let package = Package( .package(name: "Style", path: "../../Packages/Style"), .package(name: "Components", path: "../../Packages/Components"), .package(name: "PrimitivesComponents", path: "../../Packages/PrimitivesComponents"), + .package(name: "GemstonePrimitives", path: "../../Packages/GemstonePrimitives"), .package(name: "Store", path: "../../Packages/Store"), .package(name: "Preferences", path: "../../Packages/Preferences"), .package(name: "Blockchain", path: "../../Packages/Blockchain"), @@ -45,6 +46,7 @@ let package = Package( "Style", "Components", "PrimitivesComponents", + "GemstonePrimitives", "Store", "Preferences", "Blockchain", diff --git a/ios/Features/Assets/Sources/ViewModels/AssetSceneViewModel.swift b/ios/Features/Assets/Sources/ViewModels/AssetSceneViewModel.swift index c6ecc08a9d..a1b13de105 100644 --- a/ios/Features/Assets/Sources/ViewModels/AssetSceneViewModel.swift +++ b/ios/Features/Assets/Sources/ViewModels/AssetSceneViewModel.swift @@ -5,6 +5,7 @@ import BalanceService import BannerService import Components import ExplorerService +import GemstonePrimitives import Localization import Preferences import PriceAlertService @@ -380,7 +381,7 @@ public extension AssetSceneViewModel { } case .suspiciousAsset: break case .tradePerpetuals: - UIApplication.shared.open(DeepLink.perpetuals.localUrl) + UIApplication.shared.open(DeepLink.perpetuals.gemUrl) preferences.isPerpetualEnabled = true } case let .button(bannerButton): diff --git a/ios/Features/Settings/Sources/Settings/ViewModels/DeveloperViewModel.swift b/ios/Features/Settings/Sources/Settings/ViewModels/DeveloperViewModel.swift index 5cc55a3af7..f7adb41969 100644 --- a/ios/Features/Settings/Sources/Settings/ViewModels/DeveloperViewModel.swift +++ b/ios/Features/Settings/Sources/Settings/ViewModels/DeveloperViewModel.swift @@ -5,6 +5,7 @@ import BannerService import BigInt import Components import Foundation +import GemstonePrimitives import Localization import PerpetualService import Preferences @@ -292,7 +293,7 @@ public final class DeveloperViewModel { func deeplink(deeplink: DeepLink) { Task { @MainActor in - await UIApplication.shared.open(deeplink.localUrl, options: [:]) + await UIApplication.shared.open(deeplink.gemUrl, options: [:]) } } } diff --git a/ios/Gem/Navigation/NavigationHandler.swift b/ios/Gem/Navigation/NavigationHandler.swift index fc7f28368e..b48172a53d 100644 --- a/ios/Gem/Navigation/NavigationHandler.swift +++ b/ios/Gem/Navigation/NavigationHandler.swift @@ -58,9 +58,13 @@ final class NavigationHandler: Sendable { extension NavigationHandler { private func handleURLAction(_ action: URLAction) async throws { switch action { - case .walletConnect: - return + case .walletConnect: break + case let .deeplink(deeplink): try await handleDeepLink(deeplink) + } + } + private func handleDeepLink(_ deeplink: DeepLink) async throws { + switch deeplink { case let .asset(assetId): try await navigateToAsset(assetId) @@ -90,7 +94,7 @@ extension NavigationHandler { return } - selectTab(for: action.selectTab) + selectTab(for: deeplink.selectTab) } } @@ -217,11 +221,11 @@ extension NavigationHandler { // MARK: - TabItem Selection -private extension URLAction { +private extension DeepLink { var selectTab: TabItem? { switch self { case .asset, .perpetuals: .wallet - case .swap, .buy, .sell, .setPriceAlert, .walletConnect: nil + case .swap, .buy, .sell, .setPriceAlert: nil case .rewards, .gift: .settings } } diff --git a/ios/Gem/ViewModels/RootSceneViewModel.swift b/ios/Gem/ViewModels/RootSceneViewModel.swift index eee74d242d..acea92b429 100644 --- a/ios/Gem/ViewModels/RootSceneViewModel.swift +++ b/ios/Gem/ViewModels/RootSceneViewModel.swift @@ -7,6 +7,7 @@ import ConnectionsService import DeviceService import EventPresenterService import Foundation +import GemstonePrimitives import Localization import LockManager import NameService @@ -150,7 +151,7 @@ extension RootSceneViewModel { switch action { case let .walletConnect(walletConnectAction): try await handleWalletConnect(walletConnectAction) - case .asset, .swap, .perpetuals, .rewards, .gift, .buy, .sell, .setPriceAlert: + case .deeplink: await navigationHandler.handle(action) } } catch { diff --git a/ios/Packages/GemstonePrimitives/Sources/Extensions/DeepLink+GemstoneSwift.swift b/ios/Packages/GemstonePrimitives/Sources/Extensions/DeepLink+GemstoneSwift.swift new file mode 100644 index 0000000000..9cf0cb0b93 --- /dev/null +++ b/ios/Packages/GemstonePrimitives/Sources/Extensions/DeepLink+GemstoneSwift.swift @@ -0,0 +1,28 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import Gemstone +import Primitives + +public extension Primitives.DeepLink { + var url: URL { + Gemstone.deeplinkBuildUrl(deeplink: map()).asURL! + } + + var gemUrl: URL { + Gemstone.deeplinkBuildGemUrl(deeplink: map()).asURL! + } + + func map() -> Gemstone.Deeplink { + switch self { + case let .asset(assetId): .asset(assetId: assetId.identifier) + case let .swap(fromAssetId, toAssetId): .swap(fromAssetId: fromAssetId.identifier, toAssetId: toAssetId?.identifier) + case .perpetuals: .perpetuals + case let .rewards(code): .rewards(code: code) + case let .gift(code): .gift(code: code) + case let .buy(assetId, amount): .buy(assetId: assetId.identifier, amount: amount.map { Int32($0) }) + case let .sell(assetId, amount): .sell(assetId: assetId.identifier, amount: amount.map { Int32($0) }) + case let .setPriceAlert(assetId, price): .setPriceAlert(assetId: assetId.identifier, price: price) + } + } +} diff --git a/ios/Packages/GemstonePrimitives/Sources/Extensions/WalletConnectLink+GemstonePrimitives.swift b/ios/Packages/GemstonePrimitives/Sources/Extensions/WalletConnectLink+GemstonePrimitives.swift new file mode 100644 index 0000000000..ec2be570bc --- /dev/null +++ b/ios/Packages/GemstonePrimitives/Sources/Extensions/WalletConnectLink+GemstonePrimitives.swift @@ -0,0 +1,14 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Gemstone +import Primitives + +public extension Gemstone.WalletConnectLink { + func map() -> Primitives.WalletConnectAction { + switch self { + case let .connect(uri): .connect(uri: uri) + case .request: .request + case let .session(topic): .session(topic) + } + } +} diff --git a/ios/Packages/GemstonePrimitives/Sources/URLParser.swift b/ios/Packages/GemstonePrimitives/Sources/URLParser.swift new file mode 100644 index 0000000000..18cd06553c --- /dev/null +++ b/ios/Packages/GemstonePrimitives/Sources/URLParser.swift @@ -0,0 +1,33 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Foundation +import enum Gemstone.Deeplink +import func Gemstone.deeplinkDecodeUrl +import func Gemstone.walletConnectDecodeUrl +import enum Gemstone.WalletConnectLink +import Primitives + +enum URLParserError: Error { + case invalidURL(URL) +} + +public enum URLParser { + public static func from(url: URL) throws -> URLAction { + if let walletConnectLink = walletConnectDecodeUrl(url: url.absoluteString) { + return .walletConnect(walletConnectLink.map()) + } + + let deeplink: DeepLink = switch deeplinkDecodeUrl(url: url.absoluteString) { + case let .asset(assetId): try .asset(AssetId(id: assetId)) + case let .swap(fromAssetId, toAssetId): try .swap(AssetId(id: fromAssetId), toAssetId.map { try AssetId(id: $0) }) + case let .rewards(code): .rewards(code: code) + case let .gift(code): .gift(code: code) + case let .buy(assetId, amount): try .buy(AssetId(id: assetId), amount: amount.map { Int($0) }) + case let .sell(assetId, amount): try .sell(AssetId(id: assetId), amount: amount.map { Int($0) }) + case let .setPriceAlert(assetId, price): try .setPriceAlert(AssetId(id: assetId), price: price) + case .perpetuals: .perpetuals + case .none: throw URLParserError.invalidURL(url) + } + return .deeplink(deeplink) + } +} diff --git a/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkMock.swift b/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkMock.swift new file mode 100644 index 0000000000..fabf7ea052 --- /dev/null +++ b/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkMock.swift @@ -0,0 +1,22 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +enum DeepLinkMock { + static let assetBitcoin = "https://gemwallet.com/tokens/bitcoin" + static let assetBitcoinGem = "gem://tokens/bitcoin" + static let assetEthereumToken = "https://gemwallet.com/tokens/ethereum/0xdAC17F958D2ee523a2206206994597C13D831ec7" + static let swap = "https://gemwallet.com/swap/ethereum/ethereum_0xdAC17F958D2ee523a2206206994597C13D831ec7" + static let perpetuals = "https://gemwallet.com/perpetuals" + static let perpetualsGem = "gem://perpetuals" + static let rewards = "https://gemwallet.com/rewards?code=gemcoder" + static let gift = "https://gemwallet.com/gift?code=giftcode123" + static let giftGem = "gem://gift" + static let buy = "https://gemwallet.com/buy/bitcoin?amount=100" + static let sell = "https://gemwallet.com/sell/ethereum" + static let setPriceAlert = "https://gemwallet.com/setPriceAlert/bitcoin?price=2.5" + static let walletConnectConnect = "gem://wc?uri=wc:topic@2" + static let walletConnectRequest = "gem://wc?requestId=1" + static let walletConnectSession = "gem://wc?sessionTopic=abc123" + static let noPath = "gem://" + static let badPath = "gem://invalidpath" + static let badHost = "https://example.com/tokens/bitcoin" +} diff --git a/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkTests.swift b/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkTests.swift new file mode 100644 index 0000000000..c4dd6019a4 --- /dev/null +++ b/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkTests.swift @@ -0,0 +1,27 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import GemstonePrimitives +import Primitives +import PrimitivesTestKit +import Testing + +struct DeepLinkTests { + @Test + func url() { + #expect(DeepLink.asset(.mock()).url.absoluteString == DeepLinkMock.assetBitcoin) + #expect(DeepLink.asset(.mockEthereumUSDT()).url.absoluteString == DeepLinkMock.assetEthereumToken) + #expect(DeepLink.swap(.mockEthereum(), .mockEthereumUSDT()).url.absoluteString == DeepLinkMock.swap) + #expect(DeepLink.perpetuals.url.absoluteString == DeepLinkMock.perpetuals) + #expect(DeepLink.rewards(code: "gemcoder").url.absoluteString == DeepLinkMock.rewards) + #expect(DeepLink.gift(code: "giftcode123").url.absoluteString == DeepLinkMock.gift) + #expect(DeepLink.buy(.mock(), amount: 100).url.absoluteString == DeepLinkMock.buy) + #expect(DeepLink.sell(.mockEthereum(), amount: nil).url.absoluteString == DeepLinkMock.sell) + #expect(DeepLink.setPriceAlert(.mock(), price: 2.5).url.absoluteString == DeepLinkMock.setPriceAlert) + } + + @Test + func gemUrl() { + #expect(DeepLink.perpetuals.gemUrl.absoluteString == DeepLinkMock.perpetualsGem) + #expect(DeepLink.asset(.mock()).gemUrl.absoluteString == DeepLinkMock.assetBitcoinGem) + } +} diff --git a/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/URLParserTests.swift b/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/URLParserTests.swift new file mode 100644 index 0000000000..0b3aa3d80e --- /dev/null +++ b/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/URLParserTests.swift @@ -0,0 +1,35 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +@testable import GemstonePrimitives +import Primitives +import PrimitivesTestKit +import Testing + +struct URLParserTests { + @Test + func deeplink() throws { + #expect(try URLParser.from(url: #require(DeepLinkMock.assetBitcoin.asURL)) == .deeplink(.asset(.mock()))) + #expect(try URLParser.from(url: #require(DeepLinkMock.assetEthereumToken.asURL)) == .deeplink(.asset(.mockEthereumUSDT()))) + #expect(try URLParser.from(url: #require(DeepLinkMock.swap.asURL)) == .deeplink(.swap(.mockEthereum(), .mockEthereumUSDT()))) + #expect(try URLParser.from(url: #require(DeepLinkMock.perpetualsGem.asURL)) == .deeplink(.perpetuals)) + #expect(try URLParser.from(url: #require(DeepLinkMock.rewards.asURL)) == .deeplink(.rewards(code: "gemcoder"))) + #expect(try URLParser.from(url: #require(DeepLinkMock.giftGem.asURL)) == .deeplink(.gift(code: nil))) + #expect(try URLParser.from(url: #require(DeepLinkMock.buy.asURL)) == .deeplink(.buy(.mock(), amount: 100))) + #expect(try URLParser.from(url: #require(DeepLinkMock.sell.asURL)) == .deeplink(.sell(.mockEthereum(), amount: nil))) + #expect(try URLParser.from(url: #require(DeepLinkMock.setPriceAlert.asURL)) == .deeplink(.setPriceAlert(.mock(), price: 2.5))) + } + + @Test + func walletConnect() throws { + #expect(try URLParser.from(url: #require(DeepLinkMock.walletConnectConnect.asURL)) == .walletConnect(.connect(uri: "wc:topic@2"))) + #expect(try URLParser.from(url: #require(DeepLinkMock.walletConnectRequest.asURL)) == .walletConnect(.request)) + #expect(try URLParser.from(url: #require(DeepLinkMock.walletConnectSession.asURL)) == .walletConnect(.session("abc123"))) + } + + @Test + func invalidURL() throws { + #expect(throws: URLParserError.self) { try URLParser.from(url: #require(DeepLinkMock.noPath.asURL)) } + #expect(throws: URLParserError.self) { try URLParser.from(url: #require(DeepLinkMock.badPath.asURL)) } + #expect(throws: URLParserError.self) { try URLParser.from(url: #require(DeepLinkMock.badHost.asURL)) } + } +} diff --git a/ios/Packages/Primitives/Sources/DeepLink.swift b/ios/Packages/Primitives/Sources/DeepLink.swift index 929c9033f0..ac0cfbaaeb 100644 --- a/ios/Packages/Primitives/Sources/DeepLink.swift +++ b/ios/Packages/Primitives/Sources/DeepLink.swift @@ -2,9 +2,7 @@ import Foundation -public enum DeepLink: Sendable { - static let host = "gemwallet.com" - +public enum DeepLink: Equatable, Sendable { case asset(AssetId) case swap(AssetId, AssetId?) case perpetuals @@ -13,73 +11,4 @@ public enum DeepLink: Sendable { case buy(AssetId, amount: Int?) case sell(AssetId, amount: Int?) case setPriceAlert(AssetId, price: Double?) - - public enum PathComponent: String { - case tokens - case swap - case perpetuals - case rewards - case join - case gift - case buy - case sell - case setPriceAlert - } - - public var pathComponent: PathComponent { - switch self { - case .asset: .tokens - case .swap: .swap - case .perpetuals: .perpetuals - case .rewards: .rewards - case .gift: .gift - case .buy: .buy - case .sell: .sell - case .setPriceAlert: .setPriceAlert - } - } - - public var path: String { - switch self { - case let .asset(assetId): - switch assetId.tokenId { - case let .some(tokenId): "/\(pathComponent.rawValue)/\(assetId.chain.rawValue)/\(tokenId)" - case .none: "/\(pathComponent.rawValue)/\(assetId.chain.rawValue)" - } - case let .swap(fromAssetId, toAssetId): - switch toAssetId { - case let .some(id): "/\(pathComponent.rawValue)/\(fromAssetId.identifier)/\(id.identifier)" - case .none: "/\(pathComponent.rawValue)/\(fromAssetId.identifier)" - } - case .perpetuals: "/\(pathComponent.rawValue)" - case let .rewards(code): - switch code { - case let .some(code): "/\(pathComponent.rawValue)?code=\(code)" - case .none: "/\(pathComponent.rawValue)" - } - case let .gift(code): - switch code { - case let .some(code): "/\(pathComponent.rawValue)?code=\(code)" - case .none: "/\(pathComponent.rawValue)" - } - case let .buy(assetId, amount), let .sell(assetId, amount): - switch amount { - case let .some(amount): "/\(pathComponent.rawValue)/\(assetId.identifier)?amount=\(amount)" - case .none: "/\(pathComponent.rawValue)/\(assetId.identifier)" - } - case let .setPriceAlert(assetId, price): - switch price { - case let .some(price): "/\(pathComponent.rawValue)/\(assetId.identifier)?price=\(price)" - case .none: "/\(pathComponent.rawValue)/\(assetId.identifier)" - } - } - } - - public var url: URL { - URL(string: "https://\(Self.host)\(path)")! - } - - public var localUrl: URL { - URL(string: "gem://\(path)")! - } } diff --git a/ios/Packages/Primitives/Sources/URLAction.swift b/ios/Packages/Primitives/Sources/URLAction.swift index fb22974751..141cfacd0d 100644 --- a/ios/Packages/Primitives/Sources/URLAction.swift +++ b/ios/Packages/Primitives/Sources/URLAction.swift @@ -3,15 +3,8 @@ import Foundation public enum URLAction: Equatable { + case deeplink(DeepLink) case walletConnect(WalletConnectAction) - case asset(AssetId) - case swap(AssetId, AssetId?) - case perpetuals - case rewards(code: String?) - case gift(code: String?) - case buy(AssetId, amount: Int?) - case sell(AssetId, amount: Int?) - case setPriceAlert(AssetId, price: Double?) } public enum WalletConnectAction: Equatable { diff --git a/ios/Packages/Primitives/Sources/URLParser.swift b/ios/Packages/Primitives/Sources/URLParser.swift deleted file mode 100644 index 0f93b950c7..0000000000 --- a/ios/Packages/Primitives/Sources/URLParser.swift +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c). Gem Wallet. All rights reserved. - -import Foundation - -enum URLParserError: Error { - case invalidURL(URL) -} - -public enum URLParser { - public static func from(url: URL) throws -> URLAction { - if let walletConnectAction = try parseWalletConnect(url: url) { - return .walletConnect(walletConnectAction) - } - return try parseDeepLink(url: url) - } - - private static func parseWalletConnect(url: URL) throws -> WalletConnectAction? { - if url.absoluteString.contains("wc?uri") { - guard let components = url.absoluteString.components(separatedBy: "://").last?.removingPercentEncoding, components.count > 1 else { - throw AnyError("invalid wc url") - } - let uri = components.replacingOccurrences(of: "wc?uri=", with: "") - return .connect(uri: uri) - } else if url.absoluteString.contains("wc?requestId") { - return .request - } else if url.absoluteString.contains("wc?sessionTopic") { - guard let components = URLComponents(string: url.absoluteString), - let sessionTopic = components.queryItems?.first(where: { $0.name == "sessionTopic" })?.value - else { - throw AnyError("invalid sessionTopic url") - } - return .session(sessionTopic) - } - return nil - } - - private static func parseDeepLink(url: URL) throws -> URLAction { - var urlComponents = Array(url.pathComponents.dropFirst()) - - if url.scheme == "gem", let host = url.host() { - urlComponents = [host] + urlComponents - } - - if url.host() == DeepLink.host || url.scheme == "gem" { - guard let path = urlComponents.first, - let pathComponent = DeepLink.PathComponent(rawValue: path) - else { - throw URLParserError.invalidURL(url) - } - - switch pathComponent { - case .tokens: - let chain = try Chain(id: urlComponents.required(at: 1, url: url)) - return .asset(AssetId(chain: chain, tokenId: urlComponents.element(at: 2))) - case .swap: - let fromId = try AssetId(id: urlComponents.required(at: 1, url: url)) - let toId = urlComponents.element(at: 2).flatMap { try? AssetId(id: $0) } - return .swap(fromId, toId) - case .perpetuals: - return .perpetuals - case .rewards, .join: - let code = urlComponents.element(at: 1) ?? url.queryValue(for: "code") - return .rewards(code: code) - case .gift: - let code = urlComponents.element(at: 1) ?? url.queryValue(for: "code") - return .gift(code: code) - case .buy, .sell: - return try parseFiat(url: url, urlComponents: urlComponents, type: pathComponent) - case .setPriceAlert: - let price: Double? = url.queryValue(for: "price") - let assetId = try AssetId(id: urlComponents.required(at: 1, url: url)) - return .setPriceAlert(assetId, price: price) - } - } - - throw URLParserError.invalidURL(url) - } - - private static func parseFiat(url: URL, urlComponents: [String], type: DeepLink.PathComponent) throws -> URLAction { - let assetId = try AssetId(id: urlComponents.required(at: 1, url: url)) - let amount: Int? = url.queryValue(for: "amount") - switch type { - case .buy: return .buy(assetId, amount: amount) - case .sell: return .sell(assetId, amount: amount) - default: throw URLParserError.invalidURL(url) - } - } -} - -private extension [String] { - func required(at index: Int, url: URL) throws -> String { - guard let value = element(at: index) else { - throw URLParserError.invalidURL(url) - } - return value - } -} diff --git a/ios/Packages/Primitives/TestKit/AssetId+PrimitivesTestKit.swift b/ios/Packages/Primitives/TestKit/AssetId+PrimitivesTestKit.swift index 56fbd0052f..c937693e10 100644 --- a/ios/Packages/Primitives/TestKit/AssetId+PrimitivesTestKit.swift +++ b/ios/Packages/Primitives/TestKit/AssetId+PrimitivesTestKit.swift @@ -18,6 +18,10 @@ public extension AssetId { AssetId(chain: .ethereum, tokenId: .none) } + static func mockEthereumUSDT() -> AssetId { + AssetId(chain: .ethereum, tokenId: "0xdAC17F958D2ee523a2206206994597C13D831ec7") + } + static func mockSolana() -> AssetId { AssetId(chain: .solana, tokenId: .none) } diff --git a/ios/Packages/Primitives/Tests/PrimitivesTests/URLParserTests.swift b/ios/Packages/Primitives/Tests/PrimitivesTests/URLParserTests.swift deleted file mode 100644 index 0f8ed46858..0000000000 --- a/ios/Packages/Primitives/Tests/PrimitivesTests/URLParserTests.swift +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c). Gem Wallet. All rights reserved. - -import Foundation -@testable import Primitives -import Testing - -struct URLParserTests { - @Test - func assetUrl() throws { - let chainAction = try URLParser.from(url: #require(URL(string: "https://gemwallet.com/tokens/bitcoin"))) - #expect(chainAction == .asset(AssetId(chain: .bitcoin, tokenId: .none))) - - let tokenAction = try URLParser.from(url: #require(URL(string: "https://gemwallet.com/tokens/ethereum/0xdAC17F958D2ee523a2206206994597C13D831ec7"))) - #expect(tokenAction == .asset(AssetId(chain: .ethereum, tokenId: "0xdAC17F958D2ee523a2206206994597C13D831ec7"))) - } - - @Test - func gemSchemeAssetUrl() throws { - let chainAction = try URLParser.from(url: #require(URL(string: "gem://tokens/bitcoin"))) - #expect(chainAction == .asset(AssetId(chain: .bitcoin, tokenId: .none))) - - let tokenAction = try URLParser.from(url: #require(URL(string: "gem://tokens/solana/JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"))) - #expect(tokenAction == .asset(AssetId(chain: .solana, tokenId: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"))) - } - - @Test - func swapUrl() throws { - let swapFromOnly = try URLParser.from(url: #require(URL(string: "https://gemwallet.com/swap/ethereum"))) - #expect(swapFromOnly == .swap(AssetId(chain: .ethereum, tokenId: nil), nil)) - - let swapFromTo = try URLParser.from(url: #require(URL(string: "https://gemwallet.com/swap/ethereum/ethereum_0xdAC17F958D2ee523a2206206994597C13D831ec7"))) - #expect(swapFromTo == .swap( - AssetId(chain: .ethereum, tokenId: nil), - AssetId(chain: .ethereum, tokenId: "0xdAC17F958D2ee523a2206206994597C13D831ec7"), - )) - } - - @Test - func walletConnectSessionTopicUrl() throws { - let url = "gem://wc?sessionTopic=64a4f0817e3dd003cbe23202fb6ffaa16d38074de84762a5797e6092b2250a27" - let action = try URLParser.from(url: #require(URL(string: url))) - #expect(action == .walletConnect(.session("64a4f0817e3dd003cbe23202fb6ffaa16d38074de84762a5797e6092b2250a27"))) - } - - @Test - func perpetualsUrl() throws { - let perpetualsAction = try URLParser.from(url: #require(URL(string: "https://gemwallet.com/perpetuals"))) - #expect(perpetualsAction == .perpetuals) - } - - @Test - func gemUrlWithoutAction() throws { - #expect(throws: URLParserError.self) { try URLParser.from(url: #require(URL(string: "gem://"))) } - #expect(throws: URLParserError.self) { try URLParser.from(url: #require(URL(string: "gem://invalidpath"))) } - } - - @Test - func rewardsUrl() throws { - #expect(try URLParser.from(url: #require(URL(string: "https://gemwallet.com/join/gemcoder"))) == .rewards(code: "gemcoder")) - #expect(try URLParser.from(url: #require(URL(string: "https://gemwallet.com/join?code=gemcoder"))) == .rewards(code: "gemcoder")) - #expect(try URLParser.from(url: #require(URL(string: "https://gemwallet.com/rewards/gemcoder"))) == .rewards(code: "gemcoder")) - #expect(try URLParser.from(url: #require(URL(string: "https://gemwallet.com/rewards?code=gemcoder"))) == .rewards(code: "gemcoder")) - } - - @Test - func gemSchemeRewardsUrl() throws { - #expect(try URLParser.from(url: #require(URL(string: "gem://join/gemcoder"))) == .rewards(code: "gemcoder")) - #expect(try URLParser.from(url: #require(URL(string: "gem://join?code=gemcoder"))) == .rewards(code: "gemcoder")) - #expect(try URLParser.from(url: #require(URL(string: "gem://rewards/gemcoder"))) == .rewards(code: "gemcoder")) - #expect(try URLParser.from(url: #require(URL(string: "gem://rewards?code=gemcoder"))) == .rewards(code: "gemcoder")) - #expect(try URLParser.from(url: #require(URL(string: "gem://rewards"))) == .rewards(code: nil)) - } - - @Test - func giftUrl() throws { - #expect(try URLParser.from(url: #require(URL(string: "https://gemwallet.com/gift/giftcode123"))) == .gift(code: "giftcode123")) - #expect(try URLParser.from(url: #require(URL(string: "https://gemwallet.com/gift?code=giftcode123"))) == .gift(code: "giftcode123")) - } - - @Test - func gemSchemeGiftUrl() throws { - #expect(try URLParser.from(url: #require(URL(string: "gem://gift/giftcode123"))) == .gift(code: "giftcode123")) - #expect(try URLParser.from(url: #require(URL(string: "gem://gift?code=giftcode123"))) == .gift(code: "giftcode123")) - #expect(try URLParser.from(url: #require(URL(string: "gem://gift"))) == .gift(code: nil)) - } - - @Test - func buyUrl() throws { - let buyChain = try URLParser.from(url: #require(URL(string: "https://gemwallet.com/buy/bitcoin"))) - #expect(buyChain == .buy(AssetId(chain: .bitcoin, tokenId: nil), amount: nil)) - - let buyToken = try URLParser.from(url: #require(URL(string: "https://gemwallet.com/buy/ethereum_0xdAC17F958D2ee523a2206206994597C13D831ec7"))) - #expect(buyToken == .buy(AssetId(chain: .ethereum, tokenId: "0xdAC17F958D2ee523a2206206994597C13D831ec7"), amount: nil)) - - let buyWithAmount = try URLParser.from(url: #require(URL(string: "https://gemwallet.com/buy/bitcoin?amount=100"))) - #expect(buyWithAmount == .buy(AssetId(chain: .bitcoin, tokenId: nil), amount: 100)) - - let buyTokenWithAmount = try URLParser.from(url: #require(URL(string: "https://gemwallet.com/buy/ethereum_0xdAC17F958D2ee523a2206206994597C13D831ec7?amount=50"))) - #expect(buyTokenWithAmount == .buy(AssetId(chain: .ethereum, tokenId: "0xdAC17F958D2ee523a2206206994597C13D831ec7"), amount: 50)) - } - - @Test - func sellUrl() throws { - let sellChain = try URLParser.from(url: #require(URL(string: "https://gemwallet.com/sell/bitcoin"))) - #expect(sellChain == .sell(AssetId(chain: .bitcoin, tokenId: nil), amount: nil)) - - let sellWithAmount = try URLParser.from(url: #require(URL(string: "https://gemwallet.com/sell/ethereum?amount=200"))) - #expect(sellWithAmount == .sell(AssetId(chain: .ethereum, tokenId: nil), amount: 200)) - } - - @Test - func setPriceAlertUrl() throws { - let alertWithPrice = try URLParser.from(url: #require(URL(string: "https://gemwallet.com/setPriceAlert/bitcoin?price=50000"))) - #expect(alertWithPrice == .setPriceAlert(AssetId(chain: .bitcoin, tokenId: nil), price: 50000)) - - let alertTokenWithPrice = try URLParser.from(url: #require(URL(string: "https://gemwallet.com/setPriceAlert/ethereum_0xdAC17F958D2ee523a2206206994597C13D831ec7?price=1.5"))) - #expect(alertTokenWithPrice == .setPriceAlert(AssetId(chain: .ethereum, tokenId: "0xdAC17F958D2ee523a2206206994597C13D831ec7"), price: 1.5)) - } - - @Test - func setPriceAlertUrlWithoutPrice() throws { - let alertWithoutPrice = try URLParser.from(url: #require(URL(string: "https://gemwallet.com/setPriceAlert/bitcoin"))) - #expect(alertWithoutPrice == .setPriceAlert(AssetId(chain: .bitcoin, tokenId: nil), price: nil)) - } -} From 06261a60fd04054c2cc62201065b7a3d6af23aee Mon Sep 17 00:00:00 2001 From: Radmir Date: Wed, 20 May 2026 23:45:28 +0500 Subject: [PATCH 3/5] Remove deprecated deeplink variants and handlers Remove legacy DeepLink cases (swap, buy, sell, setPriceAlert) and their handling across the codebase. Updates include pruning cases from the DeepLink enum, simplifying URLParser and Gemstone mapping, removing related navigation presentation flows in NavigationHandler, and cleaning up tests/mocks that referenced those deeplinks. Also includes a tiny whitespace fix in a generated file. --- core | 2 +- ios/Gem/Navigation/NavigationHandler.swift | 27 ------------------- .../Extensions/DeepLink+GemstoneSwift.swift | 4 --- .../Sources/URLParser.swift | 4 --- .../DeepLinkMock.swift | 4 --- .../DeepLinkTests.swift | 4 --- .../URLParserTests.swift | 4 --- .../Primitives/Sources/DeepLink.swift | 4 --- .../Sources/Generated/SolanaNft.swift | 1 - 9 files changed, 1 insertion(+), 53 deletions(-) diff --git a/core b/core index 024861b2c0..f7018ad603 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 024861b2c0bc578948b181d8e11a7fb861e9dee1 +Subproject commit f7018ad603fa73a312e78ee993e13ee17565c098 diff --git a/ios/Gem/Navigation/NavigationHandler.swift b/ios/Gem/Navigation/NavigationHandler.swift index b48172a53d..96b1435faf 100644 --- a/ios/Gem/Navigation/NavigationHandler.swift +++ b/ios/Gem/Navigation/NavigationHandler.swift @@ -68,10 +68,6 @@ extension NavigationHandler { case let .asset(assetId): try await navigateToAsset(assetId) - case let .swap(fromId, toId): - try await presentSwap(from: fromId, to: toId) - return - case .perpetuals: navigationState.wallet.append(Scenes.Perpetuals()) @@ -80,18 +76,6 @@ extension NavigationHandler { case let .gift(code): navigationState.settings.append(Scenes.Referral(code: nil, giftCode: code)) - - case let .buy(assetId, amount): - try await presentBuy(assetId: assetId, amount: amount) - return - - case let .sell(assetId, amount): - try await presentSell(assetId: assetId, amount: amount) - return - - case let .setPriceAlert(assetId, price): - try await presentSetPriceAlert(assetId: assetId, price: price) - return } selectTab(for: deeplink.selectTab) @@ -198,16 +182,6 @@ extension NavigationHandler { try presentAssetInput(type: .buy(asset, amount: amount), for: asset) } - private func presentSell(assetId: AssetId, amount: Int?) async throws { - let asset = try await assetsService.getOrFetchAsset(for: assetId) - try presentAssetInput(type: .sell(asset, amount: amount), for: asset) - } - - private func presentSetPriceAlert(assetId: AssetId, price: Double?) async throws { - let asset = try await assetsService.getOrFetchAsset(for: assetId) - presenter.isPresentingPriceAlert.wrappedValue = SetPriceAlertInput(asset: asset, price: price) - } - private func presentAssetInput(type: SelectedAssetType, for asset: Asset) throws { guard let wallet else { return } try presenter.presentAssetInput(type: type, for: asset, wallet: wallet) @@ -225,7 +199,6 @@ private extension DeepLink { var selectTab: TabItem? { switch self { case .asset, .perpetuals: .wallet - case .swap, .buy, .sell, .setPriceAlert: nil case .rewards, .gift: .settings } } diff --git a/ios/Packages/GemstonePrimitives/Sources/Extensions/DeepLink+GemstoneSwift.swift b/ios/Packages/GemstonePrimitives/Sources/Extensions/DeepLink+GemstoneSwift.swift index 9cf0cb0b93..459d55ca32 100644 --- a/ios/Packages/GemstonePrimitives/Sources/Extensions/DeepLink+GemstoneSwift.swift +++ b/ios/Packages/GemstonePrimitives/Sources/Extensions/DeepLink+GemstoneSwift.swift @@ -16,13 +16,9 @@ public extension Primitives.DeepLink { func map() -> Gemstone.Deeplink { switch self { case let .asset(assetId): .asset(assetId: assetId.identifier) - case let .swap(fromAssetId, toAssetId): .swap(fromAssetId: fromAssetId.identifier, toAssetId: toAssetId?.identifier) case .perpetuals: .perpetuals case let .rewards(code): .rewards(code: code) case let .gift(code): .gift(code: code) - case let .buy(assetId, amount): .buy(assetId: assetId.identifier, amount: amount.map { Int32($0) }) - case let .sell(assetId, amount): .sell(assetId: assetId.identifier, amount: amount.map { Int32($0) }) - case let .setPriceAlert(assetId, price): .setPriceAlert(assetId: assetId.identifier, price: price) } } } diff --git a/ios/Packages/GemstonePrimitives/Sources/URLParser.swift b/ios/Packages/GemstonePrimitives/Sources/URLParser.swift index 18cd06553c..95b1383511 100644 --- a/ios/Packages/GemstonePrimitives/Sources/URLParser.swift +++ b/ios/Packages/GemstonePrimitives/Sources/URLParser.swift @@ -19,12 +19,8 @@ public enum URLParser { let deeplink: DeepLink = switch deeplinkDecodeUrl(url: url.absoluteString) { case let .asset(assetId): try .asset(AssetId(id: assetId)) - case let .swap(fromAssetId, toAssetId): try .swap(AssetId(id: fromAssetId), toAssetId.map { try AssetId(id: $0) }) case let .rewards(code): .rewards(code: code) case let .gift(code): .gift(code: code) - case let .buy(assetId, amount): try .buy(AssetId(id: assetId), amount: amount.map { Int($0) }) - case let .sell(assetId, amount): try .sell(AssetId(id: assetId), amount: amount.map { Int($0) }) - case let .setPriceAlert(assetId, price): try .setPriceAlert(AssetId(id: assetId), price: price) case .perpetuals: .perpetuals case .none: throw URLParserError.invalidURL(url) } diff --git a/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkMock.swift b/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkMock.swift index fabf7ea052..28c9648751 100644 --- a/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkMock.swift +++ b/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkMock.swift @@ -4,15 +4,11 @@ enum DeepLinkMock { static let assetBitcoin = "https://gemwallet.com/tokens/bitcoin" static let assetBitcoinGem = "gem://tokens/bitcoin" static let assetEthereumToken = "https://gemwallet.com/tokens/ethereum/0xdAC17F958D2ee523a2206206994597C13D831ec7" - static let swap = "https://gemwallet.com/swap/ethereum/ethereum_0xdAC17F958D2ee523a2206206994597C13D831ec7" static let perpetuals = "https://gemwallet.com/perpetuals" static let perpetualsGem = "gem://perpetuals" static let rewards = "https://gemwallet.com/rewards?code=gemcoder" static let gift = "https://gemwallet.com/gift?code=giftcode123" static let giftGem = "gem://gift" - static let buy = "https://gemwallet.com/buy/bitcoin?amount=100" - static let sell = "https://gemwallet.com/sell/ethereum" - static let setPriceAlert = "https://gemwallet.com/setPriceAlert/bitcoin?price=2.5" static let walletConnectConnect = "gem://wc?uri=wc:topic@2" static let walletConnectRequest = "gem://wc?requestId=1" static let walletConnectSession = "gem://wc?sessionTopic=abc123" diff --git a/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkTests.swift b/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkTests.swift index c4dd6019a4..51884a1ccb 100644 --- a/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkTests.swift +++ b/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkTests.swift @@ -10,13 +10,9 @@ struct DeepLinkTests { func url() { #expect(DeepLink.asset(.mock()).url.absoluteString == DeepLinkMock.assetBitcoin) #expect(DeepLink.asset(.mockEthereumUSDT()).url.absoluteString == DeepLinkMock.assetEthereumToken) - #expect(DeepLink.swap(.mockEthereum(), .mockEthereumUSDT()).url.absoluteString == DeepLinkMock.swap) #expect(DeepLink.perpetuals.url.absoluteString == DeepLinkMock.perpetuals) #expect(DeepLink.rewards(code: "gemcoder").url.absoluteString == DeepLinkMock.rewards) #expect(DeepLink.gift(code: "giftcode123").url.absoluteString == DeepLinkMock.gift) - #expect(DeepLink.buy(.mock(), amount: 100).url.absoluteString == DeepLinkMock.buy) - #expect(DeepLink.sell(.mockEthereum(), amount: nil).url.absoluteString == DeepLinkMock.sell) - #expect(DeepLink.setPriceAlert(.mock(), price: 2.5).url.absoluteString == DeepLinkMock.setPriceAlert) } @Test diff --git a/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/URLParserTests.swift b/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/URLParserTests.swift index 0b3aa3d80e..e546b37579 100644 --- a/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/URLParserTests.swift +++ b/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/URLParserTests.swift @@ -10,13 +10,9 @@ struct URLParserTests { func deeplink() throws { #expect(try URLParser.from(url: #require(DeepLinkMock.assetBitcoin.asURL)) == .deeplink(.asset(.mock()))) #expect(try URLParser.from(url: #require(DeepLinkMock.assetEthereumToken.asURL)) == .deeplink(.asset(.mockEthereumUSDT()))) - #expect(try URLParser.from(url: #require(DeepLinkMock.swap.asURL)) == .deeplink(.swap(.mockEthereum(), .mockEthereumUSDT()))) #expect(try URLParser.from(url: #require(DeepLinkMock.perpetualsGem.asURL)) == .deeplink(.perpetuals)) #expect(try URLParser.from(url: #require(DeepLinkMock.rewards.asURL)) == .deeplink(.rewards(code: "gemcoder"))) #expect(try URLParser.from(url: #require(DeepLinkMock.giftGem.asURL)) == .deeplink(.gift(code: nil))) - #expect(try URLParser.from(url: #require(DeepLinkMock.buy.asURL)) == .deeplink(.buy(.mock(), amount: 100))) - #expect(try URLParser.from(url: #require(DeepLinkMock.sell.asURL)) == .deeplink(.sell(.mockEthereum(), amount: nil))) - #expect(try URLParser.from(url: #require(DeepLinkMock.setPriceAlert.asURL)) == .deeplink(.setPriceAlert(.mock(), price: 2.5))) } @Test diff --git a/ios/Packages/Primitives/Sources/DeepLink.swift b/ios/Packages/Primitives/Sources/DeepLink.swift index ac0cfbaaeb..96c0243195 100644 --- a/ios/Packages/Primitives/Sources/DeepLink.swift +++ b/ios/Packages/Primitives/Sources/DeepLink.swift @@ -4,11 +4,7 @@ import Foundation public enum DeepLink: Equatable, Sendable { case asset(AssetId) - case swap(AssetId, AssetId?) case perpetuals case rewards(code: String?) case gift(code: String?) - case buy(AssetId, amount: Int?) - case sell(AssetId, amount: Int?) - case setPriceAlert(AssetId, price: Double?) } diff --git a/ios/Packages/Primitives/Sources/Generated/SolanaNft.swift b/ios/Packages/Primitives/Sources/Generated/SolanaNft.swift index f938c345fb..5664600a5e 100644 --- a/ios/Packages/Primitives/Sources/Generated/SolanaNft.swift +++ b/ios/Packages/Primitives/Sources/Generated/SolanaNft.swift @@ -4,7 +4,6 @@ import Foundation - /// Generated type representing the anonymous struct variant `ProgrammableNonFungible` of the `SolanaNftStandard` Rust enum public struct SolanaNftStandardProgrammableNonFungibleInner: Codable, Equatable, Sendable { public let rule_set: String? From d5e1fcbe2603937d6bad36ca5f820c05dc69abe1 Mon Sep 17 00:00:00 2001 From: Radmir Date: Thu, 21 May 2026 23:17:41 +0500 Subject: [PATCH 4/5] Remove gift redemption; unify URL actions Remove the gift redemption flow and consolidate URL/deeplink handling into a unified UrlAction path. Deleted gift-related UI, model fields and tests (gift codes, redemption option helpers) and removed getRedemptionOption from RewardsService and GemAPI. Refactored URL parsing and mapping: use Gemstone.urlAction/UrlAction across iOS and Android, added mapping extension (UrlAction -> Primitives) and updated Deeplink -> Nav route conversion. Android PendingNavigationCoordinator and Web deep link handling updated to consume UrlAction; tests adjusted accordingly. Note: this introduces breaking changes to DeepLink/Scenes shapes and API surface related to gift/redemption handling. --- .../android/PendingNavigationCoordinator.kt | 31 +++++++++------- .../com/gemwallet/android/WebDeepLinks.kt | 9 ++--- .../PendingNavigationCoordinatorTest.kt | 14 +++---- .../com/gemwallet/android/WebDeepLinksTest.kt | 37 ++++--------------- core | 2 +- .../Settings/Scenes/DeveloperScene.swift | 7 ---- .../ViewModels/RewardsViewModel.swift | 17 --------- ios/Gem/Navigation/NavigationHandler.swift | 5 +-- .../Settings/SettingsNavigationStack.swift | 1 - .../RewardsService/RewardsService.swift | 5 --- .../TestKit/RewardsService+TestKit.swift | 7 ---- .../GemAPI/Sources/GemAPIService.swift | 6 --- .../GemAPI/Sources/GemDeviceAPI.swift | 5 --- .../Extensions/DeepLink+GemstoneSwift.swift | 11 +++++- .../UrlAction+GemstonePrimitives.swift | 13 +++++++ .../Sources/URLParser.swift | 20 +++------- .../DeepLinkMock.swift | 18 --------- .../DeepLinkTests.swift | 23 ------------ .../URLParserTests.swift | 31 ---------------- .../Primitives/Sources/DeepLink.swift | 1 - ios/Packages/Primitives/Sources/Scenes.swift | 4 +- 21 files changed, 66 insertions(+), 201 deletions(-) create mode 100644 ios/Packages/GemstonePrimitives/Sources/Extensions/UrlAction+GemstonePrimitives.swift delete mode 100644 ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkMock.swift delete mode 100644 ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkTests.swift delete mode 100644 ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/URLParserTests.swift diff --git a/android/app/src/main/kotlin/com/gemwallet/android/PendingNavigationCoordinator.kt b/android/app/src/main/kotlin/com/gemwallet/android/PendingNavigationCoordinator.kt index ac700b6300..8416866fab 100644 --- a/android/app/src/main/kotlin/com/gemwallet/android/PendingNavigationCoordinator.kt +++ b/android/app/src/main/kotlin/com/gemwallet/android/PendingNavigationCoordinator.kt @@ -7,8 +7,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import uniffi.gemstone.UrlAction import uniffi.gemstone.WalletConnectLink -import uniffi.gemstone.walletConnectDecodeUrl +import uniffi.gemstone.urlAction import javax.inject.Inject internal sealed interface PendingNavigation { @@ -39,19 +40,23 @@ class PendingNavigationCoordinator @Inject constructor( val pendingIntent = (_pendingNavigation.value as? PendingNavigation.RawIntent)?.intent ?: return val uri = pendingIntent.dataString - uri?.let(::walletConnectDecodeUrl)?.let { link -> - when (link) { - is WalletConnectLink.Connect -> walletConnect.onPairing(link.uri) - WalletConnectLink.Request -> walletConnect.onRequest() - is WalletConnectLink.Session -> Unit + when (val action = uri?.let(::urlAction)) { + is UrlAction.WalletConnect -> { + when (val link = action.link) { + is WalletConnectLink.Connect -> walletConnect.onPairing(link.uri) + WalletConnectLink.Request -> walletConnect.onRequest() + is WalletConnectLink.Session -> Unit + } + replace(pendingIntent, replacement = null) + return } - replace(pendingIntent, replacement = null) - return - } - - uri?.toWebDeepLinkRoute()?.let { route -> - replace(pendingIntent, PendingNavigation.Route(route)) - return + is UrlAction.Deeplink -> { + action.deeplink.toRoute()?.let { route -> + replace(pendingIntent, PendingNavigation.Route(route)) + return + } + } + null -> Unit } if (!pendingIntent.hasNotificationPayload()) { diff --git a/android/app/src/main/kotlin/com/gemwallet/android/WebDeepLinks.kt b/android/app/src/main/kotlin/com/gemwallet/android/WebDeepLinks.kt index ea7168f6ba..0ee4d71408 100644 --- a/android/app/src/main/kotlin/com/gemwallet/android/WebDeepLinks.kt +++ b/android/app/src/main/kotlin/com/gemwallet/android/WebDeepLinks.kt @@ -5,12 +5,11 @@ import com.gemwallet.android.ext.toAssetId import com.gemwallet.android.ui.navigation.routes.AssetRoute import com.gemwallet.android.ui.navigation.routes.ReferralRoute import uniffi.gemstone.Deeplink -import uniffi.gemstone.deeplinkDecodeUrl -internal fun String.toWebDeepLinkRoute(): NavKey? { - return when (val deeplink = deeplinkDecodeUrl(this)) { - is Deeplink.Asset -> deeplink.assetId.toAssetId()?.let { AssetRoute(it) } - is Deeplink.Rewards -> ReferralRoute(code = deeplink.code?.takeIf(String::isNotBlank)) +internal fun Deeplink.toRoute(): NavKey? { + return when (this) { + is Deeplink.Asset -> assetId.toAssetId()?.let { AssetRoute(it) } + is Deeplink.Rewards -> ReferralRoute(code = code?.takeIf(String::isNotBlank)) else -> null } } diff --git a/android/app/src/test/kotlin/com/gemwallet/android/PendingNavigationCoordinatorTest.kt b/android/app/src/test/kotlin/com/gemwallet/android/PendingNavigationCoordinatorTest.kt index 1d2be9d72f..c66b28de55 100644 --- a/android/app/src/test/kotlin/com/gemwallet/android/PendingNavigationCoordinatorTest.kt +++ b/android/app/src/test/kotlin/com/gemwallet/android/PendingNavigationCoordinatorTest.kt @@ -16,9 +16,9 @@ import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import uniffi.gemstone.Deeplink +import uniffi.gemstone.UrlAction import uniffi.gemstone.WalletConnectLink -import uniffi.gemstone.deeplinkDecodeUrl -import uniffi.gemstone.walletConnectDecodeUrl +import uniffi.gemstone.urlAction class PendingNavigationCoordinatorTest { @@ -42,7 +42,7 @@ class PendingNavigationCoordinatorTest { fun resolve_walletConnectPairing_invokesPairingHandlerAndClears() = runTest { val handler = RecordingWalletConnect() val uri = "wc:abc@2?relay-protocol=irn" - every { walletConnectDecodeUrl(uri) } returns WalletConnectLink.Connect(uri) + every { urlAction(uri) } returns UrlAction.WalletConnect(WalletConnectLink.Connect(uri)) coordinator.setPendingIntentForTest(intent(uri = uri)) coordinator.resolve(handler) @@ -55,7 +55,7 @@ class PendingNavigationCoordinatorTest { fun resolve_walletConnectRequest_invokesRequestHandlerAndClears() = runTest { val handler = RecordingWalletConnect() val uri = "gem://wc?requestId=42" - every { walletConnectDecodeUrl(uri) } returns WalletConnectLink.Request + every { urlAction(uri) } returns UrlAction.WalletConnect(WalletConnectLink.Request) coordinator.setPendingIntentForTest(intent(uri = uri)) coordinator.resolve(handler) @@ -67,8 +67,7 @@ class PendingNavigationCoordinatorTest { @Test fun resolve_webDeepLink_storesRoute() = runTest { val uri = "https://gemwallet.com/join/gemcoder" - every { walletConnectDecodeUrl(uri) } returns null - every { deeplinkDecodeUrl(uri) } returns Deeplink.Rewards(code = "gemcoder") + every { urlAction(uri) } returns UrlAction.Deeplink(Deeplink.Rewards(code = "gemcoder")) coordinator.setPendingIntentForTest(intent(uri = uri)) coordinator.resolve(NoOpWalletConnect) @@ -80,8 +79,7 @@ class PendingNavigationCoordinatorTest { @Test fun resolve_unknownIntentWithoutNotificationPayload_clears() = runTest { val uri = "https://example.com/unknown" - every { walletConnectDecodeUrl(uri) } returns null - every { deeplinkDecodeUrl(uri) } returns null + every { urlAction(uri) } returns null coordinator.setPendingIntentForTest(intent(uri = uri)) coordinator.resolve(NoOpWalletConnect) diff --git a/android/app/src/test/kotlin/com/gemwallet/android/WebDeepLinksTest.kt b/android/app/src/test/kotlin/com/gemwallet/android/WebDeepLinksTest.kt index 45f86958d5..94e21f039c 100644 --- a/android/app/src/test/kotlin/com/gemwallet/android/WebDeepLinksTest.kt +++ b/android/app/src/test/kotlin/com/gemwallet/android/WebDeepLinksTest.kt @@ -4,47 +4,24 @@ import com.gemwallet.android.testkit.mockAssetId import com.gemwallet.android.ui.navigation.routes.AssetRoute import com.gemwallet.android.ui.navigation.routes.ReferralRoute import com.wallet.core.primitives.Chain -import io.mockk.every -import io.mockk.mockkStatic -import io.mockk.unmockkStatic -import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNull -import org.junit.Before import org.junit.Test import uniffi.gemstone.Deeplink -import uniffi.gemstone.deeplinkDecodeUrl class WebDeepLinksTest { - @Before - fun setUp() = mockkStatic("uniffi.gemstone.GemstoneKt") - - @After - fun tearDown() = unmockkStatic("uniffi.gemstone.GemstoneKt") - @Test - fun webDeepLinkRoute_mapsSupportedDeeplinks() { + fun toRoute_mapsSupportedDeeplinks() { val tokenId = "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN" - every { deeplinkDecodeUrl(any()) } returns Deeplink.Asset(assetId = "bitcoin") - assertEquals(AssetRoute(mockAssetId(Chain.Bitcoin)), "https://gemwallet.com/tokens/bitcoin".toWebDeepLinkRoute()) - - every { deeplinkDecodeUrl(any()) } returns Deeplink.Asset(assetId = "solana_$tokenId") - assertEquals(AssetRoute(mockAssetId(Chain.Solana, tokenId)), "gem://tokens/solana/$tokenId".toWebDeepLinkRoute()) - - every { deeplinkDecodeUrl(any()) } returns Deeplink.Rewards(code = "gemcoder") - assertEquals(ReferralRoute(code = "gemcoder"), "https://gemwallet.com/join/gemcoder".toWebDeepLinkRoute()) - - every { deeplinkDecodeUrl(any()) } returns Deeplink.Rewards(code = null) - assertEquals(ReferralRoute(), "https://gemwallet.com/join".toWebDeepLinkRoute()) + assertEquals(AssetRoute(mockAssetId(Chain.Bitcoin)), Deeplink.Asset(assetId = "bitcoin").toRoute()) + assertEquals(AssetRoute(mockAssetId(Chain.Solana, tokenId)), Deeplink.Asset(assetId = "solana_$tokenId").toRoute()) + assertEquals(ReferralRoute(code = "gemcoder"), Deeplink.Rewards(code = "gemcoder").toRoute()) + assertEquals(ReferralRoute(), Deeplink.Rewards(code = null).toRoute()) } @Test - fun webDeepLinkRoute_rejectsUnsupportedLinks() { - every { deeplinkDecodeUrl(any()) } returns Deeplink.Perpetuals - assertNull("https://gemwallet.com/perpetuals".toWebDeepLinkRoute()) - - every { deeplinkDecodeUrl(any()) } returns null - assertNull("https://gemwallet.com/tokens".toWebDeepLinkRoute()) + fun toRoute_rejectsUnsupportedLinks() { + assertNull(Deeplink.Perpetuals.toRoute()) } } diff --git a/core b/core index 38f23f4df9..7734958dfe 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 38f23f4df97c9013e9a8202185328c9ea32a5103 +Subproject commit 7734958dfee326f3c981a97f67c28670fd594ded diff --git a/ios/Features/Settings/Sources/Settings/Scenes/DeveloperScene.swift b/ios/Features/Settings/Sources/Settings/Scenes/DeveloperScene.swift index 704aa94fe7..259e3daf4b 100644 --- a/ios/Features/Settings/Sources/Settings/Scenes/DeveloperScene.swift +++ b/ios/Features/Settings/Sources/Settings/Scenes/DeveloperScene.swift @@ -100,13 +100,6 @@ public struct DeveloperScene: View { model.deeplink(deeplink: .rewards(code: "gemcoder")) }, ) - - NavigationCustomLink( - with: ListItemView(title: "Open Gift (code)"), - action: { - model.deeplink(deeplink: .gift(code: "GIFT-1234-1234-1234")) - }, - ) } Section("Preferences") { diff --git a/ios/Features/Settings/Sources/Settings/ViewModels/RewardsViewModel.swift b/ios/Features/Settings/Sources/Settings/ViewModels/RewardsViewModel.swift index 7461ca73b3..10e057b250 100644 --- a/ios/Features/Settings/Sources/Settings/ViewModels/RewardsViewModel.swift +++ b/ios/Features/Settings/Sources/Settings/ViewModels/RewardsViewModel.swift @@ -25,7 +25,6 @@ public final class RewardsViewModel: Sendable { private let rewardsService: RewardsServiceable private let assetsEnabler: any AssetsEnabler private let activateCode: String? - private let giftCode: String? private(set) var selectedWallet: Wallet private(set) var wallets: [Wallet] @@ -41,14 +40,12 @@ public final class RewardsViewModel: Sendable { wallet: Wallet, wallets: [Wallet], activateCode: String? = nil, - giftCode: String? = nil, ) { self.rewardsService = rewardsService self.assetsEnabler = assetsEnabler selectedWallet = wallet self.wallets = wallets self.activateCode = activateCode - self.giftCode = giftCode } // MARK: - UI Properties @@ -243,13 +240,6 @@ public final class RewardsViewModel: Sendable { if wallets.count == 1, activateCode != nil { await useReferralCode() - } else if giftCode != nil { - do { - let option = try await getRewardRedemptionOption() - showRedemptionAlert(for: option) - } catch { - showError(error.localizedDescription) - } } else if let code = activateCode { isPresentingSheet = .activateCode(code: code) } @@ -277,13 +267,6 @@ public final class RewardsViewModel: Sendable { } } - private func getRewardRedemptionOption() async throws -> RewardRedemptionOption { - guard let code = giftCode else { - throw AnyError("no gift code") - } - return try await rewardsService.getRedemptionOption(code: code) - } - func canRedeem(option: RewardRedemptionOption) -> Bool { guard let rewards else { return false } return rewards.points >= option.points diff --git a/ios/Gem/Navigation/NavigationHandler.swift b/ios/Gem/Navigation/NavigationHandler.swift index 96b1435faf..fb37f8eca1 100644 --- a/ios/Gem/Navigation/NavigationHandler.swift +++ b/ios/Gem/Navigation/NavigationHandler.swift @@ -73,9 +73,6 @@ extension NavigationHandler { case let .rewards(code): navigationState.settings.append(Scenes.Referral(code: code)) - - case let .gift(code): - navigationState.settings.append(Scenes.Referral(code: nil, giftCode: code)) } selectTab(for: deeplink.selectTab) @@ -199,7 +196,7 @@ private extension DeepLink { var selectTab: TabItem? { switch self { case .asset, .perpetuals: .wallet - case .rewards, .gift: .settings + case .rewards: .settings } } } diff --git a/ios/Gem/Navigation/Settings/SettingsNavigationStack.swift b/ios/Gem/Navigation/Settings/SettingsNavigationStack.swift index 8cb1afe33a..60ced907c2 100644 --- a/ios/Gem/Navigation/Settings/SettingsNavigationStack.swift +++ b/ios/Gem/Navigation/Settings/SettingsNavigationStack.swift @@ -177,7 +177,6 @@ struct SettingsNavigationStack: View { wallet: wallet, wallets: wallets, activateCode: scene.code, - giftCode: scene.giftCode, ), ) } diff --git a/ios/Packages/FeatureServices/RewardsService/RewardsService.swift b/ios/Packages/FeatureServices/RewardsService/RewardsService.swift index da9c1c8797..8c6c7d7dc2 100644 --- a/ios/Packages/FeatureServices/RewardsService/RewardsService.swift +++ b/ios/Packages/FeatureServices/RewardsService/RewardsService.swift @@ -10,7 +10,6 @@ public protocol RewardsServiceable: Sendable { func createReferral(wallet: Wallet, code: String) async throws -> Rewards func useReferralCode(wallet: Wallet, referralCode: String) async throws func generateReferralLink(code: String) -> URL - func getRedemptionOption(code: String) async throws -> RewardRedemptionOption func redeem(wallet: Wallet, redemptionId: String) async throws -> RedemptionResult } @@ -46,10 +45,6 @@ public struct RewardsService: RewardsServiceable, Sendable { URL(string: "\(Constants.App.website)/join?code=\(code)")! } - public func getRedemptionOption(code: String) async throws -> RewardRedemptionOption { - try await apiService.getRedemptionOption(code: code) - } - public func redeem(wallet: Wallet, redemptionId: String) async throws -> RedemptionResult { let auth = try await authService.getAuthPayload(wallet: wallet) let request = AuthenticatedRequest(auth: auth, data: RedemptionRequest(id: redemptionId)) diff --git a/ios/Packages/FeatureServices/RewardsService/TestKit/RewardsService+TestKit.swift b/ios/Packages/FeatureServices/RewardsService/TestKit/RewardsService+TestKit.swift index b506c29084..c9c34544ec 100644 --- a/ios/Packages/FeatureServices/RewardsService/TestKit/RewardsService+TestKit.swift +++ b/ios/Packages/FeatureServices/RewardsService/TestKit/RewardsService+TestKit.swift @@ -8,20 +8,17 @@ public struct RewardsServiceMock: RewardsServiceable, Sendable { public var rewardsResult: Result public var createReferralResult: Result public var useCodeError: Error? - public var redemptionOptionResult: Result public var redeemResult: Result public init( rewardsResult: Result = .success(.mock()), createReferralResult: Result = .success(.mock()), useCodeError: Error? = nil, - redemptionOptionResult: Result = .success(.mock()), redeemResult: Result = .success(.mock()), ) { self.rewardsResult = rewardsResult self.createReferralResult = createReferralResult self.useCodeError = useCodeError - self.redemptionOptionResult = redemptionOptionResult self.redeemResult = redeemResult } @@ -43,10 +40,6 @@ public struct RewardsServiceMock: RewardsServiceable, Sendable { URL(string: "\(Constants.App.website)/join?code=\(code)")! } - public func getRedemptionOption(code _: String) async throws -> RewardRedemptionOption { - try redemptionOptionResult.get() - } - public func redeem(wallet _: Wallet, redemptionId _: String) async throws -> RedemptionResult { try redeemResult.get() } diff --git a/ios/Packages/GemAPI/Sources/GemAPIService.swift b/ios/Packages/GemAPI/Sources/GemAPIService.swift index c6194f2f88..bad4e3de74 100644 --- a/ios/Packages/GemAPI/Sources/GemAPIService.swift +++ b/ios/Packages/GemAPI/Sources/GemAPIService.swift @@ -102,7 +102,6 @@ public protocol GemAPIRewardsService: Sendable { func getRewards(walletId: WalletId) async throws -> Rewards func createReferral(walletId: WalletId, request: AuthenticatedRequest) async throws -> Rewards func useReferralCode(walletId: WalletId, request: AuthenticatedRequest) async throws - func getRedemptionOption(code: String) async throws -> RewardRedemptionOption func redeem(walletId: WalletId, request: AuthenticatedRequest) async throws -> RedemptionResult } @@ -346,11 +345,6 @@ extension GemAPIService: GemAPIRewardsService { .mapResponse(as: Bool.self) } - public func getRedemptionOption(code: String) async throws -> RewardRedemptionOption { - try await requestDevice(.getDeviceRedemptionOption(code: code)) - .mapResponse(as: RewardRedemptionOption.self) - } - public func redeem(walletId: WalletId, request: AuthenticatedRequest) async throws -> RedemptionResult { try await requestDevice(.redeemDeviceRewards(walletId: walletId, request: request)) .mapResponse(as: RedemptionResult.self) diff --git a/ios/Packages/GemAPI/Sources/GemDeviceAPI.swift b/ios/Packages/GemAPI/Sources/GemDeviceAPI.swift index 038e6f8753..63be2a2480 100644 --- a/ios/Packages/GemAPI/Sources/GemDeviceAPI.swift +++ b/ios/Packages/GemAPI/Sources/GemDeviceAPI.swift @@ -34,7 +34,6 @@ public enum GemDeviceAPI: TargetType { case getDeviceRewards(walletId: WalletId) case getDeviceRewardsEvents(walletId: WalletId) - case getDeviceRedemptionOption(code: String) case createDeviceReferral(walletId: WalletId, request: AuthenticatedRequest) case useDeviceReferralCode(walletId: WalletId, request: AuthenticatedRequest) case redeemDeviceRewards(walletId: WalletId, request: AuthenticatedRequest) @@ -70,7 +69,6 @@ public enum GemDeviceAPI: TargetType { .getDeviceToken, .getDeviceRewards, .getDeviceRewardsEvents, - .getDeviceRedemptionOption, .getNotifications, .isDeviceRegistered, .getFiatAssets, @@ -147,8 +145,6 @@ public enum GemDeviceAPI: TargetType { return "/v2/devices/rewards" case .getDeviceRewardsEvents: return "/v2/devices/rewards/events" - case let .getDeviceRedemptionOption(code): - return "/v2/devices/rewards/redemptions/\(code)" case .createDeviceReferral: return "/v2/devices/rewards/referrals/create" case .useDeviceReferralCode: @@ -209,7 +205,6 @@ public enum GemDeviceAPI: TargetType { .getDeviceToken, .getDeviceRewards, .getDeviceRewardsEvents, - .getDeviceRedemptionOption, .getNotifications, .markNotificationsRead, .getTransactions, diff --git a/ios/Packages/GemstonePrimitives/Sources/Extensions/DeepLink+GemstoneSwift.swift b/ios/Packages/GemstonePrimitives/Sources/Extensions/DeepLink+GemstoneSwift.swift index 459d55ca32..8c261f3671 100644 --- a/ios/Packages/GemstonePrimitives/Sources/Extensions/DeepLink+GemstoneSwift.swift +++ b/ios/Packages/GemstonePrimitives/Sources/Extensions/DeepLink+GemstoneSwift.swift @@ -18,7 +18,16 @@ public extension Primitives.DeepLink { case let .asset(assetId): .asset(assetId: assetId.identifier) case .perpetuals: .perpetuals case let .rewards(code): .rewards(code: code) - case let .gift(code): .gift(code: code) + } + } +} + +public extension Gemstone.Deeplink { + func map() throws -> Primitives.DeepLink { + switch self { + case let .asset(assetId): try .asset(AssetId(id: assetId)) + case .perpetuals: .perpetuals + case let .rewards(code): .rewards(code: code) } } } diff --git a/ios/Packages/GemstonePrimitives/Sources/Extensions/UrlAction+GemstonePrimitives.swift b/ios/Packages/GemstonePrimitives/Sources/Extensions/UrlAction+GemstonePrimitives.swift new file mode 100644 index 0000000000..66d181418c --- /dev/null +++ b/ios/Packages/GemstonePrimitives/Sources/Extensions/UrlAction+GemstonePrimitives.swift @@ -0,0 +1,13 @@ +// Copyright (c). Gem Wallet. All rights reserved. + +import Gemstone +import Primitives + +public extension Gemstone.UrlAction { + func map() throws -> Primitives.URLAction { + switch self { + case let .deeplink(deeplink): try .deeplink(deeplink.map()) + case let .walletConnect(link): .walletConnect(link.map()) + } + } +} diff --git a/ios/Packages/GemstonePrimitives/Sources/URLParser.swift b/ios/Packages/GemstonePrimitives/Sources/URLParser.swift index 95b1383511..8fe4e73261 100644 --- a/ios/Packages/GemstonePrimitives/Sources/URLParser.swift +++ b/ios/Packages/GemstonePrimitives/Sources/URLParser.swift @@ -1,10 +1,8 @@ // Copyright (c). Gem Wallet. All rights reserved. import Foundation -import enum Gemstone.Deeplink -import func Gemstone.deeplinkDecodeUrl -import func Gemstone.walletConnectDecodeUrl -import enum Gemstone.WalletConnectLink +import enum Gemstone.UrlAction +import func Gemstone.urlAction import Primitives enum URLParserError: Error { @@ -13,17 +11,9 @@ enum URLParserError: Error { public enum URLParser { public static func from(url: URL) throws -> URLAction { - if let walletConnectLink = walletConnectDecodeUrl(url: url.absoluteString) { - return .walletConnect(walletConnectLink.map()) + guard let action = urlAction(url: url.absoluteString) else { + throw URLParserError.invalidURL(url) } - - let deeplink: DeepLink = switch deeplinkDecodeUrl(url: url.absoluteString) { - case let .asset(assetId): try .asset(AssetId(id: assetId)) - case let .rewards(code): .rewards(code: code) - case let .gift(code): .gift(code: code) - case .perpetuals: .perpetuals - case .none: throw URLParserError.invalidURL(url) - } - return .deeplink(deeplink) + return try action.map() } } diff --git a/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkMock.swift b/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkMock.swift deleted file mode 100644 index 28c9648751..0000000000 --- a/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkMock.swift +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c). Gem Wallet. All rights reserved. - -enum DeepLinkMock { - static let assetBitcoin = "https://gemwallet.com/tokens/bitcoin" - static let assetBitcoinGem = "gem://tokens/bitcoin" - static let assetEthereumToken = "https://gemwallet.com/tokens/ethereum/0xdAC17F958D2ee523a2206206994597C13D831ec7" - static let perpetuals = "https://gemwallet.com/perpetuals" - static let perpetualsGem = "gem://perpetuals" - static let rewards = "https://gemwallet.com/rewards?code=gemcoder" - static let gift = "https://gemwallet.com/gift?code=giftcode123" - static let giftGem = "gem://gift" - static let walletConnectConnect = "gem://wc?uri=wc:topic@2" - static let walletConnectRequest = "gem://wc?requestId=1" - static let walletConnectSession = "gem://wc?sessionTopic=abc123" - static let noPath = "gem://" - static let badPath = "gem://invalidpath" - static let badHost = "https://example.com/tokens/bitcoin" -} diff --git a/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkTests.swift b/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkTests.swift deleted file mode 100644 index 51884a1ccb..0000000000 --- a/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/DeepLinkTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c). Gem Wallet. All rights reserved. - -import GemstonePrimitives -import Primitives -import PrimitivesTestKit -import Testing - -struct DeepLinkTests { - @Test - func url() { - #expect(DeepLink.asset(.mock()).url.absoluteString == DeepLinkMock.assetBitcoin) - #expect(DeepLink.asset(.mockEthereumUSDT()).url.absoluteString == DeepLinkMock.assetEthereumToken) - #expect(DeepLink.perpetuals.url.absoluteString == DeepLinkMock.perpetuals) - #expect(DeepLink.rewards(code: "gemcoder").url.absoluteString == DeepLinkMock.rewards) - #expect(DeepLink.gift(code: "giftcode123").url.absoluteString == DeepLinkMock.gift) - } - - @Test - func gemUrl() { - #expect(DeepLink.perpetuals.gemUrl.absoluteString == DeepLinkMock.perpetualsGem) - #expect(DeepLink.asset(.mock()).gemUrl.absoluteString == DeepLinkMock.assetBitcoinGem) - } -} diff --git a/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/URLParserTests.swift b/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/URLParserTests.swift deleted file mode 100644 index e546b37579..0000000000 --- a/ios/Packages/GemstonePrimitives/Tests/GemstonePrimitivesTests/URLParserTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c). Gem Wallet. All rights reserved. - -@testable import GemstonePrimitives -import Primitives -import PrimitivesTestKit -import Testing - -struct URLParserTests { - @Test - func deeplink() throws { - #expect(try URLParser.from(url: #require(DeepLinkMock.assetBitcoin.asURL)) == .deeplink(.asset(.mock()))) - #expect(try URLParser.from(url: #require(DeepLinkMock.assetEthereumToken.asURL)) == .deeplink(.asset(.mockEthereumUSDT()))) - #expect(try URLParser.from(url: #require(DeepLinkMock.perpetualsGem.asURL)) == .deeplink(.perpetuals)) - #expect(try URLParser.from(url: #require(DeepLinkMock.rewards.asURL)) == .deeplink(.rewards(code: "gemcoder"))) - #expect(try URLParser.from(url: #require(DeepLinkMock.giftGem.asURL)) == .deeplink(.gift(code: nil))) - } - - @Test - func walletConnect() throws { - #expect(try URLParser.from(url: #require(DeepLinkMock.walletConnectConnect.asURL)) == .walletConnect(.connect(uri: "wc:topic@2"))) - #expect(try URLParser.from(url: #require(DeepLinkMock.walletConnectRequest.asURL)) == .walletConnect(.request)) - #expect(try URLParser.from(url: #require(DeepLinkMock.walletConnectSession.asURL)) == .walletConnect(.session("abc123"))) - } - - @Test - func invalidURL() throws { - #expect(throws: URLParserError.self) { try URLParser.from(url: #require(DeepLinkMock.noPath.asURL)) } - #expect(throws: URLParserError.self) { try URLParser.from(url: #require(DeepLinkMock.badPath.asURL)) } - #expect(throws: URLParserError.self) { try URLParser.from(url: #require(DeepLinkMock.badHost.asURL)) } - } -} diff --git a/ios/Packages/Primitives/Sources/DeepLink.swift b/ios/Packages/Primitives/Sources/DeepLink.swift index 96c0243195..bf9fe075cc 100644 --- a/ios/Packages/Primitives/Sources/DeepLink.swift +++ b/ios/Packages/Primitives/Sources/DeepLink.swift @@ -6,5 +6,4 @@ public enum DeepLink: Equatable, Sendable { case asset(AssetId) case perpetuals case rewards(code: String?) - case gift(code: String?) } diff --git a/ios/Packages/Primitives/Sources/Scenes.swift b/ios/Packages/Primitives/Sources/Scenes.swift index 199ae8359a..32174a9208 100644 --- a/ios/Packages/Primitives/Sources/Scenes.swift +++ b/ios/Packages/Primitives/Sources/Scenes.swift @@ -165,11 +165,9 @@ public enum Scenes { public struct Referral: Hashable, Codable { public let code: String? - public let giftCode: String? - public init(code: String? = nil, giftCode: String? = nil) { + public init(code: String? = nil) { self.code = code - self.giftCode = giftCode } } From b0253a63c3ed8f607491e9975171defa425367e9 Mon Sep 17 00:00:00 2001 From: Radmir Date: Fri, 22 May 2026 10:31:49 +0500 Subject: [PATCH 5/5] update core to main --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 7734958dfe..33b912b183 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 7734958dfee326f3c981a97f67c28670fd594ded +Subproject commit 33b912b1830b1095f1e25c4d9a650f3708b2b93a