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
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()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue