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..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,6 +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.urlAction
import javax.inject.Inject
internal sealed interface PendingNavigation {
@@ -37,19 +40,23 @@ class PendingNavigationCoordinator @Inject constructor(
val pendingIntent = (_pendingNavigation.value as? PendingNavigation.RawIntent)?.intent ?: return
val uri = pendingIntent.dataString
- uri?.toWalletConnectLink()?.let { link ->
- when (link) {
- is WalletConnectLink.Pairing -> walletConnect.onPairing(link.uri)
- WalletConnectLink.Request -> walletConnect.onRequest()
- 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/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..0ee4d71408 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,15 @@
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
-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()
+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
}
}
-
-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..c66b28de55 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.UrlAction
+import uniffi.gemstone.WalletConnectLink
+import uniffi.gemstone.urlAction
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 { urlAction(uri) } returns UrlAction.WalletConnect(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 { urlAction(uri) } returns UrlAction.WalletConnect(WalletConnectLink.Request)
+ coordinator.setPendingIntentForTest(intent(uri = uri))
coordinator.resolve(handler)
@@ -48,7 +66,9 @@ 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 { urlAction(uri) } returns UrlAction.Deeplink(Deeplink.Rewards(code = "gemcoder"))
+ coordinator.setPendingIntentForTest(intent(uri = uri))
coordinator.resolve(NoOpWalletConnect)
@@ -58,7 +78,9 @@ class PendingNavigationCoordinatorTest {
@Test
fun resolve_unknownIntentWithoutNotificationPayload_clears() = runTest {
- coordinator.setPendingIntentForTest(intent(uri = "https://example.com/unknown"))
+ val uri = "https://example.com/unknown"
+ every { urlAction(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..94e21f039c 100644
--- a/android/app/src/test/kotlin/com/gemwallet/android/WebDeepLinksTest.kt
+++ b/android/app/src/test/kotlin/com/gemwallet/android/WebDeepLinksTest.kt
@@ -1,6 +1,5 @@
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
@@ -8,42 +7,21 @@ import com.wallet.core.primitives.Chain
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
+import uniffi.gemstone.Deeplink
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())
+ fun toRoute_mapsSupportedDeeplinks() {
+ val tokenId = "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"
+ 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_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(),
- )
- }
-
- @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())
+ fun toRoute_rejectsUnsupportedLinks() {
+ assertNull(Deeplink.Perpetuals.toRoute())
}
}
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 274d1a95d7..33b912b183 160000
--- a/core
+++ b/core
@@ -1 +1 @@
-Subproject commit 274d1a95d72ecf8cfde99049763afab568c25cdc
+Subproject commit 33b912b1830b1095f1e25c4d9a650f3708b2b93a
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/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/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/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 fc7f28368e..fb37f8eca1 100644
--- a/ios/Gem/Navigation/NavigationHandler.swift
+++ b/ios/Gem/Navigation/NavigationHandler.swift
@@ -58,39 +58,24 @@ 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)
- case let .swap(fromId, toId):
- try await presentSwap(from: fromId, to: toId)
- return
-
case .perpetuals:
navigationState.wallet.append(Scenes.Perpetuals())
case let .rewards(code):
navigationState.settings.append(Scenes.Referral(code: code))
-
- 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: action.selectTab)
+ selectTab(for: deeplink.selectTab)
}
}
@@ -194,16 +179,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)
@@ -217,12 +192,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 .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/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/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
new file mode 100644
index 0000000000..8c261f3671
--- /dev/null
+++ b/ios/Packages/GemstonePrimitives/Sources/Extensions/DeepLink+GemstoneSwift.swift
@@ -0,0 +1,33 @@
+// 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 .perpetuals: .perpetuals
+ case let .rewards(code): .rewards(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/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..8fe4e73261
--- /dev/null
+++ b/ios/Packages/GemstonePrimitives/Sources/URLParser.swift
@@ -0,0 +1,19 @@
+// Copyright (c). Gem Wallet. All rights reserved.
+
+import Foundation
+import enum Gemstone.UrlAction
+import func Gemstone.urlAction
+import Primitives
+
+enum URLParserError: Error {
+ case invalidURL(URL)
+}
+
+public enum URLParser {
+ public static func from(url: URL) throws -> URLAction {
+ guard let action = urlAction(url: url.absoluteString) else {
+ throw URLParserError.invalidURL(url)
+ }
+ return try action.map()
+ }
+}
diff --git a/ios/Packages/Primitives/Sources/DeepLink.swift b/ios/Packages/Primitives/Sources/DeepLink.swift
index 929c9033f0..bf9fe075cc 100644
--- a/ios/Packages/Primitives/Sources/DeepLink.swift
+++ b/ios/Packages/Primitives/Sources/DeepLink.swift
@@ -2,84 +2,8 @@
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
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 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/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?
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
}
}
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))
- }
-}