diff --git a/README.md b/README.md index 9ad5531658..940ce608a1 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,44 @@ Track features development: [board link](https://soramitsucoltd.aha.io/shared/34 To build Fearless Wallet Android project, you need to provide several keys either in enviroment variables or in `local.properties` file: -``` properties +### Moonpay properties +``` MOONPAY_TEST_SECRET=stub MOONPAY_PRODUCTION_SECRET=stub ``` - Note, that with stub keys buy via moonpay will not work correctly. However, other parts of application will not be affected. +### Sora CARD SDK + +For starting Sora CARD SDK initial data have to be provided via gradle properties due to security purpose. + +```` +// PayWings repo credentials properties for getting artifacts +PAY_WINGS_REPOSITORY_URL +PAY_WINGS_USERNAME +PAY_WINGS_PASSWORD + +// Sora CARD API key +SORA_CARD_API_KEY +SORA_CARD_DOMAIN + +// Sora CARD KYC credentials +SORA_CARD_KYC_ENDPOINT_URL +SORA_CARD_KYC_USERNAME +SORA_CARD_KYC_PASSWORD +```` + +### X1 plugin + +X1 is a plugin which is embedded into webView. It requires url and id for launching. + +```` +X1_ENDPOINT_URL_RELEASE +X1_WIDGET_ID_RELEASE + +X1_ENDPOINT_URL_DEBUG +X1_WIDGET_ID_DEBUG +```` + ## License Fearless Wallet Android is available under the Apache 2.0 license. See the LICENSE file for more info. diff --git a/app/build.gradle b/app/build.gradle index 7637f0d886..3b574b5ad7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,6 +9,8 @@ apply plugin: 'kotlin-parcelize' apply plugin: 'com.google.firebase.appdistribution' apply plugin: "com.github.triplet.play" apply from: "../scripts/versions.gradle" +// Add the Crashlytics Gradle plugin +apply plugin: 'com.google.firebase.crashlytics' android { compileSdkVersion rootProject.compileSdkVersion @@ -118,6 +120,14 @@ play { } dependencies { + // Import the BoM for the Firebase platform + implementation platform('com.google.firebase:firebase-bom:31.2.3') + + // Add the dependencies for the Crashlytics and Analytics libraries + // When using the BoM, you don't specify versions in Firebase library dependencies + implementation 'com.google.firebase:firebase-crashlytics-ktx' + implementation 'com.google.firebase:firebase-analytics-ktx' + implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':core-db') implementation project(':common') @@ -146,6 +156,9 @@ dependencies { implementation project(':feature-success-api') implementation project(':feature-success-impl') + implementation project(':feature-soracard-api') + implementation project(':feature-soracard-impl') + implementation kotlinDep implementation androidDep diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 763f7cc8b2..4afc7b0e83 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,13 +9,14 @@ + android:supportsRtl="true" + android:theme="@style/Theme.AppCompat.Light" + tools:replace="android:allowBackup"> @@ -62,7 +63,8 @@ android:exported="false"> + android:resource="@xml/provider_paths" + tools:replace="android:resource" /> diff --git a/app/src/main/java/jp/co/soramitsu/app/di/app/NavigationModule.kt b/app/src/main/java/jp/co/soramitsu/app/di/app/NavigationModule.kt index f9b7b6d5c1..6b24dc669e 100644 --- a/app/src/main/java/jp/co/soramitsu/app/di/app/NavigationModule.kt +++ b/app/src/main/java/jp/co/soramitsu/app/di/app/NavigationModule.kt @@ -4,16 +4,17 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import jp.co.soramitsu.app.root.navigation.Navigator +import javax.inject.Singleton import jp.co.soramitsu.account.impl.presentation.AccountRouter +import jp.co.soramitsu.app.root.navigation.Navigator import jp.co.soramitsu.crowdloan.impl.presentation.CrowdloanRouter import jp.co.soramitsu.onboarding.impl.OnboardingRouter import jp.co.soramitsu.polkaswap.api.presentation.PolkaswapRouter -import jp.co.soramitsu.staking.impl.presentation.StakingRouter -import jp.co.soramitsu.wallet.impl.presentation.WalletRouter +import jp.co.soramitsu.soracard.api.presentation.SoraCardRouter import jp.co.soramitsu.splash.SplashRouter +import jp.co.soramitsu.staking.impl.presentation.StakingRouter import jp.co.soramitsu.success.presentation.SuccessRouter -import javax.inject.Singleton +import jp.co.soramitsu.wallet.impl.presentation.WalletRouter @InstallIn(SingletonComponent::class) @Module @@ -54,4 +55,8 @@ class NavigationModule { @Singleton @Provides fun provideCrowdloanRouter(navigator: Navigator): CrowdloanRouter = navigator + + @Singleton + @Provides + fun provideSoraCardRouter(navigator: Navigator): SoraCardRouter = navigator } diff --git a/app/src/main/java/jp/co/soramitsu/app/root/navigation/Navigator.kt b/app/src/main/java/jp/co/soramitsu/app/root/navigation/Navigator.kt index b6d1464323..f4d414d286 100644 --- a/app/src/main/java/jp/co/soramitsu/app/root/navigation/Navigator.kt +++ b/app/src/main/java/jp/co/soramitsu/app/root/navigation/Navigator.kt @@ -69,6 +69,7 @@ import jp.co.soramitsu.polkaswap.impl.presentation.swap_tokens.SwapTokensFragmen import jp.co.soramitsu.polkaswap.impl.presentation.transaction_settings.TransactionSettingsFragment import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId +import jp.co.soramitsu.soracard.api.presentation.SoraCardRouter import jp.co.soramitsu.splash.SplashRouter import jp.co.soramitsu.staking.api.domain.model.PoolInfo import jp.co.soramitsu.staking.impl.presentation.StakingRouter @@ -153,7 +154,8 @@ class Navigator : StakingRouter, CrowdloanRouter, PolkaswapRouter, - SuccessRouter { + SuccessRouter, + SoraCardRouter { private var navController: NavController? = null private var activity: AppCompatActivity? = null @@ -575,8 +577,8 @@ class Navigator : navController?.navigate(R.id.back_to_main) } - override fun returnToAssetDetails() { - navController?.navigate(R.id.back_to_asset_details) + override fun closeSwap() { + navController?.navigate(R.id.close_swap) } override fun openValidatorDetails(validatorDetails: ValidatorDetailsParcelModel) { @@ -597,12 +599,16 @@ class Navigator : navController?.navigate(R.id.sendSetupFragment, bundle) } - override fun openSwapTokensScreen(assetPayload: AssetPayload) { - val bundle = SwapTokensFragment.getBundle(assetPayload.chainAssetId, assetPayload.chainId) + override fun openSwapTokensScreen(assetId: String, chainId: String) { + val bundle = SwapTokensFragment.getBundle(assetId, chainId) navController?.navigate(R.id.swapTokensFragment, bundle) } + override fun showBuyCrypto() { + navController?.navigate(R.id.buyCryptoFragment) + } + override fun openSelectChain(assetId: String, chainId: ChainId?, chooserMode: Boolean) { val bundle = ChainSelectFragment.getBundle(assetId = assetId, chainId = chainId, chooserMode = chooserMode) navController?.navigate(R.id.chainSelectFragment, bundle) @@ -989,6 +995,10 @@ class Navigator : navController?.navigate(R.id.editPoolConfirmFragment) } + override fun openGetSoraCard() { + navController?.navigate(R.id.getSoraCardFragment) + } + override val walletSelectorPayloadFlow: Flow get() = navController?.currentBackStackEntry?.savedStateHandle ?.getLiveData(WalletSelectorPayload::class.java.name) @@ -1053,4 +1063,8 @@ class Navigator : val bundle = PoolFullUnstakeDepositorAlertFragment.getBundle(amount) navController?.navigate(R.id.poolFullUnstakeDepositorAlertFragment, bundle) } + + override fun openGetMoreXor() { + navController?.navigate(R.id.getMoreXorFragment) + } } diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml index fb89567257..1955a6a99f 100644 --- a/app/src/main/res/navigation/main_nav_graph.xml +++ b/app/src/main/res/navigation/main_nav_graph.xml @@ -388,6 +388,11 @@ android:name="jp.co.soramitsu.wallet.impl.presentation.transaction.filter.TransactionHistoryFilterFragment" android:label="TransactionHistoryFilterFragment" /> + + + + + + + app:popUpTo="@id/swapTokensFragment" + app:popUpToInclusive="true" /> : AppCompatActivity() { or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN ) + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) setContentView(layoutResource()) initViews() diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/component/AlertSheet.kt b/common/src/main/java/jp/co/soramitsu/common/compose/component/AlertSheet.kt index a5a23afad5..63a2465724 100644 --- a/common/src/main/java/jp/co/soramitsu/common/compose/component/AlertSheet.kt +++ b/common/src/main/java/jp/co/soramitsu/common/compose/component/AlertSheet.kt @@ -25,6 +25,7 @@ import jp.co.soramitsu.common.compose.theme.FearlessTheme import jp.co.soramitsu.common.compose.theme.alertYellow import jp.co.soramitsu.common.compose.theme.black2 import jp.co.soramitsu.common.compose.theme.fontSize +import jp.co.soramitsu.common.compose.theme.soraTextStyle import jp.co.soramitsu.common.compose.theme.weight import jp.co.soramitsu.common.compose.theme.white diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/component/Button.kt b/common/src/main/java/jp/co/soramitsu/common/compose/component/Button.kt index dd85b05a11..5fe442b0c6 100644 --- a/common/src/main/java/jp/co/soramitsu/common/compose/component/Button.kt +++ b/common/src/main/java/jp/co/soramitsu/common/compose/component/Button.kt @@ -1,6 +1,7 @@ package jp.co.soramitsu.common.compose.component import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope @@ -14,6 +15,7 @@ import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview @@ -22,6 +24,7 @@ import jp.co.soramitsu.common.R import jp.co.soramitsu.common.compose.theme.FearlessTheme import jp.co.soramitsu.common.compose.theme.accentButtonColors import jp.co.soramitsu.common.compose.theme.colorAccent +import jp.co.soramitsu.common.compose.theme.colorAccentDark import jp.co.soramitsu.common.compose.theme.customButtonColors import jp.co.soramitsu.common.compose.theme.customTypography import jp.co.soramitsu.common.compose.theme.grayButtonBackground @@ -47,6 +50,11 @@ fun GrayButton(text: String, enabled: Boolean = true, modifier: Modifier = Modif TextButton(text = text, enabled = enabled, colors = customButtonColors(grayButtonBackground), modifier = modifier, onClick = onClick) } +@Composable +fun TransparentButton(text: String, enabled: Boolean = true, modifier: Modifier = Modifier, onClick: () -> Unit) { + TextButton(text = text, enabled = enabled, colors = customButtonColors(Color.Unspecified, colorAccentDark), modifier = modifier, onClick = onClick) +} + @Composable fun TextButton( text: String, @@ -151,11 +159,34 @@ fun ColoredButton( ) } +@Composable +fun ShapeButton( + modifier: Modifier = Modifier, + enabled: Boolean = true, + backgroundColor: Color, + border: BorderStroke? = null, + shape: Shape = FearlessCorneredShape(cornerRadius = 4.dp, cornerCutLength = 6.dp), + contentPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 4.dp), + onClick: () -> Unit, + content: @Composable RowScope.() -> Unit +) { + TextButton( + modifier = modifier, + onClick = onClick, + shape = shape, + colors = customButtonColors(backgroundColor), + border = border, + enabled = enabled, + contentPadding = contentPadding, + content = content + ) +} + @Composable @Preview fun ButtonPreview() { FearlessTheme { - Column(modifier = Modifier.padding(16.dp)) { + Column(modifier = Modifier.padding(16.dp).background(Color.Black)) { AccentButton( "Start staking", modifier = Modifier @@ -190,6 +221,12 @@ fun ButtonPreview() { colors = customButtonColors(colorAccent), onClick = {} ) + MarginVertical(margin = 16.dp) + TransparentButton( + modifier = Modifier.height(52.dp), + text = stringResource(id = R.string.staking_redeem), + onClick = {} + ) } } } diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/component/ChainSelector.kt b/common/src/main/java/jp/co/soramitsu/common/compose/component/ChainSelector.kt index 1195590c1a..ff3e62ea7d 100644 --- a/common/src/main/java/jp/co/soramitsu/common/compose/component/ChainSelector.kt +++ b/common/src/main/java/jp/co/soramitsu/common/compose/component/ChainSelector.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import jp.co.soramitsu.common.R -import jp.co.soramitsu.common.compose.theme.FearlessThemeBlackBg import jp.co.soramitsu.common.compose.theme.backgroundBlurColor import jp.co.soramitsu.common.compose.theme.colorAccent import jp.co.soramitsu.common.compose.theme.customTypography @@ -72,14 +71,12 @@ fun ChainSelector( @Preview @Composable private fun ChainSelectorPreview() { - FearlessThemeBlackBg { - ChainSelector( - selectorViewState = ChainSelectorViewState( - selectedChainId = "id", - selectedChainName = "Kusama", - selectedChainStatusColor = colorAccent - ), - onChangeChainClick = {} - ) - } + ChainSelector( + selectorViewState = ChainSelectorViewState( + selectedChainId = "id", + selectedChainName = "Kusama", + selectedChainStatusColor = colorAccent + ), + onChangeChainClick = {} + ) } diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/component/CorneredInput.kt b/common/src/main/java/jp/co/soramitsu/common/compose/component/CorneredInput.kt index 35b5311a8c..c37a6c55da 100644 --- a/common/src/main/java/jp/co/soramitsu/common/compose/component/CorneredInput.kt +++ b/common/src/main/java/jp/co/soramitsu/common/compose/component/CorneredInput.kt @@ -10,7 +10,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import jp.co.soramitsu.common.R -import jp.co.soramitsu.common.compose.theme.FearlessThemeBlackBg import jp.co.soramitsu.common.compose.theme.black2 import jp.co.soramitsu.common.compose.theme.white04 import jp.co.soramitsu.common.compose.theme.white08 @@ -58,10 +57,8 @@ private fun SearchHint(text: String?) { @Preview @Composable private fun PreviewCorneredInput() { - FearlessThemeBlackBg { - Column() { - CorneredInput(state = "", onInput = {}) - CorneredInput(state = "AAAAAA", onInput = {}) - } + Column() { + CorneredInput(state = "", onInput = {}) + CorneredInput(state = "AAAAAA", onInput = {}) } } diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/component/ProgressDialog.kt b/common/src/main/java/jp/co/soramitsu/common/compose/component/ProgressDialog.kt new file mode 100644 index 0000000000..76edcd0ee5 --- /dev/null +++ b/common/src/main/java/jp/co/soramitsu/common/compose/component/ProgressDialog.kt @@ -0,0 +1,41 @@ +package jp.co.soramitsu.common.compose.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +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.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import jp.co.soramitsu.common.compose.theme.colorAccentDark +import jp.co.soramitsu.common.compose.theme.customColors + +@Composable +fun ProgressDialog() { + Dialog( + onDismissRequest = {}, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false + ) + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.customColors.backgroundBlurColor), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(128.dp).padding(4.dp), + color = colorAccentDark + ) + } + } +} diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/component/SwipeBox.kt b/common/src/main/java/jp/co/soramitsu/common/compose/component/SwipeBox.kt index 24617034b9..11d98f11ba 100644 --- a/common/src/main/java/jp/co/soramitsu/common/compose/component/SwipeBox.kt +++ b/common/src/main/java/jp/co/soramitsu/common/compose/component/SwipeBox.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import jp.co.soramitsu.common.compose.theme.FearlessThemeBlackBg import jp.co.soramitsu.common.compose.viewstate.AssetListItemViewState import kotlin.math.absoluteValue import kotlin.math.roundToInt @@ -189,18 +188,16 @@ private fun AssetItemSwipeBoxPreview() { hasNetworkIssue = false ) - FearlessThemeBlackBg { - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - SwipeBox( - state = SwipeBoxViewState( - leftStateWidth = 250.dp, - rightStateWidth = 90.dp - ), - swipeableState = rememberSwipeableState(SwipeState.INITIAL), - leftContent = { ActionBar(leftActionBarViewState) }, - rightContent = { ActionBar(rightActionBarViewState) }, - initialContent = { AssetListItem(state = assetListItemViewState, onClick = {}) } - ) - } + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + SwipeBox( + state = SwipeBoxViewState( + leftStateWidth = 250.dp, + rightStateWidth = 90.dp + ), + swipeableState = rememberSwipeableState(SwipeState.INITIAL), + leftContent = { ActionBar(leftActionBarViewState) }, + rightContent = { ActionBar(rightActionBarViewState) }, + initialContent = { AssetListItem(state = assetListItemViewState, onClick = {}) } + ) } } diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/component/Utils.kt b/common/src/main/java/jp/co/soramitsu/common/compose/component/Utils.kt index d21f166711..0291b1a80e 100644 --- a/common/src/main/java/jp/co/soramitsu/common/compose/component/Utils.kt +++ b/common/src/main/java/jp/co/soramitsu/common/compose/component/Utils.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.vectorResource import coil.decode.SvgDecoder import coil.request.ImageRequest @@ -19,13 +20,20 @@ fun getImageRequest(context: Context, url: String): ImageRequest { } @Composable -fun Image(modifier: Modifier = Modifier, @DrawableRes res: Int, tint: Color? = null, contentDescription: String? = null) { +fun Image( + modifier: Modifier = Modifier, + @DrawableRes res: Int, + tint: Color? = null, + contentDescription: String? = null, + contentScale: ContentScale = ContentScale.Fit +) { androidx.compose.foundation.Image( modifier = modifier, imageVector = ImageVector.vectorResource( id = res ), contentDescription = contentDescription, - colorFilter = tint?.let { ColorFilter.tint(it) } + colorFilter = tint?.let { ColorFilter.tint(it) }, + contentScale = contentScale ) } diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/theme/Color.kt b/common/src/main/java/jp/co/soramitsu/common/compose/theme/Color.kt index 9abb2aa34a..6e4b883e7d 100644 --- a/common/src/main/java/jp/co/soramitsu/common/compose/theme/Color.kt +++ b/common/src/main/java/jp/co/soramitsu/common/compose/theme/Color.kt @@ -67,6 +67,8 @@ val alertYellow = Color(0xFFEE7700) val transparent = Color(0xffffff) val colorAccentDark = Color(0xFFEE0077) +val colorAccentSecondary = Color(0xFFD5D5D5) +val soraRed = Color(0xFFEE2233) val accentButtonColors = object : ButtonColors { @Composable diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/theme/CustomTypography.kt b/common/src/main/java/jp/co/soramitsu/common/compose/theme/CustomTypography.kt index 2ab3e8bb04..46e71d37de 100644 --- a/common/src/main/java/jp/co/soramitsu/common/compose/theme/CustomTypography.kt +++ b/common/src/main/java/jp/co/soramitsu/common/compose/theme/CustomTypography.kt @@ -6,7 +6,6 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp -import jp.co.soramitsu.common.compose.component.soraTextStyle @Stable data class CustomTypography( diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/theme/SoraTextStyle.kt b/common/src/main/java/jp/co/soramitsu/common/compose/theme/SoraTextStyle.kt index 87574dde2c..e0ad85e4f3 100644 --- a/common/src/main/java/jp/co/soramitsu/common/compose/theme/SoraTextStyle.kt +++ b/common/src/main/java/jp/co/soramitsu/common/compose/theme/SoraTextStyle.kt @@ -1,4 +1,4 @@ -package jp.co.soramitsu.common.compose.component +package jp.co.soramitsu.common.compose.theme import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shadow @@ -15,9 +15,8 @@ import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextGeometricTransform import androidx.compose.ui.text.style.TextIndent import androidx.compose.ui.unit.TextUnit -import jp.co.soramitsu.common.compose.theme.Sora -// text style with changed fontFamitly = Sora and textColor = White +// text style with changed fontFamily = Sora and textColor = White fun soraTextStyle( color: Color = Color.White, fontSize: TextUnit = TextUnit.Unspecified, diff --git a/common/src/main/java/jp/co/soramitsu/common/compose/theme/Theme.kt b/common/src/main/java/jp/co/soramitsu/common/compose/theme/Theme.kt index 053035b6c6..ab7237d924 100644 --- a/common/src/main/java/jp/co/soramitsu/common/compose/theme/Theme.kt +++ b/common/src/main/java/jp/co/soramitsu/common/compose/theme/Theme.kt @@ -5,7 +5,6 @@ import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.graphics.Color @Composable fun FearlessTheme( @@ -23,22 +22,6 @@ fun FearlessTheme( } } -@Composable -fun FearlessThemeBlackBg( - darkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit -) { - CompositionLocalProvider( - FearlessColors provides flwColors, - FearlessTypography provides flwTypography - ) { - MaterialTheme( - colors = fearlessMaterialColors.copy(background = Color.Black.copy(alpha = 0.8f), surface = Color.Black.copy(alpha = 0.8f)), - content = content - ) - } -} - val MaterialTheme.customColors: CustomColors @Composable @ReadOnlyComposable diff --git a/common/src/main/java/jp/co/soramitsu/common/data/network/coingecko/CoingeckoApi.kt b/common/src/main/java/jp/co/soramitsu/common/data/network/coingecko/CoingeckoApi.kt index 39e315a297..9d4be5ab38 100644 --- a/common/src/main/java/jp/co/soramitsu/common/data/network/coingecko/CoingeckoApi.kt +++ b/common/src/main/java/jp/co/soramitsu/common/data/network/coingecko/CoingeckoApi.kt @@ -1,9 +1,9 @@ package jp.co.soramitsu.common.data.network.coingecko -import java.math.BigDecimal import jp.co.soramitsu.common.BuildConfig import retrofit2.http.GET import retrofit2.http.Query +import java.math.BigDecimal interface CoingeckoApi { @@ -14,6 +14,12 @@ interface CoingeckoApi { @Query("include_24hr_change") includeRateChange: Boolean ): Map> + @GET("//api.coingecko.com/api/v3/simple/price") + suspend fun getSingleAssetPrice( + @Query("ids") priceIds: String, + @Query("vs_currencies") currency: String + ): Map> + @GET("//api.coingecko.com/api/v3/simple/supported_vs_currencies") suspend fun getSupportedCurrencies(): List diff --git a/common/src/main/java/jp/co/soramitsu/common/data/storage/Preferences.kt b/common/src/main/java/jp/co/soramitsu/common/data/storage/Preferences.kt index 9707c833c2..6ab786d954 100644 --- a/common/src/main/java/jp/co/soramitsu/common/data/storage/Preferences.kt +++ b/common/src/main/java/jp/co/soramitsu/common/data/storage/Preferences.kt @@ -41,5 +41,7 @@ interface Preferences { initialValueProducer: InitialValueProducer? = null ): Flow + fun intFlow(field: String, initialValue: Int): Flow + fun booleanFlow(field: String, initialValue: Boolean): Flow } diff --git a/common/src/main/java/jp/co/soramitsu/common/data/storage/PreferencesImpl.kt b/common/src/main/java/jp/co/soramitsu/common/data/storage/PreferencesImpl.kt index 08582f8f43..14e5c2f000 100644 --- a/common/src/main/java/jp/co/soramitsu/common/data/storage/PreferencesImpl.kt +++ b/common/src/main/java/jp/co/soramitsu/common/data/storage/PreferencesImpl.kt @@ -114,6 +114,33 @@ class PreferencesImpl( sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) } } + + override fun intFlow( + field: String, + initialValue: Int + ): Flow = callbackFlow { + if (contains(field)) { + send(getInt(field, 0)) + } else { + putInt(field, initialValue) + send(initialValue) + } + + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == field) { + trySend(getInt(field, initialValue)) + } + } + + listeners.add(listener) + sharedPreferences.registerOnSharedPreferenceChangeListener(listener) + + awaitClose { + listeners.remove(listener) + sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) + } + } + override fun booleanFlow( field: String, initialValue: Boolean diff --git a/common/src/main/java/jp/co/soramitsu/common/presentation/ErrorDialog.kt b/common/src/main/java/jp/co/soramitsu/common/presentation/ErrorDialog.kt index d7517a3d1d..e694fd73ba 100644 --- a/common/src/main/java/jp/co/soramitsu/common/presentation/ErrorDialog.kt +++ b/common/src/main/java/jp/co/soramitsu/common/presentation/ErrorDialog.kt @@ -38,7 +38,7 @@ import jp.co.soramitsu.common.compose.component.Grip import jp.co.soramitsu.common.compose.component.H3 import jp.co.soramitsu.common.compose.component.MarginVertical import jp.co.soramitsu.common.compose.component.emptyClick -import jp.co.soramitsu.common.compose.component.soraTextStyle +import jp.co.soramitsu.common.compose.theme.soraTextStyle import jp.co.soramitsu.common.compose.theme.FearlessTheme import jp.co.soramitsu.common.compose.theme.alertYellow import jp.co.soramitsu.common.compose.theme.black2 diff --git a/common/src/main/res/drawable-xxxhdpi/noise.png b/common/src/main/res/drawable-xxxhdpi/noise.png new file mode 100644 index 0000000000..b5e0599c42 Binary files /dev/null and b/common/src/main/res/drawable-xxxhdpi/noise.png differ diff --git a/common/src/main/res/drawable/ic_card.xml b/common/src/main/res/drawable/ic_card.xml new file mode 100644 index 0000000000..5e6b4b195a --- /dev/null +++ b/common/src/main/res/drawable/ic_card.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/common/src/main/res/drawable/ic_check_small.xml b/common/src/main/res/drawable/ic_check_small.xml new file mode 100644 index 0000000000..11fb735cc7 --- /dev/null +++ b/common/src/main/res/drawable/ic_check_small.xml @@ -0,0 +1,9 @@ + + + diff --git a/common/src/main/res/drawable/ic_close_16_white_circle.xml b/common/src/main/res/drawable/ic_close_16_white_circle.xml new file mode 100644 index 0000000000..8f1ff0e0b6 --- /dev/null +++ b/common/src/main/res/drawable/ic_close_16_white_circle.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/common/src/main/res/drawable/ic_cross_small.xml b/common/src/main/res/drawable/ic_cross_small.xml new file mode 100644 index 0000000000..e578324e05 --- /dev/null +++ b/common/src/main/res/drawable/ic_cross_small.xml @@ -0,0 +1,9 @@ + + + diff --git a/common/src/main/res/drawable/ic_exclamation.xml b/common/src/main/res/drawable/ic_exclamation.xml new file mode 100644 index 0000000000..dc04a63027 --- /dev/null +++ b/common/src/main/res/drawable/ic_exclamation.xml @@ -0,0 +1,12 @@ + + + + diff --git a/common/src/main/res/drawable/ic_paypass.xml b/common/src/main/res/drawable/ic_paypass.xml new file mode 100644 index 0000000000..bcf721cdde --- /dev/null +++ b/common/src/main/res/drawable/ic_paypass.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/common/src/main/res/drawable/ic_sora_logo.xml b/common/src/main/res/drawable/ic_sora_logo.xml new file mode 100644 index 0000000000..cceea7f5b3 --- /dev/null +++ b/common/src/main/res/drawable/ic_sora_logo.xml @@ -0,0 +1,12 @@ + + + + diff --git a/common/src/main/res/drawable/logo_mc.xml b/common/src/main/res/drawable/logo_mc.xml new file mode 100644 index 0000000000..b4b8746527 --- /dev/null +++ b/common/src/main/res/drawable/logo_mc.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/common/src/main/res/drawable/logo_soracard_text.xml b/common/src/main/res/drawable/logo_soracard_text.xml new file mode 100644 index 0000000000..05336a1345 --- /dev/null +++ b/common/src/main/res/drawable/logo_soracard_text.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/common/src/main/res/values-ru/strings.xml b/common/src/main/res/values-ru/strings.xml index 9d01f8b701..94ce3d5cd4 100644 --- a/common/src/main/res/values-ru/strings.xml +++ b/common/src/main/res/values-ru/strings.xml @@ -953,4 +953,30 @@ Понимание и добровольное принятие рисков, связанных с использованием Polkaswap, включая, но не ограничиваясь, риском потери токенов. Не продолжайте, пока не ознакомитесь с %%Polkaswap FAQ%%, %%Polkaswap Меморандумом и Условиями и положениями%%, и %%Политикой конфиденциальности%%! Я подтверждаю, что ознакомился со всеми упомянутыми документами и, нажимая «Продолжить», принимаю их. + + Пополняйте карту SORA с помощью фиата или криптовалюты и оплачивайте онлайн, в магазине или в банкомате. + 0 € годовой сервисный сбор + Получите счет IBAN в евро и дебетовую карту Mastercard, подключенную к вашему кошельку SORA + Aктивировать карту + Бесплатный выпуск карты + Если вы держите, ставите или предоставляете ликвидность XOR на сумму не менее 100 евро на своем счете SORA. + У вас достаточно XOR + You need %s XOR (€%s) + Get more XOR + Select a way you want to get XOR + I already have a card + Issue card for free + Issue card for 12 € + Swap for XOR + Buy XOR with fiat + + No more free attempts + Ошибка Проверки + Выполняется Проверка + Карта ОТКЛОНЕНА + Карта В Пути + Get SORA Card + + Исключения из заявки + некоторых стран
не могут подать заявку на получение карты SORA Card в данный момент
Просмотреть список ]]>
diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 3974a65373..7d0865114c 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -23,6 +23,7 @@ Terms and Conditions and Privacy Policy
operations\nwill appear here
Settings Language + SORA Card App version Contact Email About @@ -1053,4 +1054,29 @@ Remember to make a backup of your key and keep it in a safe and private place (e Polkaswap disclaimer + Top up SORA Card with fiat or crypto and pay online, in-store or withdraw in ATM + 0 € annual service fee + Get a Euro IBAN account and Mastercard Debit Card connected to your SORA Wallet + Enable card + Free card issuance + If you hold, stake or provide liquidity for at least €100 worth of XOR in your SORA account + You have enough XOR + You need %s XOR (€%s) + Get more XOR + Select a way you want to get XOR + I already have a card + Issue card for free + Issue card for 12 € + Swap for XOR + Buy XOR with fiat + + No more free attempts + Verification Failed + Verification In Progress + Card Is REJECTED + Card Is On The Way + Get SORA Card + + Excluded of application + certain countries
can not apply for SORA Card at this moment
See the list]]>
diff --git a/core-db/src/androidTest/java/jp/co/soramitsu/coredb/migrations/V2MigrationTest.kt b/core-db/src/androidTest/java/jp/co/soramitsu/coredb/migrations/V2MigrationTest.kt index b957b455b4..7a756363af 100644 --- a/core-db/src/androidTest/java/jp/co/soramitsu/coredb/migrations/V2MigrationTest.kt +++ b/core-db/src/androidTest/java/jp/co/soramitsu/coredb/migrations/V2MigrationTest.kt @@ -6,8 +6,6 @@ import androidx.room.testing.MigrationTestHelper import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.platform.app.InstrumentationRegistry -import jp.co.soramitsu.common.data.mappers.mapCryptoTypeToEncryption -import jp.co.soramitsu.common.data.mappers.mapEncryptionToCryptoType import jp.co.soramitsu.common.data.secrets.v1.SecretStoreV1 import jp.co.soramitsu.common.data.secrets.v1.SecretStoreV1Impl import jp.co.soramitsu.common.data.secrets.v2.KeyPairSchema @@ -18,6 +16,8 @@ import jp.co.soramitsu.common.utils.deriveSeed32 import jp.co.soramitsu.common.utils.ethereumAddressFromPublicKey import jp.co.soramitsu.common.utils.map import jp.co.soramitsu.common.utils.substrateAccountId +import jp.co.soramitsu.core.crypto.mapCryptoTypeToEncryption +import jp.co.soramitsu.core.crypto.mapEncryptionToCryptoType import jp.co.soramitsu.core.model.SecuritySource import jp.co.soramitsu.coredb.AppDatabase import jp.co.soramitsu.coredb.model.chain.MetaAccountLocal diff --git a/core-db/src/main/java/jp/co/soramitsu/coredb/AppDatabase.kt b/core-db/src/main/java/jp/co/soramitsu/coredb/AppDatabase.kt index 232bed0335..ba2ad95c71 100644 --- a/core-db/src/main/java/jp/co/soramitsu/coredb/AppDatabase.kt +++ b/core-db/src/main/java/jp/co/soramitsu/coredb/AppDatabase.kt @@ -18,6 +18,7 @@ import jp.co.soramitsu.coredb.dao.ChainDao import jp.co.soramitsu.coredb.dao.MetaAccountDao import jp.co.soramitsu.coredb.dao.OperationDao import jp.co.soramitsu.coredb.dao.PhishingDao +import jp.co.soramitsu.coredb.dao.SoraCardDao import jp.co.soramitsu.coredb.dao.StakingTotalRewardDao import jp.co.soramitsu.coredb.dao.StorageDao import jp.co.soramitsu.coredb.dao.TokenPriceDao @@ -52,6 +53,7 @@ import jp.co.soramitsu.coredb.migrations.Migration_46_47 import jp.co.soramitsu.coredb.migrations.Migration_47_48 import jp.co.soramitsu.coredb.migrations.Migration_48_49 import jp.co.soramitsu.coredb.migrations.Migration_49_50 +import jp.co.soramitsu.coredb.migrations.Migration_50_51 import jp.co.soramitsu.coredb.migrations.RemoveAccountForeignKeyFromAsset_17_18 import jp.co.soramitsu.coredb.migrations.RemoveLegacyData_35_36 import jp.co.soramitsu.coredb.migrations.RemoveStakingRewardsTable_22_23 @@ -62,6 +64,7 @@ import jp.co.soramitsu.coredb.model.AddressBookContact import jp.co.soramitsu.coredb.model.AssetLocal import jp.co.soramitsu.coredb.model.OperationLocal import jp.co.soramitsu.coredb.model.PhishingLocal +import jp.co.soramitsu.coredb.model.SoraCardInfoLocal import jp.co.soramitsu.coredb.model.StorageEntryLocal import jp.co.soramitsu.coredb.model.TokenPriceLocal import jp.co.soramitsu.coredb.model.TotalRewardLocal @@ -74,7 +77,7 @@ import jp.co.soramitsu.coredb.model.chain.ChainRuntimeInfoLocal import jp.co.soramitsu.coredb.model.chain.MetaAccountLocal @Database( - version = 50, + version = 51, entities = [ AccountLocal::class, AddressBookContact::class, @@ -92,7 +95,8 @@ import jp.co.soramitsu.coredb.model.chain.MetaAccountLocal ChainRuntimeInfoLocal::class, MetaAccountLocal::class, ChainAccountLocal::class, - ChainExplorerLocal::class + ChainExplorerLocal::class, + SoraCardInfoLocal::class ] ) @TypeConverters( @@ -142,6 +146,7 @@ abstract class AppDatabase : RoomDatabase() { .addMigrations(Migration_47_48) .addMigrations(Migration_48_49) .addMigrations(Migration_49_50) + .addMigrations(Migration_50_51) .build() } return instance!! @@ -169,4 +174,6 @@ abstract class AppDatabase : RoomDatabase() { abstract fun metaAccountDao(): MetaAccountDao abstract fun addressBookDao(): AddressBookDao + + abstract fun soraCardDao(): SoraCardDao } diff --git a/core-db/src/main/java/jp/co/soramitsu/coredb/dao/SoraCardDao.kt b/core-db/src/main/java/jp/co/soramitsu/coredb/dao/SoraCardDao.kt new file mode 100644 index 0000000000..7bbaacf1ad --- /dev/null +++ b/core-db/src/main/java/jp/co/soramitsu/coredb/dao/SoraCardDao.kt @@ -0,0 +1,26 @@ +package jp.co.soramitsu.coredb.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import jp.co.soramitsu.coredb.model.SoraCardInfoLocal +import kotlinx.coroutines.flow.Flow + +@Dao +interface SoraCardDao { + @Query("select * from sora_card where :id = id") + fun observeSoraCardInfo(id: String): Flow + + @Query("select * from sora_card where :id = id") + suspend fun getSoraCardInfo(id: String): SoraCardInfoLocal? + + @Query("update sora_card set kycStatus=:kycStatus where :id = id") + suspend fun updateKycStatus(id: String, kycStatus: String) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(soraCardInfoLocal: SoraCardInfoLocal) + + @Query("delete from sora_card") + suspend fun clearTable() +} diff --git a/core-db/src/main/java/jp/co/soramitsu/coredb/di/DbModule.kt b/core-db/src/main/java/jp/co/soramitsu/coredb/di/DbModule.kt index 4adab16b70..279430ba57 100644 --- a/core-db/src/main/java/jp/co/soramitsu/coredb/di/DbModule.kt +++ b/core-db/src/main/java/jp/co/soramitsu/coredb/di/DbModule.kt @@ -5,7 +5,6 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton import jp.co.soramitsu.common.data.secrets.v1.SecretStoreV1 import jp.co.soramitsu.common.data.secrets.v2.SecretStoreV2 import jp.co.soramitsu.coredb.AppDatabase @@ -17,9 +16,11 @@ import jp.co.soramitsu.coredb.dao.ChainDao import jp.co.soramitsu.coredb.dao.MetaAccountDao import jp.co.soramitsu.coredb.dao.OperationDao import jp.co.soramitsu.coredb.dao.PhishingDao +import jp.co.soramitsu.coredb.dao.SoraCardDao import jp.co.soramitsu.coredb.dao.StakingTotalRewardDao import jp.co.soramitsu.coredb.dao.StorageDao import jp.co.soramitsu.coredb.dao.TokenPriceDao +import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module @@ -100,4 +101,10 @@ class DbModule { fun provideAddressBookDao(appDatabase: AppDatabase): AddressBookDao { return appDatabase.addressBookDao() } + + @Provides + @Singleton + fun provideSoraCardDao(appDatabase: AppDatabase): SoraCardDao { + return appDatabase.soraCardDao() + } } diff --git a/core-db/src/main/java/jp/co/soramitsu/coredb/migrations/Migrations.kt b/core-db/src/main/java/jp/co/soramitsu/coredb/migrations/Migrations.kt index a69344ea21..53b38e8084 100644 --- a/core-db/src/main/java/jp/co/soramitsu/coredb/migrations/Migrations.kt +++ b/core-db/src/main/java/jp/co/soramitsu/coredb/migrations/Migrations.kt @@ -3,6 +3,22 @@ package jp.co.soramitsu.coredb.migrations import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +val Migration_50_51 = object : Migration(50, 51) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `sora_card` ( + `id` TEXT NOT NULL, + `accessToken` TEXT NOT NULL, + `refreshToken` TEXT NOT NULL, + `accessTokenExpirationTime` INTEGER NOT NULL, + `kycStatus` TEXT NOT NULL, + PRIMARY KEY(`id`) + ) + """.trimIndent() + ) + } +} val Migration_49_50 = object : Migration(49, 50) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE chain_assets ADD COLUMN `name` TEXT DEFAULT NULL") diff --git a/core-db/src/main/java/jp/co/soramitsu/coredb/model/SoraCardInfoLocal.kt b/core-db/src/main/java/jp/co/soramitsu/coredb/model/SoraCardInfoLocal.kt new file mode 100644 index 0000000000..04dc55284f --- /dev/null +++ b/core-db/src/main/java/jp/co/soramitsu/coredb/model/SoraCardInfoLocal.kt @@ -0,0 +1,15 @@ +package jp.co.soramitsu.coredb.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity( + tableName = "sora_card" +) +data class SoraCardInfoLocal( + @PrimaryKey val id: String, + val accessToken: String, + val refreshToken: String, + val accessTokenExpirationTime: Long, + val kycStatus: String +) diff --git a/feature-account-impl/build.gradle b/feature-account-impl/build.gradle index 5fbe84f84c..bf51044a2c 100644 --- a/feature-account-impl/build.gradle +++ b/feature-account-impl/build.gradle @@ -51,6 +51,8 @@ dependencies { implementation project(':feature-account-api') implementation project(':feature-staking-api') implementation project(':feature-wallet-api') + implementation project(':feature-soracard-api') + implementation project(':feature-soracard-impl') implementation kotlinDep implementation androidDep @@ -95,5 +97,7 @@ dependencies { implementation gsonDep + implementation soraCardDep, withoutBouncycastle + api sharedFeaturesCoreDep } \ No newline at end of file diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/AccountRouter.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/AccountRouter.kt index e1a0133183..20812e47d5 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/AccountRouter.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/AccountRouter.kt @@ -84,4 +84,6 @@ interface AccountRouter : SecureRouter { fun openOptionsAddAccount(payload: AddAccountBottomSheet.Payload) fun openPolkaswapDisclaimer() + + fun openGetSoraCard() } diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileFragment.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileFragment.kt index 63d4b990b7..7c7c949103 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileFragment.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileFragment.kt @@ -17,6 +17,8 @@ import jp.co.soramitsu.common.mixin.impl.observeBrowserEvents import jp.co.soramitsu.common.presentation.FiatCurrenciesChooserBottomSheetDialog import jp.co.soramitsu.common.view.bottomSheet.list.dynamic.DynamicListBottomSheet import jp.co.soramitsu.feature_account_impl.databinding.FragmentProfileBinding +import jp.co.soramitsu.oauth.base.sdk.contract.SoraCardResult +import jp.co.soramitsu.oauth.base.sdk.signin.SoraCardSignInContract @AndroidEntryPoint class ProfileFragment : BaseFragment() { @@ -28,6 +30,23 @@ class ProfileFragment : BaseFragment() { override val viewModel: ProfileViewModel by viewModels() + private val soraCardSignIn = registerForActivityResult( + SoraCardSignInContract() + ) { result -> + when (result) { + is SoraCardResult.Failure -> {} + is SoraCardResult.Canceled -> {} + is SoraCardResult.Success -> { + viewModel.updateSoraCardInfo( + accessToken = result.accessToken, + refreshToken = result.refreshToken, + accessTokenExpirationTime = result.accessTokenExpirationTime, + kycStatus = result.status.toString() + ) + } + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -49,6 +68,7 @@ class ProfileFragment : BaseFragment() { profileCurrency.setOnClickListener { viewModel.currencyClicked() } profileExperimentalFeatures.setOnClickListener { viewModel.onExperimentalClicked() } polkaswapDisclaimerTv.setOnClickListener { viewModel.polkaswapDisclaimerClicked() } + profileSoraCard.setOnClickListener { viewModel.onSoraCardClicked() } viewModel.hasMissingAccountsFlow.observe { missingAccountsIcon.isVisible = it @@ -80,6 +100,10 @@ class ProfileFragment : BaseFragment() { viewModel.showFiatChooser.observeEvent(::showFiatChooser) viewModel.selectedFiatLiveData.observe(binding.selectedCurrencyTv::setText) + + viewModel.launchSoraCardSignIn.observeEvent { contractData -> + soraCardSignIn.launch(contractData) + } } private fun showFiatChooser(payload: DynamicListBottomSheet.Payload) { diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileViewModel.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileViewModel.kt index 420ba62281..74685dfc9c 100644 --- a/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileViewModel.kt +++ b/feature-account-impl/src/main/java/jp/co/soramitsu/account/impl/presentation/profile/ProfileViewModel.kt @@ -13,6 +13,7 @@ import jp.co.soramitsu.account.api.presentation.actions.ExternalAccountActions import jp.co.soramitsu.account.impl.presentation.AccountRouter import jp.co.soramitsu.account.impl.presentation.account.list.AccountChosenNavDirection import jp.co.soramitsu.account.impl.presentation.language.mapper.mapLanguageToLanguageModel +import jp.co.soramitsu.common.BuildConfig import jp.co.soramitsu.common.address.AddressIconGenerator import jp.co.soramitsu.common.address.AddressModel import jp.co.soramitsu.common.address.createAddressModel @@ -21,14 +22,25 @@ import jp.co.soramitsu.common.data.network.coingecko.FiatChooserEvent import jp.co.soramitsu.common.data.network.coingecko.FiatCurrency import jp.co.soramitsu.common.domain.GetAvailableFiatCurrencies import jp.co.soramitsu.common.domain.SelectedFiat +import jp.co.soramitsu.common.resources.ResourceManager +import jp.co.soramitsu.common.utils.Event import jp.co.soramitsu.common.utils.formatAsCurrency import jp.co.soramitsu.common.view.bottomSheet.list.dynamic.DynamicListBottomSheet +import jp.co.soramitsu.feature_account_impl.R +import jp.co.soramitsu.oauth.base.sdk.SoraCardEnvironmentType +import jp.co.soramitsu.oauth.base.sdk.SoraCardInfo +import jp.co.soramitsu.oauth.base.sdk.contract.SoraCardCommonVerification +import jp.co.soramitsu.oauth.base.sdk.signin.SoraCardSignInContractData +import jp.co.soramitsu.soracard.api.domain.SoraCardInteractor +import jp.co.soramitsu.soracard.impl.presentation.SoraCardItemViewState import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletInteractor import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import java.util.Locale import javax.inject.Inject private const val AVATAR_SIZE_DP = 32 @@ -37,14 +49,19 @@ private const val AVATAR_SIZE_DP = 32 class ProfileViewModel @Inject constructor( private val interactor: AccountInteractor, private val walletInteractor: WalletInteractor, + private val soraCardInteractor: SoraCardInteractor, private val router: AccountRouter, private val addressIconGenerator: AddressIconGenerator, private val externalAccountActions: ExternalAccountActions.Presentation, getTotalBalance: GetTotalBalanceUseCase, private val getAvailableFiatCurrencies: GetAvailableFiatCurrencies, - private val selectedFiat: SelectedFiat + private val selectedFiat: SelectedFiat, + private val resourceManager: ResourceManager ) : BaseViewModel(), ExternalAccountActions by externalAccountActions { + private val _launchSoraCardSignIn = MutableLiveData>() + val launchSoraCardSignIn: LiveData> = _launchSoraCardSignIn + val totalBalanceLiveData = combine(getTotalBalance(), selectedFiat.flow()) { balance, fiat -> val selectedFiatSymbol = getAvailableFiatCurrencies[fiat]?.symbol balance.balance.formatAsCurrency(selectedFiatSymbol ?: balance.fiatSymbol) @@ -67,6 +84,15 @@ class ProfileViewModel @Inject constructor( val selectedFiatLiveData: LiveData = selectedFiat.flow().asLiveData().map { it.uppercase() } + val hasMissingAccountsFlow = walletInteractor.assetsFlow().map { + it.any { it.hasAccount.not() } + }.stateIn(this, SharingStarted.Eagerly, false) + + private val soraCardState = soraCardInteractor.subscribeSoraCardInfo().map { + val kycStatus = it?.kycStatus?.let(::mapKycStatus) + SoraCardItemViewState(kycStatus, it, null, true) + } + fun aboutClicked() { router.openAboutScreen() } @@ -120,7 +146,79 @@ class ProfileViewModel @Inject constructor( router.openPolkaswapDisclaimer() } - val hasMissingAccountsFlow = walletInteractor.assetsFlow().map { - it.any { it.hasAccount.not() } - }.stateIn(this, SharingStarted.Eagerly, false) + fun onSoraCardClicked() { + launch { + soraCardState.collectLatest { + if (it.kycStatus == null) { + router.openGetSoraCard() + } else { + onSoraCardStatusClicked() + } + } + } + } + + private fun onSoraCardStatusClicked() { + launch { + soraCardState.collectLatest { soraCardState -> + _launchSoraCardSignIn.value = Event( + SoraCardSignInContractData( + locale = Locale.ENGLISH, + apiKey = BuildConfig.SORA_CARD_API_KEY, + domain = BuildConfig.SORA_CARD_DOMAIN, + environment = when { + BuildConfig.DEBUG -> SoraCardEnvironmentType.TEST + else -> SoraCardEnvironmentType.PRODUCTION + }, + soraCardInfo = soraCardState.soraCardInfo?.let { + SoraCardInfo( + accessToken = it.accessToken, + refreshToken = it.refreshToken, + accessTokenExpirationTime = it.accessTokenExpirationTime + ) + } + ) + ) + } + } + } + + private fun mapKycStatus(kycStatus: String): String? { + return when (runCatching { SoraCardCommonVerification.valueOf(kycStatus) }.getOrNull()) { + SoraCardCommonVerification.Pending -> { + resourceManager.getString(R.string.sora_card_verification_in_progress) + } + SoraCardCommonVerification.Successful -> { + resourceManager.getString(R.string.sora_card_verification_successful) + } + SoraCardCommonVerification.Rejected -> { + resourceManager.getString(R.string.sora_card_verification_rejected) + } + SoraCardCommonVerification.Failed -> { + resourceManager.getString(R.string.sora_card_verification_failed) + } + SoraCardCommonVerification.NoFreeAttempt -> { + resourceManager.getString(R.string.sora_card_no_more_free_tries) + } + else -> { + null + } + } + } + + fun updateSoraCardInfo( + accessToken: String, + refreshToken: String, + accessTokenExpirationTime: Long, + kycStatus: String + ) { + launch { + soraCardInteractor.updateSoraCardInfo( + accessToken, + refreshToken, + accessTokenExpirationTime, + kycStatus + ) + } + } } diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/feature_account_impl/di/AccountFeatureComponent.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/feature_account_impl/di/AccountFeatureComponent.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/feature-account-impl/src/main/java/jp/co/soramitsu/feature_account_impl/presentation/profile/ProfileFragment.kt b/feature-account-impl/src/main/java/jp/co/soramitsu/feature_account_impl/presentation/profile/ProfileFragment.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/feature-account-impl/src/main/res/layout/fragment_profile.xml b/feature-account-impl/src/main/res/layout/fragment_profile.xml index 5881ead336..1767ad3f69 100644 --- a/feature-account-impl/src/main/res/layout/fragment_profile.xml +++ b/feature-account-impl/src/main/res/layout/fragment_profile.xml @@ -76,6 +76,46 @@ + + + + + + + + + { diff --git a/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/presentation/swap_preview/SwapPreviewViewModel.kt b/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/presentation/swap_preview/SwapPreviewViewModel.kt index 647e4c9e3b..46c0a71de5 100644 --- a/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/presentation/swap_preview/SwapPreviewViewModel.kt +++ b/feature-polkaswap-impl/src/main/kotlin/jp/co/soramitsu/polkaswap/impl/presentation/swap_preview/SwapPreviewViewModel.kt @@ -3,7 +3,6 @@ package jp.co.soramitsu.polkaswap.impl.presentation.swap_preview import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import jp.co.soramitsu.common.base.BaseViewModel import jp.co.soramitsu.common.utils.orZero import jp.co.soramitsu.polkaswap.api.domain.PolkaswapInteractor @@ -18,6 +17,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import javax.inject.Inject @HiltViewModel class SwapPreviewViewModel @Inject constructor( @@ -54,7 +54,7 @@ class SwapPreviewViewModel @Inject constructor( } swapResult.fold( onSuccess = { - polkaswapRouter.returnToAssetDetails() + polkaswapRouter.closeSwap() polkaswapRouter.openOperationSuccess(it, chainId = soraMainChainId) }, onFailure = { diff --git a/feature-soracard-api/.gitignore b/feature-soracard-api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/feature-soracard-api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature-soracard-api/build.gradle.kts b/feature-soracard-api/build.gradle.kts new file mode 100644 index 0000000000..09e608040a --- /dev/null +++ b/feature-soracard-api/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-parcelize") + id("kotlinx-serialization") +} + +android { + compileSdk = rootProject.ext["compileSdkVersion"] as Int + + defaultConfig { + minSdk = rootProject.ext["minSdkVersion"] as Int + targetSdk = rootProject.ext["targetSdkVersion"] as Int + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + namespace = "jp.co.soramitsu.feature_soracard_api" +} + +dependencies { + implementation(libs.bundles.coroutines) + implementation(libs.kotlinx.serialization.json) + implementation(libs.sora.card) + { + exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18") + } + + implementation(projects.runtime) + implementation(projects.featureWalletApi) +} + + diff --git a/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/domain/BuyCryptoDataSource.kt b/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/domain/BuyCryptoDataSource.kt new file mode 100644 index 0000000000..c68bd22cb6 --- /dev/null +++ b/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/domain/BuyCryptoDataSource.kt @@ -0,0 +1,11 @@ +package jp.co.soramitsu.soracard.api.domain + +import jp.co.soramitsu.soracard.api.presentation.models.PaymentOrder +import jp.co.soramitsu.soracard.api.presentation.models.PaymentOrderInfo +import kotlinx.coroutines.flow.Flow + +interface BuyCryptoDataSource { + + suspend fun requestPaymentOrderStatus(paymentOrder: PaymentOrder) + suspend fun subscribePaymentOrderInfo(): Flow +} diff --git a/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/domain/BuyCryptoRepository.kt b/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/domain/BuyCryptoRepository.kt new file mode 100644 index 0000000000..d04b03546d --- /dev/null +++ b/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/domain/BuyCryptoRepository.kt @@ -0,0 +1,11 @@ +package jp.co.soramitsu.soracard.api.domain + +import jp.co.soramitsu.soracard.api.presentation.models.PaymentOrder +import jp.co.soramitsu.soracard.api.presentation.models.PaymentOrderInfo +import kotlinx.coroutines.flow.Flow + +interface BuyCryptoRepository { + + suspend fun requestPaymentOrderStatus(paymentOrder: PaymentOrder) + suspend fun subscribePaymentOrderInfo(): Flow +} diff --git a/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/domain/SoraCardInteractor.kt b/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/domain/SoraCardInteractor.kt new file mode 100644 index 0000000000..f6cdfff8cf --- /dev/null +++ b/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/domain/SoraCardInteractor.kt @@ -0,0 +1,27 @@ +package jp.co.soramitsu.soracard.api.domain + +import jp.co.soramitsu.soracard.api.presentation.models.SoraCardInfo +import jp.co.soramitsu.wallet.impl.domain.model.Asset +import kotlinx.coroutines.flow.Flow +import java.math.BigDecimal + +interface SoraCardInteractor { + val soraCardChainId: String + + suspend fun xorAssetFlow(): Flow + + fun subscribeSoraCardInfo(): Flow + + suspend fun getSoraCardInfo(): SoraCardInfo? + + suspend fun updateSoraCardKycStatus(kycStatus: String) + + suspend fun getXorPerEurRatio(priceId: String?): BigDecimal? + + suspend fun updateSoraCardInfo( + accessToken: String, + refreshToken: String, + accessTokenExpirationTime: Long, + kycStatus: String + ) +} diff --git a/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/domain/SoraCardRepository.kt b/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/domain/SoraCardRepository.kt new file mode 100644 index 0000000000..b5cef522c7 --- /dev/null +++ b/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/domain/SoraCardRepository.kt @@ -0,0 +1,20 @@ +package jp.co.soramitsu.soracard.api.domain + +import jp.co.soramitsu.soracard.api.presentation.models.SoraCardInfo +import kotlinx.coroutines.flow.Flow + +interface SoraCardRepository { + + fun subscribeSoraCardInfo(): Flow + + suspend fun getSoraCardInfo(): SoraCardInfo? + + suspend fun updateSoraCardKycStatus(kycStatus: String) + + suspend fun updateSoraCardInfo( + accessToken: String, + refreshToken: String, + accessTokenExpirationTime: Long, + kycStatus: String + ) +} diff --git a/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/presentation/SoraCardRouter.kt b/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/presentation/SoraCardRouter.kt new file mode 100644 index 0000000000..3498c85a27 --- /dev/null +++ b/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/presentation/SoraCardRouter.kt @@ -0,0 +1,13 @@ +package jp.co.soramitsu.soracard.api.presentation + +interface SoraCardRouter { + fun back() + + fun openGetMoreXor() + + fun openSwapTokensScreen(assetId: String, chainId: String) + + fun showBuyCrypto() + + fun openWebViewer(title: String, url: String) +} diff --git a/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/presentation/models/PaymentOrder.kt b/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/presentation/models/PaymentOrder.kt new file mode 100644 index 0000000000..bee9f8a664 --- /dev/null +++ b/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/presentation/models/PaymentOrder.kt @@ -0,0 +1,19 @@ +package jp.co.soramitsu.soracard.api.presentation.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PaymentOrder( + @SerialName("payment_id") val paymentId: String +) + +@Serializable +data class PaymentOrderInfo( + @SerialName("payment_id") val paymentId: String, + @SerialName("order_number") val orderNumber: String, + @SerialName("deposit_transaction_number") val depositTransactionNumber: String, + @SerialName("deposit_transaction_status") val depositTransactionStatus: String, + @SerialName("order_transaction_number") val orderTransactionNumber: String, + @SerialName("withdrawal_transaction_number") val withdrawalTransactionNumber: String +) diff --git a/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/presentation/models/SoraCardInfo.kt b/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/presentation/models/SoraCardInfo.kt new file mode 100644 index 0000000000..374715d2cd --- /dev/null +++ b/feature-soracard-api/src/main/kotlin/jp/co/soramitsu/soracard/api/presentation/models/SoraCardInfo.kt @@ -0,0 +1,9 @@ +package jp.co.soramitsu.soracard.api.presentation.models + +data class SoraCardInfo( + val id: String, + val accessToken: String, + val refreshToken: String, + val accessTokenExpirationTime: Long, + val kycStatus: String +) diff --git a/feature-soracard-impl/.gitignore b/feature-soracard-impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/feature-soracard-impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature-soracard-impl/build.gradle.kts b/feature-soracard-impl/build.gradle.kts new file mode 100644 index 0000000000..de53a4c899 --- /dev/null +++ b/feature-soracard-impl/build.gradle.kts @@ -0,0 +1,65 @@ +plugins { + id("com.android.library") + id("dagger.hilt.android.plugin") + id("kotlin-android") + id("kotlin-kapt") + id("kotlin-parcelize") + id("kotlinx-serialization") +} +apply(from = "../tests.gradle") +apply(from = "../scripts/secrets.gradle") + +android { + compileSdk = rootProject.ext["compileSdkVersion"] as Int + + defaultConfig { + minSdk = rootProject.ext["minSdkVersion"] as Int + targetSdk = rootProject.ext["targetSdkVersion"] as Int + } + + buildTypes { + release { + + } + } + + buildFeatures { + compose = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + composeOptions { + kotlinCompilerExtensionVersion = rootProject.ext["composeCompilerVersion"] as String + } + + kotlinOptions { + jvmTarget = "1.8" + } + namespace = "jp.co.soramitsu.feature_soracard_impl" +} + +dependencies { + implementation(libs.hilt.android) + kapt(libs.hilt.compiler) + implementation(libs.bundles.compose) + implementation(libs.fragmentKtx) + implementation(libs.material) + implementation(libs.kotlinx.serialization.json) + implementation(libs.xnetworking.android) + + implementation(libs.sora.ui) + implementation(libs.sora.card) + { + exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18") + } + + implementation(projects.common) + implementation(projects.runtime) + implementation(projects.featureWalletApi) + implementation(projects.featureAccountApi) //todo check neediness + implementation(projects.featureSoracardApi) +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/data/websocket/WebSocket.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/data/websocket/WebSocket.kt new file mode 100644 index 0000000000..c170a63ecb --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/data/websocket/WebSocket.kt @@ -0,0 +1,98 @@ +package jp.co.soramitsu.soracard.impl.data.websocket + +import android.util.Log +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession +import io.ktor.client.plugins.websocket.webSocket +import io.ktor.websocket.Frame +import io.ktor.websocket.close +import io.ktor.websocket.readText +import jp.co.soramitsu.xnetworking.networkclient.NetworkClientConfig +import jp.co.soramitsu.xnetworking.networkclient.SoramitsuHttpClientProvider +import jp.co.soramitsu.xnetworking.networkclient.WebSocketClientConfig +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json + +interface WebSocketListener { + + suspend fun onResponse(response: WebSocketResponse) + + fun onSocketClosed() + + fun onConnected() +} + +class WebSocket( + private val url: String, + private var listener: WebSocketListener, + private val json: Json, + connectTimeoutMillis: Long = 10_000, + pingInterval: Long = 20, + maxFrameSize: Long = Int.MAX_VALUE.toLong(), + logging: Boolean = false, + provider: SoramitsuHttpClientProvider +) { + + private var socketSession: DefaultClientWebSocketSession? = null + + private val networkClient = provider.provide( + NetworkClientConfig( + logging = logging, + requestTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS, + connectTimeoutMillis = connectTimeoutMillis, + socketTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS, + json = json, + webSocketClientConfig = WebSocketClientConfig( + pingInterval = pingInterval, + maxFrameSize = maxFrameSize + ) + ) + ) + + private suspend fun DefaultClientWebSocketSession.listenIncomingMessages() { + try { + incoming.receiveAsFlow() + .filter { it is Frame.Text } + .collect { frame -> + frame as Frame.Text + val text = frame.readText() + + log("Response", text) + this@WebSocket.listener.onResponse(response = WebSocketResponse(json = text)) + } + } catch (e: Exception) { + println("Error while receiving: ${e.message}") + } + } + + suspend fun disconnect() { + socketSession?.close() + log("Disconnected", url) + } + + suspend fun sendRequest(request: WebSocketRequest) { + networkClient.webSocket(url) { + try { + socketSession = this + listener.onConnected() + + launch { + log("Sending", request.json) + socketSession?.send(Frame.Text(request.json)) + } + + listenIncomingMessages() + } catch (e: Exception) { + println("Error while connecting") + } finally { + listener.onSocketClosed() + } + } + } + + private fun log(topic: String, message: Any?) { + Log.i("\t[SOCKET][${topic.uppercase()}]", message.toString()) + } +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/data/websocket/WebSocketRequest.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/data/websocket/WebSocketRequest.kt new file mode 100644 index 0000000000..d26f148ca5 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/data/websocket/WebSocketRequest.kt @@ -0,0 +1,8 @@ +package jp.co.soramitsu.soracard.impl.data.websocket + +import kotlinx.serialization.Serializable + +@Serializable +data class WebSocketRequest( + val json: String +) diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/data/websocket/WebSocketResponse.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/data/websocket/WebSocketResponse.kt new file mode 100644 index 0000000000..3d2cb01ee1 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/data/websocket/WebSocketResponse.kt @@ -0,0 +1,14 @@ +package jp.co.soramitsu.soracard.impl.data.websocket + +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +@Serializable +data class WebSocketResponse( + val json: String +) + +fun Json.encodeToString(rpcRequest: WebSocketResponse): String { + return encodeToString(rpcRequest) +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/di/SoraCardFeatureBindModule.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/di/SoraCardFeatureBindModule.kt new file mode 100644 index 0000000000..2160f64e20 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/di/SoraCardFeatureBindModule.kt @@ -0,0 +1,59 @@ +package jp.co.soramitsu.soracard.impl.di + +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import jp.co.soramitsu.coredb.dao.SoraCardDao +import jp.co.soramitsu.soracard.api.domain.BuyCryptoDataSource +import jp.co.soramitsu.soracard.api.domain.BuyCryptoRepository +import jp.co.soramitsu.soracard.api.domain.SoraCardInteractor +import jp.co.soramitsu.soracard.api.domain.SoraCardRepository +import jp.co.soramitsu.soracard.impl.domain.BuyCryptoDataSourceImpl +import jp.co.soramitsu.soracard.impl.domain.BuyCryptoRepositoryImpl +import jp.co.soramitsu.soracard.impl.domain.SoraCardInteractorImpl +import jp.co.soramitsu.soracard.impl.domain.SoraCardRepositoryImpl +import jp.co.soramitsu.xnetworking.networkclient.SoramitsuHttpClientProvider +import jp.co.soramitsu.xnetworking.networkclient.SoramitsuHttpClientProviderImpl +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +interface SoraCardFeatureBindModule { + @Binds + @Singleton + fun bindsSoraCardInteractor(soraCardInteractor: SoraCardInteractorImpl): SoraCardInteractor + + @Binds + fun bindsSoraCardRepository(soraCardRepository: SoraCardRepositoryImpl): SoraCardRepository +} + +@InstallIn(SingletonComponent::class) +@Module(includes = [SoraCardFeatureBindModule::class]) +class SoraCardFeatureModule { + + @Provides + fun provideSoraCardRepositoryImpl( + soraCardDao: SoraCardDao + ): SoraCardRepositoryImpl { + return SoraCardRepositoryImpl(soraCardDao) + } + + @Provides + fun provideBuyCryptoRepository( + dataSource: BuyCryptoDataSource + ): BuyCryptoRepository = BuyCryptoRepositoryImpl( + dataSource + ) + + @Provides + fun provideBuyCryptoDataSource( + clientProvider: SoramitsuHttpClientProvider + ): BuyCryptoDataSource = + BuyCryptoDataSourceImpl(clientProvider) + + @Provides + fun provideSoramitsuHttpClientProvider(): SoramitsuHttpClientProvider = + SoramitsuHttpClientProviderImpl() +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/domain/BuyCryptoDataSourceImpl.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/domain/BuyCryptoDataSourceImpl.kt new file mode 100644 index 0000000000..6227483417 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/domain/BuyCryptoDataSourceImpl.kt @@ -0,0 +1,58 @@ +package jp.co.soramitsu.soracard.impl.domain + +import jp.co.soramitsu.soracard.api.domain.BuyCryptoDataSource +import jp.co.soramitsu.soracard.impl.data.websocket.WebSocket +import jp.co.soramitsu.soracard.impl.data.websocket.WebSocketListener +import jp.co.soramitsu.soracard.impl.data.websocket.WebSocketRequest +import jp.co.soramitsu.soracard.impl.data.websocket.WebSocketResponse +import jp.co.soramitsu.soracard.api.presentation.models.PaymentOrder +import jp.co.soramitsu.soracard.api.presentation.models.PaymentOrderInfo +import jp.co.soramitsu.xnetworking.networkclient.SoramitsuHttpClientProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class BuyCryptoDataSourceImpl( + clientProvider: SoramitsuHttpClientProvider +) : BuyCryptoDataSource { + + private val json = Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + } + + private val paymentOrderFlow = MutableSharedFlow() + + private val webSocketListener = object : WebSocketListener { + override suspend fun onResponse(response: WebSocketResponse) { + val paymentOrderInfo = json.decodeFromString(response.json) + paymentOrderFlow.emit(paymentOrderInfo) + paymentOrderWebSocket.disconnect() + } + + override fun onSocketClosed() { + } + + override fun onConnected() { + } + } + + private val paymentOrderWebSocket: WebSocket = WebSocket( + url = "wss://backend.dev.sora-card.tachi.soramitsu.co.jp/ws/x1-payment-status", + listener = webSocketListener, + json = json, + logging = true, + provider = clientProvider + ) + + override suspend fun requestPaymentOrderStatus(paymentOrder: PaymentOrder) { + paymentOrderWebSocket.sendRequest( + request = WebSocketRequest(json = json.encodeToString(paymentOrder)) + ) + } + + override suspend fun subscribePaymentOrderInfo(): Flow = paymentOrderFlow +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/domain/BuyCryptoRepositoryImpl.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/domain/BuyCryptoRepositoryImpl.kt new file mode 100644 index 0000000000..053d9d940e --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/domain/BuyCryptoRepositoryImpl.kt @@ -0,0 +1,19 @@ +package jp.co.soramitsu.soracard.impl.domain + +import jp.co.soramitsu.soracard.api.domain.BuyCryptoDataSource +import jp.co.soramitsu.soracard.api.domain.BuyCryptoRepository +import jp.co.soramitsu.soracard.api.presentation.models.PaymentOrder +import jp.co.soramitsu.soracard.api.presentation.models.PaymentOrderInfo +import kotlinx.coroutines.flow.Flow + +class BuyCryptoRepositoryImpl( + private val buyCryptoDataSource: BuyCryptoDataSource +) : BuyCryptoRepository { + + override suspend fun requestPaymentOrderStatus(paymentOrder: PaymentOrder) { + buyCryptoDataSource.requestPaymentOrderStatus(paymentOrder) + } + + override suspend fun subscribePaymentOrderInfo(): Flow = + buyCryptoDataSource.subscribePaymentOrderInfo() +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/domain/SoraCardInfoMapper.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/domain/SoraCardInfoMapper.kt new file mode 100644 index 0000000000..c69b3927c3 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/domain/SoraCardInfoMapper.kt @@ -0,0 +1,16 @@ +package jp.co.soramitsu.soracard.impl.domain + +import jp.co.soramitsu.coredb.model.SoraCardInfoLocal +import jp.co.soramitsu.soracard.api.presentation.models.SoraCardInfo + +object SoraCardInfoMapper { + + fun map(infoLocal: SoraCardInfoLocal): SoraCardInfo = + SoraCardInfo( + id = infoLocal.id, + accessToken = infoLocal.accessToken, + refreshToken = infoLocal.refreshToken, + accessTokenExpirationTime = infoLocal.accessTokenExpirationTime, + kycStatus = infoLocal.kycStatus + ) +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/domain/SoraCardInteractorImpl.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/domain/SoraCardInteractorImpl.kt new file mode 100644 index 0000000000..2c38885905 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/domain/SoraCardInteractorImpl.kt @@ -0,0 +1,52 @@ +package jp.co.soramitsu.soracard.impl.domain + +import jp.co.soramitsu.account.api.domain.interfaces.AccountRepository +import jp.co.soramitsu.common.BuildConfig +import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry +import jp.co.soramitsu.runtime.multiNetwork.chain.model.soraMainChainId +import jp.co.soramitsu.runtime.multiNetwork.chain.model.soraTestChainId +import jp.co.soramitsu.soracard.api.domain.SoraCardInteractor +import jp.co.soramitsu.soracard.api.domain.SoraCardRepository +import jp.co.soramitsu.soracard.api.presentation.models.SoraCardInfo +import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletRepository +import jp.co.soramitsu.wallet.impl.domain.model.Asset +import kotlinx.coroutines.flow.Flow +import java.math.BigDecimal +import javax.inject.Inject + +class SoraCardInteractorImpl @Inject constructor( + private val chainRegistry: ChainRegistry, + private val accountRepository: AccountRepository, + private val walletRepository: WalletRepository, + private val soraCardRepository: SoraCardRepository +) : SoraCardInteractor { + + override var soraCardChainId = if (BuildConfig.DEBUG) soraTestChainId else soraMainChainId + + override suspend fun xorAssetFlow(): Flow { + val chain = chainRegistry.getChain(soraCardChainId) + val metaAccount = accountRepository.getSelectedMetaAccount() + val xorAsset = chain.assets.firstOrNull { it.isUtility } ?: error("XOR asset not found") + + return walletRepository.assetFlow(metaAccount.id, metaAccount.substrateAccountId, xorAsset, chain.minSupportedVersion) + } + + override fun subscribeSoraCardInfo(): Flow = + soraCardRepository.subscribeSoraCardInfo() + + override suspend fun getXorPerEurRatio(priceId: String?): BigDecimal? = priceId?.let { + walletRepository.getSingleAssetPriceCoingecko(priceId, "eur") + } + + override suspend fun updateSoraCardInfo(accessToken: String, refreshToken: String, accessTokenExpirationTime: Long, kycStatus: String) { + soraCardRepository.updateSoraCardInfo(accessToken, refreshToken, accessTokenExpirationTime, kycStatus) + } + + override suspend fun getSoraCardInfo(): SoraCardInfo? { + return soraCardRepository.getSoraCardInfo() + } + + override suspend fun updateSoraCardKycStatus(kycStatus: String) { + soraCardRepository.updateSoraCardKycStatus(kycStatus) + } +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/domain/SoraCardRepositoryImpl.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/domain/SoraCardRepositoryImpl.kt new file mode 100644 index 0000000000..8ccf3986a9 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/domain/SoraCardRepositoryImpl.kt @@ -0,0 +1,51 @@ +package jp.co.soramitsu.soracard.impl.domain + +import jp.co.soramitsu.coredb.dao.SoraCardDao +import jp.co.soramitsu.coredb.model.SoraCardInfoLocal +import jp.co.soramitsu.soracard.api.domain.SoraCardRepository +import jp.co.soramitsu.soracard.api.presentation.models.SoraCardInfo +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class SoraCardRepositoryImpl( + private val soraCardDao: SoraCardDao +) : SoraCardRepository { + private companion object { + const val SORA_CARD_ID = "soraCardId" + } + + override fun subscribeSoraCardInfo(): Flow { + return soraCardDao.observeSoraCardInfo(SORA_CARD_ID).map { + it?.let { + SoraCardInfoMapper.map(it) + } + } + } + + override suspend fun getSoraCardInfo(): SoraCardInfo? { + return soraCardDao.getSoraCardInfo(SORA_CARD_ID)?.let { + SoraCardInfoMapper.map(it) + } + } + + override suspend fun updateSoraCardKycStatus(kycStatus: String) { + soraCardDao.updateKycStatus(SORA_CARD_ID, kycStatus) + } + + override suspend fun updateSoraCardInfo( + accessToken: String, + refreshToken: String, + accessTokenExpirationTime: Long, + kycStatus: String + ) { + soraCardDao.insert( + SoraCardInfoLocal( + id = SORA_CARD_ID, + accessToken = accessToken, + refreshToken = refreshToken, + accessTokenExpirationTime = accessTokenExpirationTime, + kycStatus = kycStatus + ) + ) + } +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/SoraCard.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/SoraCard.kt new file mode 100644 index 0000000000..76de315db4 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/SoraCard.kt @@ -0,0 +1,96 @@ +package jp.co.soramitsu.soracard.impl.presentation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageShader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.imageResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import jp.co.soramitsu.common.R +import jp.co.soramitsu.common.compose.component.Image +import jp.co.soramitsu.common.compose.theme.soraRed + +@Composable +fun SoraCard( + onClick: (() -> Unit) +) { + val noiseImage = ImageBitmap.imageResource(R.drawable.noise) + val tiledNoise = remember(noiseImage) { ShaderBrush(ImageShader(noiseImage, TileMode.Repeated, TileMode.Repeated)) } + Box( + Modifier + .fillMaxWidth() + .height(312.dp) + .background( + color = soraRed, + shape = RoundedCornerShape(12.dp) + ) + .background( + brush = tiledNoise, + shape = RoundedCornerShape(12.dp), + alpha = 0.3f + ) + .background( + brush = Brush.horizontalGradient( + colors = listOf( + Color.White.copy(alpha = 0.16f), + Color.White.copy(alpha = 0.72f), + Color.White + ) + ), + alpha = 0.2f + ) + ) { + Image( + res = R.drawable.logo_soracard_text, + modifier = Modifier + .testTag("sora_logo_text") + .padding(top = 8.dp, start = 12.dp) + .align(Alignment.TopStart) + ) + Image( + res = R.drawable.logo_mc, + modifier = Modifier + .testTag("mc_logo") + .padding(start = 10.dp, bottom = 6.dp) + .align(Alignment.BottomStart) + ) + Image( + res = R.drawable.ic_sora_logo, + modifier = Modifier + .fillMaxSize() + .padding(vertical = 16.dp, horizontal = 77.dp) + .testTag("sora_logo") + .align(Alignment.TopCenter), + contentScale = ContentScale.FillWidth + ) + Image( + res = R.drawable.ic_paypass, + modifier = Modifier + .testTag("mc_logo") + .padding(8.dp) + .align(Alignment.CenterEnd) + ) + } +} + +@Preview +@Composable +private fun SoraCardPreview() { + SoraCard {} +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/SoraCardItem.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/SoraCardItem.kt new file mode 100644 index 0000000000..072636eaeb --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/SoraCardItem.kt @@ -0,0 +1,152 @@ +package jp.co.soramitsu.soracard.impl.presentation + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageShader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import jp.co.soramitsu.common.R +import jp.co.soramitsu.common.compose.component.H5 +import jp.co.soramitsu.common.compose.component.Image +import jp.co.soramitsu.common.compose.component.MarginVertical +import jp.co.soramitsu.common.compose.theme.soraRed +import jp.co.soramitsu.soracard.api.presentation.models.SoraCardInfo +import jp.co.soramitsu.ui_core.component.button.FilledButton +import jp.co.soramitsu.ui_core.component.button.properties.Order +import jp.co.soramitsu.ui_core.component.button.properties.Size + +data class SoraCardItemViewState( + val kycStatus: String? = null, + val soraCardInfo: SoraCardInfo? = null, + val cardLastDigits: String? = null, + val visible: Boolean = false +) + +@Composable +fun SoraCardItem( + state: SoraCardItemViewState?, + onClose: (() -> Unit), + onClick: (() -> Unit) +) { + val image = ImageBitmap.imageResource(R.drawable.noise) + val tiledNoise = remember(image) { ShaderBrush(ImageShader(image, TileMode.Repeated, TileMode.Repeated)) } + Box( + Modifier + .fillMaxWidth() + .height(80.dp) + .background( + color = soraRed, + shape = RoundedCornerShape(12.dp) + ) + .background( + brush = tiledNoise, + shape = RoundedCornerShape(12.dp), + alpha = 0.3f + ) + .background( + brush = Brush.horizontalGradient( + colors = listOf( + Color.White.copy(alpha = 0.16f), + Color.White.copy(alpha = 0.72f), + Color.White + ) + ), + shape = RoundedCornerShape(12.dp), + alpha = 0.2f + ) + .clickable(onClick = onClick) + ) { + Image( + res = R.drawable.logo_soracard_text, + modifier = Modifier + .testTag("sora_logo_text") + .padding(top = 8.dp, start = 12.dp) + .align(Alignment.TopStart) + ) + Image( + res = R.drawable.logo_mc, + modifier = Modifier + .testTag("mc_logo") + .padding(start = 10.dp, bottom = 6.dp) + .align(Alignment.BottomStart) + ) + Image( + res = R.drawable.ic_sora_logo, + modifier = Modifier + .padding(top = 4.dp) + .testTag("sora_logo") + .align(Alignment.TopCenter) + ) + Image( + res = R.drawable.ic_paypass, + modifier = Modifier + .testTag("mc_logo") + .padding(8.dp) + .align(Alignment.BottomEnd) + ) + + if (state?.cardLastDigits.isNullOrEmpty()) { + Image( + res = R.drawable.ic_close_16_white_circle, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(8.dp) + .wrapContentSize() + .clickable(onClick = onClose) + ) + } else { + H5( + text = "** ${state?.cardLastDigits}", + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 4.dp, end = 11.dp) + .wrapContentSize() + ) + } + + FilledButton( + modifier = Modifier + .padding(bottom = 6.dp) + .padding(horizontal = 16.dp) + .testTag("sora_card_status") + .align(Alignment.BottomCenter), + size = Size.ExtraSmall, + order = Order.SECONDARY, + onClick = onClick, + text = state?.kycStatus ?: stringResource(id = R.string.sora_card_get_sora_card), + elevation = 0.dp, + maxLines = 1 + ) + } +} + +@Preview +@Composable +private fun SoraCardItemItemPreview() { + Column { + val state = SoraCardItemViewState(null, null, "3455", false) + SoraCardItem(state = state, {}, {}) + MarginVertical(margin = 8.dp) + val state2 = SoraCardItemViewState(null, null, null, false) + SoraCardItem(state = state2, {}, {}) + } +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/buycrypto/BuyCryptoFragment.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/buycrypto/BuyCryptoFragment.kt new file mode 100644 index 0000000000..ea6cdffa23 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/buycrypto/BuyCryptoFragment.kt @@ -0,0 +1,25 @@ +package jp.co.soramitsu.soracard.impl.presentation.buycrypto + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import jp.co.soramitsu.common.base.BaseComposeFragment + +@AndroidEntryPoint +class BuyCryptoFragment : BaseComposeFragment() { + + override val viewModel: BuyCryptoViewModel by viewModels() + + @ExperimentalMaterialApi + @Composable + override fun Content(padding: PaddingValues, scrollState: ScrollState, modalBottomSheetState: ModalBottomSheetState) { + BuyCryptoScreen( + state = viewModel.state, + onPageFinished = viewModel::onPageFinished + ) + } +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/buycrypto/BuyCryptoScreen.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/buycrypto/BuyCryptoScreen.kt new file mode 100644 index 0000000000..deb448b653 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/buycrypto/BuyCryptoScreen.kt @@ -0,0 +1,46 @@ +package jp.co.soramitsu.soracard.impl.presentation.buycrypto + +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import jp.co.soramitsu.common.compose.component.ProgressDialog + +@Composable +fun BuyCryptoScreen( + state: BuyCryptoState, + onPageFinished: () -> Unit +) { + Box( + modifier = Modifier.fillMaxSize().imePadding(), + contentAlignment = Alignment.Center + ) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + WebView(context).apply { + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + onPageFinished() + } + } + + settings.javaScriptEnabled = true + } + }, + update = { + it.loadData(state.script, "text/html", "base64") + } + ) + + if (state.loading) { + ProgressDialog() + } + } +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/buycrypto/BuyCryptoState.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/buycrypto/BuyCryptoState.kt new file mode 100644 index 0000000000..2a36d15d93 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/buycrypto/BuyCryptoState.kt @@ -0,0 +1,6 @@ +package jp.co.soramitsu.soracard.impl.presentation.buycrypto + +data class BuyCryptoState( + val loading: Boolean = true, + val script: String = "" +) diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/buycrypto/BuyCryptoViewModel.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/buycrypto/BuyCryptoViewModel.kt new file mode 100644 index 0000000000..9d35998de1 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/buycrypto/BuyCryptoViewModel.kt @@ -0,0 +1,76 @@ +package jp.co.soramitsu.soracard.impl.presentation.buycrypto + +import android.util.Base64 +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import jp.co.soramitsu.common.BuildConfig +import jp.co.soramitsu.common.base.BaseViewModel +import jp.co.soramitsu.soracard.api.domain.BuyCryptoRepository +import jp.co.soramitsu.soracard.api.domain.SoraCardInteractor +import jp.co.soramitsu.soracard.api.presentation.SoraCardRouter +import jp.co.soramitsu.soracard.api.presentation.models.PaymentOrder +import jp.co.soramitsu.wallet.impl.domain.CurrentAccountAddressUseCase +import kotlinx.coroutines.launch +import java.util.UUID +import javax.inject.Inject + +@HiltViewModel +class BuyCryptoViewModel @Inject constructor( + private val currentAccountAddress: CurrentAccountAddressUseCase, + private val buyCryptoRepository: BuyCryptoRepository, + private val soraCardRouter: SoraCardRouter, + private val soraCardInteractor: SoraCardInteractor +) : BaseViewModel() { + + var state by mutableStateOf(BuyCryptoState()) + private set + + init { + setUpScript() + } + + fun onPageFinished() { + state = state.copy(loading = false) + } + + private fun setUpScript() { + viewModelScope.launch { + val chainId = soraCardInteractor.soraCardChainId + val address = currentAccountAddress(chainId) ?: return@launch + val payload = UUID.randomUUID().toString() + + val unencodedHtml = "" + + "
" + + "" + + "" + val encodedHtml = Base64.encodeToString(unencodedHtml.toByteArray(), Base64.NO_PADDING) + + state = state.copy(script = encodedHtml) + + subscribePaymentOrderInfo(payload) + requestPaymentOrderStatus(payload) + } + } + + private fun requestPaymentOrderStatus(payload: String) { + viewModelScope.launch { + buyCryptoRepository.requestPaymentOrderStatus(PaymentOrder(paymentId = payload)) + } + } + + private fun subscribePaymentOrderInfo(payload: String) { + viewModelScope.launch { + buyCryptoRepository.subscribePaymentOrderInfo() + .collect { + if (it.paymentId == payload && it.depositTransactionStatus == "completed") { + soraCardRouter.back() + } + } + } + } +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/get/BalanceIndicator.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/get/BalanceIndicator.kt new file mode 100644 index 0000000000..920a4fad3b --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/get/BalanceIndicator.kt @@ -0,0 +1,53 @@ +package jp.co.soramitsu.soracard.impl.presentation.get + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.MaterialTheme +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.tooling.preview.Preview +import jp.co.soramitsu.common.compose.theme.colorAccentDark +import jp.co.soramitsu.ui_core.resources.Dimens +import jp.co.soramitsu.ui_core.theme.customColors +import jp.co.soramitsu.ui_core.theme.customTypography + +@Composable +fun BalanceIndicator( + modifier: Modifier = Modifier, + percent: Float, + label: String +) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.End + ) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(Dimens.x2)), + progress = percent, + color = colorAccentDark, + backgroundColor = MaterialTheme.customColors.bgSurfaceVariant + ) + + Text( + text = label, + style = MaterialTheme.customTypography.textSBold, + color = colorAccentDark + ) + } +} + +@Composable +@Preview +private fun PreviewBalanceIndicator() { + BalanceIndicator( + modifier = Modifier.fillMaxWidth().padding(Dimens.x3), + percent = 0.75f, + label = "You have enough balance" + ) +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/get/GetSoraCardFragment.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/get/GetSoraCardFragment.kt new file mode 100644 index 0000000000..02f2cee2e4 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/get/GetSoraCardFragment.kt @@ -0,0 +1,74 @@ +package jp.co.soramitsu.soracard.impl.presentation.get + +import android.os.Bundle +import android.view.View +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import jp.co.soramitsu.common.base.BaseComposeFragment +import jp.co.soramitsu.oauth.base.sdk.contract.SoraCardContract +import jp.co.soramitsu.oauth.base.sdk.contract.SoraCardResult +import jp.co.soramitsu.oauth.base.sdk.signin.SoraCardSignInContract + +@AndroidEntryPoint +class GetSoraCardFragment : BaseComposeFragment() { + + override val viewModel: GetSoraCardViewModel by viewModels() + + private val soraCardRegistration = registerForActivityResult( + SoraCardContract() + ) { result -> + handleSoraCardResult(result) + } + + private var soraCardSignIn = registerForActivityResult( + SoraCardSignInContract() + ) { result -> + handleSoraCardResult(result) + } + + private fun handleSoraCardResult(result: SoraCardResult) { + when (result) { + is SoraCardResult.Failure -> {} + is SoraCardResult.Canceled -> {} + is SoraCardResult.Success -> { + viewModel.updateSoraCardInfo( + accessToken = result.accessToken, + refreshToken = result.refreshToken, + accessTokenExpirationTime = result.accessTokenExpirationTime, + kycStatus = result.status.toString() + ) + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.launchSoraCardRegistration.observeEvent { contractData -> + soraCardRegistration.launch(contractData) + } + + viewModel.launchSoraCardSignIn.observeEvent { contractData -> + soraCardSignIn.launch(contractData) + } + } + + @ExperimentalMaterialApi + @Composable + override fun Content(padding: PaddingValues, scrollState: ScrollState, modalBottomSheetState: ModalBottomSheetState) { + val state by viewModel.state.collectAsState() + + GetSoraCardScreenWithToolbar( + state = state, + scrollState = scrollState, + callbacks = viewModel + ) + } +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/get/GetSoraCardScreen.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/get/GetSoraCardScreen.kt new file mode 100644 index 0000000000..5fa5e1fb3f --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/get/GetSoraCardScreen.kt @@ -0,0 +1,376 @@ +package jp.co.soramitsu.soracard.impl.presentation.get + +import androidx.compose.foundation.Image +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +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.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag +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.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.dp +import androidx.compose.ui.unit.sp +import jp.co.soramitsu.common.compose.component.AccentButton +import jp.co.soramitsu.common.compose.component.MarginHorizontal +import jp.co.soramitsu.common.compose.component.MarginVertical +import jp.co.soramitsu.common.compose.component.Toolbar +import jp.co.soramitsu.common.compose.component.ToolbarViewState +import jp.co.soramitsu.common.compose.component.TransparentButton +import jp.co.soramitsu.common.compose.theme.errorRed +import jp.co.soramitsu.common.compose.theme.fearlessMaterialColors +import jp.co.soramitsu.common.utils.format +import jp.co.soramitsu.feature_soracard_impl.R +import jp.co.soramitsu.soracard.api.presentation.models.SoraCardInfo +import jp.co.soramitsu.ui_core.component.text.HtmlText +import jp.co.soramitsu.ui_core.resources.Dimens +import jp.co.soramitsu.ui_core.theme.borderRadius +import jp.co.soramitsu.ui_core.theme.customColors +import jp.co.soramitsu.ui_core.theme.customTypography +import jp.co.soramitsu.ui_core.theme.elevation +import java.math.BigDecimal + +data class GetSoraCardState( + val xorBalance: BigDecimal = BigDecimal.ZERO, + val enoughXor: Boolean = false, + val percent: BigDecimal = BigDecimal.ZERO, + val needInXor: BigDecimal = BigDecimal.ZERO, + val needInEur: BigDecimal = BigDecimal.ZERO, + val xorRatioUnavailable: Boolean = false, + val soraCardInfo: SoraCardInfo? = null +) + +interface GetSoraCardScreenInterface { + fun onEnableCard() + fun onGetMoreXor() + fun onSeeBlacklist(url: String) + fun onAlreadyHaveCard() + fun onNavigationClick() +} + +@Composable +fun GetSoraCardScreenWithToolbar( + state: GetSoraCardState, + scrollState: ScrollState, + callbacks: GetSoraCardScreenInterface +) { + val toolbarViewState = ToolbarViewState( + title = stringResource(id = R.string.profile_soracard_title), + navigationIcon = R.drawable.ic_arrow_left_24 + ) + + Column( + Modifier + .fillMaxSize() + .background(color = Color.Black) + ) { + Toolbar( + modifier = Modifier.height(62.dp), + state = toolbarViewState, + onNavigationClick = callbacks::onNavigationClick + ) + MarginVertical(margin = 8.dp) + GetSoraCardScreen( + state = state, + scrollState = scrollState, + callbacks = callbacks + ) + } +} + +@Composable +fun GetSoraCardScreen( + state: GetSoraCardState, + scrollState: ScrollState, + callbacks: GetSoraCardScreenInterface +) { + Box( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(horizontal = Dimens.x2) + .padding(bottom = Dimens.x5) + ) { + Card( + modifier = Modifier.fillMaxSize(), + cornerRadius = 12.dp, + elevation = 0.dp + ) { + Column( + modifier = Modifier + .fillMaxSize() + .background(color = Color(0xFF1D1D1F)) + .padding(16.dp) + ) { + Image( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + painter = painterResource(R.drawable.ic_sora_card), + contentDescription = null, + contentScale = ContentScale.FillWidth + ) + + MarginVertical(margin = 16.dp) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + text = stringResource(R.string.sora_card_title), + style = MaterialTheme.customTypography.headline2, + color = Color.White + ) + + MarginVertical(margin = 16.dp) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + text = stringResource(R.string.sora_card_description), + style = MaterialTheme.customTypography.paragraphM, + color = Color.White + ) + + MarginVertical(margin = 16.dp) + AnnualFee() + + MarginVertical(margin = 16.dp) + FreeCardIssuance(state) + + MarginVertical(margin = 16.dp) + BlacklistedCountries(onSeeListClicked = callbacks::onSeeBlacklist) + + MarginVertical(margin = 16.dp) + if (state.enoughXor) { + AccentButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .height(48.dp), + onClick = callbacks::onEnableCard, + text = stringResource(R.string.common_continue) + ) + } else { + AccentButton( + modifier = Modifier + .fillMaxWidth() + .testTag("GetMoreXor") + .padding(horizontal = 8.dp) + .height(48.dp), + onClick = callbacks::onGetMoreXor, + text = stringResource(R.string.sora_card_get_more_xor) + ) + } + + MarginVertical(margin = 8.dp) + TransparentButton( + modifier = Modifier + .testTag("AlreadyHaveCard") + .fillMaxWidth() + .padding(horizontal = 8.dp) + .height(48.dp), + text = stringResource(R.string.sora_card_already_have_card), + onClick = callbacks::onAlreadyHaveCard + ) + + MarginVertical(margin = 8.dp) + } + } + } +} + +@Composable +private fun BlacklistedCountries( + onSeeListClicked: (String) -> Unit +) { + MaterialTheme( + colors = fearlessMaterialColors + ) { + HtmlText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + text = stringResource(R.string.sora_card_blacklisted_countires_warning), + style = MaterialTheme.customTypography.paragraphXS.copy( + textAlign = TextAlign.Center, + color = Color.White, + fontSize = 12.sp + ), + onUrlClick = onSeeListClicked + ) + } +} + +@Composable +private fun AnnualFee() { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + cornerRadius = 12.dp, + backgroundColor = Color(0xFF131313), + elevation = 0.dp + ) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(vertical = Dimens.x2, horizontal = Dimens.x3), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_check_small), + contentDescription = null + ) + MarginHorizontal(margin = 4.dp) + Text( + text = AnnotatedString( + text = stringResource(R.string.sora_card_annual_service_fee), + spanStyles = listOf( + AnnotatedString.Range(SpanStyle(fontWeight = FontWeight.Bold, fontSize = 24.sp), 0, 3) + ) + ), + style = MaterialTheme.customTypography.textL, + color = Color.White + ) + } + } +} + +@Composable +private fun FreeCardIssuance( + state: GetSoraCardState +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + cornerRadius = 12.dp, + backgroundColor = Color(0xFF131313), + elevation = 0.dp + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = Dimens.x2, horizontal = Dimens.x3) + ) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(bottom = Dimens.x2), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = if (state.enoughXor) R.drawable.ic_check_rounded else R.drawable.ic_cross_24), + contentDescription = null, + tint = if (state.enoughXor) { + Color.Unspecified + } else { + errorRed + } + ) + MarginHorizontal(margin = 4.dp) + Text( + text = AnnotatedString( + text = stringResource(R.string.sora_card_free_card_issuance), + spanStyles = listOf( + AnnotatedString.Range(SpanStyle(fontWeight = FontWeight.Bold, fontSize = 24.sp), 0, 4) + ) + ), + style = MaterialTheme.customTypography.textL, + color = Color.White + ) + } + Text( + modifier = Modifier + .fillMaxSize(), + text = stringResource(R.string.sora_card_free_card_issuance_conditions_xor), + style = MaterialTheme.customTypography.paragraphM, + color = Color.White + ) + + BalanceIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(top = Dimens.x3, bottom = Dimens.x1), + percent = state.percent.toFloat(), + label = when { + state.xorRatioUnavailable -> { + stringResource(R.string.common_error_general_title) + } + state.enoughXor -> { + stringResource(R.string.sora_card_you_have_enough_xor) + } + else -> { + stringResource( + R.string.sora_card_you_need_xor, + state.needInXor.format(), + state.needInEur.format() + ) + } + } + ) + } + } +} + +@Composable +fun Card( + modifier: Modifier = Modifier, + elevation: Dp = MaterialTheme.elevation.l, + cornerRadius: Dp = MaterialTheme.borderRadius.xl, + backgroundColor: Color = MaterialTheme.customColors.bgSurface, + content: @Composable () -> Unit +) { + androidx.compose.material.Card( + modifier = modifier.shadow( + elevation = elevation, + ambientColor = MaterialTheme.customColors.elevation, + spotColor = MaterialTheme.customColors.elevation, + shape = RoundedCornerShape(cornerRadius) + ), + shape = RoundedCornerShape(cornerRadius), + backgroundColor = backgroundColor + ) { + content() + } +} + +@Preview +@Composable +private fun PreviewGetSoraCardScreen() { + val empty = object : GetSoraCardScreenInterface { + override fun onEnableCard() {} + override fun onGetMoreXor() {} + override fun onSeeBlacklist(url: String) {} + override fun onAlreadyHaveCard() {} + override fun onNavigationClick() {} + } + GetSoraCardScreenWithToolbar( + state = GetSoraCardState(), + scrollState = rememberScrollState(), + callbacks = empty + ) +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/get/GetSoraCardViewModel.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/get/GetSoraCardViewModel.kt new file mode 100644 index 0000000000..f60dde7fc0 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/get/GetSoraCardViewModel.kt @@ -0,0 +1,186 @@ +package jp.co.soramitsu.soracard.impl.presentation.get + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import dagger.hilt.android.lifecycle.HiltViewModel +import jp.co.soramitsu.common.BuildConfig +import jp.co.soramitsu.common.R +import jp.co.soramitsu.common.base.BaseViewModel +import jp.co.soramitsu.common.resources.ResourceManager +import jp.co.soramitsu.common.utils.Event +import jp.co.soramitsu.oauth.base.sdk.SoraCardEnvironmentType +import jp.co.soramitsu.oauth.base.sdk.SoraCardInfo +import jp.co.soramitsu.oauth.base.sdk.SoraCardKycCredentials +import jp.co.soramitsu.oauth.base.sdk.contract.SoraCardContractData +import jp.co.soramitsu.oauth.base.sdk.signin.SoraCardSignInContractData +import jp.co.soramitsu.soracard.api.domain.SoraCardInteractor +import jp.co.soramitsu.soracard.api.presentation.SoraCardRouter +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.Locale +import javax.inject.Inject + +@HiltViewModel +class GetSoraCardViewModel @Inject constructor( + private val interactor: SoraCardInteractor, + private val router: SoraCardRouter, + private val resourceManager: ResourceManager +) : BaseViewModel(), GetSoraCardScreenInterface { + private companion object { + val KYC_REAL_REQUIRED_BALANCE = BigDecimal(95) + val KYC_REQUIRED_BALANCE_WITH_BACKLASH = BigDecimal(100) + } + + private val _launchSoraCardRegistration = MutableLiveData>() + val launchSoraCardRegistration: LiveData> = _launchSoraCardRegistration + + private val _launchSoraCardSignIn = MutableLiveData>() + val launchSoraCardSignIn: LiveData> = _launchSoraCardSignIn + + val state = MutableStateFlow(GetSoraCardState()) + + init { + subscribeXorBalance() + subscribeSoraCardInfo() + } + + private fun subscribeXorBalance() { + launch { + interactor.xorAssetFlow() + .distinctUntilChanged() + .onEach { + val transferable = it.transferable + try { + val xorEurPrice = interactor.getXorPerEurRatio(it.token.configuration.priceId) ?: error("PriceId not found for XOR") + + val defaultScale = it.token.configuration.precision + val xorRequiredBalanceWithBacklash = KYC_REQUIRED_BALANCE_WITH_BACKLASH.divide(xorEurPrice, defaultScale, RoundingMode.HALF_EVEN) + val xorRealRequiredBalance = KYC_REAL_REQUIRED_BALANCE.divide(xorEurPrice, defaultScale, RoundingMode.HALF_EVEN) + val xorBalanceInEur = transferable.multiply(xorEurPrice) + + val needInXor = if (transferable.compareTo(xorRealRequiredBalance) == 1) { + BigDecimal.ZERO + } else { + xorRequiredBalanceWithBacklash.minus(transferable) + } + + val needInEur = if (xorBalanceInEur.compareTo(KYC_REAL_REQUIRED_BALANCE) == 1) { + BigDecimal.ZERO + } else { + KYC_REQUIRED_BALANCE_WITH_BACKLASH.minus(xorBalanceInEur) + } + + state.value = state.value.copy( + xorBalance = transferable, + enoughXor = transferable.compareTo(xorRealRequiredBalance) == 1, + percent = transferable.divide(xorRealRequiredBalance, defaultScale, RoundingMode.HALF_EVEN), + needInXor = needInXor, + needInEur = needInEur, + xorRatioUnavailable = false + ) + } catch (e: Exception) { + state.value = state.value.copy( + xorBalance = transferable, + enoughXor = false, + xorRatioUnavailable = true + ) + } + } + .launchIn(this) + } + } + + private fun subscribeSoraCardInfo() { + launch { + interactor.subscribeSoraCardInfo() + .distinctUntilChanged() + .collectLatest { + state.value = state.value.copy(soraCardInfo = it) + } + } + } + + override fun onEnableCard() { + _launchSoraCardRegistration.value = Event( + SoraCardContractData( + locale = Locale.ENGLISH, + apiKey = BuildConfig.SORA_CARD_API_KEY, + domain = BuildConfig.SORA_CARD_DOMAIN, + kycCredentials = SoraCardKycCredentials( + endpointUrl = BuildConfig.SORA_CARD_KYC_ENDPOINT_URL, + username = BuildConfig.SORA_CARD_KYC_USERNAME, + password = BuildConfig.SORA_CARD_KYC_PASSWORD + ), + environment = when { + BuildConfig.DEBUG -> SoraCardEnvironmentType.TEST + else -> SoraCardEnvironmentType.PRODUCTION + }, + soraCardInfo = state.value.soraCardInfo?.let { + SoraCardInfo( + accessToken = it.accessToken, + refreshToken = it.refreshToken, + accessTokenExpirationTime = it.accessTokenExpirationTime + ) + } + ) + ) + } + + override fun onAlreadyHaveCard() { + _launchSoraCardSignIn.value = Event( + SoraCardSignInContractData( + locale = Locale.ENGLISH, + apiKey = BuildConfig.SORA_CARD_API_KEY, + domain = BuildConfig.SORA_CARD_DOMAIN, + environment = when { + BuildConfig.DEBUG -> SoraCardEnvironmentType.TEST + else -> SoraCardEnvironmentType.PRODUCTION + }, + soraCardInfo = state.value.soraCardInfo?.let { + SoraCardInfo( + accessToken = it.accessToken, + refreshToken = it.refreshToken, + accessTokenExpirationTime = it.accessTokenExpirationTime + ) + } + ) + ) + } + + override fun onNavigationClick() { + router.back() + } + + fun updateSoraCardInfo( + accessToken: String, + refreshToken: String, + accessTokenExpirationTime: Long, + kycStatus: String + ) { + launch { + interactor.updateSoraCardInfo( + accessToken, + refreshToken, + accessTokenExpirationTime, + kycStatus + ) + } + } + + override fun onGetMoreXor() { + router.openGetMoreXor() + } + + override fun onSeeBlacklist(url: String) { + router.openWebViewer( + title = resourceManager.getString(R.string.sora_card_blacklisted_countires_title), + url = url + ) + } +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/getmorexor/GetMoreXorContent.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/getmorexor/GetMoreXorContent.kt new file mode 100644 index 0000000000..c2221a3a14 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/getmorexor/GetMoreXorContent.kt @@ -0,0 +1,129 @@ +package jp.co.soramitsu.soracard.impl.presentation.getmorexor + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import jp.co.soramitsu.common.compose.component.AccentButton +import jp.co.soramitsu.common.compose.component.B0 +import jp.co.soramitsu.common.compose.component.BottomSheetScreen +import jp.co.soramitsu.common.compose.component.GradientIcon +import jp.co.soramitsu.common.compose.component.GrayButton +import jp.co.soramitsu.common.compose.component.H3 +import jp.co.soramitsu.common.compose.component.MarginVertical +import jp.co.soramitsu.common.compose.theme.FearlessTheme +import jp.co.soramitsu.common.compose.theme.black2 +import jp.co.soramitsu.common.compose.theme.warningOrange +import jp.co.soramitsu.common.compose.theme.white +import jp.co.soramitsu.feature_soracard_impl.R + +interface GetMoreXorScreenInterface { + fun onSwapForXorClick() + fun onBuyXorClick() + fun onBackClicked() +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun GetMoreXorContent( + callback: GetMoreXorScreenInterface +) { + val keyboardController = LocalSoftwareKeyboardController.current + BottomSheetScreen { + Box(Modifier.fillMaxWidth()) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_close), + tint = white, + contentDescription = null, + modifier = Modifier + .size(24.dp) + .align(Alignment.End) + .clickable(onClick = callback::onBackClicked) + + ) + MarginVertical(margin = 44.dp) + + GradientIcon( + iconRes = R.drawable.ic_warning_filled, + color = warningOrange, + modifier = Modifier + .align(Alignment.CenterHorizontally) + ) + MarginVertical(margin = 16.dp) + H3( + text = stringResource(id = R.string.sora_card_get_more_xor), + color = black2, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + MarginVertical(margin = 8.dp) + B0( + text = stringResource(id = R.string.sora_card_select_xor_way), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + MarginVertical(margin = 12.dp) + + AccentButton( + text = stringResource(id = R.string.sora_card_swap_for_xor), + onClick = { + keyboardController?.hide() + callback.onSwapForXorClick() + }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + ) + MarginVertical(margin = 12.dp) + + GrayButton( + text = stringResource(id = R.string.sora_card_buy_xor), + onClick = { + keyboardController?.hide() + callback.onBuyXorClick() + }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + ) + + MarginVertical(margin = 12.dp) + } + } + } +} + +@Preview +@Composable +private fun GetMoreXorPreview() { + val emptyCallback = object : GetMoreXorScreenInterface { + override fun onBackClicked() {} + override fun onSwapForXorClick() {} + override fun onBuyXorClick() {} + } + + FearlessTheme { + GetMoreXorContent( + callback = emptyCallback + ) + } +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/getmorexor/GetMoreXorFragment.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/getmorexor/GetMoreXorFragment.kt new file mode 100644 index 0000000000..1139687ac7 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/getmorexor/GetMoreXorFragment.kt @@ -0,0 +1,20 @@ +package jp.co.soramitsu.soracard.impl.presentation.getmorexor + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import jp.co.soramitsu.common.base.BaseComposeBottomSheetDialogFragment + +@AndroidEntryPoint +class GetMoreXorFragment : BaseComposeBottomSheetDialogFragment() { + + override val viewModel: GetMoreXorViewModel by viewModels() + + @Composable + override fun Content(padding: PaddingValues) { + GetMoreXorContent( + callback = viewModel + ) + } +} diff --git a/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/getmorexor/GetMoreXorViewModel.kt b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/getmorexor/GetMoreXorViewModel.kt new file mode 100644 index 0000000000..a46c5a3487 --- /dev/null +++ b/feature-soracard-impl/src/main/kotlin/jp/co/soramitsu/soracard/impl/presentation/getmorexor/GetMoreXorViewModel.kt @@ -0,0 +1,33 @@ +package jp.co.soramitsu.soracard.impl.presentation.getmorexor + +import dagger.hilt.android.lifecycle.HiltViewModel +import jp.co.soramitsu.common.base.BaseViewModel +import jp.co.soramitsu.soracard.api.domain.SoraCardInteractor +import jp.co.soramitsu.soracard.api.presentation.SoraCardRouter +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class GetMoreXorViewModel @Inject constructor( + private val interactor: SoraCardInteractor, + private val router: SoraCardRouter +) : BaseViewModel(), GetMoreXorScreenInterface { + + override fun onBackClicked() { + router.back() + } + + override fun onSwapForXorClick() { + launch { + interactor.xorAssetFlow().firstOrNull()?.let { + router.back() + router.openSwapTokensScreen(it.token.configuration.id, it.token.configuration.chainId) + } + } + } + + override fun onBuyXorClick() { + router.showBuyCrypto() + } +} diff --git a/feature-soracard-impl/src/main/res/drawable-xxxhdpi/ic_sora_card.png b/feature-soracard-impl/src/main/res/drawable-xxxhdpi/ic_sora_card.png new file mode 100644 index 0000000000..510abfde9b Binary files /dev/null and b/feature-soracard-impl/src/main/res/drawable-xxxhdpi/ic_sora_card.png differ diff --git a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/unbond/PoolFullUnstakeDepositorAlertFragment.kt b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/unbond/PoolFullUnstakeDepositorAlertFragment.kt index 7c97d42e42..c7d5eb7c2a 100644 --- a/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/unbond/PoolFullUnstakeDepositorAlertFragment.kt +++ b/feature-staking-impl/src/main/java/jp/co/soramitsu/staking/impl/presentation/staking/unbond/PoolFullUnstakeDepositorAlertFragment.kt @@ -38,7 +38,7 @@ import jp.co.soramitsu.common.compose.component.GrayButton import jp.co.soramitsu.common.compose.component.Grip import jp.co.soramitsu.common.compose.component.H3 import jp.co.soramitsu.common.compose.component.MarginVertical -import jp.co.soramitsu.common.compose.component.soraTextStyle +import jp.co.soramitsu.common.compose.theme.soraTextStyle import jp.co.soramitsu.common.compose.theme.FearlessTheme import jp.co.soramitsu.common.compose.theme.alertYellow import jp.co.soramitsu.common.compose.theme.black2 diff --git a/feature-success-api/.gitignore b/feature-success-api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/feature-success-api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature-success-impl/.gitignore b/feature-success-impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/feature-success-impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/presentation/WalletRouter.kt b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/presentation/WalletRouter.kt index bcd21642a3..0626f35888 100644 --- a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/presentation/WalletRouter.kt +++ b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/api/presentation/WalletRouter.kt @@ -5,7 +5,7 @@ interface WalletRouter { fun backWithResult(vararg results: Pair) companion object { - const val KEY_CHAIN_ID = "chin_id" + const val KEY_CHAIN_ID = "chain_id" const val KEY_ASSET_ID = "asset_id" } } diff --git a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletInteractor.kt b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletInteractor.kt index 111de747a8..493b7dd903 100644 --- a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletInteractor.kt +++ b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletInteractor.kt @@ -1,9 +1,6 @@ package jp.co.soramitsu.wallet.impl.domain.interfaces import jp.co.soramitsu.account.api.domain.model.MetaAccount -import java.io.File -import java.math.BigDecimal -import java.math.BigInteger import jp.co.soramitsu.common.data.model.CursorPage import jp.co.soramitsu.common.data.network.runtime.binding.EqAccountInfo import jp.co.soramitsu.common.data.network.runtime.binding.EqOraclePricePoint @@ -24,6 +21,9 @@ import jp.co.soramitsu.wallet.impl.domain.model.Transfer import jp.co.soramitsu.wallet.impl.domain.model.TransferValidityStatus import jp.co.soramitsu.wallet.impl.domain.model.WalletAccount import kotlinx.coroutines.flow.Flow +import java.io.File +import java.math.BigDecimal +import java.math.BigInteger class NotValidTransferStatus(val status: TransferValidityStatus) : Exception() @@ -112,4 +112,9 @@ interface WalletInteractor { suspend fun getEquilibriumAccountInfo(asset: Chain.Asset, accountId: AccountId): EqAccountInfo? suspend fun getEquilibriumAssetRates(chainAsset: Chain.Asset): Map + + fun isShowGetSoraCard(): Boolean + fun observeIsShowSoraCard(): Flow + fun decreaseSoraCardHiddenSessions() + fun hideSoraCard() } diff --git a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletRepository.kt b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletRepository.kt index 0bcfc0b47c..269dacb94d 100644 --- a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletRepository.kt +++ b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/interfaces/WalletRepository.kt @@ -80,4 +80,6 @@ interface WalletRepository { suspend fun getRemoteConfig(): Result fun chainRegistrySyncUp() + + suspend fun getSingleAssetPriceCoingecko(priceId: String, currency: String): BigDecimal? } diff --git a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/validation/EnoughToPayFeesValidation.kt b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/validation/EnoughToPayFeesValidation.kt index 52050bea96..ef82bd9d37 100644 --- a/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/validation/EnoughToPayFeesValidation.kt +++ b/feature-wallet-api/src/main/java/jp/co/soramitsu/wallet/impl/domain/validation/EnoughToPayFeesValidation.kt @@ -1,13 +1,14 @@ package jp.co.soramitsu.wallet.impl.domain.validation -import java.math.BigDecimal import jp.co.soramitsu.account.api.domain.interfaces.AccountRepository +import jp.co.soramitsu.common.utils.orZero import jp.co.soramitsu.common.validation.DefaultFailureLevel import jp.co.soramitsu.common.validation.Validation import jp.co.soramitsu.common.validation.ValidationStatus import jp.co.soramitsu.runtime.ext.accountIdOf import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.wallet.impl.domain.interfaces.WalletRepository +import java.math.BigDecimal class EnoughToPayFeesValidation( private val feeExtractor: AmountProducer

, @@ -39,7 +40,7 @@ fun

EnoughToPayFeesValidation.Companion.assetBalanceProducer( val accountId = chain.accountIdOf(originAddressExtractor(payload)) val meta = accountRepository.getSelectedMetaAccount() - val asset = walletRepository.getAsset(meta.id, accountId, chainAssetExtractor(payload), chain.minSupportedVersion)!! + val asset = walletRepository.getAsset(meta.id, accountId, chainAssetExtractor(payload), chain.minSupportedVersion) - asset.availableForStaking + asset?.availableForStaking.orZero() } diff --git a/feature-wallet-impl/build.gradle b/feature-wallet-impl/build.gradle index 0b34a690d1..b4e0a18201 100644 --- a/feature-wallet-impl/build.gradle +++ b/feature-wallet-impl/build.gradle @@ -18,9 +18,12 @@ android { buildConfigField "String", "RAMP_TOKEN", "\"4bk3yhfrg99fer764bo7egrqmxbfene7gbrpwmp3\"" buildConfigField "String", "RAMP_HOST", "\"ri-widget-staging.firebaseapp.com\"" - buildConfigField "String", "MOONPAY_PRIVATE_KEY", readSecret("MOONPAY_TEST_SECRET") + buildConfigField "String", "MOONPAY_PRIVATE_KEY", readSecretInQuotes("MOONPAY_TEST_SECRET") buildConfigField "String", "MOONPAY_HOST", "\"buy-staging.moonpay.com\"" buildConfigField "String", "MOONPAY_PUBLIC_KEY", "\"pk_test_DMRuyL6Nf1qc9OzjPBmCFBeCGkFwiZs0\"" + + buildConfigField "String", "SORA_CONFIG_COMMON", "\"https://config.polkaswap2.io/stage/common.json\"" + buildConfigField "String", "SORA_CONFIG_MOBILE", "\"https://config.polkaswap2.io/stage/mobile.json\"" } buildTypes { @@ -28,9 +31,12 @@ android { buildConfigField "String", "RAMP_TOKEN", "\"3quzr4e6wdyccndec8jzjebzar5kxxzfy2f3us5k\"" buildConfigField "String", "RAMP_HOST", "\"buy.ramp.network\"" - buildConfigField "String", "MOONPAY_PRIVATE_KEY", readSecret("MOONPAY_PRODUCTION_SECRET") + buildConfigField "String", "MOONPAY_PRIVATE_KEY", readSecretInQuotes("MOONPAY_PRODUCTION_SECRET") buildConfigField "String", "MOONPAY_PUBLIC_KEY", "\"pk_live_Boi6Rl107p7XuJWBL8GJRzGWlmUSoxbz\"" buildConfigField "String", "MOONPAY_HOST", "\"buy.moonpay.com\"" + + buildConfigField "String", "SORA_CONFIG_COMMON", "\"https://config.polkaswap2.io/prod/common.json\"" + buildConfigField "String", "SORA_CONFIG_MOBILE", "\"https://config.polkaswap2.io/prod/mobile.json\"" } } @@ -63,6 +69,8 @@ dependencies { implementation project(':feature-account-api') implementation project(':feature-account-impl') implementation project(':runtime') + implementation project(':feature-soracard-api') + implementation project(':feature-soracard-impl') implementation kotlinDep @@ -111,6 +119,8 @@ dependencies { implementation jnaDep implementation beaconDep, withoutJna + implementation soraCardDep, withoutBouncycastle + implementation shimmerDep implementation compose diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/feature_wallet_impl/di/WalletFeatureComponent.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/feature_wallet_impl/di/WalletFeatureComponent.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/feature_wallet_impl/di/WalletFeatureDependencies.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/feature_wallet_impl/di/WalletFeatureDependencies.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/HistorySourceProvider.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/HistorySourceProvider.kt index 603b8d21f0..1d6311abc6 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/HistorySourceProvider.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/HistorySourceProvider.kt @@ -4,18 +4,20 @@ import jp.co.soramitsu.runtime.multiNetwork.ChainRegistry import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.wallet.impl.data.network.subquery.OperationsHistoryApi import jp.co.soramitsu.xnetworking.networkclient.SoramitsuNetworkClient +import jp.co.soramitsu.xnetworking.sorawallet.mainconfig.SoraRemoteConfigBuilder import jp.co.soramitsu.xnetworking.txhistory.client.sorawallet.SubQueryClientForSoraWalletFactory class HistorySourceProvider( private val walletOperationsApi: OperationsHistoryApi, private val chainRegistry: ChainRegistry, private val soramitsuNetworkClient: SoramitsuNetworkClient, - private val soraSubqueryFactory: SubQueryClientForSoraWalletFactory + private val soraSubqueryFactory: SubQueryClientForSoraWalletFactory, + private val soraRemoteConfigBuilder: SoraRemoteConfigBuilder ) { operator fun invoke(historyUrl: String, historyType: Chain.ExternalApi.Section.Type): HistorySource? { return when (historyType) { Chain.ExternalApi.Section.Type.SUBQUERY -> SubqueryHistorySource(walletOperationsApi, chainRegistry, historyUrl) - Chain.ExternalApi.Section.Type.SORA -> SoraHistorySource(soramitsuNetworkClient, soraSubqueryFactory, historyUrl) + Chain.ExternalApi.Section.Type.SORA -> SoraHistorySource(soramitsuNetworkClient, soraSubqueryFactory, soraRemoteConfigBuilder) Chain.ExternalApi.Section.Type.SUBSQUID -> SubsquidHistorySource(walletOperationsApi, historyUrl) Chain.ExternalApi.Section.Type.GIANTSQUID -> GiantsquidHistorySource(walletOperationsApi, historyUrl) else -> null diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/SoraHistorySource.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/SoraHistorySource.kt index 52077fc119..c1b5692258 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/SoraHistorySource.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/historySource/SoraHistorySource.kt @@ -7,6 +7,7 @@ import jp.co.soramitsu.wallet.impl.data.mappers.toOperation import jp.co.soramitsu.wallet.impl.domain.interfaces.TransactionFilter import jp.co.soramitsu.wallet.impl.domain.model.Operation import jp.co.soramitsu.xnetworking.networkclient.SoramitsuNetworkClient +import jp.co.soramitsu.xnetworking.sorawallet.mainconfig.SoraRemoteConfigBuilder import jp.co.soramitsu.xnetworking.txhistory.TxHistoryItem import jp.co.soramitsu.xnetworking.txhistory.TxHistoryResult import jp.co.soramitsu.xnetworking.txhistory.client.sorawallet.SubQueryClientForSoraWalletFactory @@ -14,7 +15,7 @@ import jp.co.soramitsu.xnetworking.txhistory.client.sorawallet.SubQueryClientFor class SoraHistorySource( private val soramitsuNetworkClient: SoramitsuNetworkClient, private val soraSubqueryFactory: SubQueryClientForSoraWalletFactory, - private val url: String + private val soraRemoteConfigBuilder: SoraRemoteConfigBuilder ) : HistorySource { override suspend fun getOperations( pageSize: Int, @@ -28,15 +29,14 @@ class SoraHistorySource( val soraStartPage = 1L val page = cursor?.toLongOrNull() ?: soraStartPage - val subQueryClientForSora = soraSubqueryFactory.create(soramitsuNetworkClient, url, pageSize) + val subQueryClientForSora = soraSubqueryFactory.create(soramitsuNetworkClient, pageSize, soraRemoteConfigBuilder) - val soraHistory: TxHistoryResult = subQueryClientForSora.getTransactionHistoryPaged( + val soraHistory: TxHistoryResult? = subQueryClientForSora.getTransactionHistoryPaged( accountAddress, - chain.name, page ) - val soraHistoryItems: List = soraHistory.items + val soraHistoryItems: List = soraHistory?.items.orEmpty() val soraOperations = soraHistoryItems.mapNotNull { it.toOperation(chain, chainAsset, accountAddress, filters) } return CursorPage(page.inc().toString(), soraOperations) } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/repository/WalletRepositoryImpl.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/repository/WalletRepositoryImpl.kt index 18b3a3a8ff..e83bb90ac0 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/repository/WalletRepositoryImpl.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/data/repository/WalletRepositoryImpl.kt @@ -1,8 +1,6 @@ package jp.co.soramitsu.wallet.impl.data.repository import com.opencsv.CSVReaderHeaderAware -import java.math.BigDecimal -import java.math.BigInteger import jp.co.soramitsu.account.api.domain.model.MetaAccount import jp.co.soramitsu.account.api.domain.model.accountId import jp.co.soramitsu.common.data.network.HttpExceptionHandler @@ -48,6 +46,8 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.withContext +import java.math.BigDecimal +import java.math.BigInteger class WalletRepositoryImpl( private val substrateSource: SubstrateRemoteSource, @@ -64,6 +64,12 @@ class WalletRepositoryImpl( private val remoteConfigFetcher: RemoteConfigFetcher ) : WalletRepository, UpdatesProviderUi by updatesMixin { + companion object { + private const val COINGECKO_REQUEST_DELAY_MILLIS = 60 * 1000 + } + + private val coingeckoCache = mutableMapOf>>() + override fun assetsFlow(meta: MetaAccount): Flow> { return combine( chainRegistry.chainsById, @@ -357,6 +363,25 @@ class WalletRepositoryImpl( ) } + override suspend fun getSingleAssetPriceCoingecko(priceId: String, currency: String): BigDecimal? { + coingeckoCache[priceId]?.get(currency)?.let { (cacheUntilMillis, cachedValue) -> + if (System.currentTimeMillis() <= cacheUntilMillis) { + return cachedValue + } + } + val apiValue = apiCall { + coingeckoApi.getSingleAssetPrice(priceIds = priceId, currency = currency) + }.getOrDefault(priceId, null)?.getOrDefault(currency, null)?.toBigDecimal() + + apiValue?.let { + val currencyMap = coingeckoCache[priceId] ?: mutableMapOf() + val cacheUntilMillis = System.currentTimeMillis() + COINGECKO_REQUEST_DELAY_MILLIS + currencyMap[currency] = cacheUntilMillis to apiValue + coingeckoCache[priceId] = currencyMap + } + return apiValue + } + private suspend fun getAssetPriceCoingecko(vararg priceId: String, currencyId: String): Map> { return apiCall { coingeckoApi.getAssetPrice(priceId.joinToString(","), currencyId, true) } } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/di/WalletFeatureModule.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/di/WalletFeatureModule.kt index 1945288a74..cae1623566 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/di/WalletFeatureModule.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/di/WalletFeatureModule.kt @@ -76,6 +76,8 @@ import jp.co.soramitsu.wallet.impl.presentation.balance.assetActions.buy.BuyMixi import jp.co.soramitsu.wallet.impl.presentation.send.SendSharedState import jp.co.soramitsu.wallet.impl.presentation.transaction.filter.HistoryFiltersProvider import jp.co.soramitsu.xnetworking.networkclient.SoramitsuNetworkClient +import jp.co.soramitsu.xnetworking.sorawallet.mainconfig.SoraRemoteConfigBuilder +import jp.co.soramitsu.xnetworking.sorawallet.mainconfig.SoraRemoteConfigProvider import jp.co.soramitsu.xnetworking.txhistory.client.sorawallet.SubQueryClientForSoraWalletFactory import javax.inject.Named import javax.inject.Singleton @@ -183,12 +185,14 @@ class WalletFeatureModule { walletOperationsHistoryApi: OperationsHistoryApi, chainRegistry: ChainRegistry, soramitsuNetworkClient: SoramitsuNetworkClient, - subQueryClientForSoraWalletFactory: SubQueryClientForSoraWalletFactory + subQueryClientForSoraWalletFactory: SubQueryClientForSoraWalletFactory, + soraRemoteConfigBuilder: SoraRemoteConfigBuilder ) = HistorySourceProvider( walletOperationsHistoryApi, chainRegistry, soramitsuNetworkClient, - subQueryClientForSoraWalletFactory + subQueryClientForSoraWalletFactory, + soraRemoteConfigBuilder ) @Provides @@ -196,12 +200,12 @@ class WalletFeatureModule { walletRepository: WalletRepository, addressBookRepository: AddressBookRepository, accountRepository: AccountRepository, + historyRepository: HistoryRepository, chainRegistry: ChainRegistry, fileProvider: FileProvider, preferences: Preferences, selectedFiat: SelectedFiat, - updatesMixin: UpdatesMixin, - historyRepository: HistoryRepository + updatesMixin: UpdatesMixin ): WalletInteractor = WalletInteractorImpl( walletRepository, addressBookRepository, @@ -359,4 +363,18 @@ class WalletFeatureModule { fun provideSubQueryClientForSoraWalletFactory( @ApplicationContext context: Context ): SubQueryClientForSoraWalletFactory = SubQueryClientForSoraWalletFactory(context) + + @Singleton + @Provides + fun provideSoraRemoteConfigBuilder( + client: SoramitsuNetworkClient, + @ApplicationContext context: Context + ): SoraRemoteConfigBuilder { + return SoraRemoteConfigProvider( + context = context, + client = client, + commonUrl = BuildConfig.SORA_CONFIG_COMMON, + mobileUrl = BuildConfig.SORA_CONFIG_MOBILE + ).provide() + } } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/domain/WalletInteractorImpl.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/domain/WalletInteractorImpl.kt index db740012d4..360b24a4ba 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/domain/WalletInteractorImpl.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/domain/WalletInteractorImpl.kt @@ -1,7 +1,5 @@ package jp.co.soramitsu.wallet.impl.domain -import java.math.BigDecimal -import java.math.BigInteger import jp.co.soramitsu.account.api.domain.interfaces.AccountRepository import jp.co.soramitsu.account.api.domain.model.MetaAccount import jp.co.soramitsu.account.api.domain.model.accountId @@ -48,9 +46,13 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.withIndex import kotlinx.coroutines.withContext +import java.math.BigDecimal +import java.math.BigInteger private const val QR_PREFIX_SUBSTRATE = "substrate" private const val PREFS_WALLET_SELECTED_CHAIN_ID = "wallet_selected_chain_id" +private const val PREFS_SORA_CARD_HIDDEN_SESSIONS_COUNT = "prefs_sora_card_hidden_sessions_count" +private const val SORA_CARD_HIDDEN_SESSIONS_LIMIT = 5 class WalletInteractorImpl( private val walletRepository: WalletRepository, @@ -119,7 +121,15 @@ class WalletInteractorImpl( val metaAccount = accountRepository.getSelectedMetaAccount() val (chain, chainAsset) = chainRegistry.chainWithAsset(chainId, chainAssetId) - return walletRepository.getAsset(metaAccount.id, metaAccount.accountId(chain)!!, chainAsset, chain.minSupportedVersion)!! + val accountId = metaAccount.accountId(chain)!! + return walletRepository.getAsset(metaAccount.id, accountId, chainAsset, chain.minSupportedVersion) + ?: Asset.createEmpty( + chainAsset = chainAsset, + metaId = metaAccount.id, + accountId = accountId, + minSupportedVersion = chain.minSupportedVersion, + enabled = chain.nodes.isNotEmpty() + ) } override fun operationsFirstPageFlow(chainId: ChainId, chainAssetId: String): Flow { @@ -328,6 +338,25 @@ class WalletInteractorImpl( return existingChain?.id } + override fun isShowGetSoraCard(): Boolean = + preferences.getInt(PREFS_SORA_CARD_HIDDEN_SESSIONS_COUNT, 0) <= 0 + + override fun observeIsShowSoraCard(): Flow = + preferences.intFlow(PREFS_SORA_CARD_HIDDEN_SESSIONS_COUNT, 0).map { it <= 0 } + + override fun hideSoraCard() { + preferences.putInt(PREFS_SORA_CARD_HIDDEN_SESSIONS_COUNT, SORA_CARD_HIDDEN_SESSIONS_LIMIT) + } + + override fun decreaseSoraCardHiddenSessions() { + val newCount = preferences.getInt(PREFS_SORA_CARD_HIDDEN_SESSIONS_COUNT, 0) - 1 + if (newCount <= 0) { + preferences.removeField(PREFS_SORA_CARD_HIDDEN_SESSIONS_COUNT) + } else { + preferences.putInt(PREFS_SORA_CARD_HIDDEN_SESSIONS_COUNT, newCount) + } + } + override suspend fun getEquilibriumAccountInfo(asset: Chain.Asset, accountId: AccountId): EqAccountInfo? = walletRepository.getEquilibriumAccountInfo(asset, accountId) diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/WalletRouter.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/WalletRouter.kt index 514c225a5d..72b6b64b9c 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/WalletRouter.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/WalletRouter.kt @@ -30,7 +30,7 @@ interface WalletRouter : SecureRouter, WalletRouterApi { fun openSend(assetPayload: AssetPayload?, initialSendToAddress: String? = null, currencyId: String? = null) - fun openSwapTokensScreen(assetPayload: AssetPayload) + fun openSwapTokensScreen(assetId: String, chainId: String) fun openSelectChain(assetId: String, chainId: ChainId? = null, chooserMode: Boolean = true) @@ -105,6 +105,8 @@ interface WalletRouter : SecureRouter, WalletRouterApi { fun openNetworkIssues() + fun openGetSoraCard() + fun openOptionsAddAccount(payload: AddAccountBottomSheet.Payload) fun openAlert(payload: AlertViewState) diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetselector/AssetSelectContent.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetselector/AssetSelectContent.kt index 581491c543..c111a0a33c 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetselector/AssetSelectContent.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/assetselector/AssetSelectContent.kt @@ -34,7 +34,6 @@ import jp.co.soramitsu.common.compose.component.Image import jp.co.soramitsu.common.compose.component.MarginHorizontal import jp.co.soramitsu.common.compose.component.MarginVertical import jp.co.soramitsu.common.compose.component.getImageRequest -import jp.co.soramitsu.common.compose.theme.FearlessThemeBlackBg import jp.co.soramitsu.common.compose.theme.black2 import jp.co.soramitsu.common.compose.theme.black4 import jp.co.soramitsu.common.compose.theme.gray2 @@ -208,17 +207,15 @@ private fun SelectAssetScreenPreview() { assets = items, searchQuery = null ) - FearlessThemeBlackBg { - Column( - Modifier.background(black4) - ) { - AssetSelectContent( - state = state, - callback = object : AssetSelectContentInterface { - override fun onAssetSelected(assetItemState: AssetItemState) {} - override fun onSearchInput(input: String) {} - } - ) - } + Column( + Modifier.background(black4) + ) { + AssetSelectContent( + state = state, + callback = object : AssetSelectContentInterface { + override fun onAssetSelected(assetItemState: AssetItemState) {} + override fun onSearchInput(input: String) {} + } + ) } } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/chainselector/ChainSelectContent.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/chainselector/ChainSelectContent.kt index 096b7399d2..f921058125 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/chainselector/ChainSelectContent.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/chainselector/ChainSelectContent.kt @@ -32,7 +32,6 @@ import jp.co.soramitsu.common.compose.component.Image import jp.co.soramitsu.common.compose.component.MarginHorizontal import jp.co.soramitsu.common.compose.component.MarginVertical import jp.co.soramitsu.common.compose.component.getImageRequest -import jp.co.soramitsu.common.compose.theme.FearlessThemeBlackBg import jp.co.soramitsu.common.compose.theme.black4 import jp.co.soramitsu.common.utils.clickableWithNoIndication import jp.co.soramitsu.feature_wallet_impl.R @@ -212,11 +211,9 @@ private fun SelectChainScreenPreview() { selectedChainId = null, searchQuery = null ) - FearlessThemeBlackBg { - Column( - Modifier.background(black4) - ) { - ChainSelectContent(state = state) - } + Column( + Modifier.background(black4) + ) { + ChainSelectContent(state = state) } } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/detail/BalanceDetailViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/detail/BalanceDetailViewModel.kt index afe7a8fa54..5d4b59fca0 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/detail/BalanceDetailViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/detail/BalanceDetailViewModel.kt @@ -276,7 +276,7 @@ class BalanceDetailViewModel @Inject constructor( } private fun openSwapTokensScreen(assetPayload: AssetPayload) { - router.openSwapTokensScreen(assetPayload) + router.openSwapTokensScreen(assetPayload.chainAssetId, assetPayload.chainId) } private fun receiveClicked(assetPayload: AssetPayload) { diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/BalanceListFragment.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/BalanceListFragment.kt index 0181bb7991..97a5d0df0a 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/BalanceListFragment.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/BalanceListFragment.kt @@ -19,7 +19,6 @@ import androidx.lifecycle.lifecycleScope import coil.ImageLoader import com.journeyapps.barcodescanner.ScanOptions import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject import jp.co.soramitsu.common.PLAY_MARKET_APP_URI import jp.co.soramitsu.common.PLAY_MARKET_BROWSER_URI import jp.co.soramitsu.common.base.BaseComposeFragment @@ -37,8 +36,11 @@ import jp.co.soramitsu.common.utils.hideKeyboard import jp.co.soramitsu.common.view.bottomSheet.AlertBottomSheet import jp.co.soramitsu.common.view.bottomSheet.list.dynamic.DynamicListBottomSheet import jp.co.soramitsu.feature_wallet_impl.R +import jp.co.soramitsu.oauth.base.sdk.contract.SoraCardResult +import jp.co.soramitsu.oauth.base.sdk.signin.SoraCardSignInContract import jp.co.soramitsu.wallet.impl.presentation.common.askPermissionsSafely import kotlinx.coroutines.launch +import javax.inject.Inject @AndroidEntryPoint class BalanceListFragment : BaseComposeFragment() { @@ -54,6 +56,23 @@ class BalanceListFragment : BaseComposeFragment() { } } + private val soraCardSignIn = registerForActivityResult( + SoraCardSignInContract() + ) { result -> + when (result) { + is SoraCardResult.Failure -> {} + is SoraCardResult.Canceled -> {} + is SoraCardResult.Success -> { + viewModel.updateSoraCardInfo( + accessToken = result.accessToken, + refreshToken = result.refreshToken, + accessTokenExpirationTime = result.accessTokenExpirationTime, + kycStatus = result.status.toString() + ) + } + } + } + @OptIn(ExperimentalMaterialApi::class) @Composable override fun Content(padding: PaddingValues, scrollState: ScrollState, modalBottomSheetState: ModalBottomSheetState) { @@ -99,6 +118,9 @@ class BalanceListFragment : BaseComposeFragment() { viewModel.showFiatChooser.observeEvent(::showFiatChooser) viewModel.showUnsupportedChainAlert.observeEvent { showUnsupportedChainAlert() } viewModel.openPlayMarket.observeEvent { openPlayMarket() } + viewModel.launchSoraCardSignIn.observeEvent { contractData -> + soraCardSignIn.launch(contractData) + } } fun initViews() { diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/BalanceListViewModel.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/BalanceListViewModel.kt index b4751f4881..0157460628 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/BalanceListViewModel.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/BalanceListViewModel.kt @@ -10,6 +10,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import jp.co.soramitsu.account.api.domain.interfaces.AccountRepository import jp.co.soramitsu.account.api.presentation.actions.AddAccountBottomSheet import jp.co.soramitsu.common.AlertViewState +import jp.co.soramitsu.common.BuildConfig import jp.co.soramitsu.common.address.AddressIconGenerator import jp.co.soramitsu.common.address.AddressModel import jp.co.soramitsu.common.address.createAddressModel @@ -29,7 +30,6 @@ import jp.co.soramitsu.common.domain.AppVersion import jp.co.soramitsu.common.domain.FiatCurrencies import jp.co.soramitsu.common.domain.GetAvailableFiatCurrencies import jp.co.soramitsu.common.domain.SelectedFiat -import jp.co.soramitsu.common.domain.get import jp.co.soramitsu.common.mixin.api.NetworkStateMixin import jp.co.soramitsu.common.mixin.api.NetworkStateUi import jp.co.soramitsu.common.mixin.api.UpdatesMixin @@ -53,11 +53,18 @@ import jp.co.soramitsu.core.extrinsic.KeyPairProvider import jp.co.soramitsu.core.extrinsic.mortality.IChainStateRepository import jp.co.soramitsu.fearless_utils.ss58.SS58Encoder.addressByteOrNull import jp.co.soramitsu.feature_wallet_impl.R +import jp.co.soramitsu.oauth.base.sdk.SoraCardEnvironmentType +import jp.co.soramitsu.oauth.base.sdk.SoraCardInfo +import jp.co.soramitsu.oauth.base.sdk.contract.SoraCardCommonVerification +import jp.co.soramitsu.oauth.base.sdk.signin.SoraCardSignInContractData +import jp.co.soramitsu.oauth.common.domain.KycRepository import jp.co.soramitsu.runtime.multiNetwork.chain.model.Chain import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId import jp.co.soramitsu.runtime.multiNetwork.chain.model.defaultChainSort import jp.co.soramitsu.runtime.multiNetwork.chain.model.getWithToken import jp.co.soramitsu.runtime.multiNetwork.chain.model.polkadotChainId +import jp.co.soramitsu.soracard.api.domain.SoraCardInteractor +import jp.co.soramitsu.soracard.impl.presentation.SoraCardItemViewState import jp.co.soramitsu.wallet.impl.data.mappers.mapAssetToAssetModel import jp.co.soramitsu.wallet.impl.domain.ChainInteractor import jp.co.soramitsu.wallet.impl.domain.CurrentAccountAddressUseCase @@ -88,6 +95,8 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.math.BigDecimal +import java.util.Locale +import java.util.concurrent.TimeUnit import javax.inject.Inject private const val CURRENT_ICON_SIZE = 40 @@ -95,6 +104,7 @@ private const val CURRENT_ICON_SIZE = 40 @HiltViewModel class BalanceListViewModel @Inject constructor( private val interactor: WalletInteractor, + private val soraCardInteractor: SoraCardInteractor, private val chainInteractor: ChainInteractor, private val addressIconGenerator: AddressIconGenerator, private val router: WalletRouter, @@ -108,7 +118,8 @@ class BalanceListViewModel @Inject constructor( private val currentAccountAddress: CurrentAccountAddressUseCase, private val chainRegistry: IChainRegistry, private val keyPairProvider: KeyPairProvider, - private val chainStateRepository: IChainStateRepository + private val chainStateRepository: IChainStateRepository, + private val kycRepository: KycRepository ) : BaseViewModel(), UpdatesProviderUi by updatesMixin, NetworkStateUi by networkStateMixin, WalletScreenInterface { private val accountAddressToChainIdMap = mutableMapOf() @@ -125,6 +136,9 @@ class BalanceListViewModel @Inject constructor( private val _openPlayMarket = MutableLiveData>() val openPlayMarket: LiveData> = _openPlayMarket + private val _launchSoraCardSignIn = MutableLiveData>() + val launchSoraCardSignIn: LiveData> = _launchSoraCardSignIn + private val connectingChainIdsFlow = networkStateMixin.chainConnectionsLiveData.map { it.filter { (_, isConnecting) -> isConnecting }.keys }.asFlow() @@ -141,7 +155,7 @@ class BalanceListViewModel @Inject constructor( .inBackground() private val fiatSymbolFlow = combine(selectedFiat.flow(), getAvailableFiatCurrencies.flow()) { selectedFiat: String, fiatCurrencies: FiatCurrencies -> - fiatCurrencies[selectedFiat]?.symbol + fiatCurrencies.associateBy { it.id }[selectedFiat]?.symbol }.onEach { sync() } @@ -321,15 +335,25 @@ class BalanceListViewModel @Inject constructor( .thenBy { it.chainId.defaultChainSort() } .thenBy { it.chainName } + private val soraCardState = combine( + interactor.observeIsShowSoraCard(), + soraCardInteractor.subscribeSoraCardInfo() + ) { isShow, soraCardInfo -> + val kycStatus = soraCardInfo?.kycStatus?.let(::mapKycStatus) + SoraCardItemViewState(kycStatus, soraCardInfo, null, isShow) + } + val state = combine( assetStates, assetTypeSelectorState, balanceFlow, - selectedChainId + selectedChainId, + soraCardState ) { assetsListItemStates: List, multiToggleButtonState: MultiToggleButtonState, balanceModel: BalanceModel, - selectedChainId: ChainId? -> + selectedChainId: ChainId?, + soraCardState: SoraCardItemViewState -> val selectedChainAddress = selectedChainId?.let { currentAccountAddress(chainId = it) @@ -349,12 +373,12 @@ class BalanceListViewModel @Inject constructor( assets = assetsListItemStates, multiToggleButtonState = multiToggleButtonState, balance = balanceState, - hasNetworkIssues = hasNetworkIssues + hasNetworkIssues = hasNetworkIssues, + soraCardState = soraCardState ) }.stateIn(scope = this, started = SharingStarted.Eagerly, initialValue = WalletState.default) val toolbarState = combine(currentAddressModelFlow(), selectedChainItemFlow) { addressModel, chain -> - chainsFlow LoadingState.Loaded( MainToolbarViewState( title = addressModel.nameOrAddress, @@ -365,6 +389,8 @@ class BalanceListViewModel @Inject constructor( }.stateIn(scope = this, started = SharingStarted.Eagerly, initialValue = LoadingState.Loading()) init { + updateSoraCardStatus() + router.chainSelectorPayloadFlow.map { chainId -> val walletId = interactor.getSelectedMetaAccount().id interactor.saveChainId(walletId, chainId) @@ -374,6 +400,24 @@ class BalanceListViewModel @Inject constructor( interactor.selectedMetaAccountFlow().map { wallet -> selectedChainId.value = interactor.getSavedChainId(wallet.id) }.launchIn(this) + + if (!interactor.isShowGetSoraCard()) { + interactor.decreaseSoraCardHiddenSessions() + } + } + + private fun updateSoraCardStatus() { + viewModelScope.launch { + val soraCardInfo = soraCardInteractor.getSoraCardInfo() ?: return@launch + val accessTokenExpirationTime = soraCardInfo.accessTokenExpirationTime + val accessTokenExpired = + accessTokenExpirationTime < TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + + if (!accessTokenExpired) { + val kycStatus: SoraCardCommonVerification? = kycRepository.getKycLastFinalStatus(soraCardInfo.accessToken) + soraCardInteractor.updateSoraCardKycStatus(kycStatus = kycStatus?.toString().orEmpty()) + } + } } private fun sync() { @@ -517,6 +561,18 @@ class BalanceListViewModel @Inject constructor( } } + override fun soraCardClicked() { + if (state.value.soraCardState?.kycStatus == null) { + router.openGetSoraCard() + } else { + onSoraCardStatusClicked() + } + } + + override fun soraCardClose() { + interactor.hideSoraCard() + } + fun onFiatSelected(item: FiatCurrency) { viewModelScope.launch { selectedFiat.set(item.id) @@ -574,4 +630,64 @@ class BalanceListViewModel @Inject constructor( val message = resourceManager.getString(R.string.common_copied) showMessage(message) } + + private fun mapKycStatus(kycStatus: String): String? { + return when (runCatching { SoraCardCommonVerification.valueOf(kycStatus) }.getOrNull()) { + SoraCardCommonVerification.Pending -> { + resourceManager.getString(R.string.sora_card_verification_in_progress) + } + SoraCardCommonVerification.Successful -> { + resourceManager.getString(R.string.sora_card_verification_successful) + } + SoraCardCommonVerification.Rejected -> { + resourceManager.getString(R.string.sora_card_verification_rejected) + } + SoraCardCommonVerification.Failed -> { + resourceManager.getString(R.string.sora_card_verification_failed) + } + SoraCardCommonVerification.NoFreeAttempt -> { + resourceManager.getString(R.string.sora_card_no_more_free_tries) + } + else -> { + null + } + } + } + + private fun onSoraCardStatusClicked() { + _launchSoraCardSignIn.value = Event( + SoraCardSignInContractData( + locale = Locale.ENGLISH, + apiKey = BuildConfig.SORA_CARD_API_KEY, + domain = BuildConfig.SORA_CARD_DOMAIN, + environment = when { + BuildConfig.DEBUG -> SoraCardEnvironmentType.TEST + else -> SoraCardEnvironmentType.PRODUCTION + }, + soraCardInfo = state.value.soraCardState?.soraCardInfo?.let { + SoraCardInfo( + accessToken = it.accessToken, + refreshToken = it.refreshToken, + accessTokenExpirationTime = it.accessTokenExpirationTime + ) + } + ) + ) + } + + fun updateSoraCardInfo( + accessToken: String, + refreshToken: String, + accessTokenExpirationTime: Long, + kycStatus: String + ) { + launch { + soraCardInteractor.updateSoraCardInfo( + accessToken, + refreshToken, + accessTokenExpirationTime, + kycStatus + ) + } + } } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletScreen.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletScreen.kt index 70dfdb11b1..03131d0b78 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletScreen.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletScreen.kt @@ -1,6 +1,8 @@ package jp.co.soramitsu.wallet.impl.presentation.balance.list +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -10,6 +12,7 @@ import androidx.compose.material.SwipeableState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import jp.co.soramitsu.common.compose.component.ActionItemType @@ -25,6 +28,8 @@ import jp.co.soramitsu.common.compose.component.SwipeState import jp.co.soramitsu.common.compose.theme.FearlessTheme import jp.co.soramitsu.common.compose.viewstate.AssetListItemViewState import jp.co.soramitsu.runtime.multiNetwork.chain.model.ChainId +import jp.co.soramitsu.soracard.impl.presentation.SoraCardItem +import jp.co.soramitsu.soracard.impl.presentation.SoraCardItemViewState import jp.co.soramitsu.wallet.impl.presentation.balance.list.model.AssetType import jp.co.soramitsu.wallet.impl.presentation.common.AssetsList import jp.co.soramitsu.wallet.impl.presentation.common.AssetsListInterface @@ -32,10 +37,13 @@ import jp.co.soramitsu.wallet.impl.presentation.common.AssetsListInterface interface WalletScreenInterface : AssetsListInterface { fun onAddressClick() fun onBalanceClicked() + fun soraCardClicked() + fun soraCardClose() fun onNetworkIssuesClicked() fun assetTypeChanged(type: AssetType) } +@OptIn(ExperimentalFoundationApi::class) @Composable fun WalletScreen( data: WalletState, @@ -63,11 +71,35 @@ fun WalletScreen( Modifier .fillMaxSize() .padding(bottom = 80.dp) + .pointerInput(Unit) { + detectTapGestures( + onPress = { /* Called when the gesture starts */ }, + onDoubleTap = { /* Called on Double Tap */ }, + onLongPress = { + throw RuntimeException("Test Crash in the NFT section") // Force a crash + /* Called on Long Press */ + }, + onTap = { /* Called on Tap */ } + ) + } ) } else { + val header: @Composable (() -> Unit)? = when { + data.soraCardState?.visible != true -> null + else -> { + { + SoraCardItem( + state = data.soraCardState, + onClose = callback::soraCardClose, + onClick = callback::soraCardClicked + ) + } + } + } AssetsList( data = data, - callback = callback + callback = callback, + header = header ) } } @@ -78,6 +110,8 @@ fun WalletScreen( private fun PreviewWalletScreen() { @OptIn(ExperimentalMaterialApi::class) val emptyCallback = object : WalletScreenInterface { + override fun soraCardClicked() {} + override fun soraCardClose() {} override fun onAddressClick() {} override fun onBalanceClicked() {} override fun onNetworkIssuesClicked() {} @@ -86,15 +120,38 @@ private fun PreviewWalletScreen() { override fun actionItemClicked(actionType: ActionItemType, chainId: ChainId, chainAssetId: String, swipeableState: SwipeableState) {} } + val assets: List = listOf( + AssetListItemViewState( + assetIconUrl = "", + assetChainName = "Chain", + assetSymbol = "SMB", + displayName = "Sora", + assetName = "Sora Asset", + assetTokenFiat = null, + assetTokenRate = null, + assetTransferableBalance = null, + assetTransferableBalanceFiat = null, + assetChainUrls = emptyMap(), + chainId = "", + chainAssetId = "", + isSupported = true, + isHidden = false, + hasAccount = true, + priceId = null, + hasNetworkIssue = false + ) + ) + FearlessTheme { Surface(Modifier.background(Color.Black)) { Column { WalletScreen( data = WalletState( multiToggleButtonState = MultiToggleButtonState(AssetType.Currencies, listOf(AssetType.Currencies, AssetType.NFTs)), - assets = emptyList(), + assets = assets, balance = AssetBalanceViewState("TRANSFERABLE BALANCE", "ADDRESS", true, ChangeBalanceViewState("+100%", "+50$")), - hasNetworkIssues = true + hasNetworkIssues = true, + soraCardState = SoraCardItemViewState(null, null, null, true) ), callback = emptyCallback ) diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletState.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletState.kt index 2ad15d7a29..6cb7b22672 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletState.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/balance/list/WalletState.kt @@ -4,6 +4,7 @@ import jp.co.soramitsu.common.compose.component.AssetBalanceViewState import jp.co.soramitsu.common.compose.component.ChangeBalanceViewState import jp.co.soramitsu.common.compose.component.MultiToggleButtonState import jp.co.soramitsu.common.compose.viewstate.AssetListItemViewState +import jp.co.soramitsu.soracard.impl.presentation.SoraCardItemViewState import jp.co.soramitsu.wallet.impl.presentation.balance.list.model.AssetType import jp.co.soramitsu.wallet.impl.presentation.common.AssetListState @@ -11,14 +12,16 @@ data class WalletState( override val assets: List, val multiToggleButtonState: MultiToggleButtonState, val balance: AssetBalanceViewState, - val hasNetworkIssues: Boolean + val hasNetworkIssues: Boolean, + val soraCardState: SoraCardItemViewState? ) : AssetListState(assets) { companion object { val default = WalletState( multiToggleButtonState = MultiToggleButtonState(AssetType.Currencies, listOf(AssetType.Currencies, AssetType.NFTs)), assets = emptyList(), balance = AssetBalanceViewState("", "", false, ChangeBalanceViewState("", "")), - hasNetworkIssues = false + hasNetworkIssues = false, + soraCardState = null ) } } diff --git a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/AssetsList.kt b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/AssetsList.kt index 18d2755273..ad8ac3d3d1 100644 --- a/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/AssetsList.kt +++ b/feature-wallet-impl/src/main/java/jp/co/soramitsu/wallet/impl/presentation/common/AssetsList.kt @@ -31,7 +31,8 @@ interface AssetsListInterface { @Composable fun AssetsList( data: AssetListState, - callback: AssetsListInterface + callback: AssetsListInterface, + header: (@Composable () -> Unit)? = null ) { val listState = rememberLazyListState(0) val isShowHidden = remember { mutableStateOf(false) } @@ -50,6 +51,9 @@ fun AssetsList( state = listState, verticalArrangement = Arrangement.spacedBy(8.dp) ) { + if (header != null) { + item { header() } + } items(data.visibleAssets, key = { it.key }) { assetState -> SwipeableAssetListItem( assetState = assetState, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c5b0880011..03c0b0e2aa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,23 @@ [versions] -dagger = "2.43" +dagger = "2.45" compose = "1.2.1" fragmentKtx = "1.5.2" composeThemeAdapter = "1.2.1" material = "1.6.1" coroutines = "1.6.4" +soraUi = "0.0.63" +soraCard = "0.0.30" +kotlinxSerializationjson = "1.4.1" +xNetworking = "0.0.55" [libraries] +sora-ui = { module = "jp.co.soramitsu:ui-core", version.ref = "soraUi" } +sora-card = { module = "jp.co.soramitsu:android-sora-card", version.ref = "soraCard" } + +xnetworking-android = { module = "jp.co.soramitsu:XNetworking-android", version.ref = "xNetworking" } + +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationjson" } + hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "dagger" } hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "dagger" } diff --git a/scripts/secrets.gradle b/scripts/secrets.gradle index b84f1fc285..ddc33eab05 100644 --- a/scripts/secrets.gradle +++ b/scripts/secrets.gradle @@ -8,9 +8,12 @@ if (localPropertiesFile.exists()) { ext.readSecret = { secretName -> def localPropSecret = localProperties.getProperty(secretName) - def secret = (localPropSecret != null) ? localPropSecret : System.getenv(secretName) + return secret +} +ext.readSecretInQuotes = { secretName -> + def secret = readSecret(secretName) return maybeWrapInQuotes(secret) } diff --git a/settings.gradle b/settings.gradle index 4680957025..53d722aecf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,3 +18,5 @@ include ':feature-polkaswap-api' include ':feature-polkaswap-impl' include ':feature-success-api' include ':feature-success-impl' +include ':feature-soracard-api' +include ':feature-soracard-impl'