feat(wallet): scaffold wallet module structure
Task 1 of Phase 1 - Module Scaffolding - Created features/wallet/api module with WalletEntryPoint and WalletState - Created features/wallet/impl module with Metro DI setup - Created features/wallet/test module with FakeWalletEntryPoint - Added PaymentFlowNode placeholder with Appyx navigation - Added Cardano client library dependencies (0.7.1) - Added proguard rules for Cardano library - Added basic unit tests for WalletState The module follows Element X patterns: - Metro for dependency injection (@ContributesTo, @ContributesBinding, @ContributesNode) - Appyx for navigation (BaseFlowNode pattern) - api/impl/test module separation - Feature entry point pattern for navigation This module scaffolding blocks all subsequent tasks (2-8) in Phase 1.
This commit is contained in:
parent
cbb220f08d
commit
225afc3108
14 changed files with 484 additions and 0 deletions
18
features/wallet/api/build.gradle.kts
Normal file
18
features/wallet/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Sulkta Coop.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.wallet.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
7
features/wallet/api/src/main/AndroidManifest.xml
Normal file
7
features/wallet/api/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2026 Sulkta Coop.
|
||||
~
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
<manifest />
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Sulkta Coop.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package io.element.android.features.wallet.api
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
/**
|
||||
* Entry point for the Cardano wallet feature.
|
||||
* Provides navigation to payment flows and wallet management.
|
||||
*/
|
||||
interface WalletEntryPoint : FeatureEntryPoint {
|
||||
/**
|
||||
* Builder for creating wallet flow nodes.
|
||||
*/
|
||||
interface Builder {
|
||||
fun setRoomId(roomId: RoomId): Builder
|
||||
fun setRecipientUserId(userId: UserId?): Builder
|
||||
fun setRecipientAddress(address: String?): Builder
|
||||
fun setAmount(amount: String?): Builder
|
||||
fun build(): Node
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for the payment flow.
|
||||
*/
|
||||
fun paymentFlowBuilder(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: Callback,
|
||||
): Builder
|
||||
|
||||
/**
|
||||
* Callback for wallet flow events.
|
||||
*/
|
||||
interface Callback : Plugin {
|
||||
fun onPaymentSent(txHash: String)
|
||||
fun onPaymentCancelled()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Sulkta Coop.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package io.element.android.features.wallet.api
|
||||
|
||||
/**
|
||||
* Represents the current state of the Cardano wallet.
|
||||
*/
|
||||
data class WalletState(
|
||||
val hasWallet: Boolean,
|
||||
val address: String?,
|
||||
val balanceLovelace: Long?,
|
||||
val balanceAda: String?,
|
||||
val isLoading: Boolean,
|
||||
val error: String?,
|
||||
) {
|
||||
companion object {
|
||||
val Initial = WalletState(
|
||||
hasWallet = false,
|
||||
address = null,
|
||||
balanceLovelace = null,
|
||||
balanceAda = null,
|
||||
isLoading = true,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
55
features/wallet/impl/build.gradle.kts
Normal file
55
features/wallet/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Sulkta Coop.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import extension.setupDependencyInjection
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.wallet.impl"
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
api(projects.features.wallet.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrix.impl)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.cryptography.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
|
||||
// Cardano - using Koios backend (no API key required)
|
||||
implementation("com.bloxbean.cardano:cardano-client-lib:0.7.1")
|
||||
implementation("com.bloxbean.cardano:cardano-client-backend-koios:0.7.1")
|
||||
implementation("com.bloxbean.cardano:cardano-client-crypto:0.7.1")
|
||||
|
||||
// Biometric
|
||||
implementation(libs.androidx.biometric)
|
||||
|
||||
// JSON
|
||||
implementation(libs.serialization.json)
|
||||
|
||||
// Coroutines
|
||||
implementation(libs.coroutines.core)
|
||||
|
||||
// Testing
|
||||
testImplementation(projects.features.wallet.test)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.coroutines.test)
|
||||
}
|
||||
10
features/wallet/impl/proguard-rules.pro
vendored
Normal file
10
features/wallet/impl/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Cardano client library uses reflection for CBOR serialization
|
||||
-keep class com.bloxbean.cardano.** { *; }
|
||||
-keepclassmembers class * {
|
||||
@com.fasterxml.jackson.annotation.* *;
|
||||
}
|
||||
|
||||
# Keep the Cardano model classes
|
||||
-keep class com.bloxbean.cardano.client.api.model.** { *; }
|
||||
-keep class com.bloxbean.cardano.client.backend.model.** { *; }
|
||||
-keep class com.bloxbean.cardano.client.transaction.spec.** { *; }
|
||||
7
features/wallet/impl/src/main/AndroidManifest.xml
Normal file
7
features/wallet/impl/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2026 Sulkta Coop.
|
||||
~
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
<manifest />
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Sulkta Coop.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package io.element.android.features.wallet.impl
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.wallet.api.WalletEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultWalletEntryPoint @Inject constructor() : WalletEntryPoint {
|
||||
class Builder(
|
||||
private val parentNode: Node,
|
||||
private val buildContext: BuildContext,
|
||||
private val callback: WalletEntryPoint.Callback,
|
||||
) : WalletEntryPoint.Builder {
|
||||
private var roomId: RoomId? = null
|
||||
private var recipientUserId: UserId? = null
|
||||
private var recipientAddress: String? = null
|
||||
private var amount: String? = null
|
||||
|
||||
override fun setRoomId(roomId: RoomId): Builder {
|
||||
this.roomId = roomId
|
||||
return this
|
||||
}
|
||||
|
||||
override fun setRecipientUserId(userId: UserId?): Builder {
|
||||
this.recipientUserId = userId
|
||||
return this
|
||||
}
|
||||
|
||||
override fun setRecipientAddress(address: String?): Builder {
|
||||
this.recipientAddress = address
|
||||
return this
|
||||
}
|
||||
|
||||
override fun setAmount(amount: String?): Builder {
|
||||
this.amount = amount
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
val inputs = PaymentFlowNode.Inputs(
|
||||
roomId = requireNotNull(roomId) { "roomId must be set" },
|
||||
recipientUserId = recipientUserId,
|
||||
recipientAddress = recipientAddress,
|
||||
amount = amount,
|
||||
)
|
||||
return parentNode.createNode<PaymentFlowNode>(buildContext, listOf(inputs, callback))
|
||||
}
|
||||
}
|
||||
|
||||
override fun paymentFlowBuilder(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: WalletEntryPoint.Callback,
|
||||
): WalletEntryPoint.Builder {
|
||||
return Builder(parentNode, buildContext, callback)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Sulkta Coop.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package io.element.android.features.wallet.impl
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.wallet.api.WalletEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@AssistedInject
|
||||
class PaymentFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
) : BaseFlowNode<PaymentFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Confirm,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
@Parcelize
|
||||
data class Inputs(
|
||||
val roomId: RoomId,
|
||||
val recipientUserId: UserId?,
|
||||
val recipientAddress: String?,
|
||||
val amount: String?,
|
||||
) : NodeInputs, Parcelable
|
||||
|
||||
private val callback: WalletEntryPoint.Callback = callback()
|
||||
private val inputs: Inputs = plugins.filterIsInstance<Inputs>().first()
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
data object Confirm : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object Success : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Confirm -> {
|
||||
// TODO: Implement PaymentConfirmNode
|
||||
createNode<PlaceholderNode>(buildContext, listOf(PlaceholderNode.Inputs("Payment Confirm")))
|
||||
}
|
||||
NavTarget.Success -> {
|
||||
// TODO: Implement PaymentSuccessNode
|
||||
createNode<PlaceholderNode>(buildContext, listOf(PlaceholderNode.Inputs("Payment Success")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
BackstackView()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder node for development. Will be replaced with actual implementations.
|
||||
*/
|
||||
@ContributesNode(SessionScope::class)
|
||||
@AssistedInject
|
||||
class PlaceholderNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
@Parcelize
|
||||
data class Inputs(val label: String) : NodeInputs, Parcelable
|
||||
|
||||
private val inputs: Inputs = plugins.filterIsInstance<Inputs>().first()
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(text = "Placeholder: ${inputs.label}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Sulkta Coop.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package io.element.android.features.wallet.impl.di
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.ObjectFactory
|
||||
import dev.zacsweers.metro.Provides
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* DI module providing wallet-related dependencies.
|
||||
*/
|
||||
@ContributesTo(AppScope::class)
|
||||
@ObjectFactory
|
||||
interface WalletModule {
|
||||
companion object {
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun provideWalletJson(): Json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Sulkta Coop.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package io.element.android.features.wallet.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.wallet.api.WalletState
|
||||
import org.junit.Test
|
||||
|
||||
class WalletStateTest {
|
||||
@Test
|
||||
fun `initial state has correct defaults`() {
|
||||
val state = WalletState.Initial
|
||||
|
||||
assertThat(state.hasWallet).isFalse()
|
||||
assertThat(state.address).isNull()
|
||||
assertThat(state.balanceLovelace).isNull()
|
||||
assertThat(state.balanceAda).isNull()
|
||||
assertThat(state.isLoading).isTrue()
|
||||
assertThat(state.error).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `state can be updated`() {
|
||||
val state = WalletState(
|
||||
hasWallet = true,
|
||||
address = "addr1test",
|
||||
balanceLovelace = 10_000_000L,
|
||||
balanceAda = "10",
|
||||
isLoading = false,
|
||||
error = null,
|
||||
)
|
||||
|
||||
assertThat(state.hasWallet).isTrue()
|
||||
assertThat(state.address).isEqualTo("addr1test")
|
||||
assertThat(state.balanceLovelace).isEqualTo(10_000_000L)
|
||||
assertThat(state.balanceAda).isEqualTo("10")
|
||||
assertThat(state.isLoading).isFalse()
|
||||
}
|
||||
}
|
||||
19
features/wallet/test/build.gradle.kts
Normal file
19
features/wallet/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Sulkta Coop.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.wallet.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.features.wallet.api)
|
||||
implementation(projects.tests.testutils)
|
||||
implementation(libs.coroutines.core)
|
||||
}
|
||||
7
features/wallet/test/src/main/AndroidManifest.xml
Normal file
7
features/wallet/test/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2026 Sulkta Coop.
|
||||
~
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
<manifest />
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Sulkta Coop.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package io.element.android.features.wallet.test
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.features.wallet.api.WalletEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeWalletEntryPoint : WalletEntryPoint {
|
||||
class Builder : WalletEntryPoint.Builder {
|
||||
override fun setRoomId(roomId: RoomId): Builder = this
|
||||
override fun setRecipientUserId(userId: UserId?): Builder = this
|
||||
override fun setRecipientAddress(address: String?): Builder = this
|
||||
override fun setAmount(amount: String?): Builder = this
|
||||
override fun build(): Node = lambdaError()
|
||||
}
|
||||
|
||||
override fun paymentFlowBuilder(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: WalletEntryPoint.Callback,
|
||||
): WalletEntryPoint.Builder {
|
||||
return Builder()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue