Updated on 2026-08-14

This commit is contained in:
Tangem 2022-06-29 16:43:34 +03:00
commit f9d8d99eb1
34 changed files with 1020 additions and 365 deletions

@ -1 +1 @@
Subproject commit f50133547b5e19fb7ad32e64370573d60f6791fd
Subproject commit 64ae04d2086e5a605dd4da3cba4af32a60d46c79

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.home.compose
package com.tangem.tap.common.compose
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -12,7 +11,6 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun TextAutoSize(
modifier: Modifier = Modifier,

View file

@ -38,6 +38,11 @@ fun SpacerH24(modifier: Modifier = Modifier) {
SpacerH(24.dp, modifier)
}
@Composable
fun SpacerH32(modifier: Modifier = Modifier) {
SpacerH(32.dp, modifier)
}
// ***************************** Vertical
@Composable
fun SpacerW(width: Dp, modifier: Modifier = Modifier) {
@ -64,6 +69,11 @@ fun SpacerW24(modifier: Modifier = Modifier) {
SpacerW(24.dp, modifier)
}
@Composable
fun SpacerW32(modifier: Modifier = Modifier) {
SpacerW(32.dp, modifier)
}
// ***************************** Size
@Composable
fun SpacerS(size: Dp, modifier: Modifier = Modifier) {
@ -88,4 +98,9 @@ fun SpacerS16(modifier: Modifier = Modifier) {
@Composable
fun SpacerS24(modifier: Modifier = Modifier) {
SpacerS(24.dp, modifier)
}
@Composable
fun SpacerS32(modifier: Modifier = Modifier) {
SpacerS(32.dp, modifier)
}

View file

@ -0,0 +1,50 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.animation.core.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
/**
[REDACTED_AUTHOR]
*/
typealias AnimatedValue = Pair<Float, Float>
@Composable
fun AnimatedValue.toAnimatable(
isPaused: Boolean,
duration: Int,
easing: Easing = LinearEasing,
): Animatable<Float, AnimationVector1D> {
return animatable(
values = this,
isPaused = isPaused,
duration = duration,
easing = easing,
)
}
@Composable
fun animatable(
values: AnimatedValue,
duration: Int,
isPaused: Boolean = false,
easing: Easing = LinearEasing,
): Animatable<Float, AnimationVector1D> {
val animatable = remember { Animatable(values.first) }
LaunchedEffect(isPaused) {
if (isPaused) {
animatable.stop()
} else {
animatable.animateTo(
targetValue = values.second,
animationSpec = tween(
durationMillis = duration,
easing = easing
)
)
}
}
return animatable
}

View file

@ -0,0 +1,19 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
/**
[REDACTED_AUTHOR]
*/
@Composable
fun Dp.toPx(): Float {
val currentDp = this
return with(LocalDensity.current) { currentDp.toPx() }
}
fun DpSize.halfWidth(): Dp = this.width / 2
fun DpSize.halfHeight(): Dp = this.height / 2

View file

@ -0,0 +1,19 @@
package com.tangem.tap.common.compose.extensions
import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.core.graphics.drawable.toBitmap
/**
[REDACTED_AUTHOR]
*/
@Composable
fun asImageBitmap(@DrawableRes drawableId: Int): ImageBitmap {
val drawable = AppCompatResources.getDrawable(LocalContext.current, drawableId)
?: throw NullPointerException()
return drawable.toBitmap().asImageBitmap()
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tangem_sdk_new.extensions.pxToDp
/**
[REDACTED_AUTHOR]
*/
@Composable
fun Painter.dpSize(): DpSize = DpSize(
intrinsicSize.width.pxToDp().dp,
intrinsicSize.height.pxToDp().dp,
)
@Composable
private fun Float.dpToPx(): Float = LocalContext.current.dpToPx(this)
@Composable
private fun Float.pxToDp(): Float = LocalContext.current.pxToDp(this)

View file

@ -93,32 +93,39 @@ fun View.invisible(invisible: Boolean = true, invokeBeforeStateChanged: (() -> U
}
fun Context.dpToPixels(dp: Int): Int =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
fun Context.pixelsToDp(pixels: Int): Int {
return (pixels.toFloat() /
(resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
(resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
.toInt()
}
fun Context.dpToPixels(dp: Float): Float =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, this.resources.displayMetrics)
fun Context.pixelsToDp(pixels: Float): Float =
(pixels / (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
tailrec fun Context?.getActivity(): Activity? = this as? Activity
?: (this as? ContextWrapper)?.baseContext?.getActivity()
?: (this as? ContextWrapper)?.baseContext?.getActivity()
fun MaterialCardView.setMargins(
marginLeftDp: Int = 16,
marginTopDp: Int = 8,
marginRightDp: Int = 16,
marginBottomDp: Int = 8
marginLeftDp: Int = 16,
marginTopDp: Int = 8,
marginRightDp: Int = 16,
marginBottomDp: Int = 8
) {
val params = this.layoutParams
(params as ViewGroup.MarginLayoutParams).setMargins(
context.dpToPixels(marginLeftDp),
context.dpToPixels(marginTopDp),
context.dpToPixels(marginRightDp),
context.dpToPixels(marginBottomDp)
context.dpToPixels(marginLeftDp),
context.dpToPixels(marginTopDp),
context.dpToPixels(marginRightDp),
context.dpToPixels(marginBottomDp)
)
this.layoutParams = params
}
@ -128,11 +135,11 @@ fun Activity.setSystemBarTextColor(setTextDark: Boolean) {
val flags = this.window.decorView.systemUiVisibility
// Update the SystemUiVisibility dependening on whether we want a Light or Dark theme.
this.window.decorView.systemUiVisibility =
if (setTextDark) {
flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
} else {
flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
if (setTextDark) {
flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
} else {
flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
}
}
@ -150,7 +157,7 @@ fun Context.copyToClipboard(value: Any, label: String = "") {
fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
?: return default
?: return default
val clipData = clipboard.primaryClip ?: return default
if (clipData.itemCount == 0) return default
@ -172,10 +179,10 @@ fun Fragment.shareText(text: String) {
}
fun Context.safeStartActivity(
intent: Intent,
options: Bundle? = null,
fallback: ((ActivityNotFoundException) -> Unit)? = null,
finally: VoidCallback? = null
intent: Intent,
options: Bundle? = null,
fallback: ((ActivityNotFoundException) -> Unit)? = null,
finally: VoidCallback? = null
) {
try {
this.startActivity(intent, options)

View file

@ -1,153 +0,0 @@
package com.tangem.tap.features.home.compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.scaleIn
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.wallet.R
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun FirstStoriesContent(
paused: Boolean, duration: Int = 8_000,
hideContent: (Boolean) -> Unit
) {
val screenState = remember { mutableStateOf(StartingScreenState.INIT) }
val progress = remember { Animatable(0f) }
LaunchedEffect(paused) {
if (paused) {
progress.stop()
} else {
progress.animateTo(
targetValue = 2f,
animationSpec = tween(
durationMillis = duration,
easing = LinearEasing
)
)
}
}
when (progress.value) {
in 0f..0.2f -> screenState.value = StartingScreenState.INIT
in 0.2f..0.3f -> screenState.value = StartingScreenState.BUY
in 0.3f..0.4f -> screenState.value = StartingScreenState.STORE
in 0.4f..0.5f -> screenState.value = StartingScreenState.SEND
in 0.5f..0.6f -> screenState.value = StartingScreenState.PAY
in 0.6f..0.7f -> screenState.value = StartingScreenState.EXCHANGE
in 0.7f..0.8f -> screenState.value = StartingScreenState.BORROW
in 0.8f..1f -> screenState.value = StartingScreenState.LEND
in 1f..1.2f -> screenState.value = StartingScreenState.SHOW_CARD
in 1.2f..2f -> screenState.value = StartingScreenState.MEET_TANGEM
}
if (screenState.value == StartingScreenState.INIT) hideContent(true)
if (screenState.value == StartingScreenState.BUY) hideContent(false)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier =
Modifier
.fillMaxSize(),
) {
val text = when (screenState.value) {
StartingScreenState.INIT -> null
StartingScreenState.BUY -> R.string.story_meet_buy
StartingScreenState.STORE -> R.string.story_meet_store
StartingScreenState.SEND -> R.string.story_meet_send
StartingScreenState.PAY -> R.string.story_meet_pay
StartingScreenState.EXCHANGE -> R.string.story_meet_exchange
StartingScreenState.BORROW -> R.string.story_meet_borrow
StartingScreenState.LEND -> R.string.story_meet_lend
StartingScreenState.SHOW_CARD -> R.string.story_meet_title
StartingScreenState.MEET_TANGEM -> R.string.story_meet_title
}
val style = TextStyle(
fontSize = 60.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White,
textAlign = TextAlign.Center,
)
if (screenState.value != StartingScreenState.MEET_TANGEM) {
TextAutoSize(
modifier = Modifier
.padding(start = 20.dp, end = 20.dp, bottom = 100.dp)
.alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f),
text = text?.let { stringResource(text) } ?: "",
textStyle = style,
fontSizeRange = FontSizeRange(20.sp, 60.sp)
)
}
AnimatedVisibility(
visible = screenState.value == StartingScreenState.MEET_TANGEM,
enter = slideInVertically() { it / 2 }
) {
TextAutoSize(
modifier = Modifier
.fillMaxWidth()
.padding(start = 40.dp, end = 40.dp),
text = text?.let { stringResource(text) } ?: "",
textStyle = style,
fontSizeRange = FontSizeRange(20.sp, 60.sp)
)
}
AnimatedVisibility(
visible = screenState.value == StartingScreenState.SHOW_CARD ||
screenState.value == StartingScreenState.MEET_TANGEM,
enter = scaleIn(initialScale = 3f)
) {
Image(
painter = painterResource(
id = R.drawable.meet_tangem
),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
}
}
}
enum class StartingScreenState {
INIT, BUY, STORE, SEND, PAY, EXCHANGE, BORROW, LEND, SHOW_CARD, MEET_TANGEM
}
@Preview
@Composable
fun Stories1Preview() {
FirstStoriesContent(false, 7_000) {}
}

View file

@ -24,8 +24,11 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.SpacerS24
import androidx.compose.ui.unit.sp
import com.tangem.tap.common.compose.SpacerS24
import com.tangem.tap.features.home.compose.content.*
import com.tangem.tap.features.home.compose.views.HomeButtons
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.wallet.R
@ -39,8 +42,6 @@ fun StoriesScreen(
onSearchTokensClick: () -> Unit,
) {
val steps = 6
val stepDuration = 8_000
val currentStep = remember { mutableStateOf(1) }
val isDarkBackground = currentStep.value !in 3..5
@ -55,7 +56,7 @@ fun StoriesScreen(
val isPressed = remember { mutableStateOf(false) }
val needsToBePaused =
remember(homeState) { mutableStateOf(homeState.value.btnScanState.progressState == ProgressState.Loading) }
val pause = isPressed.value || needsToBePaused.value
val isPaused = isPressed.value || needsToBePaused.value
val hideContent = remember { mutableStateOf(true) }
@ -122,9 +123,8 @@ fun StoriesScreen(
StoriesProgressBar(
steps = steps,
currentStep = currentStep.value,
// paused = isPressed.value,
stepDuration = stepDuration,
paused = pause,
stepDuration = currentStep.duration(),
paused = isPaused,
onStepFinished = goToNextScreen,
)
Image(
@ -139,12 +139,12 @@ fun StoriesScreen(
colorFilter = if (isDarkBackground) null else ColorFilter.tint(Color.Black)
)
when (currentStep.value) {
1 -> FirstStoriesContent(pause, stepDuration) { hideContent.value = it }
2 -> StoriesRevolutionaryWallet()
3 -> StoriesUltraSecureBackup()
4 -> StoriesThousandsOfCurrencies()
5 -> StoriesWeb3()
6 -> StoriesWalletForEveryone()
1 -> FirstStoriesContent(isPaused, currentStep.duration()) { hideContent.value = it }
2 -> StoriesRevolutionaryWallet(currentStep.duration())
3 -> StoriesUltraSecureBackup(isPaused, currentStep.duration())
4 -> StoriesCurrencies(isPaused, currentStep.duration())
5 -> StoriesWeb3(isPaused, currentStep.duration())
6 -> StoriesWalletForEveryone(currentStep.duration())
}
}
Column(
@ -157,8 +157,7 @@ fun StoriesScreen(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.height(48.dp)
,
.height(48.dp),
colors = ButtonDefaults.textButtonColors(
backgroundColor = Color.White,
contentColor = Color(0xFF080C10)
@ -188,9 +187,13 @@ fun StoriesScreen(
}
}
private fun MutableState<Int>.duration(): Int = when (this.value) {
1 -> 8000
else -> 6000
}
@Preview
@Composable
fun InstagramScreenPreview() {
fun StoriesScreenPreview() {
StoriesScreen(onScanButtonClick = {}, onShopButtonClick = {}, onSearchTokensClick = {})
}

View file

@ -0,0 +1,152 @@
package com.tangem.tap.features.home.compose
import androidx.compose.animation.core.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.extensions.AnimatedValue
import com.tangem.tap.common.compose.extensions.toAnimatable
@Composable
fun HorizontalSlidingImage(
painter: Painter,
paused: Boolean,
duration: Int,
itemSize: DpSize,
startOffset: Float,
targetOffset: Float,
contentDescription: String,
) {
val translateX = AnimatedValue(startOffset * -1f, (startOffset + targetOffset) * -1f)
Image(
modifier = Modifier
.requiredWidth(itemSize.width)
.requiredHeight(itemSize.height)
.graphicsLayer(
translationX = translateX.toAnimatable(isPaused = paused, duration = duration).value
),
alignment = Alignment.TopStart,
contentScale = ContentScale.FillBounds,
painter = painter,
contentDescription = contentDescription,
)
}
@Composable
fun StoriesTextAnimation(
slideInDuration: Int = 500,
slideInDelay: Int = 200,
slideDistance: Dp = 60.dp,
label: String = "",
content: @Composable (Modifier) -> Unit
) {
val isLaunched = remember { mutableStateOf(false) }
val transition = updateTransition(targetState = isLaunched.value, label = label)
val offsetY = transition.animateDp(
transitionSpec = {
tween(
durationMillis = slideInDuration,
delayMillis = slideInDelay,
easing = FastOutSlowInEasing
)
},
label = "Slide in"
) { value -> if (value) 0.dp else slideDistance }
val alpha = transition.animateFloat(
transitionSpec = {
tween(
durationMillis = slideInDuration * 2,
delayMillis = slideInDelay,
easing = FastOutSlowInEasing
)
},
label = "Visibility"
) { value -> if (value) 1f else 0f }
content(
Modifier
.absoluteOffset(y = offsetY.value)
.alpha(alpha.value)
)
LaunchedEffect(Unit) { isLaunched.value = true }
}
@Composable
fun StoriesBottomImageAnimation(
initialScale: Float = 2.5f,
firstStepDuration: Int,
totalDuration: Int,
content: @Composable (Modifier) -> Unit
) {
val scaleSwitchBarrier = 1.15f
val secondStepDuration = totalDuration - firstStepDuration
val isFirstStepLaunched = remember { mutableStateOf(false) }
val isSecondStepLaunched = remember { mutableStateOf(false) }
val firstTransition = updateTransition(
targetState = isFirstStepLaunched.value,
label = "Image appearing"
)
val firstScaleStep = firstTransition.animateFloat(
transitionSpec = {
tween(
durationMillis = firstStepDuration,
easing = FastOutLinearInEasing,
)
},
label = "Appearing scale"
) { value -> if (value) scaleSwitchBarrier else initialScale }
val secondTransition = updateTransition(
targetState = isSecondStepLaunched.value,
label = "Image slow outgoing"
)
val secondScaleStep = secondTransition.animateFloat(
transitionSpec = {
tween(
durationMillis = secondStepDuration,
easing = LinearEasing,
)
},
label = "Outgoing scale"
) { value -> if (value) 1f else scaleSwitchBarrier }
val fadeIn = firstTransition.animateFloat(
transitionSpec = { tween(durationMillis = 400) },
label = "Fade in on start"
) { value -> if (value) 1f else 0f }
if (firstScaleStep.value == scaleSwitchBarrier) {
isSecondStepLaunched.value = true
}
val modifier = if (!isSecondStepLaunched.value) {
Modifier.scale(firstScaleStep.value)
} else {
Modifier.scale(secondScaleStep.value)
}.alpha(fadeIn.value)
content(modifier)
LaunchedEffect(Unit) { isFirstStepLaunched.value = true }
}

View file

@ -1,162 +0,0 @@
package com.tangem.tap.features.home.compose
import android.content.Context
import android.graphics.Typeface
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tap.common.compose.SpacerS16
import com.tangem.tap.common.compose.SpacerS24
import com.tangem.tap.common.compose.extensions.toAndroidGraphicsColor
import com.tangem.wallet.R
@Composable
fun StoriesGeneralContent(
titleText: String,
subtitleText: String,
imageSource: Int?,
isDarkBackground: Boolean,
subtitleTextId: Int? = null,
imageComposable: (() -> Unit)? = null,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier =
Modifier
.fillMaxWidth()
.fillMaxHeight(),
) {
Text(
text = titleText,
fontSize = 32.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier
.padding(start = 40.dp, end = 40.dp),
color = if (isDarkBackground) Color.White else Color(0xFF090E13),
textAlign = TextAlign.Center
)
SpacerS16()
SubtitleText(subtitleText, subtitleTextId)
SpacerS24()
if (imageSource != null) {
Image(
painter = painterResource(id = imageSource),
contentDescription = null,
contentScale = if (isDarkBackground) ContentScale.Inside else ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
}
imageComposable?.invoke()
}
}
@Composable
fun SubtitleText(subtitleText: String, subtitleTextId: Int?) {
val color = Color(0xFFA6AAAD)
if (subtitleTextId == null) {
Text(
text = subtitleText,
fontSize = 20.sp,
fontWeight = FontWeight.Normal,
modifier = Modifier.padding(start = 40.dp, end = 40.dp),
color = color,
textAlign = TextAlign.Center
)
} else {
HtmlText(subtitleTextId) { context ->
val padding = context.dpToPx(40f).toInt()
TextView(context).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
setPadding(padding, 0, padding, 0)
textAlignment = View.TEXT_ALIGNMENT_CENTER
typeface = Typeface.DEFAULT
setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
setTextColor(color.toAndroidGraphicsColor())
}
}
}
}
@Composable
fun StoriesRevolutionaryWallet() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_awe_title),
subtitleText = stringResource(id = R.string.story_awe_description),
imageSource = R.drawable.revolutionary_wallet,
isDarkBackground = true
)
}
@Composable
fun StoriesUltraSecureBackup() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_backup_title),
subtitleText = stringResource(id = R.string.story_backup_description),
imageSource = R.drawable.floating_cards,
subtitleTextId = R.string.story_backup_description,
isDarkBackground = false
)
}
@Composable
fun StoriesThousandsOfCurrencies() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_currencies_title),
subtitleText = stringResource(id = R.string.story_currencies_description),
imageSource = R.drawable.thousands_of_currencies,
isDarkBackground = false
)
}
@Composable
fun StoriesWeb3() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_web3_title),
subtitleText = stringResource(id = R.string.story_web3_description),
imageSource = R.drawable.web_3,
isDarkBackground = false
)
}
@Composable
fun StoriesWalletForEveryone() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_finish_title),
subtitleText = stringResource(id = R.string.story_finish_description),
imageSource = R.drawable.wallet_for_everyone,
isDarkBackground = true
)
}
@Composable
fun HtmlText(
stringResId: Int,
modifier: Modifier = Modifier,
factory: (Context) -> TextView
) {
AndroidView(factory, modifier) { it.text = it.context.getText(stringResId) }
}

View file

@ -0,0 +1,263 @@
package com.tangem.tap.features.home.compose.content
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.tap.common.compose.SpacerH16
import com.tangem.tap.common.compose.SpacerH32
import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation
import com.tangem.tap.features.home.compose.StoriesTextAnimation
import com.tangem.wallet.R
@Composable
fun StoriesRevolutionaryWallet(stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_awe_title),
subtitleText = stringResource(id = R.string.story_awe_description).annotated(),
isDarkBackground = true,
)
},
bottomContent = {
StoriesBottomImageAnimation(
totalDuration = stepDuration,
firstStepDuration = 300,
) { modifier ->
StoriesImage(
modifier = modifier,
drawableResId = R.drawable.revolutionary_wallet,
isDarkBackground = true,
)
}
}
)
}
@Composable
fun StoriesUltraSecureBackup(isPaused: Boolean, stepDuration: Int) {
val subtitleText = buildAnnotatedString {
append(stringResource(id = R.string.story_backup_description_1))
append(" ")
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(stringResource(id = R.string.story_backup_description_2_bold))
}
append(" ")
append(stringResource(id = R.string.story_backup_description_3))
}
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_backup_title),
subtitleText = subtitleText,
isDarkBackground = false,
)
},
bottomContent = {
FloatingCardsContent(isPaused, stepDuration)
}
)
}
@Composable
fun StoriesCurrencies(isPaused: Boolean, stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_currencies_title),
subtitleText = stringResource(id = R.string.story_currencies_description).annotated(),
isDarkBackground = false,
)
},
bottomContent = {
StoriesCurrenciesContent(paused = isPaused, duration = stepDuration)
}
)
}
@Composable
fun StoriesWeb3(isPaused: Boolean, stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_web3_title),
subtitleText = stringResource(id = R.string.story_web3_description).annotated(),
isDarkBackground = false,
)
},
bottomContent = {
StoriesWeb3Content(paused = isPaused, duration = stepDuration)
}
)
}
@Composable
fun StoriesWalletForEveryone(stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_finish_title),
subtitleText = stringResource(id = R.string.story_finish_description).annotated(),
isDarkBackground = true,
)
},
bottomContent = {
StoriesBottomImageAnimation(
totalDuration = stepDuration,
firstStepDuration = 500,
) { modifier ->
StoriesImage(
modifier = modifier,
drawableResId = R.drawable.wallet_for_everyone,
isDarkBackground = true,
)
}
}
)
}
@Composable
private fun SplitContent(
topContent: @Composable () -> Unit,
bottomContent: @Composable () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top
) {
topContent()
bottomContent()
}
}
@Composable
private fun TopContent(
titleText: String,
subtitleText: AnnotatedString,
isDarkBackground: Boolean,
) {
SpacerH32()
StoriesTitleText(
text = titleText,
isDarkBackground = isDarkBackground,
)
SpacerH16()
StoriesSubtitleText(
subtitleText = subtitleText,
)
SpacerH32()
}
@Composable
private fun StoriesTitleText(
text: String,
isDarkBackground: Boolean,
) {
StoriesTextAnimation(
slideInDuration = 500,
slideInDelay = 150,
) { modifier ->
Text(
modifier = modifier
.padding(start = 40.dp, end = 40.dp),
text = text,
fontSize = 32.sp,
fontWeight = FontWeight.SemiBold,
color = if (isDarkBackground) Color.White else Color(0xFF090E13),
textAlign = TextAlign.Center
)
}
}
@Composable
private fun StoriesSubtitleText(
subtitleText: AnnotatedString,
) {
val color = Color(0xFFA6AAAD)
StoriesTextAnimation(
slideInDuration = 500,
slideInDelay = 400,
) { modifier ->
Text(
modifier = modifier
.padding(start = 40.dp, end = 40.dp),
fontWeight = FontWeight.Normal,
text = subtitleText,
fontSize = 20.sp,
color = color,
textAlign = TextAlign.Center
)
}
}
@Composable
private fun StoriesImage(
modifier: Modifier = Modifier,
@DrawableRes drawableResId: Int,
isDarkBackground: Boolean,
) {
Image(
painter = painterResource(id = drawableResId),
contentDescription = null,
contentScale = if (isDarkBackground) ContentScale.Inside else ContentScale.FillWidth,
modifier = modifier.fillMaxWidth()
)
}
private fun String.annotated(): AnnotatedString {
val source = this
return buildAnnotatedString { append(source) }
}
@Preview
@Composable
private fun RevolutionaryWalletPreview() {
StoriesRevolutionaryWallet(6000)
}
@Preview
@Composable
private fun UltraSecureBackupPreview() {
StoriesUltraSecureBackup(false, 6000)
}
@Preview
@Composable
private fun CurrenciesPreview() {
StoriesCurrencies(false, 6000)
}
@Preview
@Composable
private fun Web3Preview() {
StoriesWeb3(false, 6000)
}
@Preview
@Composable
private fun WalletForEveryonePreview() {
StoriesWalletForEveryone(6000)
}

