diff --git a/app/build.gradle b/app/build.gradle
index 8719953..57ddff8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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 {
@@ -27,9 +28,10 @@ android {
}
buildFeatures {
- viewBinding true
+ compose true
}
+
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
@@ -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')
diff --git a/app/src/main/java/com/solana/pay/sample/MainActivity.kt b/app/src/main/java/com/solana/pay/sample/MainActivity.kt
index 5728078..e15eb0c 100644
--- a/app/src/main/java/com/solana/pay/sample/MainActivity.kt
+++ b/app/src/main/java/com/solana/pay/sample/MainActivity.kt
@@ -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,
@@ -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
+ )
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/solana/pay/sample/SolanaPayActivity.kt b/app/src/main/java/com/solana/pay/sample/SolanaPayActivity.kt
index ccc6b7f..c9f14ba 100644
--- a/app/src/main/java/com/solana/pay/sample/SolanaPayActivity.kt
+++ b/app/src/main/java/com/solana/pay/sample/SolanaPayActivity.kt
@@ -8,21 +8,31 @@ 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
@@ -30,12 +40,11 @@ class SolanaPayActivity : AppCompatActivity() {
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
@@ -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() {
@@ -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))
@@ -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
+ )
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 3e9e08b..0000000
--- a/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_solana_pay.xml b/app/src/main/res/layout/activity_solana_pay.xml
deleted file mode 100644
index 0999a8e..0000000
--- a/app/src/main/res/layout/activity_solana_pay.xml
+++ /dev/null
@@ -1,129 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file