Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
263620b
core: move ApprovalData out of the swap module
gemdev111 Aug 3, 2026
b99f361
core: split WalletConnect request handling behind a feature
gemdev111 Aug 3, 2026
84d8ca9
core: give the signing payload types a neutral home
gemdev111 Aug 3, 2026
6a30254
core: add the shared payment models
gemdev111 Aug 3, 2026
dbd3c28
core: add the payment crate with the WalletConnect Pay provider
gemdev111 Aug 3, 2026
4874c4c
core: expose payments through gemstone
gemdev111 Aug 3, 2026
b1256b1
iOS: add the signing request kernel
gemdev111 Aug 3, 2026
b8bc8dd
iOS: add PaymentService
gemdev111 Aug 3, 2026
ee76c4f
iOS: extract TransferService out of the Transfer feature
gemdev111 Aug 3, 2026
3141a14
iOS: pay payment links
gemdev111 Aug 3, 2026
b5612b0
iOS: show a payment through its own sheets
gemdev111 Aug 3, 2026
362dc0f
iOS: wire payments into the app
gemdev111 Aug 3, 2026
e88fbe6
Android: consume the payment models
gemdev111 Aug 3, 2026
67fbe40
Localize the payment link strings
gemdev111 Aug 3, 2026
dfc82d3
iOS: decode and scan payment links
gemdev111 Aug 3, 2026
bade0b3
iOS: run the payment tests in the framework plan
gemdev111 Aug 3, 2026
a8978ba
iOS: move the signing sheets out of WalletConnector
gemdev111 Aug 3, 2026
073fefd
iOS: consolidate the sheet presenters and their imports
gemdev111 Aug 4, 2026
2607188
iOS: drop the unused Localization dependency from ChainServices
gemdev111 Aug 4, 2026
cc17a00
iOS: remove the duplicate NetworkAssets navigation destination
gemdev111 Aug 4, 2026
78e5158
Android: extract the signable transaction mapper
gemdev111 Aug 4, 2026
06c2d1d
Android: share the sign message review components
gemdev111 Aug 4, 2026
abff806
Android: decode payment links picked from the gallery
gemdev111 Aug 4, 2026
c5c42c0
Android: pay WalletConnect Pay links
gemdev111 Aug 4, 2026
13c6ceb
Android: reconcile payments with their chain transaction
gemdev111 Aug 4, 2026
341c441
Android: name the merchant on payment activity
gemdev111 Aug 4, 2026
8c01705
Android: log why a confirm preload failed
gemdev111 Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ dependencies {
implementation(project(":features:wallet-details:presents"))
implementation(project(":features:bridge:presents"))
implementation(project(":features:bridge:viewmodels"))
implementation(project(":features:payment:presents"))
implementation(project(":features:payment:viewmodels"))
implementation(project(":features:assets:presents"))
implementation(project(":features:assets:viewmodels"))
implementation(project(":features:perpetual:presents"))
Expand Down
8 changes: 8 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@
<data android:scheme="https" android:host="gemwallet.com" android:pathPrefix="/join/" />
<data android:scheme="https" android:host="gemwallet.com" android:pathPrefix="/tokens/" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="https" android:host="pay.walletconnect.com" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import com.gemwallet.android.ui.navigation.routes.PaymentRoute
import uniffi.gemstone.UrlAction
import uniffi.gemstone.WalletConnectLink
import uniffi.gemstone.urlAction
Expand Down Expand Up @@ -56,6 +57,10 @@ class PendingNavigationCoordinator @Inject constructor(
return
}
}
is UrlAction.Payment -> {
replace(pendingIntent, PendingNavigation.Route(PaymentRoute(action.link.provider.name, action.link.id)))
return
}
null -> Unit
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import uniffi.gemstone.GemPreferences
import uniffi.gemstone.GemServiceStatus
import uniffi.gemstone.serviceStatusTimeoutSeconds
import uniffi.gemstone.WalletConnectSimulationClient
import com.gemwallet.android.application.device.coordinators.GetDeviceId
import com.gemwallet.android.data.service.walletconnect.reown.ReownWalletConnectClient
import kotlinx.coroutines.runBlocking
import uniffi.gemstone.GemPaymentService
import uniffi.gemstone.WalletConnectSimulationClientInterface
import javax.inject.Singleton

Expand Down Expand Up @@ -105,6 +109,17 @@ object GatewayModule {
return ServiceStatusService(GemServiceStatus(provider))
}

@Provides
@Singleton
fun provideGemPaymentService(
alienProvider: AlienProvider,
getDeviceId: GetDeviceId,
): GemPaymentService = GemPaymentService(
provider = alienProvider,
appId = ReownWalletConnectClient.PROJECT_ID,
clientId = runBlocking { getDeviceId.getDeviceId() },
)

