diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 1655f303f0..ea77304740 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -24,6 +24,7 @@ dependencies {
implementation(project(":libs:auth"))
/** Features */
+ implementation(project(":features:onboarding"))
implementation(project(":features:referral:presentation"))
implementation(project(":features:referral:domain"))
implementation(project(":features:referral:data"))
diff --git a/app/src/main/res/drawable/ic_onboarding_chat_24.xml b/app/src/main/res/drawable/ic_onboarding_chat_24.xml
new file mode 100644
index 0000000000..a54ee4625a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_onboarding_chat_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_onboarding_qr_code_scan_24.xml b/app/src/main/res/drawable/ic_onboarding_qr_code_scan_24.xml
new file mode 100644
index 0000000000..93e77daea8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_onboarding_qr_code_scan_24.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/common/src/main/java/com/tangem/common/module/ModuleErrorCode.kt b/common/src/main/java/com/tangem/common/module/ModuleErrorCode.kt
index 11da28c037..ef7941cc63 100644
--- a/common/src/main/java/com/tangem/common/module/ModuleErrorCode.kt
+++ b/common/src/main/java/com/tangem/common/module/ModuleErrorCode.kt
@@ -10,4 +10,5 @@ object ModuleErrorCode {
const val NETWORK = 20000
const val DOMAIN = 30000
const val SALT_PAY = 40000
+ const val ONBOARDING = 50000
}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt
index 7e33fefae3..ce19ff204e 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt
@@ -3,6 +3,7 @@ package com.tangem.datasource.asset
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.BufferedReader
+import java.io.InputStream
import javax.inject.Inject
/**
@@ -15,9 +16,13 @@ internal class AndroidAssetReader @Inject constructor(
) : AssetReader {
override fun readJson(fileName: String): String {
- return context.assets
- .open("$fileName.json")
+ return openFile("$fileName.json")
.bufferedReader()
.use(BufferedReader::readText)
}
+
+ override fun openFile(fileName: String): InputStream {
+ return context.assets
+ .open(fileName)
+ }
}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt
index 6475ad3909..a3196c64aa 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt
@@ -1,5 +1,7 @@
package com.tangem.datasource.asset
+import java.io.InputStream
+
/**
* Asset file reader
*
@@ -9,4 +11,7 @@ interface AssetReader {
/** Read content of json file [fileName] from asset */
fun readJson(fileName: String): String
+
+ /** Open a file [file] from asset as InputStream */
+ fun openFile(file: String): InputStream
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt
index eafe741803..2575fee4ad 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt
@@ -141,19 +141,27 @@ private fun TangemTextField(
},
trailingIcon = {
val iconRes by rememberUpdatedState(
- newValue = if (isError) R.drawable.ic_alert_24 else R.drawable.ic_close_24,
+ newValue = if (isError) {
+ R.drawable.ic_alert_24
+ } else if (value.text.isNotEmpty()) {
+ R.drawable.ic_close_24
+ } else {
+ null
+ },
)
- IconButton(
- modifier = Modifier.size(32.dp),
- onClick = onClear,
- enabled = !isError && enabled,
- ) {
- Icon(
- modifier = Modifier.size(24.dp),
- painter = painterResource(id = iconRes),
- tint = colors.trailingIconColor(enabled = enabled, isError = isError).value,
- contentDescription = "Clear input",
- )
+ if (iconRes != null) {
+ IconButton(
+ modifier = Modifier.size(32.dp),
+ onClick = onClear,
+ enabled = !isError && enabled,
+ ) {
+ Icon(
+ modifier = Modifier.size(24.dp),
+ painter = painterResource(id = iconRes!!),
+ tint = colors.trailingIconColor(enabled = enabled, isError = isError).value,
+ contentDescription = "Clear input",
+ )
+ }
}
},
)
@@ -193,7 +201,7 @@ private fun TangemTextFieldSize.toShape(): Shape = when (this) {
TangemTextFieldSize.Default -> TangemTheme.shapes.roundedCornersSmall2
}
-internal object TangemTextFieldsDefault {
+object TangemTextFieldsDefault {
val defaultTextFieldColors: TangemTextFieldColors
@Composable @Stable get() = TangemTextFieldColors(
textColor = TangemTheme.colors.text.primary1,
@@ -224,7 +232,7 @@ internal object TangemTextFieldsDefault {
}
@Immutable
-internal data class TangemTextFieldColors(
+data class TangemTextFieldColors(
private val textColor: Color,
private val disabledTextColor: Color,
private val cursorColor: Color,
diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt
index 96c0d99ffc..9d9362b84d 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt
@@ -47,6 +47,7 @@ data class TangemDimens internal constructor(
val size24: Dp = 24.dp,
val size28: Dp = 28.dp,
val size32: Dp = 32.dp,
+ val size34: Dp = 34.dp,
val size36: Dp = 36.dp,
val size40: Dp = 40.dp,
val size42: Dp = 42.dp,
diff --git a/core/ui/src/main/res/drawable/card_placeholder_black.xml b/core/ui/src/main/res/drawable/card_placeholder_black.xml
new file mode 100644
index 0000000000..83fab8e0f7
--- /dev/null
+++ b/core/ui/src/main/res/drawable/card_placeholder_black.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_onboarding_text_edit_56.xml b/core/ui/src/main/res/drawable/ic_onboarding_text_edit_56.xml
new file mode 100644
index 0000000000..11e420c412
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_onboarding_text_edit_56.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt
index 952424e215..6f28c0350e 100644
--- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt
+++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt
@@ -2,19 +2,25 @@ package com.tangem.utils.coroutines
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asCoroutineDispatcher
+import java.util.concurrent.Executors
import javax.inject.Inject
interface CoroutineDispatcherProvider {
val main: CoroutineDispatcher
val io: CoroutineDispatcher
+ val single: CoroutineDispatcher
}
class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcherProvider {
override val main: CoroutineDispatcher = Dispatchers.Main
override val io: CoroutineDispatcher = Dispatchers.IO
+ override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
}
class TestingCoroutineDispatcherProvider(
override val main: CoroutineDispatcher = Dispatchers.Unconfined,
override val io: CoroutineDispatcher = Dispatchers.Unconfined,
-) : CoroutineDispatcherProvider
\ No newline at end of file
+ override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher(),
+) : CoroutineDispatcherProvider
+
diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt
index 59452dbf7a..872aeb1c64 100644
--- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt
+++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt
@@ -8,6 +8,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
suspend fun runCatching(dispatcher: CoroutineDispatcher, block: suspend () -> R): Result {
return runCatching {
@@ -25,11 +27,20 @@ class Debouncer {
private var debounceJob: Job? = null
- fun debounce(waitMs: Long = 300L, coroutineScope: CoroutineScope, destinationFunction: () -> Unit) {
+ fun debounce(
+ coroutineScope: CoroutineScope,
+ waitMs: Long = 300L,
+ context: CoroutineContext = EmptyCoroutineContext,
+ destinationFunction: suspend () -> Unit,
+ ) {
debounceJob?.cancel()
- debounceJob = coroutineScope.launch {
+ debounceJob = coroutineScope.launch(context) {
delay(waitMs)
destinationFunction.invoke()
}
}
+
+ fun release() {
+ debounceJob?.cancel()
+ }
}
\ No newline at end of file
diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Char.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Char.kt
new file mode 100644
index 0000000000..0eed425110
--- /dev/null
+++ b/core/utils/src/main/java/com/tangem/utils/extensions/Char.kt
@@ -0,0 +1,6 @@
+package com.tangem.utils.extensions
+
+/**
+[REDACTED_AUTHOR]
+ */
+fun Char.isNotWhitespace(): Boolean = !this.isWhitespace()
\ No newline at end of file
diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt
new file mode 100644
index 0000000000..89fd92851b
--- /dev/null
+++ b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt
@@ -0,0 +1,6 @@
+package com.tangem.utils.extensions
+
+/**
+[REDACTED_AUTHOR]
+ */
+fun Collection.isSingleItem(): Boolean = this.size == 1
\ No newline at end of file
diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Int.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Int.kt
new file mode 100644
index 0000000000..af00c9f75a
--- /dev/null
+++ b/core/utils/src/main/java/com/tangem/utils/extensions/Int.kt
@@ -0,0 +1,8 @@
+package com.tangem.utils.extensions
+
+/**
+[REDACTED_AUTHOR]
+ */
+fun Int.isEven(): Boolean = this % 2 == 0
+
+fun Int.isOdd(): Boolean = !this.isEven()
\ No newline at end of file
diff --git a/features/onboarding/.gitignore b/features/onboarding/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/features/onboarding/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts
new file mode 100644
index 0000000000..61df104ad9
--- /dev/null
+++ b/features/onboarding/build.gradle.kts
@@ -0,0 +1,50 @@
+plugins {
+ alias(deps.plugins.android.library)
+ alias(deps.plugins.kotlin.android)
+ alias(deps.plugins.kotlin.kapt)
+ alias(deps.plugins.kotlin.serialization)
+ alias(deps.plugins.hilt.android)
+ id("configuration")
+}
+
+dependencies {
+ /** Core modules */
+ implementation(project(":common"))
+ implementation(project(":core:featuretoggles"))
+ implementation(project(":core:datasource"))
+ implementation(project(":core:analytics"))
+ implementation(project(":core:utils"))
+ implementation(project(":core:ui"))
+ implementation(project(":core:res"))
+
+ /** AndroidX libraries */
+ implementation(deps.androidx.core.ktx)
+ implementation(deps.androidx.appCompat)
+ implementation(deps.androidx.fragment.ktx)
+ implementation(deps.androidx.constraintLayout)
+ implementation(deps.androidx.activity.compose)
+ implementation(deps.lifecycle.runtime.ktx)
+ implementation(deps.lifecycle.common.java8)
+ implementation(deps.lifecycle.viewModel.ktx)
+
+ /** Compose libraries */
+ implementation(deps.compose.material)
+ implementation(deps.compose.animation)
+ implementation(deps.compose.foundation)
+ implementation(deps.compose.ui)
+ implementation(deps.compose.ui.tooling)
+ implementation(deps.compose.coil)
+ implementation(deps.compose.shimmer)
+
+ implementation(deps.tangem.card.core)
+
+ /** Other libraries */
+ implementation(deps.compose.shimmer)
+ implementation(deps.kotlin.immutable.collections)
+ implementation(deps.kotlin.serialization)
+ implementation(deps.timber)
+
+ /** DI */
+ implementation(deps.hilt.android)
+ kapt(deps.hilt.kapt)
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/AndroidManifest.xml b/features/onboarding/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..21015a0f53
--- /dev/null
+++ b/features/onboarding/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/features/onboarding/src/main/assets/mnemonic/mnemonic_dictionary_en.txt b/features/onboarding/src/main/assets/mnemonic/mnemonic_dictionary_en.txt
new file mode 100644
index 0000000000..f78ccaf2ce
--- /dev/null
+++ b/features/onboarding/src/main/assets/mnemonic/mnemonic_dictionary_en.txt
@@ -0,0 +1,2048 @@
+abandon
+ability
+able
+about
+above
+absent
+absorb
+abstract
+absurd
+abuse
+access
+accident
+account
+accuse
+achieve
+acid
+acoustic
+acquire
+across
+act
+action
+actor
+actress
+actual
+adapt
+add
+addict
+address
+adjust
+admit
+adult
+advance
+advice
+aerobic
+affair
+afford
+afraid
+again
+age
+agent
+agree
+ahead
+aim
+air
+airport
+aisle
+alarm
+album
+alcohol
+alert
+alien
+all
+alley
+allow
+almost
+alone
+alpha
+already
+also
+alter
+always
+amateur
+amazing
+among
+amount
+amused
+analyst
+anchor
+ancient
+anger
+angle
+angry
+animal
+ankle
+announce
+annual
+another
+answer
+antenna
+antique
+anxiety
+any
+apart
+apology
+appear
+apple
+approve
+april
+arch
+arctic
+area
+arena
+argue
+arm
+armed
+armor
+army
+around
+arrange
+arrest
+arrive
+arrow
+art
+artefact
+artist
+artwork
+ask
+aspect
+assault
+asset
+assist
+assume
+asthma
+athlete
+atom
+attack
+attend
+attitude
+attract
+auction
+audit
+august
+aunt
+author
+auto
+autumn
+average
+avocado
+avoid
+awake
+aware
+away
+awesome
+awful
+awkward
+axis
+baby
+bachelor
+bacon
+badge
+bag
+balance
+balcony
+ball
+bamboo
+banana
+banner
+bar
+barely
+bargain
+barrel
+base
+basic
+basket
+battle
+beach
+bean
+beauty
+because
+become
+beef
+before
+begin
+behave
+behind
+believe
+below
+belt
+bench
+benefit
+best
+betray
+better
+between
+beyond
+bicycle
+bid
+bike
+bind
+biology
+bird
+birth
+bitter
+black
+blade
+blame
+blanket
+blast
+bleak
+bless
+blind
+blood
+blossom
+blouse
+blue
+blur
+blush
+board
+boat
+body
+boil
+bomb
+bone
+bonus
+book
+boost
+border
+boring
+borrow
+boss
+bottom
+bounce
+box
+boy
+bracket
+brain
+brand
+brass
+brave
+bread
+breeze
+brick
+bridge
+brief
+bright
+bring
+brisk
+broccoli
+broken
+bronze
+broom
+brother
+brown
+brush
+bubble
+buddy
+budget
+buffalo
+build
+bulb
+bulk
+bullet
+bundle
+bunker
+burden
+burger
+burst
+bus
+business
+busy
+butter
+buyer
+buzz
+cabbage
+cabin
+cable
+cactus
+cage
+cake
+call
+calm
+camera
+camp
+can
+canal
+cancel
+candy
+cannon
+canoe
+canvas
+canyon
+capable
+capital
+captain
+car
+carbon
+card
+cargo
+carpet
+carry
+cart
+case
+cash
+casino
+castle
+casual
+cat
+catalog
+catch
+category
+cattle
+caught
+cause
+caution
+cave
+ceiling
+celery
+cement
+census
+century
+cereal
+certain
+chair
+chalk
+champion
+change
+chaos
+chapter
+charge
+chase
+chat
+cheap
+check
+cheese
+chef
+cherry
+chest
+chicken
+chief
+child
+chimney
+choice
+choose
+chronic
+chuckle
+chunk
+churn
+cigar
+cinnamon
+circle
+citizen
+city
+civil
+claim
+clap
+clarify
+claw
+clay
+clean
+clerk
+clever
+click
+client
+cliff
+climb
+clinic
+clip
+clock
+clog
+close
+cloth
+cloud
+clown
+club
+clump
+cluster
+clutch
+coach
+coast
+coconut
+code
+coffee
+coil
+coin
+collect
+color
+column
+combine
+come
+comfort
+comic
+common
+company
+concert
+conduct
+confirm
+congress
+connect
+consider
+control
+convince
+cook
+cool
+copper
+copy
+coral
+core
+corn
+correct
+cost
+cotton
+couch
+country
+couple
+course
+cousin
+cover
+coyote
+crack
+cradle
+craft
+cram
+crane
+crash
+crater
+crawl
+crazy
+cream
+credit
+creek
+crew
+cricket
+crime
+crisp
+critic
+crop
+cross
+crouch
+crowd
+crucial
+cruel
+cruise
+crumble
+crunch
+crush
+cry
+crystal
+cube
+culture
+cup
+cupboard
+curious
+current
+curtain
+curve
+cushion
+custom
+cute
+cycle
+dad
+damage
+damp
+dance
+danger
+daring
+dash
+daughter
+dawn
+day
+deal
+debate
+debris
+decade
+december
+decide
+decline
+decorate
+decrease
+deer
+defense
+define
+defy
+degree
+delay
+deliver
+demand
+demise
+denial
+dentist
+deny
+depart
+depend
+deposit
+depth
+deputy
+derive
+describe
+desert
+design
+desk
+despair
+destroy
+detail
+detect
+develop
+device
+devote
+diagram
+dial
+diamond
+diary
+dice
+diesel
+diet
+differ
+digital
+dignity
+dilemma
+dinner
+dinosaur
+direct
+dirt
+disagree
+discover
+disease
+dish
+dismiss
+disorder
+display
+distance
+divert
+divide
+divorce
+dizzy
+doctor
+document
+dog
+doll
+dolphin
+domain
+donate
+donkey
+donor
+door
+dose
+double
+dove
+draft
+dragon
+drama
+drastic
+draw
+dream
+dress
+drift
+drill
+drink
+drip
+drive
+drop
+drum
+dry
+duck
+dumb
+dune
+during
+dust
+dutch
+duty
+dwarf
+dynamic
+eager
+eagle
+early
+earn
+earth
+easily
+east
+easy
+echo
+ecology
+economy
+edge
+edit
+educate
+effort
+egg
+eight
+either
+elbow
+elder
+electric
+elegant
+element
+elephant
+elevator
+elite
+else
+embark
+embody
+embrace
+emerge
+emotion
+employ
+empower
+empty
+enable
+enact
+end
+endless
+endorse
+enemy
+energy
+enforce
+engage
+engine
+enhance
+enjoy
+enlist
+enough
+enrich
+enroll
+ensure
+enter
+entire
+entry
+envelope
+episode
+equal
+equip
+era
+erase
+erode
+erosion
+error
+erupt
+escape
+essay
+essence
+estate
+eternal
+ethics
+evidence
+evil
+evoke
+evolve
+exact
+example
+excess
+exchange
+excite
+exclude
+excuse
+execute
+exercise
+exhaust
+exhibit
+exile
+exist
+exit
+exotic
+expand
+expect
+expire
+explain
+expose
+express
+extend
+extra
+eye
+eyebrow
+fabric
+face
+faculty
+fade
+faint
+faith
+fall
+false
+fame
+family
+famous
+fan
+fancy
+fantasy
+farm
+fashion
+fat
+fatal
+father
+fatigue
+fault
+favorite
+feature
+february
+federal
+fee
+feed
+feel
+female
+fence
+festival
+fetch
+fever
+few
+fiber
+fiction
+field
+figure
+file
+film
+filter
+final
+find
+fine
+finger
+finish
+fire
+firm
+first
+fiscal
+fish
+fit
+fitness
+fix
+flag
+flame
+flash
+flat
+flavor
+flee
+flight
+flip
+float
+flock
+floor
+flower
+fluid
+flush
+fly
+foam
+focus
+fog
+foil
+fold
+follow
+food
+foot
+force
+forest
+forget
+fork
+fortune
+forum
+forward
+fossil
+foster
+found
+fox
+fragile
+frame
+frequent
+fresh
+friend
+fringe
+frog
+front
+frost
+frown
+frozen
+fruit
+fuel
+fun
+funny
+furnace
+fury
+future
+gadget
+gain
+galaxy
+gallery
+game
+gap
+garage
+garbage
+garden
+garlic
+garment
+gas
+gasp
+gate
+gather
+gauge
+gaze
+general
+genius
+genre
+gentle
+genuine
+gesture
+ghost
+giant
+gift
+giggle
+ginger
+giraffe
+girl
+give
+glad
+glance
+glare
+glass
+glide
+glimpse
+globe
+gloom
+glory
+glove
+glow
+glue
+goat
+goddess
+gold
+good
+goose
+gorilla
+gospel
+gossip
+govern
+gown
+grab
+grace
+grain
+grant
+grape
+grass
+gravity
+great
+green
+grid
+grief
+grit
+grocery
+group
+grow
+grunt
+guard
+guess
+guide
+guilt
+guitar
+gun
+gym
+habit
+hair
+half
+hammer
+hamster
+hand
+happy
+harbor
+hard
+harsh
+harvest
+hat
+have
+hawk
+hazard
+head
+health
+heart
+heavy
+hedgehog
+height
+hello
+helmet
+help
+hen
+hero
+hidden
+high
+hill
+hint
+hip
+hire
+history
+hobby
+hockey
+hold
+hole
+holiday
+hollow
+home
+honey
+hood
+hope
+horn
+horror
+horse
+hospital
+host
+hotel
+hour
+hover
+hub
+huge
+human
+humble
+humor
+hundred
+hungry
+hunt
+hurdle
+hurry
+hurt
+husband
+hybrid
+ice
+icon
+idea
+identify
+idle
+ignore
+ill
+illegal
+illness
+image
+imitate
+immense
+immune
+impact
+impose
+improve
+impulse
+inch
+include
+income
+increase
+index
+indicate
+indoor
+industry
+infant
+inflict
+inform
+inhale
+inherit
+initial
+inject
+injury
+inmate
+inner
+innocent
+input
+inquiry
+insane
+insect
+inside
+inspire
+install
+intact
+interest
+into
+invest
+invite
+involve
+iron
+island
+isolate
+issue
+item
+ivory
+jacket
+jaguar
+jar
+jazz
+jealous
+jeans
+jelly
+jewel
+job
+join
+joke
+journey
+joy
+judge
+juice
+jump
+jungle
+junior
+junk
+just
+kangaroo
+keen
+keep
+ketchup
+key
+kick
+kid
+kidney
+kind
+kingdom
+kiss
+kit
+kitchen
+kite
+kitten
+kiwi
+knee
+knife
+knock
+know
+lab
+label
+labor
+ladder
+lady
+lake
+lamp
+language
+laptop
+large
+later
+latin
+laugh
+laundry
+lava
+law
+lawn
+lawsuit
+layer
+lazy
+leader
+leaf
+learn
+leave
+lecture
+left
+leg
+legal
+legend
+leisure
+lemon
+lend
+length
+lens
+leopard
+lesson
+letter
+level
+liar
+liberty
+library
+license
+life
+lift
+light
+like
+limb
+limit
+link
+lion
+liquid
+list
+little
+live
+lizard
+load
+loan
+lobster
+local
+lock
+logic
+lonely
+long
+loop
+lottery
+loud
+lounge
+love
+loyal
+lucky
+luggage
+lumber
+lunar
+lunch
+luxury
+lyrics
+machine
+mad
+magic
+magnet
+maid
+mail
+main
+major
+make
+mammal
+man
+manage
+mandate
+mango
+mansion
+manual
+maple
+marble
+march
+margin
+marine
+market
+marriage
+mask
+mass
+master
+match
+material
+math
+matrix
+matter
+maximum
+maze
+meadow
+mean
+measure
+meat
+mechanic
+medal
+media
+melody
+melt
+member
+memory
+mention
+menu
+mercy
+merge
+merit
+merry
+mesh
+message
+metal
+method
+middle
+midnight
+milk
+million
+mimic
+mind
+minimum
+minor
+minute
+miracle
+mirror
+misery
+miss
+mistake
+mix
+mixed
+mixture
+mobile
+model
+modify
+mom
+moment
+monitor
+monkey
+monster
+month
+moon
+moral
+more
+morning
+mosquito
+mother
+motion
+motor
+mountain
+mouse
+move
+movie
+much
+muffin
+mule
+multiply
+muscle
+museum
+mushroom
+music
+must
+mutual
+myself
+mystery
+myth
+naive
+name
+napkin
+narrow
+nasty
+nation
+nature
+near
+neck
+need
+negative
+neglect
+neither
+nephew
+nerve
+nest
+net
+network
+neutral
+never
+news
+next
+nice
+night
+noble
+noise
+nominee
+noodle
+normal
+north
+nose
+notable
+note
+nothing
+notice
+novel
+now
+nuclear
+number
+nurse
+nut
+oak
+obey
+object
+oblige
+obscure
+observe
+obtain
+obvious
+occur
+ocean
+october
+odor
+off
+offer
+office
+often
+oil
+okay
+old
+olive
+olympic
+omit
+once
+one
+onion
+online
+only
+open
+opera
+opinion
+oppose
+option
+orange
+orbit
+orchard
+order
+ordinary
+organ
+orient
+original
+orphan
+ostrich
+other
+outdoor
+outer
+output
+outside
+oval
+oven
+over
+own
+owner
+oxygen
+oyster
+ozone
+pact
+paddle
+page
+pair
+palace
+palm
+panda
+panel
+panic
+panther
+paper
+parade
+parent
+park
+parrot
+party
+pass
+patch
+path
+patient
+patrol
+pattern
+pause
+pave
+payment
+peace
+peanut
+pear
+peasant
+pelican
+pen
+penalty
+pencil
+people
+pepper
+perfect
+permit
+person
+pet
+phone
+photo
+phrase
+physical
+piano
+picnic
+picture
+piece
+pig
+pigeon
+pill
+pilot
+pink
+pioneer
+pipe
+pistol
+pitch
+pizza
+place
+planet
+plastic
+plate
+play
+please
+pledge
+pluck
+plug
+plunge
+poem
+poet
+point
+polar
+pole
+police
+pond
+pony
+pool
+popular
+portion
+position
+possible
+post
+potato
+pottery
+poverty
+powder
+power
+practice
+praise
+predict
+prefer
+prepare
+present
+pretty
+prevent
+price
+pride
+primary
+print
+priority
+prison
+private
+prize
+problem
+process
+produce
+profit
+program
+project
+promote
+proof
+property
+prosper
+protect
+proud
+provide
+public
+pudding
+pull
+pulp
+pulse
+pumpkin
+punch
+pupil
+puppy
+purchase
+purity
+purpose
+purse
+push
+put
+puzzle
+pyramid
+quality
+quantum
+quarter
+question
+quick
+quit
+quiz
+quote
+rabbit
+raccoon
+race
+rack
+radar
+radio
+rail
+rain
+raise
+rally
+ramp
+ranch
+random
+range
+rapid
+rare
+rate
+rather
+raven
+raw
+razor
+ready
+real
+reason
+rebel
+rebuild
+recall
+receive
+recipe
+record
+recycle
+reduce
+reflect
+reform
+refuse
+region
+regret
+regular
+reject
+relax
+release
+relief
+rely
+remain
+remember
+remind
+remove
+render
+renew
+rent
+reopen
+repair
+repeat
+replace
+report
+require
+rescue
+resemble
+resist
+resource
+response
+result
+retire
+retreat
+return
+reunion
+reveal
+review
+reward
+rhythm
+rib
+ribbon
+rice
+rich
+ride
+ridge
+rifle
+right
+rigid
+ring
+riot
+ripple
+risk
+ritual
+rival
+river
+road
+roast
+robot
+robust
+rocket
+romance
+roof
+rookie
+room
+rose
+rotate
+rough
+round
+route
+royal
+rubber
+rude
+rug
+rule
+run
+runway
+rural
+sad
+saddle
+sadness
+safe
+sail
+salad
+salmon
+salon
+salt
+salute
+same
+sample
+sand
+satisfy
+satoshi
+sauce
+sausage
+save
+say
+scale
+scan
+scare
+scatter
+scene
+scheme
+school
+science
+scissors
+scorpion
+scout
+scrap
+screen
+script
+scrub
+sea
+search
+season
+seat
+second
+secret
+section
+security
+seed
+seek
+segment
+select
+sell
+seminar
+senior
+sense
+sentence
+series
+service
+session
+settle
+setup
+seven
+shadow
+shaft
+shallow
+share
+shed
+shell
+sheriff
+shield
+shift
+shine
+ship
+shiver
+shock
+shoe
+shoot
+shop
+short
+shoulder
+shove
+shrimp
+shrug
+shuffle
+shy
+sibling
+sick
+side
+siege
+sight
+sign
+silent
+silk
+silly
+silver
+similar
+simple
+since
+sing
+siren
+sister
+situate
+six
+size
+skate
+sketch
+ski
+skill
+skin
+skirt
+skull
+slab
+slam
+sleep
+slender
+slice
+slide
+slight
+slim
+slogan
+slot
+slow
+slush
+small
+smart
+smile
+smoke
+smooth
+snack
+snake
+snap
+sniff
+snow
+soap
+soccer
+social
+sock
+soda
+soft
+solar
+soldier
+solid
+solution
+solve
+someone
+song
+soon
+sorry
+sort
+soul
+sound
+soup
+source
+south
+space
+spare
+spatial
+spawn
+speak
+special
+speed
+spell
+spend
+sphere
+spice
+spider
+spike
+spin
+spirit
+split
+spoil
+sponsor
+spoon
+sport
+spot
+spray
+spread
+spring
+spy
+square
+squeeze
+squirrel
+stable
+stadium
+staff
+stage
+stairs
+stamp
+stand
+start
+state
+stay
+steak
+steel
+stem
+step
+stereo
+stick
+still
+sting
+stock
+stomach
+stone
+stool
+story
+stove
+strategy
+street
+strike
+strong
+struggle
+student
+stuff
+stumble
+style
+subject
+submit
+subway
+success
+such
+sudden
+suffer
+sugar
+suggest
+suit
+summer
+sun
+sunny
+sunset
+super
+supply
+supreme
+sure
+surface
+surge
+surprise
+surround
+survey
+suspect
+sustain
+swallow
+swamp
+swap
+swarm
+swear
+sweet
+swift
+swim
+swing
+switch
+sword
+symbol
+symptom
+syrup
+system
+table
+tackle
+tag
+tail
+talent
+talk
+tank
+tape
+target
+task
+taste
+tattoo
+taxi
+teach
+team
+tell
+ten
+tenant
+tennis
+tent
+term
+test
+text
+thank
+that
+theme
+then
+theory
+there
+they
+thing
+this
+thought
+three
+thrive
+throw
+thumb
+thunder
+ticket
+tide
+tiger
+tilt
+timber
+time
+tiny
+tip
+tired
+tissue
+title
+toast
+tobacco
+today
+toddler
+toe
+together
+toilet
+token
+tomato
+tomorrow
+tone
+tongue
+tonight
+tool
+tooth
+top
+topic
+topple
+torch
+tornado
+tortoise
+toss
+total
+tourist
+toward
+tower
+town
+toy
+track
+trade
+traffic
+tragic
+train
+transfer
+trap
+trash
+travel
+tray
+treat
+tree
+trend
+trial
+tribe
+trick
+trigger
+trim
+trip
+trophy
+trouble
+truck
+true
+truly
+trumpet
+trust
+truth
+try
+tube
+tuition
+tumble
+tuna
+tunnel
+turkey
+turn
+turtle
+twelve
+twenty
+twice
+twin
+twist
+two
+type
+typical
+ugly
+umbrella
+unable
+unaware
+uncle
+uncover
+under
+undo
+unfair
+unfold
+unhappy
+uniform
+unique
+unit
+universe
+unknown
+unlock
+until
+unusual
+unveil
+update
+upgrade
+uphold
+upon
+upper
+upset
+urban
+urge
+usage
+use
+used
+useful
+useless
+usual
+utility
+vacant
+vacuum
+vague
+valid
+valley
+valve
+van
+vanish
+vapor
+various
+vast
+vault
+vehicle
+velvet
+vendor
+venture
+venue
+verb
+verify
+version
+very
+vessel
+veteran
+viable
+vibrant
+vicious
+victory
+video
+view
+village
+vintage
+violin
+virtual
+virus
+visa
+visit
+visual
+vital
+vivid
+vocal
+voice
+void
+volcano
+volume
+vote
+voyage
+wage
+wagon
+wait
+walk
+wall
+walnut
+want
+warfare
+warm
+warrior
+wash
+wasp
+waste
+water
+wave
+way
+wealth
+weapon
+wear
+weasel
+weather
+web
+wedding
+weekend
+weird
+welcome
+west
+wet
+whale
+what
+wheat
+wheel
+when
+where
+whip
+whisper
+wide
+width
+wife
+wild
+will
+win
+window
+wine
+wing
+wink
+winner
+winter
+wire
+wisdom
+wise
+wish
+witness
+wolf
+woman
+wonder
+wood
+wool
+word
+work
+world
+worry
+worth
+wrap
+wreck
+wrestle
+wrist
+write
+wrong
+yard
+year
+yellow
+you
+young
+youth
+zebra
+zero
+zone
+zoo
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt
new file mode 100644
index 0000000000..116b7239c3
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt
@@ -0,0 +1,32 @@
+package com.tangem.feature.onboarding.data
+
+import com.tangem.crypto.bip39.*
+import com.tangem.datasource.asset.AssetReader
+
+/**
+[REDACTED_AUTHOR]
+ */
+internal class DefaultMnemonicRepository(
+ private val assetReader: AssetReader,
+) : MnemonicRepository {
+
+ // TODO: change initialization after adding it to the sdk
+ private val bip39Wordlist: Wordlist by lazy {
+ assetReader.openFile(MNEMONIC_FILE_NAME)
+ .use { BIP39Wordlist(it) }
+ }
+
+ private val bip39Words: Set by lazy { bip39Wordlist.words.toHashSet() }
+
+ override fun getWordsDictionary(): Set = bip39Words
+
+ override fun generateDefaultMnemonic(): Mnemonic {
+ return DefaultMnemonic(EntropyLength.Bits128Length, bip39Wordlist)
+ }
+
+ override fun createMnemonic(mnemonicString: String): Mnemonic = DefaultMnemonic(mnemonicString, bip39Wordlist)
+
+ companion object {
+ private const val MNEMONIC_FILE_NAME = "mnemonic/mnemonic_dictionary_en.txt"
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt
new file mode 100644
index 0000000000..b1b7ebfa2c
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt
@@ -0,0 +1,18 @@
+package com.tangem.feature.onboarding.data
+
+import com.tangem.common.core.TangemSdkError
+import com.tangem.crypto.bip39.Mnemonic
+
+/**
+[REDACTED_AUTHOR]
+ */
+interface MnemonicRepository {
+
+ fun getWordsDictionary(): Set
+
+ @Throws(TangemSdkError.MnemonicException::class)
+ fun generateDefaultMnemonic(): Mnemonic
+
+ @Throws(TangemSdkError.MnemonicException::class)
+ fun createMnemonic(mnemonicString: String): Mnemonic
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/di/OnboardingDataModule.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/di/OnboardingDataModule.kt
new file mode 100644
index 0000000000..fe7e292403
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/di/OnboardingDataModule.kt
@@ -0,0 +1,21 @@
+package com.tangem.feature.onboarding.data.di
+
+import com.tangem.datasource.asset.AssetReader
+import com.tangem.feature.onboarding.data.DefaultMnemonicRepository
+import com.tangem.feature.onboarding.data.MnemonicRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+class OnboardingDataModule {
+
+ @Provides
+ @Singleton
+ fun provideSeedPhraseSdkRepository(assetReader: AssetReader): MnemonicRepository {
+ return DefaultMnemonicRepository(assetReader)
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt
new file mode 100644
index 0000000000..d0cd349b55
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt
@@ -0,0 +1,107 @@
+package com.tangem.feature.onboarding.domain
+
+import com.tangem.common.core.TangemSdkError
+import com.tangem.crypto.bip39.Mnemonic
+import com.tangem.crypto.bip39.MnemonicErrorResult
+import com.tangem.feature.onboarding.data.MnemonicRepository
+import com.tangem.utils.extensions.isNotWhitespace
+
+/**
+[REDACTED_AUTHOR]
+ */
+internal class DefaultSeedPhraseInteractor constructor(
+ private val repository: MnemonicRepository,
+) : SeedPhraseInteractor {
+
+ private var currentMnemonic: Mnemonic? = null
+ private val partWordFinder: PartWordFinder = PartWordFinder()
+
+ override suspend fun generateMnemonic(): Result {
+ return runCatching {
+ repository.generateDefaultMnemonic().apply {
+ currentMnemonic = this
+ }
+ }
+ }
+
+ override suspend fun getMnemonicComponents(): Result> {
+ return runCatching {
+ val mnemonic = currentMnemonic ?: repository.generateDefaultMnemonic().apply {
+ currentMnemonic = this
+ }
+ mnemonic.mnemonicComponents
+ }
+ }
+
+ override suspend fun isWordMatch(word: String): Boolean {
+ return repository.getWordsDictionary().contains(word)
+ }
+
+ override suspend fun validateMnemonicString(text: String): Result> {
+ val inputWords = text.split("\\s+".toRegex()).toSet()
+ val wordsDictionary = repository.getWordsDictionary()
+
+ val invalidWords = inputWords
+ .filter { it.isNotEmpty() && !wordsDictionary.contains(it) }
+ .toSet()
+
+ return if (invalidWords.isNotEmpty()) {
+ Result.failure(SeedPhraseError.InvalidWords(invalidWords))
+ } else {
+ try {
+ val mnemonic = repository.createMnemonic(text)
+ Result.success(mnemonic.mnemonicComponents)
+ } catch (ex: TangemSdkError.MnemonicException) {
+ val error = ex.mnemonicResult.mapToError()
+ Result.failure(error)
+ }
+ }
+ }
+
+ override suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): List {
+ if (text.isEmpty() || cursorPosition == 0 || hasSelection) return emptyList()
+ val word = partWordFinder.getLeadPartOfWord(text, cursorPosition)
+ ?: return emptyList()
+
+ val suggestions = repository.getWordsDictionary()
+ .filter { it.startsWith(word, ignoreCase = false) && it != word }
+ .toList()
+
+ return suggestions
+ }
+
+ override suspend fun insertSuggestionWord(
+ text: String,
+ suggestion: String,
+ cursorPosition: Int,
+ ): InsertSuggestionResult {
+ val textPartLead = text.substring(0, cursorPosition)
+ val textPartLast = text.substring(cursorPosition, text.length)
+ val wordPartLast = partWordFinder.getLastPartOfWord(text, suggestion, cursorPosition)
+
+ val textBuilder = StringBuilder()
+ .append(textPartLead)
+ .append(wordPartLast)
+
+ if (textPartLast.isEmpty() || textPartLast[0].isNotWhitespace()) {
+ textBuilder.append(SeedPhraseInteractor.MNEMONIC_DELIMITER)
+ }
+ textBuilder.append(textPartLast)
+
+ val textWithSuggestion = textBuilder.toString()
+ val newCursorPosition = cursorPosition + wordPartLast.length + SeedPhraseInteractor.MNEMONIC_DELIMITER.length
+
+ return InsertSuggestionResult(textWithSuggestion, newCursorPosition)
+ }
+}
+
+private fun MnemonicErrorResult.mapToError(): SeedPhraseError = when (this) {
+ MnemonicErrorResult.InvalidWordCount -> SeedPhraseError.InvalidEntropyLength
+ MnemonicErrorResult.InvalidEntropyLength -> SeedPhraseError.InvalidEntropyLength
+ MnemonicErrorResult.InvalidWordsFile -> SeedPhraseError.InvalidWordsFile
+ MnemonicErrorResult.InvalidChecksum -> SeedPhraseError.InvalidChecksum
+ MnemonicErrorResult.MnenmonicCreationFailed -> SeedPhraseError.MnenmonicCreationFailed
+ MnemonicErrorResult.NormalizationFailed -> SeedPhraseError.NormalizationFailed
+ MnemonicErrorResult.UnsupportedLanguage -> SeedPhraseError.UnsupportedLanguage
+ is MnemonicErrorResult.InvalidWords -> SeedPhraseError.InvalidWords(this.words)
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/OnboardingModuleError.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/OnboardingModuleError.kt
new file mode 100644
index 0000000000..60a22f0fec
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/OnboardingModuleError.kt
@@ -0,0 +1,40 @@
+package com.tangem.feature.onboarding.domain
+
+import com.tangem.common.module.ModuleError
+import com.tangem.common.module.ModuleErrorCode
+import com.tangem.common.module.ModuleMessage
+
+/**
+[REDACTED_AUTHOR]
+ * For each pool of errors, use the codes lying in range 100 (200..299, 300..399, etc)
+ */
+abstract class OnboardingModuleError(
+ subCode: Int,
+ override val message: String,
+ override val data: Any?,
+) : ModuleMessage, ModuleError() {
+ override val code: Int = ModuleErrorCode.ONBOARDING + subCode
+
+ companion object {
+ internal const val ERROR_CODE_SEED_PHRASE = 100
+ }
+}
+
+sealed class SeedPhraseError(
+ subCode: Int = 0,
+ message: String? = null,
+ data: Any? = null,
+) : OnboardingModuleError(
+ subCode = ERROR_CODE_SEED_PHRASE + subCode,
+ message = message ?: this::class.java.simpleName,
+ data = data,
+) {
+ object InvalidWordCount : SeedPhraseError(subCode = 0)
+ object InvalidEntropyLength : SeedPhraseError(subCode = 1)
+ object InvalidWordsFile : SeedPhraseError(subCode = 2)
+ object InvalidChecksum : SeedPhraseError(subCode = 3)
+ object MnenmonicCreationFailed : SeedPhraseError(subCode = 4)
+ object NormalizationFailed : SeedPhraseError(subCode = 5)
+ object UnsupportedLanguage : SeedPhraseError(subCode = 6)
+ data class InvalidWords(val words: Set) : SeedPhraseError(subCode = 7)
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/PartWordFinder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/PartWordFinder.kt
new file mode 100644
index 0000000000..97ca9cd8d9
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/PartWordFinder.kt
@@ -0,0 +1,50 @@
+package com.tangem.feature.onboarding.domain
+
+import com.tangem.utils.extensions.isNotWhitespace
+
+/**
+[REDACTED_AUTHOR]
+ * Class which provides helper functions to determine lead or last part of words for suggestions
+ */
+internal class PartWordFinder {
+
+ /**
+ * Searches and return part of a word on the left side of the [cursorPosition] into the [text]
+ */
+ fun getLeadPartOfWord(text: String, cursorPosition: Int): String? {
+ val charText = text.toCharArray().asList()
+ val cursorIsOutOfTextRange = cursorPosition > charText.size || cursorPosition < 0
+ val cursorAtStartOfText = cursorPosition == 0
+ val hadWhiteSpaceOnLeftOfCursor = charText[cursorPosition - 1].isWhitespace()
+ val cursorInMiddleOfWord = charText.size > cursorPosition && charText[cursorPosition].isNotWhitespace()
+
+ @Suppress("ComplexCondition")
+ if (cursorIsOutOfTextRange || cursorAtStartOfText || hadWhiteSpaceOnLeftOfCursor || cursorInMiddleOfWord) {
+ return null
+ }
+
+ for (charIndex in cursorPosition - 1 downTo 0) {
+ val char = charText[charIndex]
+ when {
+ charIndex == 0 -> {
+ return text.substring(charIndex, cursorPosition)
+ }
+ char.isWhitespace() -> {
+ return text.substring(charIndex + 1, cursorPosition)
+ }
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * Returns a substring of the [word] which excludes the first word part found on the left of [cursorPosition] in
+ * the [text]
+ */
+ fun getLastPartOfWord(text: String, word: String, cursorPosition: Int): String {
+ val leadPart = getLeadPartOfWord(text, cursorPosition) ?: return ""
+
+ return word.substring(leadPart.length, word.length)
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt
new file mode 100644
index 0000000000..43ed7ac068
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt
@@ -0,0 +1,24 @@
+package com.tangem.feature.onboarding.domain
+
+import com.tangem.crypto.bip39.Mnemonic
+
+/**
+[REDACTED_AUTHOR]
+ */
+interface SeedPhraseInteractor {
+ suspend fun generateMnemonic(): Result
+ suspend fun getMnemonicComponents(): Result>
+ suspend fun isWordMatch(word: String): Boolean
+ suspend fun validateMnemonicString(text: String): Result>
+ suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): List
+ suspend fun insertSuggestionWord(text: String, suggestion: String, cursorPosition: Int): InsertSuggestionResult
+
+ companion object {
+ const val MNEMONIC_DELIMITER = " "
+ }
+}
+
+data class InsertSuggestionResult(
+ val text: String,
+ val cursorPosition: Int,
+)
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/di/OnboardingDomainModule.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/di/OnboardingDomainModule.kt
new file mode 100644
index 0000000000..108700efe3
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/di/OnboardingDomainModule.kt
@@ -0,0 +1,21 @@
+package com.tangem.feature.onboarding.domain.di
+
+import com.tangem.feature.onboarding.data.MnemonicRepository
+import com.tangem.feature.onboarding.domain.DefaultSeedPhraseInteractor
+import com.tangem.feature.onboarding.domain.SeedPhraseInteractor
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+class OnboardingDomainModule {
+
+ @Provides
+ @Singleton
+ fun provideSeedPhraseInteractor(repository: MnemonicRepository): SeedPhraseInteractor {
+ return DefaultSeedPhraseInteractor(repository)
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/ButtonState.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/ButtonState.kt
new file mode 100644
index 0000000000..842a7a5141
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/ButtonState.kt
@@ -0,0 +1,11 @@
+package com.tangem.feature.onboarding.presentation.wallet2.model
+
+/**
+[REDACTED_AUTHOR]
+ */
+data class ButtonState(
+ val enabled: Boolean = true,
+ val isClickable: Boolean = true,
+ val showProgress: Boolean = false,
+ val onClick: () -> Unit,
+)
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/DescriptionResource.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/DescriptionResource.kt
new file mode 100644
index 0000000000..db8f0d469b
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/DescriptionResource.kt
@@ -0,0 +1,8 @@
+package com.tangem.feature.onboarding.presentation.wallet2.model
+
+import androidx.annotation.StringRes
+
+data class DescriptionResource(
+ @StringRes val titleRes: Int,
+ @StringRes val subTitleRes: Int,
+)
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt
new file mode 100644
index 0000000000..9eea61c93b
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt
@@ -0,0 +1,71 @@
+package com.tangem.feature.onboarding.presentation.wallet2.model
+
+import androidx.compose.ui.text.input.TextFieldValue
+import com.tangem.feature.onboarding.domain.SeedPhraseError
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+
+/**
+[REDACTED_AUTHOR]
+ */
+data class OnboardingSeedPhraseState(
+ val step: OnboardingSeedPhraseStep,
+ val introState: IntroState,
+ val aboutState: AboutState,
+ val yourSeedPhraseState: YourSeedPhraseState,
+ val checkSeedPhraseState: CheckSeedPhraseState,
+ val importSeedPhraseState: ImportSeedPhraseState,
+ val menuButtonChat: ButtonState,
+ val isOnboardingFinished: Boolean = false,
+)
+
+enum class OnboardingSeedPhraseStep {
+ Intro, AboutSeedPhrase, YourSeedPhrase, CheckSeedPhrase, ImportSeedPhrase
+}
+
+data class IntroState(
+ val cardImageUrl: String? = null,
+ val buttonCreateWallet: ButtonState,
+ val buttonOtherOptions: ButtonState,
+)
+
+data class AboutState(
+ val buttonReadMoreAboutSeedPhrase: ButtonState,
+ val buttonGenerateSeedPhrase: ButtonState,
+ val buttonImportSeedPhrase: ButtonState,
+)
+
+data class YourSeedPhraseState(
+ val mnemonicGridItems: ImmutableList = persistentListOf(),
+ val buttonContinue: ButtonState,
+)
+
+data class MnemonicGridItem(
+ val index: Int,
+ val mnemonic: String,
+)
+
+data class CheckSeedPhraseState(
+ val tvSecondPhrase: TextFieldState,
+ val tvSeventhPhrase: TextFieldState,
+ val tvEleventhPhrase: TextFieldState,
+ val buttonCreateWallet: ButtonState,
+)
+
+data class ImportSeedPhraseState(
+ val tvSeedPhrase: TextFieldState,
+ val onSuggestedPhraseClick: (Int) -> Unit,
+ val buttonCreateWallet: ButtonState,
+ val invalidWords: Set = emptySet(),
+ val suggestionsList: List = emptyList(),
+ val error: SeedPhraseError? = null,
+)
+
+data class TextFieldState(
+ val onTextFieldValueChanged: (TextFieldValue) -> Unit,
+ val textFieldValue: TextFieldValue = TextFieldValue(),
+ val label: String? = null,
+ val isError: Boolean = false,
+ val isFocused: Boolean = false,
+ val onFocusChanged: (Boolean) -> Unit = {},
+)
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt
new file mode 100644
index 0000000000..419b277b9d
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt
@@ -0,0 +1,56 @@
+package com.tangem.feature.onboarding.presentation.wallet2.model
+
+import androidx.compose.ui.text.input.TextFieldValue
+
+/**
+[REDACTED_AUTHOR]
+ */
+data class UiActions(
+ val introActions: IntroUiAction,
+ val aboutActions: AboutUiAction,
+ val yourSeedPhraseActions: YourSeedPhraseUiAction,
+ val checkSeedPhraseActions: CheckSeedPhraseUiAction,
+ val importSeedPhraseActions: ImportSeedPhraseUiAction,
+ val menuChatClick: () -> Unit,
+)
+
+data class IntroUiAction(
+ val buttonCreateWalletClick: () -> Unit,
+ val buttonOtherOptionsClick: () -> Unit,
+)
+
+data class AboutUiAction(
+ val buttonReadMoreAboutSeedPhraseClick: () -> Unit,
+ val buttonGenerateSeedPhraseClick: () -> Unit,
+ val buttonImportSeedPhraseClick: () -> Unit,
+)
+
+data class YourSeedPhraseUiAction(
+ val buttonContinueClick: () -> Unit,
+)
+
+data class CheckSeedPhraseUiAction(
+ val secondTextFieldAction: TextFieldUiAction,
+ val seventhTextFieldAction: TextFieldUiAction,
+ val eleventhTextFieldAction: TextFieldUiAction,
+ val buttonCreateWalletClick: () -> Unit,
+)
+
+data class ImportSeedPhraseUiAction(
+ val phraseTextFieldAction: TextFieldUiAction,
+ val suggestedPhraseClick: (Int) -> Unit,
+ val buttonCreateWalletClick: () -> Unit,
+)
+
+data class TextFieldUiAction(
+ val onTextFieldChanged: (TextFieldValue) -> Unit,
+ val onFocusChanged: (Boolean) -> Unit = {},
+)
+
+
+
+enum class SeedPhraseField {
+ Second,
+ Seventh,
+ Eleventh,
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt
new file mode 100644
index 0000000000..fc583bccc7
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt
@@ -0,0 +1,148 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import com.tangem.core.ui.components.SecondaryButton
+import com.tangem.core.ui.components.SpacerH32
+import com.tangem.core.ui.components.SpacerW8
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.feature.onboarding.R
+import com.tangem.feature.onboarding.presentation.wallet2.model.AboutState
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
+
+/**
+[REDACTED_AUTHOR]
+ */
+@Composable
+fun AboutSeedPhraseScreen(state: AboutState, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ ) {
+ Box(
+ modifier = Modifier.weight(weight = 0.8f),
+ ) {
+ EditIconBlock()
+ }
+ Box(
+ modifier = Modifier.weight(weight = 1.2f),
+ ) {
+ Column {
+ OnboardingDescriptionBlock {
+ Description(
+ titleRes = R.string.onboarding_seed_intro_title,
+ subTitleRes = R.string.onboarding_seed_intro_message,
+ )
+ }
+ ReadMoreBlock(state)
+ }
+ }
+ Box {
+ OnboardingActionBlock(
+ firstActionContent = {
+ SecondaryButton(
+ modifier = Modifier
+ .fillMaxWidth(),
+ text = stringResource(id = R.string.onboarding_seed_intro_button_generate),
+ enabled = state.buttonGenerateSeedPhrase.enabled,
+ showProgress = state.buttonGenerateSeedPhrase.showProgress,
+ onClick = state.buttonGenerateSeedPhrase.onClick,
+ )
+ },
+ secondActionContent = {
+ SecondaryButton(
+ modifier = Modifier
+ .fillMaxWidth(),
+ text = stringResource(id = R.string.onboarding_seed_intro_button_import),
+ enabled = state.buttonImportSeedPhrase.enabled,
+ showProgress = state.buttonImportSeedPhrase.showProgress,
+ onClick = state.buttonImportSeedPhrase.onClick,
+ )
+ },
+ )
+ }
+ }
+}
+
+@Composable
+private fun EditIconBlock() {
+ Column(
+ modifier = Modifier
+ .fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Image(
+ modifier = Modifier
+ .size(TangemTheme.dimens.size44),
+ painter = painterResource(id = R.drawable.ic_onboarding_text_edit_56),
+ contentDescription = null,
+ )
+ SpacerH32()
+ Box(
+ modifier = Modifier
+ .background(
+ color = TangemTheme.colors.icon.warning.copy(alpha = 0.12f),
+ shape = RoundedCornerShape(TangemTheme.dimens.radius8),
+ ),
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(
+ horizontal = TangemTheme.dimens.size12,
+ vertical = TangemTheme.dimens.size4,
+ ),
+ text = stringResource(id = R.string.onboarding_seed_phrase_intro_legacy),
+ color = TangemTheme.colors.text.warning,
+ style = TangemTheme.typography.body1,
+ )
+ }
+ }
+}
+
+@Composable
+private fun ReadMoreBlock(state: AboutState) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = TangemTheme.dimens.size16),
+ ) {
+ val shape = TangemTheme.shapes.roundedCornersLarge
+ Row(
+ modifier = Modifier
+ .align(Alignment.Center)
+ .clip(shape)
+ .border(
+ width = TangemTheme.dimens.size1,
+ color = TangemTheme.colors.stroke.primary,
+ shape = shape,
+ )
+ .clickable(onClick = state.buttonReadMoreAboutSeedPhrase.onClick)
+ .padding(
+ horizontal = TangemTheme.dimens.size16,
+ vertical = TangemTheme.dimens.size8,
+ ),
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_arrow_top_right_24),
+ contentDescription = null,
+ )
+ SpacerW8()
+ Text(
+ text = stringResource(id = R.string.onboarding_seed_button_read_more),
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt
new file mode 100644
index 0000000000..b67af3906f
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt
@@ -0,0 +1,98 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import com.tangem.core.ui.R
+import com.tangem.core.ui.components.OutlineTextField
+import com.tangem.core.ui.components.PrimaryButtonIconLeft
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.feature.onboarding.presentation.wallet2.model.CheckSeedPhraseState
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
+
+/**
+[REDACTED_AUTHOR]
+ */
+@Suppress("ReusedModifierInstance")
+@Composable
+fun CheckSeedPhraseScreen(state: CheckSeedPhraseState, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier
+ .fillMaxSize(),
+ ) {
+ LazyColumn(
+ modifier = modifier
+ .weight(1f)
+ .fillMaxWidth(),
+ ) {
+
+ item {
+ OnboardingDescriptionBlock(
+ modifier = Modifier
+ .weight(1f)
+ .padding(top = TangemTheme.dimens.size48)
+ .padding(horizontal = TangemTheme.dimens.size16),
+ ) {
+ Description(
+ titleRes = R.string.onboarding_seed_user_validation_title,
+ subTitleRes = R.string.onboarding_seed_user_validation_message,
+ )
+ }
+ }
+ item {
+ CheckSeedPhraseBlock(
+ modifier = Modifier
+ .imePadding()
+ .weight(1f)
+ .padding(top = TangemTheme.dimens.size20)
+ .padding(horizontal = TangemTheme.dimens.size16),
+ state = state,
+ )
+ }
+ }
+ OnboardingActionBlock(
+ firstActionContent = {
+ PrimaryButtonIconLeft(
+ modifier = Modifier
+ .fillMaxWidth(),
+ text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet),
+ icon = painterResource(id = R.drawable.ic_tangem_24),
+ enabled = state.buttonCreateWallet.enabled,
+ showProgress = state.buttonCreateWallet.showProgress,
+ onClick = state.buttonCreateWallet.onClick,
+ )
+ },
+ )
+ }
+}
+
+@Composable
+private fun CheckSeedPhraseBlock(state: CheckSeedPhraseState, modifier: Modifier = Modifier) {
+ Column(modifier) {
+ OutlineTextField(
+ value = state.tvSecondPhrase.textFieldValue,
+ onValueChange = state.tvSecondPhrase.onTextFieldValueChanged,
+ label = state.tvSecondPhrase.label,
+ isError = state.tvSecondPhrase.isError,
+ )
+
+ OutlineTextField(
+ value = state.tvSeventhPhrase.textFieldValue,
+ onValueChange = state.tvSeventhPhrase.onTextFieldValueChanged,
+ label = state.tvSeventhPhrase.label,
+ isError = state.tvSeventhPhrase.isError,
+ )
+
+ OutlineTextField(
+ value = state.tvEleventhPhrase.textFieldValue,
+ onValueChange = state.tvEleventhPhrase.onTextFieldValueChanged,
+ label = state.tvEleventhPhrase.label,
+ isError = state.tvEleventhPhrase.isError,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt
new file mode 100644
index 0000000000..1db145ce7f
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt
@@ -0,0 +1,140 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui
+
+import androidx.compose.animation.*
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.unit.IntOffset
+import com.tangem.core.ui.components.PrimaryButton
+import com.tangem.core.ui.components.PrimaryButtonIconLeft
+import com.tangem.core.ui.components.TangemTextFieldsDefault
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.feature.onboarding.R
+import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseState
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
+import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.InvalidWordsColorTransformation
+import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseErrorConverter
+
+/**
+[REDACTED_AUTHOR]
+ */
+@Composable
+fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ ) {
+ Box(
+ modifier = Modifier.weight(1f),
+ ) {
+ Column {
+ OnboardingDescriptionBlock(
+ modifier = Modifier.padding(top = TangemTheme.dimens.size16),
+ ) {
+ DescriptionSubTitleText(text = stringResource(id = R.string.onboarding_seed_import_message))
+ }
+ PhraseBlock(
+ modifier = Modifier
+ .padding(top = TangemTheme.dimens.size62)
+ .padding(horizontal = TangemTheme.dimens.size16),
+ state = state,
+ )
+ SuggestionsBlock(
+ state = state,
+ )
+ }
+ }
+ Box {
+ OnboardingActionBlock(
+ firstActionContent = {
+ PrimaryButtonIconLeft(
+ modifier = Modifier
+ .fillMaxWidth(),
+ text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet),
+ icon = painterResource(id = R.drawable.ic_tangem_24),
+ enabled = state.buttonCreateWallet.enabled,
+ showProgress = state.buttonCreateWallet.showProgress,
+ onClick = state.buttonCreateWallet.onClick,
+ )
+ },
+ )
+ }
+ }
+}
+
+@Composable
+private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modifier) {
+ // TODO: replace by TextReference
+ val errorConverter = remember { SeedPhraseErrorConverter() }
+
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ ) {
+ OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(TangemTheme.dimens.size142),
+ value = state.tvSeedPhrase.textFieldValue,
+ onValueChange = state.tvSeedPhrase.onTextFieldValueChanged,
+ textStyle = TangemTheme.typography.body1,
+ singleLine = false,
+ visualTransformation = InvalidWordsColorTransformation(
+ wordsToBrush = state.invalidWords,
+ style = SpanStyle(color = TangemTheme.colors.text.warning),
+ ),
+ colors = TangemTextFieldsDefault.defaultTextFieldColors,
+ )
+
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(TangemTheme.dimens.size32),
+ ) {
+ val message = errorConverter.convert(LocalContext.current to state.error)
+ if (message != null) {
+ Text(
+ modifier = Modifier.fillMaxSize(),
+ text = message,
+ style = TangemTheme.typography.caption.copy(
+ color = TangemTheme.colors.text.warning,
+ ),
+ )
+ }
+ }
+ }
+}
+
+@Suppress("ReusedModifierInstance")
+@Composable
+private fun SuggestionsBlock(state: ImportSeedPhraseState, modifier: Modifier = Modifier) {
+ AnimatedVisibility(
+ enter = fadeIn() + slideIn(initialOffset = { IntOffset(x = 200, y = 0) }),
+ exit = slideOut(targetOffset = { IntOffset(x = -200, y = 0) }) + fadeOut(),
+ visible = state.suggestionsList.isNotEmpty(),
+ ) {
+ LazyRow(
+ modifier = modifier.fillMaxSize(),
+ contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16),
+ ) {
+ items(state.suggestionsList.size) { index ->
+ PrimaryButton(
+ modifier = Modifier
+ .height(TangemTheme.dimens.size46)
+ .padding(all = TangemTheme.dimens.size4),
+ text = state.suggestionsList[index],
+ onClick = { state.onSuggestedPhraseClick(index) },
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/IntroScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/IntroScreen.kt
new file mode 100644
index 0000000000..c99a233f33
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/IntroScreen.kt
@@ -0,0 +1,102 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import coil.compose.SubcomposeAsyncImage
+import coil.request.ImageRequest
+import com.tangem.core.ui.components.PrimaryButtonIconLeft
+import com.tangem.core.ui.components.SecondaryButton
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.feature.onboarding.R
+import com.tangem.feature.onboarding.presentation.wallet2.model.IntroState
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
+
+/**
+[REDACTED_AUTHOR]
+ */
+@Composable
+fun IntroScreen(state: IntroState, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ ) {
+ Box(
+ modifier = Modifier.weight(weight = 1.5f),
+ ) {
+ CardImageBlock(state.cardImageUrl)
+ }
+ Box(
+ modifier = Modifier.weight(weight = 1f),
+ ) {
+ OnboardingDescriptionBlock {
+ Description(
+ titleRes = R.string.onboarding_create_wallet_options_title,
+ subTitleRes = R.string.onboarding_create_wallet_options_message,
+ )
+ }
+ }
+ Box {
+ OnboardingActionBlock(
+ firstActionContent = {
+ PrimaryButtonIconLeft(
+ modifier = Modifier
+ .fillMaxWidth(),
+ text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet),
+ icon = painterResource(id = R.drawable.ic_tangem_24),
+ enabled = state.buttonCreateWallet.enabled,
+ showProgress = state.buttonCreateWallet.showProgress,
+ onClick = state.buttonCreateWallet.onClick,
+ )
+ },
+ secondActionContent = {
+ SecondaryButton(
+ modifier = Modifier
+ .fillMaxWidth(),
+ text = stringResource(id = R.string.onboarding_create_wallet_options_button_options),
+ enabled = state.buttonOtherOptions.enabled,
+ showProgress = state.buttonOtherOptions.showProgress,
+ onClick = state.buttonOtherOptions.onClick,
+ )
+ },
+ )
+ }
+ }
+}
+
+@Composable
+private fun CardImageBlock(cardImageUrl: String? = null) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ val cardImageModifier = Modifier
+ .fillMaxSize()
+ .align(Alignment.Center)
+
+ SubcomposeAsyncImage(
+ modifier = cardImageModifier.padding(horizontal = TangemTheme.dimens.size34),
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(cardImageUrl)
+ .crossfade(true)
+ .build(),
+ loading = { CardPlaceHolder(cardImageModifier) },
+ error = { CardPlaceHolder(cardImageModifier) },
+ contentDescription = null,
+ )
+ }
+}
+
+@Composable
+private fun CardPlaceHolder(modifier: Modifier = Modifier) {
+ Image(
+ modifier = modifier,
+ painter = painterResource(id = R.drawable.card_placeholder_black),
+ contentScale = ContentScale.Fit,
+ contentDescription = null,
+ )
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt
new file mode 100644
index 0000000000..76a7e48923
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt
@@ -0,0 +1,91 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.tangem.core.ui.components.PrimaryButton
+import com.tangem.core.ui.components.SpacerW32
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.feature.onboarding.R
+import com.tangem.feature.onboarding.presentation.wallet2.model.MnemonicGridItem
+import com.tangem.feature.onboarding.presentation.wallet2.model.YourSeedPhraseState
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
+import kotlinx.collections.immutable.ImmutableList
+
+/**
+[REDACTED_AUTHOR]
+ */
+@Composable
+fun YourSeedPhraseScreen(state: YourSeedPhraseState, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ ) {
+ Box(
+ modifier = Modifier.weight(weight = 0.6f),
+ ) {
+ OnboardingDescriptionBlock(
+ modifier = Modifier.align(Alignment.Center),
+ ) {
+ Description(
+ titleRes = R.string.onboarding_seed_generate_title,
+ subTitleRes = R.string.onboarding_seed_generate_message,
+ )
+ }
+ }
+ Box(
+ modifier = Modifier.weight(weight = 1f),
+ ) {
+ PhraseGreedBlock(state.mnemonicGridItems)
+ }
+
+ Box {
+ OnboardingActionBlock(
+ firstActionContent = {
+ PrimaryButton(
+ modifier = Modifier
+ .fillMaxWidth(),
+ text = stringResource(id = R.string.common_continue),
+ onClick = state.buttonContinue.onClick,
+ )
+ },
+ )
+ }
+ }
+}
+
+@Composable
+private fun PhraseGreedBlock(mnemonicGridItems: ImmutableList) {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(count = 2),
+ ) {
+ this.items(
+ count = mnemonicGridItems.size,
+ ) { index ->
+ Row(
+ modifier = Modifier.padding(all = TangemTheme.dimens.size8),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ val item = mnemonicGridItems[index]
+ SpacerW32()
+ Text(
+ modifier = Modifier.width(TangemTheme.dimens.size40),
+ text = "${item.index}.",
+ style = TangemTheme.typography.body2,
+ color = TangemTheme.colors.text.secondary,
+ )
+ Text(
+ text = item.mnemonic,
+ style = TangemTheme.typography.button,
+ color = TangemTheme.colors.text.primary1,
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingActionBlock.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingActionBlock.kt
new file mode 100644
index 0000000000..b7706cb69e
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingActionBlock.kt
@@ -0,0 +1,38 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.tangem.core.ui.components.SpacerH12
+import com.tangem.core.ui.res.TangemTheme
+
+/**
+[REDACTED_AUTHOR]
+ */
+@Composable
+fun OnboardingActionBlock(
+ firstActionContent: @Composable (() -> Unit)? = null,
+ secondActionContent: @Composable (() -> Unit)? = null,
+) {
+ if (firstActionContent == null && secondActionContent == null) return
+
+ Box(
+ modifier = Modifier
+ .padding(horizontal = TangemTheme.dimens.size16)
+ .padding(
+ top = TangemTheme.dimens.size8,
+ bottom = TangemTheme.dimens.size32,
+ ),
+ ) {
+ Column(
+ modifier = Modifier.align(Alignment.BottomCenter),
+ ) {
+ firstActionContent?.invoke()
+ if (firstActionContent != null && secondActionContent != null) SpacerH12()
+ secondActionContent?.invoke()
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt
new file mode 100644
index 0000000000..27a76c7f38
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt
@@ -0,0 +1,72 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui.components
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import com.tangem.core.ui.components.SpacerH12
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.feature.onboarding.presentation.wallet2.model.DescriptionResource
+import kotlinx.collections.immutable.ImmutableList
+
+/**
+ * UI component witch provides single and carousel description for the onboarding process
+ */
+@Composable
+fun OnboardingDescriptionBlock(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = TangemTheme.dimens.size24),
+ ) {
+ content()
+ }
+}
+
+@Composable
+fun Description(@StringRes titleRes: Int, @StringRes subTitleRes: Int) {
+ Column {
+ DescriptionTitleText(text = stringResource(id = titleRes))
+ SpacerH12()
+ DescriptionSubTitleText(text = stringResource(id = subTitleRes))
+ }
+}
+
+@Suppress("UnusedPrivateMember")
+@Composable
+fun OnboardingCarouselDescriptionBlock(
+ descriptionsList: ImmutableList,
+ modifier: Modifier = Modifier,
+) {
+ OnboardingDescriptionBlock(modifier) {
+ // TODO: implement
+ }
+}
+
+@Composable
+fun DescriptionTitleText(text: String) {
+ Text(
+ text = text,
+ style = TangemTheme.typography.h2,
+ color = TangemTheme.colors.text.primary1,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
+
+@Composable
+fun DescriptionSubTitleText(text: String) {
+ Text(
+ text = text,
+ style = TangemTheme.typography.subtitle1,
+ color = TangemTheme.colors.text.secondary,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/CheckSeedPhraseStateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/CheckSeedPhraseStateBuilder.kt
new file mode 100644
index 0000000000..aad9659b9d
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/CheckSeedPhraseStateBuilder.kt
@@ -0,0 +1,77 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders
+
+import androidx.compose.ui.text.input.TextFieldValue
+import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
+import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField
+
+/**
+[REDACTED_AUTHOR]
+ */
+class CheckSeedPhraseStateBuilder {
+
+ fun updateTextField(
+ uiState: OnboardingSeedPhraseState,
+ field: SeedPhraseField,
+ textFieldValue: TextFieldValue,
+ ): OnboardingSeedPhraseState = when (field) {
+ SeedPhraseField.Second -> uiState.copy(
+ checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
+ tvSecondPhrase = uiState.checkSeedPhraseState.tvSecondPhrase.copy(
+ textFieldValue = textFieldValue,
+ ),
+ ),
+ )
+ SeedPhraseField.Seventh -> uiState.copy(
+ checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
+ tvSeventhPhrase = uiState.checkSeedPhraseState.tvSeventhPhrase.copy(
+ textFieldValue = textFieldValue,
+ ),
+ ),
+ )
+ SeedPhraseField.Eleventh -> uiState.copy(
+ checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
+ tvEleventhPhrase = uiState.checkSeedPhraseState.tvEleventhPhrase.copy(
+ textFieldValue = textFieldValue,
+ ),
+ ),
+ )
+ }
+
+ fun updateTextFieldError(
+ uiState: OnboardingSeedPhraseState,
+ field: SeedPhraseField,
+ hasError: Boolean,
+ ): OnboardingSeedPhraseState = when (field) {
+ SeedPhraseField.Second -> uiState.copy(
+ checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
+ tvSecondPhrase = uiState.checkSeedPhraseState.tvSecondPhrase.copy(
+ isError = hasError,
+ ),
+ ),
+ )
+ SeedPhraseField.Seventh -> uiState.copy(
+ checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
+ tvSeventhPhrase = uiState.checkSeedPhraseState.tvSeventhPhrase.copy(
+ isError = hasError,
+ ),
+ ),
+ )
+ SeedPhraseField.Eleventh -> uiState.copy(
+ checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
+ tvEleventhPhrase = uiState.checkSeedPhraseState.tvEleventhPhrase.copy(
+ isError = hasError,
+ ),
+ ),
+ )
+ }
+
+ fun updateCreateWalletButton(uiState: OnboardingSeedPhraseState, enabled: Boolean): OnboardingSeedPhraseState {
+ return uiState.copy(
+ checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
+ buttonCreateWallet = uiState.checkSeedPhraseState.buttonCreateWallet.copy(
+ enabled = enabled,
+ ),
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt
new file mode 100644
index 0000000000..5ffb9b5c41
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt
@@ -0,0 +1,67 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders
+
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
+import com.tangem.feature.onboarding.domain.InsertSuggestionResult
+import com.tangem.feature.onboarding.domain.SeedPhraseError
+import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
+
+/**
+[REDACTED_AUTHOR]
+ */
+class ImportSeedPhraseStateBuilder {
+
+ fun updateTextField(
+ uiState: OnboardingSeedPhraseState,
+ textFieldValue: TextFieldValue,
+ ): OnboardingSeedPhraseState = uiState.copy(
+ importSeedPhraseState = uiState.importSeedPhraseState.copy(
+ tvSeedPhrase = uiState.importSeedPhraseState.tvSeedPhrase.copy(
+ textFieldValue = textFieldValue,
+ ),
+ ),
+ )
+
+ fun updateCreateWalletButton(uiState: OnboardingSeedPhraseState, enabled: Boolean): OnboardingSeedPhraseState {
+ return uiState.copy(
+ importSeedPhraseState = uiState.importSeedPhraseState.copy(
+ buttonCreateWallet = uiState.importSeedPhraseState.buttonCreateWallet.copy(
+ enabled = enabled,
+ ),
+ ),
+ )
+ }
+
+ fun updateInvalidWords(uiState: OnboardingSeedPhraseState, invalidWords: Set): OnboardingSeedPhraseState =
+ uiState.copy(
+ importSeedPhraseState = uiState.importSeedPhraseState.copy(
+ invalidWords = invalidWords,
+ ),
+ )
+
+ fun updateSuggestions(uiState: OnboardingSeedPhraseState, suggestions: List): OnboardingSeedPhraseState =
+ uiState.copy(
+ importSeedPhraseState = uiState.importSeedPhraseState.copy(
+ suggestionsList = suggestions,
+ ),
+ )
+
+ fun insertSuggestionWord(
+ uiState: OnboardingSeedPhraseState,
+ insertResult: InsertSuggestionResult,
+ ): OnboardingSeedPhraseState {
+ val newTextFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue.copy(
+ text = insertResult.text,
+ selection = TextRange(insertResult.cursorPosition, insertResult.cursorPosition),
+ )
+ return updateTextField(uiState, newTextFieldValue)
+ }
+
+ fun updateError(uiState: OnboardingSeedPhraseState, error: SeedPhraseError?): OnboardingSeedPhraseState {
+ return uiState.copy(
+ importSeedPhraseState = uiState.importSeedPhraseState.copy(
+ error = error,
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt
new file mode 100644
index 0000000000..f240c101ce
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt
@@ -0,0 +1,126 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders
+
+import com.tangem.feature.onboarding.presentation.wallet2.model.*
+import kotlinx.collections.immutable.ImmutableList
+
+/**
+[REDACTED_AUTHOR]
+ */
+class StateBuilder(
+ private val uiActions: UiActions,
+) {
+
+ val checkSeedPhrase: CheckSeedPhraseStateBuilder = CheckSeedPhraseStateBuilder()
+ val importSeedPhrase: ImportSeedPhraseStateBuilder = ImportSeedPhraseStateBuilder()
+
+ fun init(): OnboardingSeedPhraseState = OnboardingSeedPhraseState(
+ step = OnboardingSeedPhraseStep.Intro,
+ introState = IntroState(
+ buttonCreateWallet = ButtonState(
+ onClick = uiActions.introActions.buttonCreateWalletClick,
+ ),
+ buttonOtherOptions = ButtonState(
+ onClick = uiActions.introActions.buttonOtherOptionsClick,
+ ),
+ ),
+ aboutState = AboutState(
+ buttonReadMoreAboutSeedPhrase = ButtonState(
+ onClick = uiActions.aboutActions.buttonReadMoreAboutSeedPhraseClick,
+ ),
+ buttonGenerateSeedPhrase = ButtonState(
+ onClick = uiActions.aboutActions.buttonGenerateSeedPhraseClick,
+ ),
+ buttonImportSeedPhrase = ButtonState(
+ onClick = uiActions.aboutActions.buttonImportSeedPhraseClick,
+ ),
+ ),
+ yourSeedPhraseState = YourSeedPhraseState(
+ buttonContinue = ButtonState(
+ onClick = uiActions.yourSeedPhraseActions.buttonContinueClick,
+ ),
+ ),
+ checkSeedPhraseState = CheckSeedPhraseState(
+ tvSecondPhrase = TextFieldState(
+ label = "2",
+ isFocused = true,
+ onTextFieldValueChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onTextFieldChanged,
+ onFocusChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onFocusChanged,
+ ),
+ tvSeventhPhrase = TextFieldState(
+ label = "7",
+ isFocused = false,
+ onTextFieldValueChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onTextFieldChanged,
+ onFocusChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onFocusChanged,
+ ),
+ tvEleventhPhrase = TextFieldState(
+ label = "11",
+ isFocused = false,
+ onTextFieldValueChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onTextFieldChanged,
+ onFocusChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onFocusChanged,
+ ),
+ buttonCreateWallet = ButtonState(
+ enabled = false,
+ onClick = uiActions.checkSeedPhraseActions.buttonCreateWalletClick,
+ ),
+ ),
+ importSeedPhraseState = ImportSeedPhraseState(
+ tvSeedPhrase = TextFieldState(
+ onTextFieldValueChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onTextFieldChanged,
+ onFocusChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onFocusChanged,
+ ),
+ onSuggestedPhraseClick = uiActions.importSeedPhraseActions.suggestedPhraseClick,
+ buttonCreateWallet = ButtonState(
+ enabled = false,
+ onClick = uiActions.importSeedPhraseActions.buttonCreateWalletClick,
+ ),
+ ),
+ menuButtonChat = ButtonState(
+ onClick = uiActions.menuChatClick,
+ ),
+ )
+
+ fun changeStep(uiState: OnboardingSeedPhraseState, step: OnboardingSeedPhraseStep): OnboardingSeedPhraseState =
+ uiState.copy(
+ step = step,
+ )
+
+ fun generateMnemonicComponents(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState {
+ return uiState.copy(
+ aboutState = uiState.aboutState.copy(
+ buttonGenerateSeedPhrase = uiState.aboutState.buttonGenerateSeedPhrase.copy(
+ showProgress = true,
+ ),
+ buttonImportSeedPhrase = uiState.aboutState.buttonImportSeedPhrase.copy(
+ enabled = false,
+ ),
+ ),
+ )
+ }
+
+ fun mnemonicGenerated(
+ uiState: OnboardingSeedPhraseState,
+ mnemonicGridItems: ImmutableList,
+ ): OnboardingSeedPhraseState {
+ return updateMnemonicComponents(uiState, mnemonicGridItems)
+ .copy(
+ step = OnboardingSeedPhraseStep.YourSeedPhrase,
+ aboutState = uiState.aboutState.copy(
+ buttonGenerateSeedPhrase = uiState.aboutState.buttonGenerateSeedPhrase.copy(
+ showProgress = false,
+ ),
+ buttonImportSeedPhrase = uiState.aboutState.buttonImportSeedPhrase.copy(
+ enabled = true,
+ ),
+ ),
+ )
+ }
+
+ fun updateMnemonicComponents(
+ uiState: OnboardingSeedPhraseState,
+ mnemonicGridItems: ImmutableList,
+ ): OnboardingSeedPhraseState = uiState.copy(
+ yourSeedPhraseState = uiState.yourSeedPhraseState.copy(
+ mnemonicGridItems = mnemonicGridItems,
+ ),
+ )
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/InvalidWordsColorTransformation.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/InvalidWordsColorTransformation.kt
new file mode 100644
index 0000000000..7d6f3a4685
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/InvalidWordsColorTransformation.kt
@@ -0,0 +1,79 @@
+package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
+
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TransformedText
+import androidx.compose.ui.text.input.VisualTransformation
+import com.tangem.utils.extensions.isSingleItem
+
+/**
+[REDACTED_AUTHOR]
+ */
+class InvalidWordsColorTransformation(
+ private val wordsToBrush: Set,
+ private val style: SpanStyle,
+) : VisualTransformation {
+
+ override fun filter(text: AnnotatedString): TransformedText {
+ val plainText = text.text
+ if (wordsToBrush.isSingleItem() && wordsToBrush.first() == plainText) {
+ return plainText.annotate().toTransformedText()
+ }
+
+ val wordList = makeSeparatedWordlist(plainText)
+ val annotatedString = makeAnnotatedString(wordList)
+
+ return annotatedString.toTransformedText()
+ }
+
+ private fun makeSeparatedWordlist(plainText: String): List {
+ val plainTextCharArray = plainText.toCharArray()
+ val wordlist = mutableListOf()
+ var wordBuilder = StringBuilder()
+
+ for (index in plainTextCharArray.indices) {
+ val char = plainTextCharArray[index]
+ if (char.isWhitespace()) {
+ if (wordBuilder.isEmpty()) {
+ wordlist.add(WORD_SEPARATOR)
+ } else {
+ wordlist.add(wordBuilder.toString())
+ wordBuilder = StringBuilder()
+ wordlist.add(WORD_SEPARATOR)
+ }
+ } else {
+ if (index == plainTextCharArray.lastIndex) {
+ wordBuilder.append(char)
+ wordlist.add(wordBuilder.toString())
+ wordBuilder = StringBuilder()
+ } else {
+ wordBuilder.append(char)
+ }
+ }
+ }
+ return wordlist.toList()
+ }
+
+ private fun makeAnnotatedString(wordList: List): AnnotatedString = buildAnnotatedString {
+ wordList.forEach {
+ if (it == WORD_SEPARATOR) {
+ append(WHITE_SPACE)
+ } else if (wordsToBrush.contains(it)) {
+ append(it.annotate())
+ } else {
+ append(it)
+ }
+ }
+ }
+
+ private fun String.annotate(): AnnotatedString = AnnotatedString(this, style)
+
+ private fun AnnotatedString.toTransformedText(): TransformedText = TransformedText(this, OffsetMapping.Identity)
+
+ companion object {
+ private const val WHITE_SPACE = " "
+ private const val WORD_SEPARATOR = "_$@-*"
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt
new file mode 100644
index 0000000000..dbb553ba63
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt
@@ -0,0 +1,28 @@
+package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
+
+import android.content.Context
+import com.tangem.common.module.ModuleMessageConverter
+import com.tangem.feature.onboarding.R
+import com.tangem.feature.onboarding.domain.SeedPhraseError
+
+/**
+[REDACTED_AUTHOR]
+ */
+class SeedPhraseErrorConverter : ModuleMessageConverter, String?> {
+
+ override fun convert(source: Pair): String? {
+ val (context, message) = source
+
+ val convertedMessage = when (message) {
+ SeedPhraseError.InvalidEntropyLength -> {
+ context.getString(R.string.onboarding_seed_mnemonic_invalid_checksum)
+ }
+ is SeedPhraseError.InvalidWords -> {
+ context.getString(R.string.onboarding_seed_mnemonic_wrong_words)
+ }
+ else -> null
+ }
+
+ return convertedMessage
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt
new file mode 100644
index 0000000000..4fbee7352f
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt
@@ -0,0 +1,318 @@
+package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.tangem.feature.onboarding.domain.SeedPhraseError
+import com.tangem.feature.onboarding.domain.SeedPhraseInteractor
+import com.tangem.feature.onboarding.presentation.wallet2.model.*
+import com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders.StateBuilder
+import com.tangem.utils.coroutines.CoroutineDispatcherProvider
+import com.tangem.utils.coroutines.Debouncer
+import com.tangem.utils.extensions.isEven
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.*
+import java.util.concurrent.atomic.AtomicBoolean
+import javax.inject.Inject
+
+/**
+[REDACTED_AUTHOR]
+ */
+@HiltViewModel
+class SeedPhraseViewModel @Inject constructor(
+ private val interactor: SeedPhraseInteractor,
+ private val dispatchers: CoroutineDispatcherProvider,
+) : ViewModel() {
+
+ private var uiBuilder = StateBuilder(createUiActions())
+
+ var uiState: OnboardingSeedPhraseState by mutableStateOf(uiBuilder.init())
+ private set
+
+ val currentStep: OnboardingSeedPhraseStep
+ get() = uiState.step
+
+ private val textFieldsDebouncers = mutableMapOf()
+ private var suggestionWordInserted: AtomicBoolean = AtomicBoolean(false)
+
+ override fun onCleared() {
+ textFieldsDebouncers.forEach { entry -> entry.value.release() }
+ textFieldsDebouncers.clear()
+ super.onCleared()
+ }
+
+ private fun createUiActions(): UiActions = UiActions(
+ introActions = IntroUiAction(
+ buttonCreateWalletClick = ::buttonCreateWalletClick,
+ buttonOtherOptionsClick = ::buttonOtherOptionsClick,
+ ),
+ aboutActions = AboutUiAction(
+ buttonReadMoreAboutSeedPhraseClick = ::buttonReadMoreAboutSeedPhraseClick,
+ buttonGenerateSeedPhraseClick = ::buttonGenerateSeedPhraseClick,
+ buttonImportSeedPhraseClick = ::buttonImportSeedPhraseClick,
+ ),
+ yourSeedPhraseActions = YourSeedPhraseUiAction(
+ buttonContinueClick = ::buttonContinueClick,
+ ),
+ checkSeedPhraseActions = CheckSeedPhraseUiAction(
+ buttonCreateWalletClick = ::buttonCreateWalletWithSeedPhraseClick,
+ secondTextFieldAction = TextFieldUiAction(
+ onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Second, value) },
+ ),
+ seventhTextFieldAction = TextFieldUiAction(
+ onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Seventh, value) },
+ ),
+ eleventhTextFieldAction = TextFieldUiAction(
+ onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Eleventh, value) },
+ ),
+ ),
+ importSeedPhraseActions = ImportSeedPhraseUiAction(
+ phraseTextFieldAction = TextFieldUiAction(
+ onTextFieldChanged = { value -> onSeedPhraseTextFieldChanged(value) },
+ ),
+ suggestedPhraseClick = ::buttonSuggestedPhraseClick,
+ buttonCreateWalletClick = ::buttonCreateWalletWithSeedPhraseClick,
+ ),
+ menuChatClick = ::menuChatClick,
+ )
+
+ // region CheckSeedPhrase
+ private fun onTextFieldChanged(field: SeedPhraseField, textFieldValue: TextFieldValue) {
+ viewModelScope.launchSingle {
+ updateUi { uiBuilder.checkSeedPhrase.updateTextField(uiState, field, textFieldValue) }
+
+ val fieldState = field.getState(uiState)
+ if (fieldState.textFieldValue.text.isEmpty()) {
+ updateUi { uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError = false) }
+ return@launchSingle
+ }
+
+ createOrGetDebouncer(field.name).debounce(viewModelScope, context = dispatchers.io) {
+ val hasError = !interactor.isWordMatch(textFieldValue.text)
+ if (fieldState.isError != hasError) {
+ updateUi { uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError) }
+ }
+
+ val allFieldsWithoutError = SeedPhraseField.values()
+ .map { field -> field.getState(uiState) }
+ .all { fieldState -> !fieldState.isError }
+
+ if (uiState.checkSeedPhraseState.buttonCreateWallet.enabled != allFieldsWithoutError) {
+ updateUi { uiBuilder.checkSeedPhrase.updateCreateWalletButton(uiState, allFieldsWithoutError) }
+ }
+ }
+ }
+ }
+ // endregion CheckSeedPhrase
+
+ // region ImportSeedPhrase
+ private fun onSeedPhraseTextFieldChanged(textFieldValue: TextFieldValue) {
+ val oldTextFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue
+ val isSameText = textFieldValue.text == oldTextFieldValue.text
+ val isCursorMoved = textFieldValue.selection != oldTextFieldValue.selection
+
+ val mediateState = uiBuilder.importSeedPhrase.updateTextField(uiState, textFieldValue)
+ uiState = uiBuilder.importSeedPhrase.updateCreateWalletButton(mediateState, enabled = false)
+
+ val fieldState = uiState.importSeedPhraseState.tvSeedPhrase
+ val inputMnemonic = fieldState.textFieldValue.text
+
+ val debouncer = createOrGetDebouncer(MNEMONIC_DEBOUNCER)
+ when {
+ suggestionWordInserted.getAndSet(false) -> {
+ debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) {
+ validateMnemonic(inputMnemonic)
+ }
+ }
+ isSameText && !isCursorMoved -> {
+ // do nothing
+ }
+ isSameText && isCursorMoved -> {
+ debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) {
+ updateSuggestions(fieldState)
+ }
+ }
+ else -> {
+ debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) {
+ updateSuggestions(fieldState)
+ validateMnemonic(inputMnemonic)
+ }
+ }
+ }
+ }
+
+ private suspend fun validateMnemonic(inputMnemonic: String) {
+ if (inputMnemonic.isEmpty() && uiState.importSeedPhraseState.error != null) {
+ updateUi { uiBuilder.importSeedPhrase.updateError(uiState, null) }
+ return
+ }
+
+ interactor.validateMnemonicString(inputMnemonic)
+ .onSuccess {
+ updateUi { uiBuilder.importSeedPhrase.updateError(uiState, null) }
+ }
+ .onFailure {
+ val error = it as? SeedPhraseError ?: return
+
+ val mediateState = when (error) {
+ is SeedPhraseError.InvalidWords -> {
+ uiBuilder.importSeedPhrase.updateInvalidWords(uiState, error.words)
+ }
+ else -> uiState
+ }
+ updateUi { uiBuilder.importSeedPhrase.updateError(mediateState, error) }
+ }
+ }
+
+ private suspend fun updateSuggestions(fieldState: TextFieldState) {
+ val suggestions = interactor.getSuggestions(
+ text = fieldState.textFieldValue.text,
+ hasSelection = !fieldState.textFieldValue.selection.collapsed,
+ cursorPosition = fieldState.textFieldValue.selection.end,
+ )
+ updateUi { uiBuilder.importSeedPhrase.updateSuggestions(uiState, suggestions) }
+ }
+ // endregion ImportSeedPhrase
+
+ // region ButtonClickHandlers
+ @Suppress("EmptyFunctionBlock")
+ private fun buttonCreateWalletClick() {
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ private fun buttonCreateWalletWithSeedPhraseClick() {
+ }
+
+ private fun buttonOtherOptionsClick() {
+ viewModelScope.launchSingle {
+ updateUi { uiBuilder.changeStep(uiState, OnboardingSeedPhraseStep.AboutSeedPhrase) }
+ }
+ }
+
+ private fun buttonReadMoreAboutSeedPhraseClick() {
+ // TODO: open the about info through a router
+ }
+
+ private fun buttonGenerateSeedPhraseClick() {
+ viewModelScope.launchSingle {
+ updateUi { uiBuilder.generateMnemonicComponents(uiState) }
+ delay(DELAY_GENERATE_SEED_PHRASE)
+ interactor.generateMnemonic()
+ .onSuccess { mnemonic ->
+ val mnemonicGridItems = generateMnemonicGridList(mnemonic.mnemonicComponents)
+ updateUi { uiBuilder.mnemonicGenerated(uiState, mnemonicGridItems.toImmutableList()) }
+ }
+ .onFailure {
+ // TODO: show error
+ }
+ }
+ }
+
+ private suspend fun generateMnemonicGridList(mnemonicComponents: List): ImmutableList {
+ val size = mnemonicComponents.size
+ val splitIndex = if (size.isEven()) size / 2 else size / 2 + 1
+ val leftColumn = mnemonicComponents.subList(0, splitIndex)
+ val rightColumn = mnemonicComponents.subList(splitIndex, size)
+
+ val mnemonicGridItems = mutableListOf()
+ for (index in 0 until splitIndex) {
+ if (index <= leftColumn.size) {
+ val item = MnemonicGridItem(
+ index = index + 1,
+ mnemonic = leftColumn[index],
+ )
+ mnemonicGridItems.add(item)
+ }
+ if (index < rightColumn.size) {
+ val item = MnemonicGridItem(
+ index = index + splitIndex + 1,
+ mnemonic = rightColumn[index],
+ )
+ mnemonicGridItems.add(item)
+ }
+ }
+ return mnemonicGridItems.toImmutableList()
+ }
+
+ private fun buttonImportSeedPhraseClick() {
+ viewModelScope.launchSingle {
+ updateUi { uiBuilder.changeStep(uiState, OnboardingSeedPhraseStep.ImportSeedPhrase) }
+ }
+ }
+
+ private fun buttonContinueClick() {
+ viewModelScope.launchSingle {
+ updateUi { uiBuilder.changeStep(uiState, OnboardingSeedPhraseStep.CheckSeedPhrase) }
+ }
+ }
+
+ private fun buttonSuggestedPhraseClick(suggestionIndex: Int) {
+ viewModelScope.launchSingle {
+ suggestionWordInserted.set(true)
+ val textFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue
+ val word = uiState.importSeedPhraseState.suggestionsList[suggestionIndex]
+ val cursorPosition = textFieldValue.selection.end
+
+ val insertResult = interactor.insertSuggestionWord(
+ text = textFieldValue.text,
+ suggestion = word,
+ cursorPosition = cursorPosition,
+ )
+ updateUi {
+ val mediateState = uiBuilder.importSeedPhrase.insertSuggestionWord(uiState, insertResult)
+ uiBuilder.importSeedPhrase.updateSuggestions(mediateState, emptyList())
+ }
+ }
+ }
+
+ private fun menuChatClick() {
+ // TODO: open the support chat through a router
+ }
+ // endregion ButtonClickHandlers
+
+ // region Utils
+ /**
+ * Updating the UI with a contract where all copying of objects are called in the Single thread context and
+ * updating the UiState in the main context.
+ */
+ private suspend fun updateUi(updateBlock: suspend () -> OnboardingSeedPhraseState) {
+ withSingleContext {
+ val newState = updateBlock.invoke()
+ withMainContext { uiState = newState }
+ }
+ }
+
+ private fun createOrGetDebouncer(name: String): Debouncer {
+ return textFieldsDebouncers[name] ?: Debouncer().apply { textFieldsDebouncers[name] = this }
+ }
+
+ private fun SeedPhraseField.getState(uiState: OnboardingSeedPhraseState): TextFieldState = when (this) {
+ SeedPhraseField.Second -> uiState.checkSeedPhraseState.tvSecondPhrase
+ SeedPhraseField.Seventh -> uiState.checkSeedPhraseState.tvSeventhPhrase
+ SeedPhraseField.Eleventh -> uiState.checkSeedPhraseState.tvEleventhPhrase
+ }
+
+ private fun CoroutineScope.launchSingle(block: suspend CoroutineScope.() -> Unit): Job {
+ return viewModelScope.launch(dispatchers.single, block = block)
+ }
+
+ private suspend fun withMainContext(block: suspend CoroutineScope.() -> T): T {
+ return withContext(dispatchers.main, block)
+ }
+
+ private suspend fun withSingleContext(block: suspend CoroutineScope.() -> T): T {
+ return withContext(dispatchers.single, block)
+ }
+ // endregion Utils
+
+ companion object {
+ private const val MNEMONIC_DEBOUNCER = "MnemonicDebouncer"
+ private const val MNEMONIC_DEBOUNCE_DELAY = 700L
+ private const val DELAY_GENERATE_SEED_PHRASE = 300L
+ }
+}
\ No newline at end of file
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt
index 1c1ea1538f..ffd683f981 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt
@@ -359,7 +359,7 @@ internal class SwapViewModel @Inject constructor(
lastAmount.value = cutValue
uiState =
stateBuilder.updateSwapAmount(uiState, inputNumberFormatter.formatWithThousands(cutValue, decimals))
- amountDebouncer.debounce(DEBOUNCE_AMOUNT_DELAY, viewModelScope) {
+ amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) {
startLoadingQuotes(fromToken, toToken, lastAmount.value)
}
}
diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt
index 5d0b3a45e4..c1af6d55fe 100644
--- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt
+++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt
@@ -20,6 +20,7 @@ internal fun BaseExtension.configureCompilerOptions() {
internal fun BaseExtension.configureCompose(project: Project) {
val useCompose = with(project.path) {
contains(":ui") ||
+ contains(":onboarding") ||
contains(":presentation") ||
contains(":app") || // TODO: [REDACTED_JIRA]
contains(":tester:impl") // TODO: Rename module
diff --git a/settings.gradle.kts b/settings.gradle.kts
index c8c3af9b91..70f5aeb9e2 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -46,6 +46,7 @@ include(":libs:auth")
// endregion Libs modules
// region Feature modules
+include(":features:onboarding")
include(":features:referral:data")
include(":features:referral:domain")
include(":features:referral:presentation")