Skip to content

Commit

Permalink
KTX Sample App UX Simplification (#571)
Browse files Browse the repository at this point in the history
* Remove button, enable memo publish button pre-connection

* Remove disconnect method as we are not using anymore

* Move to unified connect-if-needed operation

* Consume new api endpoint to make the rpc calls wrok

* Finish up new UX flow

* Fix local properties file issue
  • Loading branch information
creativedrewy authored Oct 16, 2023
1 parent 1a327af commit 4470e94
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 142 deletions.
10 changes: 10 additions & 0 deletions examples/example-clientlib-ktx-app/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ android {
vectorDrawables {
useSupportLibrary true
}

Properties properties = new Properties()
def propertiesFile = project.rootProject.file('local.properties')
if (propertiesFile.exists()) {
properties.load(propertiesFile.newDataInputStream())

buildConfigField "String", "HELIUS_KEY", "\"${properties.getProperty("HELIUS_KEY")}\""
} else {
buildConfigField "String", "HELIUS_KEY", "\"\""
}
}

buildTypes {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ fun SampleScreen(

Text(
modifier = Modifier.weight(1f),
text = if (viewState.canTransact && viewState.solBalance >= 0) viewState.solBalance.toString() else "-",
text = if (viewState.solBalance >= 0) viewState.solBalance.toString() else "-",
style = MaterialTheme.typography.h5,
maxLines = 1,
overflow = TextOverflow.Ellipsis
Expand All @@ -151,30 +151,6 @@ fun SampleScreen(
)
}
}

val buttonText = when {
viewState.canTransact && viewState.walletFound -> "Disconnect"
!viewState.walletFound -> "Please install a compatible wallet"
else -> "Add funds to get started"
}

Button(
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp),
enabled = viewState.canTransact,
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Red.copy(red = 0.7f)
),
onClick = {
viewModel.disconnect()
}
) {
Text(
color = MaterialTheme.colors.onPrimary,
text = buttonText
)
}
}
}

Expand Down Expand Up @@ -231,9 +207,10 @@ fun SampleScreen(
modifier = Modifier
.weight(1f)
.padding(end = 8.dp),
enabled = viewState.canTransact && memoText.isNotEmpty(),
onClick = {
viewModel.publishMemo(intentSender, memoText)
if (memoText.isNotEmpty()) {
viewModel.publishMemo(intentSender, memoText)
}
}
) {
Text("Publish Memo")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.solanamobile.ktxclientsample.usecase

import android.content.SharedPreferences
import com.solana.core.PublicKey
import java.lang.IllegalArgumentException
import javax.inject.Inject

sealed class WalletConnection
Expand All @@ -18,6 +19,10 @@ class PersistanceUseCase @Inject constructor(
private val sharedPreferences: SharedPreferences
) {

val connected: Connected
get() = getWalletConnection() as? Connected
?: throw IllegalArgumentException("Only use this property when you are sure you have a valid connection.")

private var connection: WalletConnection = NotConnected

fun getWalletConnection(): WalletConnection {
Expand Down Expand Up @@ -49,16 +54,6 @@ class PersistanceUseCase @Inject constructor(
connection = Connected(pubKey, accountLabel, token)
}

fun clearConnection() {
sharedPreferences.edit().apply {
putString(PUBKEY_KEY, "")
putString(ACCOUNT_LABEL, "")
putString(AUTH_TOKEN_KEY, "")
}.apply()

connection = NotConnected
}

companion object {
const val PUBKEY_KEY = "stored_pubkey"
const val ACCOUNT_LABEL = "stored_account_label"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,25 @@ import com.solana.core.PublicKey
import com.solana.models.SignatureStatusRequestConfiguration
import com.solana.networking.Commitment
import com.solana.networking.HttpNetworkingRouter
import com.solana.networking.Network
import com.solana.networking.RPCEndpoint
import com.solanamobile.ktxclientsample.BuildConfig
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import java.net.URL
import javax.inject.Inject

class SolanaRpcUseCase @Inject constructor() {

private val api: Api

init {
val endPoint = RPCEndpoint.devnetSolana
val url = URL("https://devnet.helius-rpc.com/?api-key=${BuildConfig.HELIUS_KEY}")

val endPoint = RPCEndpoint.custom(url, url, Network.devnet)
val network = HttpNetworkingRouter(endPoint)

api = Solana(network).api
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import javax.inject.Inject

data class SampleViewState(
val isLoading: Boolean = false,
val canTransact: Boolean = false,
val solBalance: Double = 0.0,
val userAddress: String = "",
val userLabel: String = "",
Expand Down Expand Up @@ -51,19 +50,11 @@ class SampleViewModel @Inject constructor(
val persistedConn = persistanceUseCase.getWalletConnection()

if (persistedConn is Connected) {
_state.value.copy(
isLoading = true,
canTransact = true,
userAddress = persistedConn.publicKey.toBase58(),
userLabel = persistedConn.accountLabel,
).updateViewState()

viewModelScope.launch {
val balance = solanaRpcUseCase.getBalance(persistedConn.publicKey)

_state.value.copy(
isLoading = false,
solBalance = balance
userAddress = persistedConn.publicKey.toBase58(),
userLabel = persistedConn.accountLabel,
solBalance = solanaRpcUseCase.getBalance(persistedConn.publicKey)
).updateViewState()
}

Expand All @@ -73,128 +64,132 @@ class SampleViewModel @Inject constructor(

fun addFunds(sender: ActivityResultSender) {
viewModelScope.launch {
val conn = persistanceUseCase.getWalletConnection()
if (connectIfNeeded(sender)) {
val conn = persistanceUseCase.connected

if (conn is Connected) {
requestAirdrop(conn.publicKey)
} else {
when (val result = walletAdapter.connect(sender)) {
is TransactionResult.Success -> {
val currentConn = Connected(
PublicKey(result.authResult.publicKey),
result.authResult.accountLabel ?: "",
result.authResult.authToken
)

val balance = solanaRpcUseCase.getBalance(currentConn.publicKey)

persistanceUseCase.persistConnection(currentConn.publicKey, currentConn.accountLabel, currentConn.authToken)

_state.value.copy(
isLoading = true,
solBalance = balance,
userAddress = currentConn.publicKey.toBase58(),
userLabel = currentConn.accountLabel
).updateViewState()

requestAirdrop(currentConn.publicKey)
}

is TransactionResult.NoWalletFound -> {
_state.value.copy(
walletFound = false
).updateViewState()

}

is TransactionResult.Failure -> {
_state.value.copy(
isLoading = false,
canTransact = false,
userAddress = "",
userLabel = "",
).updateViewState()
}
}
}
}
}

private suspend fun requestAirdrop(publicKey: PublicKey) {
try {
val tx = solanaRpcUseCase.requestAirdrop(publicKey)
val confirmed = solanaRpcUseCase.awaitConfirmationAsync(tx).await()
fun publishMemo(sender: ActivityResultSender, memoText: String) {
viewModelScope.launch {
if (connectIfNeeded(sender)) {
val conn = persistanceUseCase.connected

if (confirmed) {
_state.value.copy(
isLoading = false,
solBalance = solanaRpcUseCase.getBalance(publicKey)
).updateViewState()
} else {
_state.value.copy(
isLoading = false,
isLoading = true
).updateViewState()

viewModelScope.launch {
val blockHash = solanaRpcUseCase.getLatestBlockHash()

val tx = Transaction()
tx.add(MemoProgram.writeUtf8(conn.publicKey, memoText))
tx.setRecentBlockHash(blockHash!!)
tx.feePayer = conn.publicKey

val bytes = tx.serialize(SerializeConfig(requireAllSignatures = false))
val result = walletAdapter.transact(sender) {
signAndSendTransactions(arrayOf(bytes))
}

when (result) {
is TransactionResult.Success -> {
val updatedAuth = result.authResult
//TODO: At some point in the future add a method to just persist
//just the auth token value as that is all we need in this case
persistanceUseCase.persistConnection(
PublicKey(updatedAuth.publicKey),
updatedAuth.accountLabel ?: "",
updatedAuth.authToken
)

val sig = result.payload.signatures.firstOrNull()
val readableSig = Base58.encode(sig)

_state.value.copy(
isLoading = false,
memoTx = readableSig
).updateViewState()

//Clear out the recent transaction
delay(5000)
_state.value.copy(memoTx = "").updateViewState()
}
else -> {
_state.value.copy(
isLoading = false,
).updateViewState()
}
}
}
}
} catch (e: Throwable) {
_state.value.copy(
isLoading = false,
userAddress = "Error airdropping",
userLabel = "",
).updateViewState()
}
}

fun publishMemo(sender: ActivityResultSender, memoText: String) {
private suspend fun connectIfNeeded(sender: ActivityResultSender): Boolean {
val conn = persistanceUseCase.getWalletConnection()

if (conn is Connected) {
_state.value.copy(
isLoading = true
).updateViewState()
return if (conn is Connected) {
true
} else {
when (val result = walletAdapter.connect(sender)) {
is TransactionResult.Success -> {
val currentConn = Connected(
PublicKey(result.authResult.publicKey),
result.authResult.accountLabel ?: "",
result.authResult.authToken
)

viewModelScope.launch {
val blockHash = solanaRpcUseCase.getLatestBlockHash()
persistanceUseCase.persistConnection(currentConn.publicKey, currentConn.accountLabel, currentConn.authToken)

val tx = Transaction()
tx.add(MemoProgram.writeUtf8(conn.publicKey, memoText))
tx.setRecentBlockHash(blockHash!!)
tx.feePayer = conn.publicKey
_state.value.copy(
userAddress = currentConn.publicKey.toBase58(),
userLabel = currentConn.accountLabel,
solBalance = solanaRpcUseCase.getBalance(currentConn.publicKey)
).updateViewState()

val bytes = tx.serialize(SerializeConfig(requireAllSignatures = false))
val result = walletAdapter.transact(sender) {
signAndSendTransactions(arrayOf(bytes))
true
}
is TransactionResult.NoWalletFound -> {
_state.value.copy(
walletFound = false
).updateViewState()

(result as? TransactionResult.Success)?.let { txResult ->
val updatedAuth = txResult.authResult
//TODO: At some point in the future add a method to just persist
//just the auth token value as that is all we need in this case
persistanceUseCase.persistConnection(PublicKey(updatedAuth.publicKey), updatedAuth.accountLabel ?: "", updatedAuth.authToken)

val sig = txResult.payload.signatures.firstOrNull()
val readableSig = Base58.encode(sig)

false
}
is TransactionResult.Failure -> {
_state.value.copy(
isLoading = false,
memoTx = readableSig
userAddress = "",
userLabel = "",
).updateViewState()

//Clear out the recent transaction
delay(5000)
_state.value.copy(memoTx = "").updateViewState()
false
}
}
}

}

fun disconnect() {
viewModelScope.launch {
val conn = persistanceUseCase.getWalletConnection()
if (conn is Connected) {
persistanceUseCase.clearConnection()
private suspend fun requestAirdrop(publicKey: PublicKey) {
try {
val tx = solanaRpcUseCase.requestAirdrop(publicKey)
val confirmed = solanaRpcUseCase.awaitConfirmationAsync(tx).await()

SampleViewState().updateViewState()
if (confirmed) {
_state.value.copy(
isLoading = false,
solBalance = solanaRpcUseCase.getBalance(publicKey)
).updateViewState()
}
} catch (e: Throwable) {
_state.value.copy(
userAddress = "Error airdropping",
userLabel = "",
).updateViewState()
}

_state.value.copy(isLoading = false).updateViewState()
}
}

0 comments on commit 4470e94

Please sign in to comment.