Updated on 2026-08-14
This commit is contained in:
commit
cad324a23b
47 changed files with 4143 additions and 20 deletions
|
|
@ -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"))
|
||||
|
|
|
|||
9
app/src/main/res/drawable/ic_onboarding_chat_24.xml
Normal file
9
app/src/main/res/drawable/ic_onboarding_chat_24.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M9,22C8.735,22 8.48,21.895 8.293,21.707C8.105,21.52 8,21.265 8,21V18H4C3.47,18 2.961,17.789 2.586,17.414C2.211,17.039 2,16.53 2,16V4C2,2.89 2.9,2 4,2H20C20.53,2 21.039,2.211 21.414,2.586C21.789,2.961 22,3.47 22,4V16C22,16.53 21.789,17.039 21.414,17.414C21.039,17.789 20.53,18 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22H9ZM10,16V19.08L13.08,16H20V4H4V16H10ZM6,7H18V9H6V7ZM6,11H15V13H6V11Z"
|
||||
android:fillColor="#656565"/>
|
||||
</vector>
|
||||
13
app/src/main/res/drawable/ic_onboarding_qr_code_scan_24.xml
Normal file
13
app/src/main/res/drawable/ic_onboarding_qr_code_scan_24.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24h-24z"/>
|
||||
<path
|
||||
android:pathData="M4,4H10V10H4V4ZM20,4V10H14V4H20ZM14,15H16V13H14V11H16V13H18V11H20V13H18V15H20V18H18V20H16V18H13V20H11V16H14V15ZM16,15V18H18V15H16ZM4,20V14H10V20H4ZM6,6V8H8V6H6ZM16,6V8H18V6H16ZM6,16V18H8V16H6ZM4,11H6V13H4V11ZM9,11H13V15H11V13H9V11ZM11,6H13V10H11V6ZM2,2V6H0V2C0,1.47 0.211,0.961 0.586,0.586C0.961,0.211 1.47,0 2,0L6,0V2H2ZM22,0C22.53,0 23.039,0.211 23.414,0.586C23.789,0.961 24,1.47 24,2V6H22V2H18V0H22ZM2,18V22H6V24H2C1.47,24 0.961,23.789 0.586,23.414C0.211,23.039 0,22.53 0,22V18H2ZM22,22V18H24V22C24,22.53 23.789,23.039 23.414,23.414C23.039,23.789 22.53,24 22,24H18V22H22Z"
|
||||
android:fillColor="#000000"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -10,4 +10,5 @@ object ModuleErrorCode {
|
|||
const val NETWORK = 20000
|
||||
const val DOMAIN = 30000
|
||||
const val SALT_PAY = 40000
|
||||
const val ONBOARDING = 50000
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
88
core/ui/src/main/res/drawable/card_placeholder_black.xml
Normal file
88
core/ui/src/main/res/drawable/card_placeholder_black.xml
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="214dp"
|
||||
android:height="130dp"
|
||||
android:viewportWidth="214"
|
||||
android:viewportHeight="130">
|
||||
<path
|
||||
android:fillColor="#1B1D1C"
|
||||
android:pathData="M1.043,8.273C1.019,4.256 4.265,1 8.294,1H205.706C209.735,1 213.019,4.256 213.043,8.273L213.709,121.725C213.733,125.742 210.487,128.998 206.459,128.998H9.046C5.018,128.998 1.733,125.742 1.709,121.725L1.043,8.273Z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M1.043,8.273C1.019,4.256 4.265,1 8.294,1H205.706C209.735,1 213.019,4.256 213.043,8.273L213.709,121.725C213.733,125.742 210.487,128.998 206.459,128.998H9.046C5.018,128.998 1.733,125.742 1.709,121.725L1.043,8.273Z"
|
||||
android:strokeWidth="0.5" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M25.274,26.4L25.274,32.64L28.079,32.64C28.188,32.639 28.292,32.596 28.369,32.522C28.446,32.447 28.49,32.346 28.491,32.241L28.491,26.4L25.274,26.4Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M19,32.241C19.001,32.346 19.045,32.447 19.122,32.522C19.199,32.596 19.303,32.639 19.412,32.64L22.217,32.64L22.217,26.4L19,26.4L19,32.241Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M28.491,23.2L28.491,20.409C28.491,20.301 28.448,20.197 28.372,20.12C28.296,20.043 28.194,20 28.086,20L19.405,20C19.297,20 19.195,20.043 19.119,20.12C19.043,20.197 19,20.301 19,20.409L19,23.2L28.491,23.2Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M43.503,24.163C42.922,23.921 42.292,23.812 41.661,23.846C40.827,23.844 39.996,23.934 39.183,24.113L39.183,25.532C39.832,25.455 40.483,25.412 41.136,25.4C41.486,25.385 41.836,25.418 42.177,25.5C42.286,25.532 42.385,25.59 42.465,25.669C42.546,25.748 42.605,25.846 42.637,25.953C42.74,26.334 42.783,26.728 42.766,27.122L42.766,27.539C41.997,27.498 41.385,27.476 40.965,27.476C40.519,27.454 40.073,27.539 39.667,27.725C39.347,27.903 39.109,28.195 38.999,28.54C38.847,29.044 38.779,29.569 38.796,30.094C38.74,30.771 38.901,31.448 39.257,32.029C39.445,32.243 39.683,32.409 39.95,32.513C40.216,32.618 40.505,32.659 40.791,32.632C41.127,32.632 42.172,32.605 42.633,32.007C42.709,31.912 42.774,31.808 42.826,31.698L42.877,32.496L44.572,32.496L44.572,27.222C44.596,26.539 44.52,25.857 44.346,25.196C44.284,24.977 44.178,24.772 44.033,24.594C43.888,24.417 43.708,24.27 43.503,24.163ZM42.748,29.256C42.796,29.736 42.713,30.219 42.508,30.656C42.409,30.782 42.272,30.876 42.117,30.924C41.942,30.972 41.76,30.995 41.578,30.992C41.294,31.028 41.006,30.953 40.777,30.783C40.623,30.523 40.558,30.221 40.592,29.922C40.582,29.681 40.607,29.44 40.666,29.206C40.684,29.139 40.716,29.075 40.761,29.021C40.805,28.966 40.861,28.921 40.924,28.889C41.098,28.821 41.285,28.791 41.472,28.803L42.748,28.803L42.748,29.256Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M58.169,24.645C58.133,24.584 58.095,24.526 58.053,24.471C57.494,23.736 56.162,23.846 56.162,23.846C55.722,23.831 55.29,23.964 54.929,24.222C54.564,24.573 54.329,25.042 54.263,25.55C54.001,27.328 54.001,29.136 54.263,30.914C54.326,31.417 54.555,31.882 54.911,32.233C55.299,32.52 55.774,32.659 56.251,32.623C56.251,32.623 56.278,32.623 56.296,32.623C56.296,32.623 57.498,32.72 58.084,32.104C58.095,32.607 58.062,33.11 57.985,33.606C57.966,33.738 57.915,33.864 57.837,33.971C57.759,34.078 57.656,34.164 57.539,34.222C57.166,34.351 56.773,34.406 56.381,34.382L54.535,34.35L54.535,35.788C54.876,35.853 55.219,35.9 55.563,35.93C55.988,35.972 56.39,35.99 56.774,35.99C57.49,36.044 58.205,35.884 58.834,35.53C59.055,35.374 59.242,35.174 59.386,34.943C59.529,34.71 59.626,34.451 59.67,34.18C59.813,33.297 59.875,32.402 59.853,31.507L59.853,23.979L58.209,23.979L58.169,24.645ZM57.963,30.221C57.936,30.452 57.824,30.664 57.65,30.813C57.447,30.94 57.212,30.999 56.975,30.983C56.711,31.006 56.446,30.95 56.211,30.823C56.028,30.636 55.923,30.385 55.916,30.12C55.815,28.876 55.815,27.625 55.916,26.381C55.923,26.125 56.025,25.882 56.202,25.701C56.442,25.579 56.709,25.526 56.975,25.55C57.227,25.529 57.479,25.599 57.686,25.747C57.87,25.947 57.981,26.207 57.999,26.482C58.066,27.072 58.093,27.666 58.079,28.26C58.101,28.916 58.062,29.573 57.963,30.221Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M66.713,24.124C66.131,23.787 65.461,23.636 64.793,23.691C64.098,23.642 63.404,23.802 62.799,24.152C62.358,24.473 62.062,24.959 61.975,25.502C61.831,26.388 61.77,27.286 61.793,28.184C61.769,29.083 61.833,29.983 61.984,30.87C62.031,31.139 62.131,31.395 62.278,31.623C62.425,31.851 62.616,32.046 62.84,32.197C63.478,32.531 64.194,32.681 64.911,32.631C65.367,32.63 65.823,32.603 66.276,32.552C66.653,32.517 67.025,32.453 67.392,32.359L67.392,30.916C66.554,31.004 65.867,31.05 65.33,31.05C64.933,31.074 64.535,31.025 64.155,30.907C64.038,30.857 63.936,30.778 63.856,30.677C63.777,30.577 63.723,30.458 63.7,30.331C63.618,29.823 63.585,29.307 63.6,28.792L67.574,28.792L67.574,28.165C67.596,27.241 67.548,26.317 67.433,25.4C67.368,24.896 67.109,24.438 66.713,24.124ZM65.849,27.428L63.6,27.428C63.6,26.95 63.632,26.473 63.696,26C63.704,25.887 63.736,25.777 63.79,25.678C63.843,25.579 63.917,25.492 64.005,25.424C64.473,25.233 64.994,25.233 65.462,25.424C65.548,25.494 65.619,25.583 65.671,25.683C65.723,25.782 65.754,25.892 65.762,26.004C65.833,26.475 65.862,26.952 65.849,27.428Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M78.824,25.12C78.747,24.744 78.542,24.406 78.243,24.161C77.886,23.928 77.46,23.817 77.032,23.844C77.032,23.844 76.991,23.844 76.963,23.844C76.963,23.844 75.581,23.767 75.01,24.464C74.948,24.542 74.892,24.625 74.844,24.712C74.752,24.478 74.592,24.275 74.383,24.129C74.024,23.923 73.61,23.826 73.195,23.848C73.195,23.848 71.813,23.767 71.242,24.468C71.178,24.545 71.122,24.629 71.076,24.717L71.03,23.984L69.349,23.984L69.349,32.48L71.191,32.48L71.191,28.051C71.182,27.52 71.201,26.988 71.251,26.459C71.263,26.198 71.362,25.949 71.532,25.749C71.736,25.584 71.997,25.506 72.26,25.531C72.493,25.513 72.727,25.557 72.937,25.658C73.015,25.731 73.076,25.819 73.119,25.916C73.162,26.013 73.185,26.118 73.186,26.223C73.234,26.747 73.254,27.272 73.245,27.798L73.245,32.48L75.088,32.48L75.088,28.051C75.088,27.314 75.088,26.785 75.143,26.459C75.156,26.197 75.258,25.946 75.434,25.749C75.642,25.586 75.906,25.508 76.171,25.531C76.405,25.513 76.641,25.557 76.852,25.658C76.929,25.732 76.99,25.82 77.032,25.917C77.074,26.014 77.096,26.118 77.096,26.223C77.147,26.747 77.167,27.272 77.156,27.798L77.156,32.48L78.999,32.48L78.999,26.884C79.009,26.291 78.951,25.699 78.824,25.12Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M51.691,24.171C51.307,23.928 50.851,23.814 50.394,23.845C50.394,23.845 50.356,23.845 50.338,23.845C50.338,23.845 48.938,23.773 48.364,24.465C48.323,24.514 48.286,24.565 48.252,24.619L48.21,23.981L46.507,23.981L46.507,32.48L48.373,32.48L48.373,28.049C48.363,27.521 48.384,26.992 48.434,26.465C48.448,26.203 48.553,25.952 48.733,25.755C48.958,25.584 49.242,25.503 49.526,25.528C49.774,25.511 50.023,25.555 50.249,25.655C50.333,25.728 50.401,25.816 50.448,25.915C50.496,26.014 50.521,26.121 50.524,26.23C50.578,26.75 50.6,27.273 50.59,27.796L50.59,32.48L52.456,32.48L52.456,26.963C52.471,26.356 52.419,25.749 52.302,25.153C52.226,24.765 52.009,24.417 51.691,24.171Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M36.543,21.44L34.653,21.44L34.653,23.877L33.477,23.877L33.477,25.434L34.653,25.434L34.653,32.48L36.543,32.48L36.543,25.434L37.981,25.434L37.981,23.877L36.543,23.877L36.543,21.44Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M23.463,105.118L21.826,104.112L21.904,106H21.024L21.087,104.112L19.441,105.125L19.02,104.335L20.707,103.493L19.016,102.645L19.428,101.882L21.08,102.875L21.001,101H21.874L21.818,102.882L23.451,101.882L23.885,102.645L22.184,103.5L23.895,104.342L23.463,105.118Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M32.161,105.118L30.524,104.112L30.602,106H29.722L29.785,104.112L28.139,105.125L27.718,104.335L29.405,103.493L27.715,102.645L28.126,101.882L29.778,102.875L29.699,101H30.573L30.517,102.882L32.149,101.882L32.583,102.645L30.883,103.5L32.593,104.342L32.161,105.118Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M40.86,105.118L39.222,104.112L39.3,106H38.42L38.483,104.112L36.837,105.125L36.416,104.335L38.104,103.493L36.413,102.645L36.825,101.882L38.476,102.875L38.398,101H39.271L39.215,102.882L40.847,101.882L41.282,102.645L39.581,103.5L41.292,104.342L40.86,105.118Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M49.558,105.118L47.92,104.112L47.998,106H47.119L47.181,104.112L45.535,105.125L45.114,104.335L46.802,103.493L45.111,102.645L45.523,101.882L47.174,102.875L47.096,101H47.969L47.913,102.882L49.546,101.882L49.98,102.645L48.279,103.5L49.99,104.342L49.558,105.118Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M63.14,105.118L61.502,104.112L61.58,106H60.701L60.763,104.112L59.117,105.125L58.696,104.335L60.384,103.493L58.693,102.645L59.105,101.882L60.756,102.875L60.678,101H61.551L61.495,102.882L63.128,101.882L63.562,102.645L61.861,103.5L63.572,104.342L63.14,105.118Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M71.838,105.118L70.2,104.112L70.279,106H69.399L69.462,104.112L67.815,105.125L67.394,104.335L69.082,103.493L67.391,102.645L67.803,101.882L69.454,102.875L69.376,101H70.249L70.193,102.882L71.826,101.882L72.26,102.645L70.559,103.5L72.27,104.342L71.838,105.118Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M80.536,105.118L78.898,104.112L78.977,106H78.097L78.16,104.112L76.514,105.125L76.093,104.335L77.78,103.493L76.089,102.645L76.501,101.882L78.152,102.875L78.074,101H78.947L78.891,102.882L80.524,101.882L80.958,102.645L79.257,103.5L80.968,104.342L80.536,105.118Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M89.234,105.118L87.597,104.112L87.675,106H86.795L86.858,104.112L85.212,105.125L84.791,104.335L86.478,103.493L84.788,102.645L85.2,101.882L86.851,102.875L86.772,101H87.646L87.59,102.882L89.222,101.882L89.657,102.645L87.956,103.5L89.666,104.342L89.234,105.118Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M102.816,105.118L101.179,104.112L101.257,106H100.377L100.44,104.112L98.794,105.125L98.373,104.335L100.06,103.493L98.369,102.645L98.781,101.882L100.433,102.875L100.354,101H101.227L101.171,102.882L102.804,101.882L103.238,102.645L101.538,103.5L103.248,104.342L102.816,105.118Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M111.515,105.118L109.877,104.112L109.955,106H109.075L109.138,104.112L107.492,105.125L107.071,104.335L108.758,103.493L107.068,102.645L107.48,101.882L109.131,102.875L109.053,101H109.926L109.87,102.882L111.502,101.882L111.937,102.645L110.236,103.5L111.947,104.342L111.515,105.118Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M120.213,105.118L118.575,104.112L118.653,106H117.774L117.836,104.112L116.19,105.125L115.769,104.335L117.457,103.493L115.766,102.645L116.178,101.882L117.829,102.875L117.751,101H118.624L118.568,102.882L120.201,101.882L120.635,102.645L118.934,103.5L120.645,104.342L120.213,105.118Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M128.911,105.118L127.273,104.112L127.352,106H126.472L126.535,104.112L124.888,105.125L124.467,104.335L126.155,103.493L124.464,102.645L124.876,101.882L126.527,102.875L126.449,101H127.322L127.266,102.882L128.899,101.882L129.333,102.645L127.632,103.5L129.343,104.342L128.911,105.118Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M142.493,105.118L140.855,104.112L140.933,106H140.054L140.116,104.112L138.47,105.125L138.049,104.335L139.737,103.493L138.046,102.645L138.458,101.882L140.109,102.875L140.031,101H140.904L140.848,102.882L142.481,101.882L142.915,102.645L141.214,103.5L142.925,104.342L142.493,105.118Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M151.191,105.118L149.553,104.112L149.632,106H148.752L148.815,104.112L147.169,105.125L146.748,104.335L148.435,103.493L146.744,102.645L147.156,101.882L148.807,102.875L148.729,101H149.602L149.546,102.882L151.179,101.882L151.613,102.645L149.912,103.5L151.623,104.342L151.191,105.118Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M159.889,105.118L158.252,104.112L158.33,106H157.45L157.513,104.112L155.867,105.125L155.446,104.335L157.133,103.493L155.443,102.645L155.854,101.882L157.506,102.875L157.427,101H158.3L158.244,102.882L159.877,101.882L160.311,102.645L158.611,103.5L160.321,104.342L159.889,105.118Z" />
|
||||
<path
|
||||
android:fillColor="#A5A5A5"
|
||||
android:pathData="M168.588,105.118L166.95,104.112L167.028,106H166.148L166.211,104.112L164.565,105.125L164.144,104.335L165.831,103.493L164.141,102.645L164.553,101.882L166.204,102.875L166.126,101H166.999L166.943,102.882L168.575,101.882L169.01,102.645L167.309,103.5L169.02,104.342L168.588,105.118Z" />
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="56dp"
|
||||
android:height="56dp"
|
||||
android:viewportWidth="56"
|
||||
android:viewportHeight="56">
|
||||
<path
|
||||
android:pathData="M23.333,49H11.667C9.077,49 7,46.923 7,44.333V11.667C7,9.077 9.077,7 11.667,7H44.333C46.923,7 49,9.077 49,11.667V24.103C48.3,23.823 47.53,23.66 46.76,23.66C45.897,23.66 45.08,23.847 44.333,24.197V11.667H11.667V44.333H23.59L23.333,44.59V49ZM16.333,21H39.667V16.333H16.333V21ZM16.333,39.667H28.257L32.667,35.28V35H16.333V39.667ZM16.333,30.333H37.613L39.667,28.28V25.667H16.333V30.333ZM50.633,31.687L47.647,28.7C47.157,28.21 46.34,28.21 45.85,28.7L43.517,31.033L48.3,35.817L50.633,33.483C51.123,32.993 51.123,32.177 50.633,31.687ZM28,51.333H32.807L46.923,37.17L42.14,32.387L28,46.527V51.333Z"
|
||||
android:fillColor="#919191"/>
|
||||
</vector>
|
||||
|
|
@ -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
|
||||
override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher(),
|
||||
) : CoroutineDispatcherProvider
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <R> runCatching(dispatcher: CoroutineDispatcher, block: suspend () -> R): Result<R> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.utils.extensions
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
fun Char.isNotWhitespace(): Boolean = !this.isWhitespace()
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.utils.extensions
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
fun <T> Collection<T>.isSingleItem(): Boolean = this.size == 1
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.utils.extensions
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
fun Int.isEven(): Boolean = this % 2 == 0
|
||||
|
||||
fun Int.isOdd(): Boolean = !this.isEven()
|
||||
1
features/onboarding/.gitignore
vendored
Normal file
1
features/onboarding/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
50
features/onboarding/build.gradle.kts
Normal file
50
features/onboarding/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
2
features/onboarding/src/main/AndroidManifest.xml
Normal file
2
features/onboarding/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.tangem.feature.onboarding" />
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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<String> by lazy { bip39Wordlist.words.toHashSet() }
|
||||
|
||||
override fun getWordsDictionary(): Set<String> = 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String>
|
||||
|
||||
@Throws(TangemSdkError.MnemonicException::class)
|
||||
fun generateDefaultMnemonic(): Mnemonic
|
||||
|
||||
@Throws(TangemSdkError.MnemonicException::class)
|
||||
fun createMnemonic(mnemonicString: String): Mnemonic
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Mnemonic> {
|
||||
return runCatching {
|
||||
repository.generateDefaultMnemonic().apply {
|
||||
currentMnemonic = this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getMnemonicComponents(): Result<List<String>> {
|
||||
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<List<String>> {
|
||||
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<String> {
|
||||
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)
|
||||
}
|
||||
|
|
@ -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<String>) : SeedPhraseError(subCode = 7)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.feature.onboarding.domain
|
||||
|
||||
import com.tangem.crypto.bip39.Mnemonic
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface SeedPhraseInteractor {
|
||||
suspend fun generateMnemonic(): Result<Mnemonic>
|
||||
suspend fun getMnemonicComponents(): Result<List<String>>
|
||||
suspend fun isWordMatch(word: String): Boolean
|
||||
suspend fun validateMnemonicString(text: String): Result<List<String>>
|
||||
suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): List<String>
|
||||
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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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<MnemonicGridItem> = 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<String> = emptySet(),
|
||||
val suggestionsList: List<String> = 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 = {},
|
||||
)
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<MnemonicGridItem>) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DescriptionResource>,
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String>): OnboardingSeedPhraseState =
|
||||
uiState.copy(
|
||||
importSeedPhraseState = uiState.importSeedPhraseState.copy(
|
||||
invalidWords = invalidWords,
|
||||
),
|
||||
)
|
||||
|
||||
fun updateSuggestions(uiState: OnboardingSeedPhraseState, suggestions: List<String>): 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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<MnemonicGridItem>,
|
||||
): 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<MnemonicGridItem>,
|
||||
): OnboardingSeedPhraseState = uiState.copy(
|
||||
yourSeedPhraseState = uiState.yourSeedPhraseState.copy(
|
||||
mnemonicGridItems = mnemonicGridItems,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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<String>,
|
||||
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<String> {
|
||||
val plainTextCharArray = plainText.toCharArray()
|
||||
val wordlist = mutableListOf<String>()
|
||||
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<String>): 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 = "_$@-*"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Pair<Context, SeedPhraseError?>, String?> {
|
||||
|
||||
override fun convert(source: Pair<Context, SeedPhraseError?>): 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Debouncer>()
|
||||
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<String>): ImmutableList<MnemonicGridItem> {
|
||||
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<MnemonicGridItem>()
|
||||
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 <T> withMainContext(block: suspend CoroutineScope.() -> T): T {
|
||||
return withContext(dispatchers.main, block)
|
||||
}
|
||||
|
||||
private suspend fun <T> 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue