Updated on 2026-08-14
This commit is contained in:
parent
285bed34a4
commit
e5c441a9f0
19 changed files with 573 additions and 2 deletions
|
|
@ -9,4 +9,13 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
/** Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.models)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.runtime)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.virtualaccount.onboarding.component
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface VirtualAccountOnboardingComponent : ComposableContentComponent {
|
||||
|
||||
sealed class Params {
|
||||
|
||||
abstract val userWalletId: UserWalletId
|
||||
|
||||
data class Deeplink(override val userWalletId: UserWalletId, val deeplink: String) : Params()
|
||||
|
||||
data class FromMain(override val userWalletId: UserWalletId) : Params()
|
||||
|
||||
data class FromDetailsScreen(override val userWalletId: UserWalletId) : Params()
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, VirtualAccountOnboardingComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.virtualaccount.onboarding.deeplink
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
interface OnboardVirtualAccountsDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(uri: Uri): OnboardVirtualAccountsDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -11,11 +11,23 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
/** Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.error)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** Api */
|
||||
implementation(projects.features.virtualAccounts.onboarding.api)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.configToggles)
|
||||
/** Domain */
|
||||
implementation(projects.domain.common)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.visa)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.foundation)
|
||||
|
|
@ -27,4 +39,7 @@ dependencies {
|
|||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.arrow.core)
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.virtualaccount.onboarding.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.virtualaccount.onboarding.model.VirtualAccountOnboardingModel
|
||||
import com.tangem.features.virtualaccount.onboarding.ui.VirtualAccountOnboardingScreen
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultVirtualAccountOnboardingComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: VirtualAccountOnboardingComponent.Params,
|
||||
) : VirtualAccountOnboardingComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: VirtualAccountOnboardingModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
VirtualAccountOnboardingScreen(modifier = modifier, state = state)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : VirtualAccountOnboardingComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: VirtualAccountOnboardingComponent.Params,
|
||||
): DefaultVirtualAccountOnboardingComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.virtualaccount.onboarding.deeplink
|
||||
|
||||
import android.net.Uri
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultOnboardVirtualAccountsDeepLinkHandler @AssistedInject constructor(
|
||||
@Assisted uri: Uri,
|
||||
appRouter: AppRouter,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
) : OnboardVirtualAccountsDeepLinkHandler {
|
||||
|
||||
init {
|
||||
val userWalletId = userWalletsListRepository.selectedUserWallet.value?.walletId
|
||||
if (userWalletId == null) {
|
||||
TangemLogger.e("Can not open virtual account onboarding deeplink: no selected wallet")
|
||||
} else {
|
||||
val mode = AppRoute.VirtualAccountOnboarding.Mode.Deeplink(
|
||||
userWalletId = userWalletId,
|
||||
deeplink = uri.toString(),
|
||||
)
|
||||
appRouter.push(AppRoute.VirtualAccountOnboarding(mode))
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : OnboardVirtualAccountsDeepLinkHandler.Factory {
|
||||
override fun create(uri: Uri): DefaultOnboardVirtualAccountsDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.features.virtualaccount.onboarding.di
|
||||
|
||||
import com.tangem.features.virtualaccount.onboarding.component.DefaultVirtualAccountOnboardingComponent
|
||||
import com.tangem.features.virtualaccount.onboarding.component.VirtualAccountOnboardingComponent
|
||||
import com.tangem.features.virtualaccount.onboarding.deeplink.DefaultOnboardVirtualAccountsDeepLinkHandler
|
||||
import com.tangem.features.virtualaccount.onboarding.deeplink.OnboardVirtualAccountsDeepLinkHandler
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface VirtualAccountOnboardingFeatureModule {
|
||||
|
||||
@Binds
|
||||
fun bindFactory(impl: DefaultVirtualAccountOnboardingComponent.Factory): VirtualAccountOnboardingComponent.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindOnboardVirtualAccountsDeepLinkHandlerFactory(
|
||||
impl: DefaultOnboardVirtualAccountsDeepLinkHandler.Factory,
|
||||
): OnboardVirtualAccountsDeepLinkHandler.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.virtualaccount.onboarding.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.virtualaccount.onboarding.model.VirtualAccountOnboardingModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface VirtualAccountOnboardingModelsModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(VirtualAccountOnboardingModel::class)
|
||||
fun bindVirtualAccountOnboardingModel(model: VirtualAccountOnboardingModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package com.tangem.features.virtualaccount.onboarding.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.features.virtualaccount.onboarding.component.VirtualAccountOnboardingComponent
|
||||
import com.tangem.features.virtualaccount.onboarding.ui.VirtualAccountOnboardingUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class VirtualAccountOnboardingModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<VirtualAccountOnboardingComponent.Params>()
|
||||
|
||||
val uiState: StateFlow<VirtualAccountOnboardingUM>
|
||||
field = MutableStateFlow<VirtualAccountOnboardingUM>(VirtualAccountOnboardingUM.Loading(onBack = ::back))
|
||||
|
||||
init {
|
||||
when (params) {
|
||||
is VirtualAccountOnboardingComponent.Params.Deeplink -> validateDeeplinkAndShow(params.deeplink)
|
||||
is VirtualAccountOnboardingComponent.Params.FromMain,
|
||||
is VirtualAccountOnboardingComponent.Params.FromDetailsScreen,
|
||||
-> showOnboarding()
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateDeeplinkAndShow(deeplink: String) {
|
||||
modelScope.launch {
|
||||
onboardingRepository.validateDeeplink(deeplink)
|
||||
.onRight { isValid -> if (isValid) showOnboarding() else back() }
|
||||
.onLeft { back() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun showOnboarding() {
|
||||
uiState.update {
|
||||
VirtualAccountOnboardingUM.Content(
|
||||
onBack = ::back,
|
||||
isLoading = false,
|
||||
onGetCardClick = ::onGetCardClick,
|
||||
onTermsClick = ::onTermsClick,
|
||||
onPrivacyClick = ::onPrivacyClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTermsClick() {
|
||||
// TODO([REDACTED_TASK_KEY]): open the provider Terms of Use link.
|
||||
}
|
||||
|
||||
private fun onPrivacyClick() {
|
||||
// TODO([REDACTED_TASK_KEY]): open the provider Privacy Policy link.
|
||||
}
|
||||
|
||||
private fun onGetCardClick() {
|
||||
modelScope.launch {
|
||||
setLoading(isLoading = true)
|
||||
delay(STUB_GET_CARD_DELAY_MS)
|
||||
// TODO: create order and sign challenge [REDACTED_JIRA]
|
||||
setLoading(isLoading = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLoading(isLoading: Boolean) {
|
||||
uiState.update { state ->
|
||||
when (state) {
|
||||
is VirtualAccountOnboardingUM.Content -> state.copy(isLoading = isLoading)
|
||||
is VirtualAccountOnboardingUM.Loading -> state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun back() {
|
||||
router.pop()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// TODO([REDACTED_TASK_KEY]): remove the stub delay once create-order + sign-challenge is implemented.
|
||||
const val STUB_GET_CARD_DELAY_MS = 3000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
package com.tangem.features.virtualaccount.onboarding.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.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.graphics.Brush
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.ds2.button.Close
|
||||
import com.tangem.core.ui.ds2.button.TangemButton
|
||||
import com.tangem.core.ui.extensions.annotatedReference
|
||||
import com.tangem.core.ui.extensions.appendColored
|
||||
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.features.virtualaccount.onboarding.impl.R
|
||||
|
||||
private const val GRADIENT_TRANSPARENT_STOP = 0.45f
|
||||
private const val GRADIENT_OPAQUE_STOP = 0.72f
|
||||
|
||||
private const val TERMS_LINK_TAG = "VA_TERMS"
|
||||
private const val PRIVACY_LINK_TAG = "VA_PRIVACY"
|
||||
|
||||
@Composable
|
||||
internal fun VirtualAccountOnboardingScreen(state: VirtualAccountOnboardingUM, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors3.bg.primary),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.bg_virtual_account_onboarding),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
alignment = Alignment.TopCenter,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colorStops = arrayOf(
|
||||
0f to TangemTheme.colors3.bg.primary.copy(alpha = 0f),
|
||||
GRADIENT_TRANSPARENT_STOP to TangemTheme.colors3.bg.primary.copy(alpha = 0f),
|
||||
GRADIENT_OPAQUE_STOP to TangemTheme.colors3.bg.primary,
|
||||
1f to TangemTheme.colors3.bg.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
when (state) {
|
||||
is VirtualAccountOnboardingUM.Loading -> Loading(modifier = Modifier.fillMaxSize())
|
||||
is VirtualAccountOnboardingUM.Content -> Content(state = state)
|
||||
}
|
||||
|
||||
TangemButton.Close(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.statusBarsPadding()
|
||||
.padding(top = 4.dp, end = 16.dp),
|
||||
onClick = state.onBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Loading(modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier, contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(color = TangemTheme.colors3.icon.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: VirtualAccountOnboardingUM.Content, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding(),
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Send USD from your bank. Receive USDC",
|
||||
style = TangemTheme.typography3.heading.medium,
|
||||
color = TangemTheme.colors3.text.primary,
|
||||
)
|
||||
Text(
|
||||
text = "A dedicated account with US banking details — no deposit or maintenance fees",
|
||||
style = TangemTheme.typography3.subheading.medium,
|
||||
color = TangemTheme.colors3.text.secondary,
|
||||
)
|
||||
}
|
||||
|
||||
TermsCard(
|
||||
modifier = Modifier.padding(top = 24.dp, start = 8.dp, end = 8.dp),
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TermsCard(state: VirtualAccountOnboardingUM.Content, modifier: Modifier = Modifier) {
|
||||
val shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp, bottomStart = 28.dp, bottomEnd = 28.dp)
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape)
|
||||
.background(TangemTheme.colors3.bg.opaque.primary)
|
||||
.border(width = 1.dp, color = TangemTheme.colors3.border.secondary, shape = shape),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp),
|
||||
text = buildTermsAndPolicy(
|
||||
onTermsClick = state.onTermsClick,
|
||||
onPrivacyClick = state.onPrivacyClick,
|
||||
).resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography3.caption.medium,
|
||||
color = TangemTheme.colors3.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
TangemButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 12.dp),
|
||||
variant = TangemButton.Variant.Primary,
|
||||
size = TangemButton.Size.X12,
|
||||
text = stringReference("Open account"),
|
||||
iconEnd = TangemIconUM.Icon(R.drawable.ic_tangem_24),
|
||||
isLoading = state.isLoading,
|
||||
onClick = state.onGetCardClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun buildTermsAndPolicy(onTermsClick: () -> Unit, onPrivacyClick: () -> Unit) = annotatedReference {
|
||||
val linkColor = TangemTheme.colors3.text.primary
|
||||
append("By using service, you agree with provider ")
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
tag = TERMS_LINK_TAG,
|
||||
linkInteractionListener = { onTermsClick() },
|
||||
),
|
||||
block = { appendColored(text = "Terms of Use", color = linkColor) },
|
||||
)
|
||||
append(" and ")
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
tag = PRIVACY_LINK_TAG,
|
||||
linkInteractionListener = { onPrivacyClick() },
|
||||
),
|
||||
block = { appendColored(text = "Privacy Policy", color = linkColor) },
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun VirtualAccountOnboardingScreenPreview(
|
||||
@PreviewParameter(VirtualAccountOnboardingStateProvider::class)
|
||||
state: VirtualAccountOnboardingUM,
|
||||
) {
|
||||
TangemThemePreviewRedesign {
|
||||
VirtualAccountOnboardingScreen(state = state, modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
|
||||
private class VirtualAccountOnboardingStateProvider :
|
||||
CollectionPreviewParameterProvider<VirtualAccountOnboardingUM>(
|
||||
listOf(
|
||||
VirtualAccountOnboardingUM.Loading(onBack = {}),
|
||||
VirtualAccountOnboardingUM.Content(
|
||||
onBack = {},
|
||||
isLoading = false,
|
||||
onGetCardClick = {},
|
||||
onTermsClick = {},
|
||||
onPrivacyClick = {},
|
||||
),
|
||||
VirtualAccountOnboardingUM.Content(
|
||||
onBack = {},
|
||||
isLoading = true,
|
||||
onGetCardClick = {},
|
||||
onTermsClick = {},
|
||||
onPrivacyClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.virtualaccount.onboarding.ui
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
/**
|
||||
* UI model for the Virtual Account onboarding screen.
|
||||
*/
|
||||
@Immutable
|
||||
internal sealed class VirtualAccountOnboardingUM {
|
||||
|
||||
abstract val onBack: () -> Unit
|
||||
|
||||
data class Loading(override val onBack: () -> Unit) : VirtualAccountOnboardingUM()
|
||||
|
||||
data class Content(
|
||||
override val onBack: () -> Unit,
|
||||
val isLoading: Boolean,
|
||||
val onGetCardClick: () -> Unit,
|
||||
val onTermsClick: () -> Unit,
|
||||
val onPrivacyClick: () -> Unit,
|
||||
) : VirtualAccountOnboardingUM()
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Loading…
Add table
Add a link
Reference in a new issue