@Provides
@Singleton
fun provideWalletConnectSimulationService(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ import com.gemwallet.android.ui.navigation.routes.settingsRoute
import com.gemwallet.android.ui.navigation.routes.transactionsRoute
import com.gemwallet.android.ui.theme.alpha10
import kotlinx.coroutines.launch
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.gemwallet.android.ui.components.QrCodeScannerModal
import uniffi.gemstone.UrlAction
import uniffi.gemstone.urlAction

@Composable
fun MainScreen(
Expand All @@ -66,6 +71,18 @@ fun MainScreen(
val pendingCount by viewModel.pendingTxCount.collectAsStateWithLifecycle()
val assetsViewModel: AssetsViewModel = hiltViewModel()
val isRootRouteActive = navigator.backStack.lastOrNull() == WalletRootRoute
var isPresentingScanner by remember { mutableStateOf(false) }

QrCodeScannerModal(
isVisible = isPresentingScanner,
onDismissRequest = { isPresentingScanner = false },
onResult = { scanned ->
isPresentingScanner = false
(runCatching { urlAction(scanned) }.getOrNull() as? UrlAction.Payment)?.let {
navigator.openPayment(it.link)
}
},
)

BackHandler(isRootRouteActive && currentTab.value != assetsRoute) {
currentTab.value = assetsRoute
Expand Down Expand Up @@ -185,6 +202,7 @@ fun MainScreen(
AssetsAction.ShowWallets -> navigator.openWallets()
AssetsAction.Manage -> navigator.openAssetsManage()
AssetsAction.Search -> navigator.openAssetsSearch()
AssetsAction.Scan -> isPresentingScanner = true
AssetsAction.Send -> navigator.openRecipient()
AssetsAction.Receive -> navigator.openReceive()
AssetsAction.Buy -> navigator.openBuy()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import com.gemwallet.android.ui.navigation.routes.BridgeConnectionDetailsRoute
import com.gemwallet.android.ui.navigation.routes.BridgeConnectionsRoute
import com.gemwallet.android.ui.navigation.routes.AddContactRoute
import com.gemwallet.android.ui.navigation.routes.ConfirmRoute
import com.gemwallet.android.ui.navigation.routes.PaymentRoute
import uniffi.gemstone.GemPaymentLink
import com.gemwallet.android.ui.navigation.routes.ContactsRoute
import com.gemwallet.android.ui.navigation.routes.CurrenciesRoute
import com.gemwallet.android.ui.navigation.routes.EditContactRoute
Expand Down Expand Up @@ -279,6 +281,7 @@ class WalletNavigator(
val pack = params.pack() ?: return
push(ConfirmRoute(pack))
}
fun openPayment(link: GemPaymentLink) = push(PaymentRoute(link.provider.name, link.id))
fun openNftList() = push(NftListRoute)
fun openNftCollection(nftCollectionId: String) = push(NftCollectionRoute(nftCollectionId))
fun openNftUnverifiedCollections() = push(NftUnverifiedCollectionsRoute)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import com.gemwallet.android.ui.navigation.routes.assetScreen
import com.gemwallet.android.ui.navigation.routes.networkAssetsScreen
import com.gemwallet.android.ui.navigation.routes.bridgesScreen
import com.gemwallet.android.ui.navigation.routes.confirm
import com.gemwallet.android.ui.navigation.routes.payment
import com.gemwallet.android.ui.navigation.routes.fiatScreen
import com.gemwallet.android.ui.navigation.routes.nftCollection
import com.gemwallet.android.ui.navigation.routes.perpetualScreen
Expand Down Expand Up @@ -172,6 +173,8 @@ fun WalletNavGraph(
cancelAction = onCancel,
)

payment(cancelAction = onCancel)

nftCollection(
cancelAction = onCancel,
collectionIdAction = navigator::openNftCollection,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.gemwallet.android.ui.navigation.routes

import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import com.gemwallet.android.features.payment.presents.PaymentScene
import com.gemwallet.android.ui.models.actions.CancelAction
import kotlinx.serialization.Serializable
import uniffi.gemstone.GemPaymentProviderName

@Serializable
data class PaymentRoute(val provider: String, val paymentId: String) : NavKey

fun EntryProviderScope<NavKey>.payment(cancelAction: CancelAction) {
entry<PaymentRoute> { key ->
PaymentScene(
provider = GemPaymentProviderName.valueOf(key.provider),
paymentId = key.paymentId,
onCancel = { cancelAction() },
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import com.gemwallet.android.blockchain.gemstone.toPrimitives
import com.wallet.core.primitives.SimulationResult
import uniffi.gemstone.SignDigestType
import uniffi.gemstone.WalletConnectSimulationClientInterface
import uniffi.gemstone.WalletConnectTransactionType
import uniffi.gemstone.SignableTransactionType

class WalletConnectSimulationService(
private val client: WalletConnectSimulationClientInterface,
) {
suspend fun simulateSignMessage(chain: String, signType: SignDigestType, data: String, sessionDomain: String): SimulationResult =
client.simulateSignMessage(chain = chain, signType = signType, data = data, sessionDomain = sessionDomain).toPrimitives()

suspend fun simulateSendTransaction(chain: String, transactionType: WalletConnectTransactionType, data: String): SimulationResult =
suspend fun simulateSendTransaction(chain: String, transactionType: SignableTransactionType, data: String): SimulationResult =
client.simulateSendTransaction(chain = chain, transactionType = transactionType, data = data).toPrimitives()
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import uniffi.gemstone.SwapperProviderMode
import uniffi.gemstone.SwapperProviderType
import uniffi.gemstone.swapperProviderConfig
import uniffi.gemstone.swapperProviderFromStr
import com.gemwallet.android.ext.getPaymentMetadata

@OptIn(ExperimentalCoroutinesApi::class)
class GetTransactionDetailsImpl(
Expand Down Expand Up @@ -259,7 +260,9 @@ class TransactionDetailsAggregateImpl(
TransactionType.Transfer,
TransactionType.TransferNFT -> when (data.transaction.direction) {
TransactionDirection.SelfTransfer,
TransactionDirection.Outgoing -> TransactionDetailsValue.Destination.Recipient(
TransactionDirection.Outgoing -> data.transaction.getPaymentMetadata()?.merchant?.name?.let {
TransactionDetailsValue.Destination.Merchant(it)
} ?: TransactionDetailsValue.Destination.Recipient(
data = data.transaction.to,
chain = data.asset.chain,
name = data.toAddress?.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import java.math.BigInteger
import com.gemwallet.android.ext.getPaymentMetadata

class GetTransactionsImpl(
private val transactionsRepository: TransactionRepository,
Expand Down Expand Up @@ -66,7 +67,7 @@ class TransactionDataAggregateImpl(

override val asset: Asset = data.asset

override val addressName: String? = when (data.transaction.type) {
override val addressName: String? = data.transaction.getPaymentMetadata()?.merchant?.name ?: when (data.transaction.type) {
TransactionType.StakeDelegate,
TransactionType.StakeUndelegate,
TransactionType.StakeRedelegate,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.gemwallet.android.data.repositories.bridge

import com.wallet.core.primitives.WalletConnectionSessionAppMetadata
import com.wallet.core.primitives.WalletConnectAppMetadata
import kotlinx.coroutines.flow.Flow

interface WalletConnectClient {
Expand Down Expand Up @@ -54,7 +54,7 @@ sealed interface WalletConnectEvent {
data class WalletConnectSession(
val topic: String,
val expiry: Long,
val metadata: WalletConnectionSessionAppMetadata?,
val metadata: WalletConnectAppMetadata?,
val namespaces: Map<String, WalletConnectSessionNamespace>,
val redirect: String?,
)
Expand Down Expand Up @@ -95,7 +95,7 @@ data class WalletConnectSessionRequest(

data class WalletConnectAuthenticationRequest(
val id: Long,
val metadata: WalletConnectionSessionAppMetadata?,
val metadata: WalletConnectAppMetadata?,
val payloadParams: WalletConnectAuthPayloadParams,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.gemwallet.android.cases.transactions.SaveTransactions
import com.gemwallet.android.data.repositories.session.SessionRepository
import com.gemwallet.android.data.repositories.transactions.TransactionRepository
import com.gemwallet.android.data.repositories.transactions.TransactionsRepositoryImpl
import uniffi.gemstone.GemPaymentService
import com.gemwallet.android.data.service.store.database.TransactionsDao
import dagger.Module
import dagger.Provides
Expand All @@ -27,12 +28,14 @@ object TransactionsModule {
sessionRepository: SessionRepository,
transactionsDao: TransactionsDao,
gateway: GemGateway,
paymentService: GemPaymentService,
): TransactionsRepositoryImpl = TransactionsRepositoryImpl(
sessionRepository = sessionRepository,
transactionsDao = transactionsDao,
transactionStatusService = TransactionStatusService(
gateway = gateway,
),
paymentService = paymentService,
)

@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import com.gemwallet.android.data.service.store.database.entities.DbTransactionE
import com.gemwallet.android.data.service.store.database.entities.DbTxSwapMetadata
import com.gemwallet.android.data.service.store.database.entities.toDTO
import com.gemwallet.android.data.service.store.database.entities.toRecord
import com.gemwallet.android.ext.getTransactionPaymentMetadata
import com.gemwallet.android.ext.toGem
import com.wallet.core.primitives.TransactionPaymentMetadata
import uniffi.gemstone.GemPaymentService
import uniffi.gemstone.GemPaymentStatus
import com.gemwallet.android.ext.getTransactionSwapMetadata
import com.gemwallet.android.ext.isCompleted
import com.gemwallet.android.ext.toIdentifier
Expand Down Expand Up @@ -44,6 +49,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
Expand All @@ -63,6 +69,7 @@ class TransactionsRepositoryImpl(
private val sessionRepository: SessionRepository,
private val transactionsDao: TransactionsDao,
private val transactionStatusService: TransactionStatusService,
private val paymentService: GemPaymentService,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
) : TransactionRepository,
GetChangedTransactions,
Expand Down Expand Up @@ -258,9 +265,59 @@ class TransactionsRepositoryImpl(
return ((sourceTimeout + destinationTimeout) * 3).coerceAtLeast(DateUtils.DAY_IN_MILLIS)
}

private suspend fun checkPayment(
transaction: DbTransactionExtended,
metadata: TransactionPaymentMetadata,
): DbTransactionExtended? {
val outcome = try {
paymentService.getPaymentStatus(metadata.provider.toGem(), metadata.paymentId)
} catch (_: Throwable) {
return transaction.copy(transaction = transaction.transaction.copy(updatedAt = System.currentTimeMillis()))
}
val state = when (outcome.status) {
GemPaymentStatus.SUCCEEDED -> TransactionState.Confirmed
GemPaymentStatus.FAILED, GemPaymentStatus.EXPIRED, GemPaymentStatus.CANCELLED -> TransactionState.Failed
GemPaymentStatus.PROCESSING, GemPaymentStatus.REQUIRES_ACTION -> return transaction.copy(
transaction = transaction.transaction.copy(updatedAt = System.currentTimeMillis()),
)
}
val isAwaitingPaymentHash = transaction.transaction.hash == metadata.paymentId
if (state == TransactionState.Confirmed && isAwaitingPaymentHash) {
val settledHash = outcome.transactionId
?: return transaction.copy(transaction = transaction.transaction.copy(updatedAt = System.currentTimeMillis()))
val walletId = transaction.transaction.walletId
val settledId = TransactionId(transaction.transaction.assetId.chain, settledHash)
if (transactionsDao.getTransactionState(settledId, walletId) != null) {
transactionsDao.delete(transaction.transaction.id, walletId)
return transactionsDao.getExtendedTransaction(walletId, settledId).firstOrNull()
}
transactionsDao.updateTransactionId(
oldId = transaction.transaction.id,
newId = settledId,
walletId = walletId,
hash = settledHash,
)
return transaction.copy(
transaction = transaction.transaction.copy(
id = settledId,
hash = settledHash,
state = state,
updatedAt = System.currentTimeMillis(),
),
)
}
return transaction.copy(
transaction = transaction.transaction.copy(state = state, updatedAt = System.currentTimeMillis()),
)
}

private suspend fun checkTransaction(transaction: DbTransactionExtended): DbTransactionExtended? {
val transactionRecord = transaction.transaction
val chain = transactionRecord.assetId.chain
val paymentMetadata = getTransactionPaymentMetadata(transactionRecord.type, transactionRecord.metadata)
if (paymentMetadata != null) {
return checkPayment(transaction, paymentMetadata)
}
val swapMetadata = getTransactionSwapMetadata(transactionRecord.type, transactionRecord.metadata)
val swapProvider = swapMetadata?.provider?.toSwapProvider()
if (transactionRecord.type == TransactionType.Swap && transactionRecord.state == TransactionState.InTransit && swapProvider == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import androidx.room.PrimaryKey
import com.wallet.core.primitives.Chain
import com.wallet.core.primitives.Wallet
import com.wallet.core.primitives.WalletConnection
import com.wallet.core.primitives.WalletConnectionSession
import com.wallet.core.primitives.WalletConnectionSessionAppMetadata
import com.wallet.core.primitives.WalletConnectSession
import com.wallet.core.primitives.WalletConnectAppMetadata
import com.wallet.core.primitives.WalletConnectionState

@Entity(
Expand Down Expand Up @@ -38,14 +38,14 @@ data class DbConnection(
fun DbConnection.toDTO(wallet: Wallet): WalletConnection {
return WalletConnection(
wallet = wallet,
session = WalletConnectionSession(
session = WalletConnectSession(
id = id,
sessionId = sessionId,
state = state,
createdAt = createdAt,
expireAt = expireAt,
chains = chains,
metadata = WalletConnectionSessionAppMetadata(
metadata = WalletConnectAppMetadata(
name = appName,
description = appDescription,
icon = appIcon,
Expand Down
Loading
Loading