Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.compose' version '2.0.21'
}

android {
Expand All @@ -27,9 +28,10 @@ android {
}

buildFeatures {
viewBinding true
compose true
}


compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
Expand All @@ -41,12 +43,26 @@ android {
}

dependencies {
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.3.0'
// Compose BOM
implementation platform('androidx.compose:compose-bom:2024.10.01')

// Core Android dependencies
implementation 'androidx.core:core-ktx:1.17.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.9.4'
implementation 'androidx.appcompat:appcompat:1.7.1'

// Compose dependencies
implementation 'androidx.activity:activity-compose:1.9.3'
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.material:material-icons-extended'

// Material Components (needed for themes.xml)
implementation 'com.google.android.material:material:1.13.0'

// Other dependencies
implementation 'com.solanamobile:digital-asset-links-android:1.0.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2'
implementation project(path: ':pay')
Expand Down
53 changes: 44 additions & 9 deletions app/src/main/java/com/solana/pay/sample/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,23 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContract
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.solana.pay.SolanaPayAndroidContract
import com.solana.pay.sample.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
private lateinit var viewBinding: ActivityMainBinding
class MainActivity : ComponentActivity() {

private data class SolanaPayResult(
val result: Int,
Expand All @@ -42,12 +50,39 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
setContent {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MainScreen(
onLaunchSolanaPay = {
// NOTE: this is an arbitrary address - please don't perform any real transfers to it!
solanaPayActivityResult.launch(Uri.parse("solana:84npKJKZy8ixjdq8UChZULDUea2Twt8ThxjiqKd7QZ54?amount=100&memo=Test%20xfer"))
}
)
}
}
}
}
}

viewBinding.buttonInternalUri.setOnClickListener {
// NOTE: this is an arbitrary address - please don't perform any real transfers to it!
solanaPayActivityResult.launch(Uri.parse("solana:84npKJKZy8ixjdq8UChZULDUea2Twt8ThxjiqKd7QZ54?amount=100&memo=Test%20xfer"))
@Composable
fun MainScreen(onLaunchSolanaPay: () -> Unit) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
contentAlignment = Alignment.TopStart
) {
Button(
onClick = onLaunchSolanaPay
) {
Text(
text = stringResource(R.string.label_internal_solana_pay_uri),
fontSize = 22.sp
)
}
}
}
218 changes: 179 additions & 39 deletions app/src/main/java/com/solana/pay/sample/SolanaPayActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,43 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.lifecycleScope
import com.solana.digitalassetlinks.AndroidAppPackageVerifier
import com.solana.pay.SolanaPayAndroidContract
import com.solana.pay.SolanaPayTransactionRequest
import com.solana.pay.SolanaPayURI
import com.solana.pay.sample.databinding.ActivitySolanaPayBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URI
import kotlin.random.Random

class SolanaPayActivity : AppCompatActivity() {
private lateinit var viewBinding: ActivitySolanaPayBinding
class SolanaPayActivity : ComponentActivity() {

private lateinit var entrypoint: Entrypoint
private lateinit var sourceVerificationStatus: SourceVerification
private lateinit var solanaPayUri: SolanaPayURI

private var verifier: AndroidAppPackageVerifier? = null

private val uiState = mutableStateOf(SolanaPayUiState())

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

viewBinding = ActivitySolanaPayBinding.inflate(layoutInflater)
setContentView(viewBinding.root)

setResult(Activity.RESULT_CANCELED)

val uri = intent.data
Expand All @@ -58,32 +67,40 @@ class SolanaPayActivity : AppCompatActivity() {
deriveEntrypoint()
doSourceVerification()

viewBinding.apply {
btnSimulateAuthorizeSubmit.setOnClickListener {
Log.d(TAG, "Simulating authorization and successful submission of transaction")
val result = Intent().putExtra(SolanaPayAndroidContract.EXTRA_SIGNATURE, createFakeTransactionSignatureBase58())
setResult(Activity.RESULT_OK, result)
finish()
}
btnSimulateAuthorizeButSubmitError.setOnClickListener {
Log.d(TAG, "Simulating authorization and unsuccessful submission of transaction")
val result = Intent().putExtra(SolanaPayAndroidContract.EXTRA_SIGNATURE, createFakeTransactionSignatureBase58())
setResult(SolanaPayAndroidContract.RESULT_FAILED, result)
finish()
}
btnSimulateNotAuthorized.setOnClickListener {
Log.d(TAG, "Simulating user declined to authorize transaction")
setResult(SolanaPayAndroidContract.RESULT_DECLINED)
finish()
}
btnSimulateWalletRejectsTransaction.setOnClickListener {
Log.d(TAG, "Simulating wallet failed to verify transaction validity")
setResult(SolanaPayAndroidContract.RESULT_NOT_VERIFIED)
finish()
setContent {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
SolanaPayScreen(
uiState = uiState.value,
onSimulateAuthorizeSubmit = {
Log.d(TAG, "Simulating authorization and successful submission of transaction")
val result = Intent().putExtra(SolanaPayAndroidContract.EXTRA_SIGNATURE, createFakeTransactionSignatureBase58())
setResult(Activity.RESULT_OK, result)
finish()
},
onSimulateAuthorizeButSubmitError = {
Log.d(TAG, "Simulating authorization and unsuccessful submission of transaction")
val result = Intent().putExtra(SolanaPayAndroidContract.EXTRA_SIGNATURE, createFakeTransactionSignatureBase58())
setResult(SolanaPayAndroidContract.RESULT_FAILED, result)
finish()
},
onSimulateNotAuthorized = {
Log.d(TAG, "Simulating user declined to authorize transaction")
setResult(SolanaPayAndroidContract.RESULT_DECLINED)
finish()
},
onSimulateWalletRejectsTransaction = {
Log.d(TAG, "Simulating wallet failed to verify transaction validity")
setResult(SolanaPayAndroidContract.RESULT_NOT_VERIFIED)
finish()
}
)
}
}
}

updateUI()
}

override fun onDestroy() {
Expand Down Expand Up @@ -175,16 +192,14 @@ class SolanaPayActivity : AppCompatActivity() {
}

private fun updateUI() {
viewBinding.apply {
tvEntrypointType.text = entrypoint.toString()
tvSourceVerification.text = sourceVerificationStatus.toString()
tvSolanaPayUri.text = solanaPayUri.uri.toString()
val en = sourceVerificationStatus in listOf(
uiState.value = SolanaPayUiState(
entrypointType = entrypoint.toString(),
sourceVerification = sourceVerificationStatus.toString(),
solanaPayUri = solanaPayUri.uri.toString(),
isButtonsEnabled = sourceVerificationStatus in listOf(
SourceVerification.VERIFIED, SourceVerification.NOT_VERIFIABLE
)
btnSimulateAuthorizeSubmit.isEnabled = en
btnSimulateAuthorizeButSubmitError.isEnabled = en
}
)
}

private fun createFakeTransactionSignatureBase58(): String = Base58EncodeUseCase(Random.Default.nextBytes(64))
Expand All @@ -200,4 +215,129 @@ class SolanaPayActivity : AppCompatActivity() {
companion object {
private val TAG = SolanaPayActivity::class.simpleName
}
}

data class SolanaPayUiState(
val entrypointType: String = "",
val sourceVerification: String = "",
val solanaPayUri: String = "",
val isButtonsEnabled: Boolean = false
)


@Composable
fun SolanaPayScreen(
uiState: SolanaPayUiState,
onSimulateAuthorizeSubmit: () -> Unit,
onSimulateAuthorizeButSubmitError: () -> Unit,
onSimulateNotAuthorized: () -> Unit,
onSimulateWalletRejectsTransaction: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(8.dp)
) {
// Entrypoint Type Row
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.label_entrypoint_type),
fontSize = 26.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = uiState.entrypointType,
fontSize = 26.sp,
modifier = Modifier.weight(1f)
)
}

// Source Verification Row
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.label_source_verification),
fontSize = 26.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = uiState.sourceVerification,
fontSize = 26.sp,
modifier = Modifier.weight(1f)
)
}

// Solana Pay URI Section
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.label_solana_pay_uri),
fontSize = 26.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = uiState.solanaPayUri,
fontSize = 26.sp,
modifier = Modifier.fillMaxWidth()
)

// Simulation Options Section
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.label_simulation_options),
fontSize = 26.sp,
fontWeight = FontWeight.Bold
)

// Simulation Buttons
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onSimulateAuthorizeSubmit,
enabled = uiState.isButtonsEnabled
) {
Text(
text = stringResource(R.string.label_simulate_authorize_submit),
fontSize = 22.sp
)
}

Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onSimulateAuthorizeButSubmitError,
enabled = uiState.isButtonsEnabled
) {
Text(
text = stringResource(R.string.label_simulate_authorize_but_submit_error),
fontSize = 22.sp
)
}

Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onSimulateNotAuthorized
) {
Text(
text = stringResource(R.string.label_simulate_not_authorized),
fontSize = 22.sp
)
}

Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onSimulateWalletRejectsTransaction
) {
Text(
text = stringResource(R.string.label_simulate_wallet_rejects_transaction),
fontSize = 22.sp
)
}
}
}
Loading