View file

@ -0,0 +1,144 @@
package com.tangem.tap.features.home.compose.content
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.SpacerH
import com.tangem.tap.common.compose.extensions.dpSize
import com.tangem.tap.common.compose.extensions.halfHeight
import com.tangem.tap.common.compose.extensions.toPx
import com.tangem.tap.common.extensions.isEven
import com.tangem.tap.features.home.compose.HorizontalSlidingImage
import com.tangem.wallet.R
@Composable
fun StoriesCurrenciesContent(
paused: Boolean,
duration: Int,
) {
val currencyDrawableList = remember {
listOf(
R.drawable.currency0,
R.drawable.currency1,
R.drawable.currency2,
R.drawable.currency3,
R.drawable.currency4,
)
}
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val decreaseRate = remember { 1f / currencyDrawableList.size }
val designItemHeight = remember { 82.dp }
LightenBox {
Column(modifier = Modifier.graphicsLayer(clip = false)) {
currencyDrawableList.forEachIndexed { index, drawableResId ->
val painter = painterResource(id = drawableResId)
val scaledItemSize = scaleToDesignSize(painter.dpSize(), designItemHeight = designItemHeight)
val itemOversizedScreenWidthBy = scaledItemSize.width - screenWidth
val moveItemToStartOfScreen = itemOversizedScreenWidthBy / 2
val chessOffset = if (index.isEven()) 0.dp else scaledItemSize.halfHeight()
val animateFrom = chessOffset - moveItemToStartOfScreen
val animateTo = 50.dp - (50.dp * index * decreaseRate)
HorizontalSlidingImage(
paused = paused,
duration = duration,
painter = painter,
itemSize = scaledItemSize,
startOffset = animateFrom.toPx(),
targetOffset = animateTo.toPx(),
contentDescription = "Currency row",
)
SpacerH(12.dp)
}
}
}
}
@Composable
fun StoriesWeb3Content(
paused: Boolean,
duration: Int,
) {
val dappsItemList = remember {
listOf(
R.drawable.dapps0,
R.drawable.dapps1,
R.drawable.dapps2,
R.drawable.dapps3,
R.drawable.dapps4,
R.drawable.dapps5,
)
}
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val decreaseRate = remember { 1f / dappsItemList.size }
val designItemHeight = 75.dp
LightenBox {
Column(modifier = Modifier.graphicsLayer(clip = false)) {
dappsItemList.forEachIndexed { index, drawableResId ->
val painter = painterResource(id = drawableResId)
val scaledItemSize = scaleToDesignSize(painter.dpSize(), designItemHeight = designItemHeight)
val itemOversizedScreenWidthBy = scaledItemSize.width - screenWidth
val moveItemToStartOfScreen = itemOversizedScreenWidthBy / 2
val chessOffset = if (index.isEven()) 0.dp else scaledItemSize.width / 3
val animateFrom = chessOffset - moveItemToStartOfScreen
val animateTo = 70.dp - (70.dp * index * decreaseRate)
HorizontalSlidingImage(
paused = paused,
duration = duration,
painter = painter,
itemSize = scaledItemSize,
startOffset = animateFrom.toPx(),
targetOffset = animateTo.toPx(),
contentDescription = "Web3 row",
)
}
}
}
}
@Composable
private fun LightenBox(content: @Composable () -> Unit) {
Box() {
content()
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 250.dp)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.White.copy(alpha = 0f),
Color.White.copy(alpha = 0.75f),
Color.White.copy(alpha = 0.95f),
Color.White
)
)
)
) {}
}
}
private fun scaleToDesignSize(itemSize: DpSize, designItemHeight: Dp): DpSize {
val scaleRate = itemSize.height / designItemHeight
return itemSize / scaleRate
}

