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)) - } -}