View file

@ -0,0 +1,163 @@
package com.tangem.tap.features.home.compose.content
import androidx.annotation.StringRes
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.tap.common.compose.FontSizeRange
import com.tangem.tap.common.compose.TextAutoSize
import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation
import com.tangem.tap.features.home.compose.StoriesTextAnimation
import com.tangem.wallet.R
@Composable
fun FirstStoriesContent(
isPaused: Boolean, duration: Int = 8_000,
hideContent: (Boolean) -> Unit
) {
val screenState = remember { mutableStateOf(StartingScreenState.INIT) }
val progress = remember { Animatable(0f) }
LaunchedEffect(isPaused) {
if (isPaused) {
progress.stop()
} else {
progress.animateTo(
targetValue = 2f,
animationSpec = tween(
durationMillis = duration,
easing = LinearEasing
)
)
}
}
when (progress.value) {
in 0f..0.2f -> screenState.value = StartingScreenState.INIT
in 0.2f..0.3f -> screenState.value = StartingScreenState.BUY
in 0.3f..0.4f -> screenState.value = StartingScreenState.STORE
in 0.4f..0.5f -> screenState.value = StartingScreenState.SEND
in 0.5f..0.6f -> screenState.value = StartingScreenState.PAY
in 0.6f..0.7f -> screenState.value = StartingScreenState.EXCHANGE
in 0.7f..0.8f -> screenState.value = StartingScreenState.BORROW
in 0.8f..1f -> screenState.value = StartingScreenState.LEND
in 1f..1.2f -> screenState.value = StartingScreenState.SHOW_CARD
in 1.2f..2f -> screenState.value = StartingScreenState.MEET_TANGEM
}
if (screenState.value == StartingScreenState.INIT) hideContent(true)
if (screenState.value == StartingScreenState.BUY) hideContent(false)
val style = TextStyle(
fontSize = 60.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White,
textAlign = TextAlign.Center,
)
val textId = screenState.textId()
Box(modifier = Modifier.fillMaxSize()) {
if (screenState.isSplashingTextDisplaying()) {
TextAutoSize(
modifier = Modifier
.align(Alignment.Center)
.padding(start = 20.dp, end = 20.dp, bottom = 100.dp),
text = textId?.let { stringResource(textId) } ?: "",
textStyle = style,
fontSizeRange = FontSizeRange(20.sp, 60.sp)
)
} else {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.weight(0.7f)
) {
if (screenState.isMeetTangemDisplaying()) {
StoriesTextAnimation(
slideInDelay = 0,
) { modifier ->
TextAutoSize(
modifier = modifier
.padding(start = 20.dp, end = 20.dp, top = 50.dp)
.alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f),
text = textId?.let { stringResource(textId) } ?: "",
textStyle = style,
fontSizeRange = FontSizeRange(30.sp, 50.sp)
)
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1.2f)
.wrapContentSize(),
) {
StoriesBottomImageAnimation(
totalDuration = duration,
firstStepDuration = 400,
) { modifier ->
Image(
modifier = modifier.fillMaxWidth(),
painter = painterResource(id = R.drawable.meet_tangem),
contentDescription = "Tangem Wallet card",
)
}
}
}
}
}
}
private enum class StartingScreenState {
INIT, BUY, STORE, SEND, PAY, EXCHANGE, BORROW, LEND, SHOW_CARD, MEET_TANGEM
}
@StringRes
private fun MutableState<StartingScreenState>.textId(): Int? = when (this.value) {
StartingScreenState.INIT -> null
StartingScreenState.BUY -> R.string.story_meet_buy
StartingScreenState.STORE -> R.string.story_meet_store
StartingScreenState.SEND -> R.string.story_meet_send
StartingScreenState.PAY -> R.string.story_meet_pay
StartingScreenState.EXCHANGE -> R.string.story_meet_exchange
StartingScreenState.BORROW -> R.string.story_meet_borrow
StartingScreenState.LEND -> R.string.story_meet_lend
StartingScreenState.SHOW_CARD -> R.string.story_meet_title
StartingScreenState.MEET_TANGEM -> R.string.story_meet_title
}
private fun MutableState<StartingScreenState>.isSplashingTextDisplaying(): Boolean {
return this.value != StartingScreenState.MEET_TANGEM &&
this.value != StartingScreenState.SHOW_CARD
}
private fun MutableState<StartingScreenState>.isMeetTangemDisplaying(): Boolean {
return this.value == StartingScreenState.MEET_TANGEM
}
@Preview
@Composable
fun FirstStoriesPreview() {
FirstStoriesContent(false, 8000) {}
}

View file

@ -0,0 +1,103 @@
package com.tangem.tap.features.home.compose.content
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import com.tangem.tap.common.compose.extensions.AnimatedValue
import com.tangem.tap.common.compose.extensions.asImageBitmap
import com.tangem.tap.common.compose.extensions.toAnimatable
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Composable
fun FloatingCardsContent(
isPaused: Boolean,
stepDuration: Int,
) {
val imageBitmap = asImageBitmap(R.drawable.card_placeholder_wallet)
val cards = listOf(
FloatingCard.first(),
FloatingCard.second(),
FloatingCard.third(),
)
Box() {
cards.forEach { floatingCard ->
FloatingCard.Item(
isPaused = isPaused,
imageBitmap = imageBitmap,
cardValues = floatingCard,
stepDuration = stepDuration,
)
}
}
}
private data class CardValues(
val translateX: AnimatedValue = AnimatedValue(0f, 0f),
val translateY: AnimatedValue = AnimatedValue(0f, 0f),
val rotationX: AnimatedValue = AnimatedValue(0f, 0f),
val rotationY: AnimatedValue = AnimatedValue(0f, 0f),
val rotationZ: AnimatedValue = AnimatedValue(0f, 0f),
val scale: AnimatedValue = AnimatedValue(1f, 1f),
)
private class FloatingCard {
companion object {
@Composable
fun Item(
isPaused: Boolean,
stepDuration: Int,
imageBitmap: ImageBitmap,
cardValues: CardValues,
) {
Image(
bitmap = imageBitmap,
contentDescription = "Floating Tangem card",
modifier = Modifier
.graphicsLayer(
translationX = cardValues.translateX.toAnimatable(isPaused, stepDuration).value,
translationY = cardValues.translateY.toAnimatable(isPaused, stepDuration).value,
rotationX = cardValues.rotationX.toAnimatable(isPaused, stepDuration).value,
rotationY = cardValues.rotationY.toAnimatable(isPaused, stepDuration).value,
rotationZ = cardValues.rotationZ.toAnimatable(isPaused, stepDuration).value,
scaleX = cardValues.scale.toAnimatable(isPaused, stepDuration).value,
scaleY = cardValues.scale.toAnimatable(isPaused, stepDuration).value,
),
)
}
fun first(): CardValues = CardValues(
translateX = -400f to -350f,
translateY = 30f to 32f,
rotationX = 10f to 15f,
rotationY = 15f to 15f,
rotationZ = 40f to 27f,
scale = 0.6f to 0.6f,
)
fun second(): CardValues = CardValues(
translateX = 350f to 300f,
translateY = -70f to 0f,
rotationX = 30f to 48f,
rotationY = 0f to 5f,
rotationZ = -34f to -42f,
scale = 0.47f to 0.35f,
)
fun third(): CardValues = CardValues(
translateX = 320f to 250f,
translateY = 500f to 500f,
rotationX = 0f to 3f,
rotationY = 10f to 10f,
rotationZ = -45f to -30f,
scale = 0.6f to 0.75f,
)
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose
package com.tangem.tap.features.home.compose.views
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose
package com.tangem.tap.features.home.compose.views
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -23,7 +23,9 @@
<string name="story_awe_title">Revolutionary Hardware Wallet</string>
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>
<string name="story_backup_title">Ultra Secure Backup</string>
<string name="story_backup_description">Up to <b>3 physical cards</b> to one wallet</string>
<string name="story_backup_description_1">Up to</string>
<string name="story_backup_description_2_bold">3 physical cards</string>
<string name="story_backup_description_3">to one wallet</string>
<string name="story_currencies_title">Thousands of Currencies</string>
<string name="story_currencies_description">A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously all in one card</string>
<string name="story_web3_title">DeFi Compatible</string>

View file

@ -23,7 +23,9 @@
<string name="story_awe_title">Revolutionary Hardware Wallet</string>
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>
<string name="story_backup_title">Ultra Secure Backup</string>
<string name="story_backup_description">Up to <b>3 physical cards</b> to one wallet</string>
<string name="story_backup_description_1">Up to</string>
<string name="story_backup_description_2_bold">3 physical cards</string>
<string name="story_backup_description_3">to one wallet</string>
<string name="story_currencies_title">Thousands of Currencies</string>
<string name="story_currencies_description">A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously all in one card</string>
<string name="story_web3_title">DeFi Compatible</string>

View file

@ -23,7 +23,9 @@
<string name="story_awe_title">Revolutionary Hardware Wallet</string>
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>
<string name="story_backup_title">Ultra Secure Backup</string>
<string name="story_backup_description">Up to <b>3 physical cards</b> to one wallet</string>
<string name="story_backup_description_1">Up to</string>
<string name="story_backup_description_2_bold">3 physical cards</string>
<string name="story_backup_description_3">to one wallet</string>
<string name="story_currencies_title">Thousands of Currencies</string>
<string name="story_currencies_description">A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously all in one card</string>
<string name="story_web3_title">DeFi Compatible</string>

View file

@ -23,7 +23,9 @@
<string name="story_awe_title">Революционный аппаратный кошелек</string>
<string name="story_awe_description">Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте.</string>
<string name="story_backup_title">Все ключи в безопасности</string>
<string name="story_backup_description">До <b>трех карт</b> с одним кошельком</string>
<string name="story_backup_description_1">До</string>
<string name="story_backup_description_2_bold">трех карт</string>
<string name="story_backup_description_3">с одним кошельком</string>
<string name="story_currencies_title">Тысячи криптовалют</string>
<string name="story_currencies_description">Аппаратный кошелек для ваших биткоинов, эфира и многих других валют одновременно — все в одной карте</string>
<string name="story_web3_title">Поддержка DeFi</string>

View file

@ -23,7 +23,9 @@
<string name="story_awe_title">Revolutionary Hardware Wallet</string>
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>
<string name="story_backup_title">Ultra Secure Backup</string>
<string name="story_backup_description">Up to <b>3 physical cards</b> to one wallet</string>
<string name="story_backup_description_1">Up to</string>
<string name="story_backup_description_2_bold">3 physical cards</string>
<string name="story_backup_description_3">to one wallet</string>
<string name="story_currencies_title">Thousands of Currencies</string>
<string name="story_currencies_description">A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously all in one card</string>
<string name="story_web3_title">DeFi Compatible</string>