From 012c2e02eb6828333c528d4a5e81a2513326cdfc Mon Sep 17 00:00:00 2001 From: polstianka Date: Fri, 4 Oct 2024 00:47:08 -0700 Subject: [PATCH] bug fixeds --- .../main/java/com/tonapps/wallet/api/API.kt | 2 +- .../wallet/data/account/AccountRepository.kt | 15 +- .../data/account/source/DatabaseSource.kt | 1 + .../wallet/data/backup/BackupRepository.kt | 18 ++ .../main/java/com/tonapps/tonkeeper/App.kt | 15 +- .../java/com/tonapps/tonkeeper/AppConfig.kt | 17 +- .../tonapps/tonkeeper/api/AccountEventWrap.kt | 30 +- .../tonkeeper/core/deeplink/DeepLink.kt | 84 ------ .../tonkeeper/core/history/HistoryHelper.kt | 8 + .../list/holder/HistoryActionHolder.kt | 16 +- .../tonapps/tonkeeper/deeplink/DeepLink.kt | 24 ++ .../tonkeeper/deeplink/DeepLinkRoute.kt | 195 +++++++++++++ .../tonapps/tonkeeper/extensions/Context.kt | 4 + .../com/tonapps/tonkeeper/koin/KoinModule.kt | 2 + .../tonkeeper/koin/viewModelWalletModule.kt | 3 + .../tonkeeper/manager/assets/AssetsManager.kt | 1 - .../tonkeeper/manager/push/PushManager.kt | 10 +- .../manager/tonconnect/TonConnectManager.kt | 6 +- ...ngTransaction.kt => SendingTransaction.kt} | 3 +- .../manager/tx/TransactionManager.kt | 99 ++----- .../java/com/tonapps/tonkeeper/os/Android.kt | 3 - .../tonapps/tonkeeper/ui/base/BaseWalletVM.kt | 56 ++++ .../ui/component/MainRecyclerView.kt | 1 + .../screen/backup/check/BackupCheckScreen.kt | 31 +- .../collectibles/CollectiblesViewModel.kt | 2 +- .../collectibles/list/holder/NftHolder.kt | 12 +- .../ui/screen/events/EventsScreen.kt | 34 ++- .../ui/screen/events/EventsUiState.kt | 12 + .../ui/screen/events/EventsViewModel.kt | 266 ++++++++--------- .../tonkeeper/ui/screen/init/InitEvent.kt | 1 - .../tonkeeper/ui/screen/init/InitScreen.kt | 1 - .../tonkeeper/ui/screen/init/InitViewModel.kt | 10 +- .../tonkeeper/ui/screen/nft/NftScreen.kt | 1 + .../ui/screen/phrase/PhraseScreen.kt | 8 +- .../screen/purchase/web/PurchaseWebScreen.kt | 7 + .../tonkeeper/ui/screen/root/RootActivity.kt | 26 +- .../tonkeeper/ui/screen/root/RootEvent.kt | 33 +-- .../tonkeeper/ui/screen/root/RootViewModel.kt | 276 ++++++++++-------- .../screen/settings/main/SettingsViewModel.kt | 14 +- .../staking/withdraw/StakeWithdrawScreen.kt | 144 +++++++++ .../withdraw/StakeWithdrawViewModel.kt | 231 +++++++++++++++ .../ui/screen/wallet/main/WalletViewModel.kt | 13 +- .../wallet/main/list/holder/StakedHolder.kt | 9 + .../wallet/main/list/holder/TokenHolder.kt | 12 +- .../main/res/layout/fragment_backup_check.xml | 77 +++-- .../res/layout/fragment_stake_withdraw.xml | 138 +++++++++ .../src/main/res/values/strings.xml | 5 + .../main/java/com/tonapps/extensions/Uri.kt | 26 +- .../main/java/com/tonapps/network/SSEvent.kt | 9 +- .../src/main/java/uikit/base/BaseFragment.kt | 1 + .../src/main/java/uikit/widget/PhraseWords.kt | 4 +- .../src/main/java/uikit/widget/ToastView.kt | 41 ++- 52 files changed, 1458 insertions(+), 599 deletions(-) delete mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/deeplink/DeepLink.kt create mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/deeplink/DeepLink.kt create mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/deeplink/DeepLinkRoute.kt rename apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tx/{PendingTransaction.kt => SendingTransaction.kt} (89%) create mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsUiState.kt create mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/staking/withdraw/StakeWithdrawScreen.kt create mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/staking/withdraw/StakeWithdrawViewModel.kt create mode 100644 apps/wallet/instance/app/src/main/res/layout/fragment_stake_withdraw.xml diff --git a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/API.kt b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/API.kt index 886bf8cb3..825d918e2 100644 --- a/apps/wallet/api/src/main/java/com/tonapps/wallet/api/API.kt +++ b/apps/wallet/api/src/main/java/com/tonapps/wallet/api/API.kt @@ -226,7 +226,7 @@ class API( fun realtime(accountId: String, testnet: Boolean): Flow { val endpoint = if (testnet) config.tonapiSSETestnetEndpoint else config.tonapiSSEEndpoint - val url = "$endpoint/sse/transactions?account=$accountId" + val url = "$endpoint/sse/traces?account=$accountId" return tonAPIHttpClient.sse(url) } diff --git a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/AccountRepository.kt b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/AccountRepository.kt index 9d288ae61..e6a2932e6 100644 --- a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/AccountRepository.kt +++ b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/AccountRepository.kt @@ -1,6 +1,7 @@ package com.tonapps.wallet.data.account import android.content.Context +import android.util.Log import com.tonapps.blockchain.ton.contract.BaseWalletContract import com.tonapps.blockchain.ton.contract.WalletVersion import com.tonapps.blockchain.ton.contract.walletVersion @@ -397,7 +398,7 @@ class AccountRepository( return } - val entity = database.getAccount(id) + val entity = database.getAccount(id) ?: database.getAccounts().firstOrNull() if (entity == null) { setSelectedWallet(null) } else { @@ -486,16 +487,4 @@ class AccountRepository( transfers = transfers ) } - - /*suspend fun createSignedMessage( - wallet: WalletEntity, - seqno: Int, - privateKeyEd25519: PrivateKeyEd25519 = EmptyPrivateKeyEd25519, - validUntil: Long, - transfers: List, - internalMessage: Boolean = false, - ): Cell { - val data = messageBody(wallet, seqno, validUntil, transfers, internalMessage) - return wallet.sign(privateKeyEd25519, data.seqNo, data.body) - }*/ } \ No newline at end of file diff --git a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/source/DatabaseSource.kt b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/source/DatabaseSource.kt index ef1aac911..f78e1ad37 100644 --- a/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/source/DatabaseSource.kt +++ b/apps/wallet/data/account/src/main/java/com/tonapps/wallet/data/account/source/DatabaseSource.kt @@ -5,6 +5,7 @@ import android.content.Context import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper +import android.util.Log import androidx.core.database.getStringOrNull import com.tonapps.blockchain.ton.contract.walletVersion import com.tonapps.extensions.toByteArray diff --git a/apps/wallet/data/backup/src/main/java/com/tonapps/wallet/data/backup/BackupRepository.kt b/apps/wallet/data/backup/src/main/java/com/tonapps/wallet/data/backup/BackupRepository.kt index 78d6f876f..acff20258 100644 --- a/apps/wallet/data/backup/src/main/java/com/tonapps/wallet/data/backup/BackupRepository.kt +++ b/apps/wallet/data/backup/src/main/java/com/tonapps/wallet/data/backup/BackupRepository.kt @@ -79,4 +79,22 @@ class BackupRepository( backportToRN(_stream.value) return entity } + + fun addBackups( + walletIds: List, + source: BackupEntity.Source = BackupEntity.Source.LOCAL, + date: Long = System.currentTimeMillis() + ) { + for (walletId in walletIds) { + addBackup(walletId, source, date) + } + } + + fun addBackupsAsync( + walletIds: List, + source: BackupEntity.Source = BackupEntity.Source.LOCAL, + date: Long = System.currentTimeMillis() + ) { + scope.launch(Dispatchers.IO) { addBackups(walletIds, source, date) } + } } \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/App.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/App.kt index 9f985bd2d..6c0ad466d 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/App.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/App.kt @@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.camera.camera2.Camera2Config import androidx.camera.core.CameraXConfig import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.imagepipeline.core.DownsampleMode import com.facebook.imagepipeline.core.ImagePipelineConfig import com.facebook.imagepipeline.core.ImageTranscoderType import com.facebook.imagepipeline.core.MemoryChunkType @@ -88,7 +89,9 @@ class App: Application(), CameraXConfig.Provider, KoinComponent { configBuilder.setMemoryChunkType(MemoryChunkType.BUFFER_MEMORY) configBuilder.setImageTranscoderType(ImageTranscoderType.JAVA_TRANSCODER) configBuilder.experiment().setNativeCodeDisabled(true) - configBuilder.setDownsampleEnabled(false) + configBuilder.experiment().setUseDownsampligRatioForResizing(true) + configBuilder.experiment().useBitmapPrepareToDraw = true + configBuilder.setDownsampleMode(DownsampleMode.ALWAYS) Fresco.initialize(this, configBuilder.build()) } @@ -98,14 +101,4 @@ class App: Application(), CameraXConfig.Provider, KoinComponent { .fromConfig(Camera2Config.defaultConfig()) .setMinimumLoggingLevel(Log.ERROR).build() } - - fun isOriginalAppInstalled(): Boolean { - val pm = packageManager - return try { - pm.getPackageInfo("com.ton_keeper", 0) - true - } catch (e: Exception) { - false - } - } } \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/AppConfig.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/AppConfig.kt index 3c84ea4e0..dda081b02 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/AppConfig.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/AppConfig.kt @@ -2,22 +2,19 @@ package com.tonapps.tonkeeper import android.app.ActivityManager import android.content.Context -import android.os.Build -import com.tonapps.tonkeeper.koin.remoteConfig -import com.tonapps.tonkeeper.os.isMediatek -import com.tonapps.wallet.api.entity.FlagsEntity +import android.os.BatteryManager -val Context.featureFlags: FlagsEntity - get() = this.remoteConfig?.flags ?: FlagsEntity() +val Context.batteryLevel: Int + get() { + val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager + return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) + } val Context.isLowDevice: Boolean get() { - if (isMediatek()) { - return true - } val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager return activityManager.isLowRamDevice } val Context.isBlurDisabled: Boolean - get() = isLowDevice || featureFlags.disableBlur || (Build.VERSION_CODES.S > Build.VERSION.SDK_INT && featureFlags.disableLegacyBlur) \ No newline at end of file + get() = isLowDevice && 20 >= batteryLevel \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/api/AccountEventWrap.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/api/AccountEventWrap.kt index 0cff3bb5c..0330e895c 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/api/AccountEventWrap.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/api/AccountEventWrap.kt @@ -1,5 +1,6 @@ package com.tonapps.tonkeeper.api +import com.tonapps.wallet.api.entity.AccountEventEntity import io.tonapi.models.AccountEvent data class AccountEventWrap( @@ -8,15 +9,42 @@ data class AccountEventWrap( val eventIds: List, ) { + companion object { + + fun cached(event: AccountEvent): AccountEventWrap { + return AccountEventWrap(event, cached = true, eventIds = listOf(event.eventId)) + } + + fun cached(event: AccountEventEntity): AccountEventWrap { + return cached(event.body) + } + } + val eventId: String get() = event.eventId val timestamp: Long - get() = event.timestamp + get() = if (inProgress) { + System.currentTimeMillis() + } else { + event.timestamp + } val lt: Long get() = event.lt val inProgress: Boolean get() = event.inProgress + + constructor(event: AccountEvent) : this( + event = event, + cached = false, + eventIds = listOf(event.eventId) + ) + + constructor(event: AccountEventEntity) : this( + event = event.body, + cached = false, + eventIds = listOf(event.body.eventId) + ) } \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/deeplink/DeepLink.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/deeplink/DeepLink.kt deleted file mode 100644 index 5fa8f3d1e..000000000 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/deeplink/DeepLink.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tonapps.tonkeeper.core.deeplink - -import android.content.Context -import android.net.Uri -import android.util.Log -import androidx.core.net.toUri -import com.tonapps.tonkeeperx.R -import com.tonapps.tonkeeper.ui.screen.root.RootActivity -import com.tonapps.tonkeeper.ui.screen.main.MainScreen -import uikit.extensions.activity -import uikit.extensions.findFragment - -object DeepLink { - - const val TON_SCHEME = "ton" - const val TONKEEPER_SCHEME = "tonkeeper" - const val TONCONNECT_SCHEME = "tc" - - const val TONKEEPER_HOST = "app.tonkeeper.com" - - private val supportedSchemes = listOf(TON_SCHEME, TONKEEPER_SCHEME, TONCONNECT_SCHEME) - private val supportedHosts = listOf(TONKEEPER_HOST) - - fun isSupportedUri(uri: Uri): Boolean { - return supportedSchemes.contains(uri.scheme) || supportedHosts.contains(uri.host) - } - - fun fixDeepLink(uri: Uri): Uri { - return uri.toString() - .replace("ton://", "https://app.tonkeeper.com/") - .replace("tonkeeper://", "https://app.tonkeeper.com/") - .toUri() - } - - fun isSupportedUrl(url: String?): Boolean { - if (url == null) { - return false - } - return isSupportedUri(Uri.parse(url)) - } - - fun isSupportedScheme(url: String?): Boolean { - if (url == null) { - return false - } - return supportedSchemes.any { url.startsWith("$it://") } - } - - fun getTonkeeperUriFirstPath(uri: Uri): String { - val pathSegments = uri.pathSegments.toMutableList() - if (uri.scheme == TONKEEPER_SCHEME) { - pathSegments.add(0, uri.host!!) - } - if (pathSegments.isEmpty()) { - return "" - } - return pathSegments.first() - } - - fun isTonkeeperUri(uri: Uri): Boolean { - return uri.host == TONKEEPER_HOST || uri.scheme == TONKEEPER_SCHEME - } - - fun isTonConnectUri(uri: Uri): Boolean { - if (uri.scheme == TONCONNECT_SCHEME) { - return true - } - return getTonkeeperUriFirstPath(uri) == "ton-connect" - } - - data class Transfer( - val address: String, - val amount: Long?, - val text: String? - ) { - - constructor(uri: Uri) : this( - address = uri.host ?: "", - amount = uri.getQueryParameter("amount")?.toLongOrNull(), - text = uri.getQueryParameter("text") - ) - } - -} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/HistoryHelper.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/HistoryHelper.kt index 8b761cf88..dad4a5b9e 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/HistoryHelper.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/HistoryHelper.kt @@ -270,6 +270,14 @@ class HistoryHelper( ) } + fun transform(loading: Boolean, items: List): List { + return if (loading) { + withLoadingItem(items) + } else { + removeLoadingItem(items) + } + } + fun withLoadingItem(items: List): List { val last = items.lastOrNull() if (last is HistoryItem.Loader) { diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/list/holder/HistoryActionHolder.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/list/holder/HistoryActionHolder.kt index 8e442d3f3..6946865d4 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/list/holder/HistoryActionHolder.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/history/list/holder/HistoryActionHolder.kt @@ -7,6 +7,7 @@ import android.view.View import android.view.ViewGroup import androidx.annotation.ColorInt import androidx.appcompat.widget.AppCompatTextView +import com.facebook.imagepipeline.common.ResizeOptions import com.facebook.imagepipeline.postprocessors.BlurPostProcessor import com.facebook.imagepipeline.request.ImageRequestBuilder import com.tonapps.extensions.logError @@ -125,7 +126,10 @@ class HistoryActionHolder( private fun loadIcon(uri: Uri) { iconView.imageTintList = null - iconView.setImageURI(uri, this) + + val builder = ImageRequestBuilder.newBuilderWithSource(uri) + builder.resizeOptions = ResizeOptions.forSquareSize(128) + iconView.setImageRequest(builder.build()) } private fun bindPending(pending: Boolean) { @@ -218,14 +222,12 @@ class HistoryActionHolder( } private fun loadNftImage(uri: Uri, blur: Boolean) { + val builder = ImageRequestBuilder.newBuilderWithSource(uri) + builder.resizeOptions = ResizeOptions.forSquareSize(320) if (blur) { - val request = ImageRequestBuilder.newBuilderWithSource(uri) - .setPostprocessor(BlurPostProcessor(25, context, 3)) - .build() - nftIconView.setImageRequest(request) - } else { - nftIconView.setImageURI(uri, null) + builder.setPostprocessor(BlurPostProcessor(25, context, 3)) } + nftIconView.setImageRequest(builder.build()) } @ColorInt diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/deeplink/DeepLink.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/deeplink/DeepLink.kt new file mode 100644 index 000000000..a395e3bb0 --- /dev/null +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/deeplink/DeepLink.kt @@ -0,0 +1,24 @@ +package com.tonapps.tonkeeper.deeplink + +import android.net.Uri + +data class DeepLink( + val route: DeepLinkRoute, + val fromQR: Boolean, + val referrer: Uri?, +) { + + val isUnknown: Boolean + get() = route is DeepLinkRoute.Unknown + + constructor( + uri: Uri, + fromQR: Boolean, + referrer: Uri? + ): this( + route = DeepLinkRoute.resolve(uri), + fromQR = fromQR, + referrer = referrer + ) + +} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/deeplink/DeepLinkRoute.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/deeplink/DeepLinkRoute.kt new file mode 100644 index 000000000..46a3f3ce6 --- /dev/null +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/deeplink/DeepLinkRoute.kt @@ -0,0 +1,195 @@ +package com.tonapps.tonkeeper.deeplink + +import android.net.Uri +import androidx.core.net.toUri +import com.tonapps.blockchain.ton.extensions.safePublicKey +import com.tonapps.extensions.hostOrNull +import com.tonapps.extensions.pathOrNull +import com.tonapps.extensions.query +import com.tonapps.extensions.queryBoolean +import com.tonapps.extensions.queryPositiveLong +import org.ton.api.pub.PublicKeyEd25519 + +sealed class DeepLinkRoute { + + data class Unknown(val uri: Uri): DeepLinkRoute() + + sealed class Tabs(val tabUri: String): DeepLinkRoute() { + data object Main: Tabs("tonkeeper://wallet") + data object Activity: Tabs("tonkeeper://activity") + data object Browser: Tabs("tonkeeper://browser") + data object Collectibles: Tabs("tonkeeper://collectibles") + } + + data object Backups: DeepLinkRoute() + data object Staking: DeepLinkRoute() + data object Purchase: DeepLinkRoute() + data object Send: DeepLinkRoute() + data object Settings: DeepLinkRoute() + data object SettingsSecurity: DeepLinkRoute() + data object SettingsCurrency: DeepLinkRoute() + data object SettingsLanguage: DeepLinkRoute() + data object SettingsNotifications: DeepLinkRoute() + data object EditWalletLabel: DeepLinkRoute() + data object Camera: DeepLinkRoute() + data object Receive: DeepLinkRoute() + data object ManageAssets: DeepLinkRoute() + data object WalletPicker: DeepLinkRoute() + + data class StakingPool(val poolAddress: String): DeepLinkRoute() { + + constructor(uri: Uri) : this( + poolAddress = uri.pathOrNull ?: throw IllegalArgumentException("Pool address is required") + ) + } + + data class Swap( + val from: String, + val to: String? + ): DeepLinkRoute() { + + constructor(uri: Uri) : this( + from = uri.query("ft") ?: "TON", + to = uri.query("tt") + ) + } + + data class Transfer( + val exp: Long, + val address: String, + val amount: Long?, + val text: String?, + val jettonAddress: String?, + val bin: String? + ): DeepLinkRoute() { + + val isExpired: Boolean + get() = exp > 0 && exp < (System.currentTimeMillis() / 1000) + + constructor(uri: Uri) : this( + exp = uri.queryPositiveLong("exp") ?: 0, + address = uri.pathOrNull ?: throw IllegalArgumentException("Address is required"), + amount = uri.queryPositiveLong("amount"), + text = uri.query("text"), + jettonAddress = uri.query("jettonAddress"), + bin = uri.query("bin") + ) + } + + + data class PickWallet(val walletId: String): DeepLinkRoute() { + + constructor(uri: Uri) : this( + walletId = uri.pathOrNull ?: throw IllegalArgumentException("Wallet id is required") + ) + } + + data class Battery(val promocode: String?): DeepLinkRoute() { + + constructor(uri: Uri) : this( + promocode = uri.query("promocode") + ) + } + + data class AccountEvent( + val eventId: String, + val address: String? + ): DeepLinkRoute() { + + constructor(uri: Uri) : this( + eventId = uri.pathOrNull ?: throw IllegalArgumentException("Event id is required"), + address = uri.query("address") + ) + } + + data class Exchange(val methodName: String): DeepLinkRoute() { + + constructor(uri: Uri) : this( + methodName = uri.pathOrNull ?: throw IllegalArgumentException("Method name is required") + ) + } + + data class DApp(val url: String): DeepLinkRoute() { + + constructor(uri: Uri) : this( + url = uri.pathOrNull?.let { + "https://$it" + } ?: throw IllegalArgumentException("DApp url is required") + ) + } + + data class Signer( + val publicKey: PublicKeyEd25519, + val name: String?, + val local: Boolean, + ): DeepLinkRoute() { + + constructor(uri: Uri) : this( + publicKey = uri.query("pk")?.safePublicKey() ?: throw IllegalArgumentException("Public key is required"), + name = uri.query("name"), + local = uri.queryBoolean("local") + ) + } + + data class TonConnect(val uri: Uri): DeepLinkRoute() + + companion object { + + private const val PREFIX = "tonkeeper://" + + fun resolve(input: Uri): DeepLinkRoute { + val uri = normalize(input) + val domain = uri.hostOrNull ?: return Unknown(uri) + try { + return when (domain) { + "backup" -> Backups + "staking" -> Staking + "buy-ton" -> Purchase + "send" -> Send + "wallet", "main" -> Tabs.Main + "activity", "history" -> Tabs.Activity + "browser" -> Tabs.Browser + "collectibles" -> Tabs.Collectibles + "settings" -> Settings + "pool" -> StakingPool(uri) + "swap" -> Swap(uri) + "transfer" -> Transfer(uri) + "pick" -> PickWallet(uri) + "battery" -> Battery(uri) + "action" -> AccountEvent(uri) + "exchange" -> try { + Exchange(uri) + } catch (e: Throwable) { + Purchase + } + "dapp" -> DApp(uri) + "ton-connect" -> TonConnect(uri) + "signer" -> Signer(uri) + "security" -> SettingsSecurity + "currency" -> SettingsCurrency + "language" -> SettingsLanguage + "notifications", "push" -> SettingsNotifications + "edit", "customization" -> EditWalletLabel + "camera", "scan", "scanner" -> Camera + "qr", "receive" -> Receive + "manage" -> ManageAssets + "picker", "wallets" -> WalletPicker + else -> throw IllegalArgumentException("Unknown domain: $domain") + } + } catch (e: Throwable) { + return Unknown(uri) + } + } + + private fun normalize(uri: Uri): Uri { + return uri.toString() + .replace("ton://", PREFIX) + .replace("https://app.tonkeeper.com/", PREFIX) + .replace("http://app.tonkeeper.com/", PREFIX) + .replace("app://tonkeeper.com/", PREFIX) + .replace("tc://", "${PREFIX}/ton-connect") + .replace("///", "//") + .toUri() + } + } +} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/Context.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/Context.kt index 118f94cd3..8eb4ea843 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/Context.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/Context.kt @@ -45,6 +45,10 @@ fun Context.showToast(@StringRes resId: Int) { navigation?.toast(resId) } +fun Context.showToast(test: String) { + navigation?.toast(test) +} + fun Context.copyWithToast(text: String, color: Int = backgroundContentTintColor) { navigation?.toast(getString(Localization.copied), color) copyToClipboard(text) diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/KoinModule.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/KoinModule.kt index c69cf4886..b005db3b3 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/KoinModule.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/KoinModule.kt @@ -26,6 +26,7 @@ import com.tonapps.tonkeeper.ui.screen.wallet.picker.PickerViewModel import com.tonapps.tonkeeper.ui.screen.settings.passcode.ChangePasscodeViewModel import com.tonapps.tonkeeper.ui.screen.settings.security.SecurityViewModel import com.tonapps.tonkeeper.ui.screen.settings.theme.ThemeViewModel +import com.tonapps.tonkeeper.ui.screen.staking.withdraw.StakeWithdrawViewModel import com.tonapps.tonkeeper.ui.screen.w5.stories.W5StoriesViewModel import com.tonapps.tonkeeper.ui.screen.tonconnect.TonConnectViewModel import com.tonapps.tonkeeper.ui.screen.wallet.main.list.WalletAdapter @@ -35,6 +36,7 @@ import com.tonapps.wallet.data.settings.SettingsRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.singleOf diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/viewModelWalletModule.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/viewModelWalletModule.kt index fcd13a4a3..09114d356 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/viewModelWalletModule.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/viewModelWalletModule.kt @@ -27,6 +27,8 @@ import com.tonapps.tonkeeper.ui.screen.staking.viewer.StakeViewerViewModel import com.tonapps.tonkeeper.ui.screen.staking.unstake.UnStakeViewModel import com.tonapps.tonkeeper.ui.screen.staking.stake.StakingViewModel import com.tonapps.tonkeeper.ui.screen.send.transaction.SendTransactionViewModel +import com.tonapps.tonkeeper.ui.screen.staking.withdraw.StakeWithdrawViewModel +import org.koin.androidx.viewmodel.dsl.viewModel val viewModelWalletModule = module { viewModelOf(::WalletViewModel) @@ -54,4 +56,5 @@ val viewModelWalletModule = module { viewModelOf(::UnStakeViewModel) viewModelOf(::StakingViewModel) viewModelOf(::SendTransactionViewModel) + viewModelOf(::StakeWithdrawViewModel) } \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/assets/AssetsManager.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/assets/AssetsManager.kt index d6f5cbb2e..41d936cff 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/assets/AssetsManager.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/assets/AssetsManager.kt @@ -41,7 +41,6 @@ class AssetsManager( ): List? { val tokens = getTokens(wallet, currency, refresh) val staked = getStaked(wallet, tokens.map { it.token }, currency, refresh) - Log.d("TokenLogNew", "staked: $staked") val filteredTokens = tokens.filter { !it.token.isLiquid } val list = (filteredTokens + staked).sortedBy { it.fiat }.reversed() if (list.isEmpty()) { diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/push/PushManager.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/push/PushManager.kt index ca2157aa7..5cb8d319b 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/push/PushManager.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/push/PushManager.kt @@ -40,6 +40,10 @@ class PushManager( scope.launch { wallet(wallet, state) } } + fun walletsAsync(wallets: List, state: State) { + scope.launch { wallets(wallets, state) } + } + suspend fun wallet(wallet: WalletEntity, state: State) = wallets(listOf(wallet), state) suspend fun wallets(wallets: List, state: State): Boolean = withContext(Dispatchers.IO) { @@ -54,6 +58,11 @@ class PushManager( if (wallets.isEmpty()) { return true } + + for (wallet in wallets) { + settingsRepository.setPushWallet(wallet.id, true) + } + val firebaseToken = getFirebaseToken() ?: return false val accounts = wallets.map { it.accountId } val successful = api.pushSubscribe( @@ -64,7 +73,6 @@ class PushManager( ) if (successful) { for (wallet in wallets) { - settingsRepository.setPushWallet(wallet.id, true) val apps = dAppsRepository.getConnections(wallet.accountId, wallet.testnet) for ((app, connections) in apps) { dAppPush( diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tonconnect/TonConnectManager.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tonconnect/TonConnectManager.kt index f9a001dc4..140216ee4 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tonconnect/TonConnectManager.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tonconnect/TonConnectManager.kt @@ -10,12 +10,11 @@ import com.tonapps.blockchain.ton.proof.TONProof import com.tonapps.extensions.appVersionName import com.tonapps.extensions.filterList import com.tonapps.extensions.flat -import com.tonapps.extensions.getQueryLong import com.tonapps.extensions.hasQuery import com.tonapps.extensions.mapList import com.tonapps.network.simple import com.tonapps.security.CryptoBox -import com.tonapps.tonkeeper.extensions.toast +import com.tonapps.tonkeeper.extensions.showToast import com.tonapps.tonkeeper.manager.push.PushManager import com.tonapps.tonkeeper.manager.tonconnect.bridge.Bridge import com.tonapps.tonkeeper.manager.tonconnect.bridge.JsonBuilder @@ -41,7 +40,6 @@ import kotlinx.coroutines.withContext import org.json.JSONObject import uikit.extensions.activity import uikit.extensions.addForResult -import uikit.navigation.Navigation.Companion.navigation import uikit.navigation.NavigationActivity import java.util.concurrent.CancellationException @@ -189,7 +187,7 @@ class TonConnectManager( return null } catch (e: Exception) { if (!uri.hasQuery("open") && !uri.hasQuery("ret")) { - context.navigation?.toast(Localization.invalid_link) + context.showToast(Localization.invalid_link) return null } else { return returnUri diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tx/PendingTransaction.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tx/SendingTransaction.kt similarity index 89% rename from apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tx/PendingTransaction.kt rename to apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tx/SendingTransaction.kt index 860565b77..1e1406853 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tx/PendingTransaction.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tx/SendingTransaction.kt @@ -1,12 +1,11 @@ package com.tonapps.tonkeeper.manager.tx -import android.util.Log import com.tonapps.blockchain.ton.extensions.parseCell import com.tonapps.security.hex import com.tonapps.wallet.data.account.entities.WalletEntity import org.ton.cell.Cell -data class PendingTransaction( +data class SendingTransaction( val wallet: WalletEntity, val boc: Cell, val timestamp: Long = System.currentTimeMillis() diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tx/TransactionManager.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tx/TransactionManager.kt index f11cf46e7..2dae567b7 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tx/TransactionManager.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/manager/tx/TransactionManager.kt @@ -1,6 +1,7 @@ package com.tonapps.tonkeeper.manager.tx import com.tonapps.blockchain.ton.extensions.base64 +import com.tonapps.extensions.MutableEffectFlow import com.tonapps.extensions.join import com.tonapps.wallet.api.API import com.tonapps.wallet.api.SendBlockchainState @@ -10,27 +11,24 @@ import com.tonapps.wallet.data.account.entities.WalletEntity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn -import kotlinx.coroutines.flow.timeout -import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import org.ton.cell.Cell -import java.util.concurrent.ConcurrentHashMap -import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @OptIn(ExperimentalCoroutinesApi::class) @@ -39,70 +37,34 @@ class TransactionManager( private val api: API ) { - private val eventFlowMap = ConcurrentHashMap>() private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - private val _sentTransactionFlow = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - private val sentTransactionFlow = _sentTransactionFlow.shareIn(scope, SharingStarted.Eagerly, 1) - fun getEventsFlow( - wallet: WalletEntity - ): Flow { - val key = eventFlowMapKey(wallet) - return eventFlowMap.getOrPut(key) { - createEventsFlow(wallet) - } + private val _sendingTransactionFlow = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private val sendingTransactionFlow = _sendingTransactionFlow.asSharedFlow() + + private val _transactionFlow = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private val transactionFlow = _transactionFlow.asSharedFlow() + + init { + sendingTransactionFlow.mapNotNull { getTransaction(it.wallet, it.hash) }.onEach { transaction -> + _transactionFlow.tryEmit(transaction) + }.launchIn(scope) + + accountRepository.selectedWalletFlow.flatMapLatest { wallet -> + realtime(wallet) + }.filterNotNull().onEach { transaction -> + _transactionFlow.tryEmit(transaction) + }.launchIn(scope) } - private fun createEventsFlow( - wallet: WalletEntity - ) = join( - pendingEventsFlow(wallet), - realtime(wallet) - ).distinctUntilChanged { old, new -> - old.pending == new.pending && old.lt == new.lt - }.flowOn(Dispatchers.IO).shareIn(scope, SharingStarted.Eagerly, 1) + fun eventsFlow(wallet: WalletEntity) = transactionFlow.filter { + it.accountId == wallet.accountId && it.testnet == wallet.testnet + } private fun realtime(wallet: WalletEntity) = api.realtime( accountId = wallet.accountId, testnet = wallet.testnet - ).map { - TransactionEvent(it.json) - }.flatMapLatest { - transactionFlow(wallet, it.hash) - } - - private fun pendingEventsFlow( - wallet: WalletEntity - ): Flow = sentTransactionFlow.filter { - it.wallet.id == wallet.id - }.flatMapLatest { transactionFlow(it.wallet, it.hash) } - - @OptIn(FlowPreview::class) - private fun transactionFlow( - wallet: WalletEntity, - hash: String, - ): Flow = flow { - val initialTx = getTransaction(wallet, hash) ?: return@flow - emit(initialTx) - - if (initialTx.pending) { - delay(25.seconds) - while (currentCoroutineContext().isActive) { - val finalTx = getTransaction(wallet, hash) - if (finalTx == null || finalTx.pending) { - delay(10.seconds) - continue - } - finalTx.addEventId(hash) - finalTx.addEventId(initialTx.body.eventId) - emit(finalTx) - break - } - } - }.timeout(2.minutes) + ).map { it.data }.map { getTransaction(wallet, it) } private suspend fun getTransaction( wallet: WalletEntity, @@ -137,7 +99,7 @@ class TransactionManager( api.sendToBlockchain(boc, wallet.testnet) } if (state == SendBlockchainState.SUCCESS) { - _sentTransactionFlow.tryEmit(PendingTransaction(wallet.copy(), boc)) + _sendingTransactionFlow.tryEmit(SendingTransaction(wallet.copy(), boc)) return state } @@ -154,11 +116,4 @@ class TransactionManager( boc: Cell, withBattery: Boolean ) = send(wallet, boc.base64(), withBattery) - - private fun eventFlowMapKey(wallet: WalletEntity): String { - if (wallet.testnet) { - return wallet.accountId + "_testnet" - } - return wallet.accountId - } } \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/os/Android.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/os/Android.kt index a7c98883f..8951e92b9 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/os/Android.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/os/Android.kt @@ -1,5 +1,2 @@ package com.tonapps.tonkeeper.os -import android.os.Build - -fun isMediatek() = Build.HARDWARE?.startsWith("mt", ignoreCase = true) ?: false \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/base/BaseWalletVM.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/base/BaseWalletVM.kt index 3ad085d53..bf001eb1c 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/base/BaseWalletVM.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/base/BaseWalletVM.kt @@ -2,17 +2,24 @@ package com.tonapps.tonkeeper.ui.base import android.app.Application import android.content.Context +import android.os.Handler import android.util.Log +import androidx.annotation.StringRes import androidx.annotation.UiThread import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import com.tonapps.extensions.bestMessage import com.tonapps.extensions.isUIThread +import com.tonapps.tonkeeper.extensions.showToast import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.observeOn import kotlinx.coroutines.flow.onEach @@ -42,6 +49,13 @@ abstract class BaseWalletVM( val context: Context get() = holder?.uiContext ?: getApplication() + private val uiHandler: Handler by lazy { + Handler(context.mainLooper) + } + + private val navigation: Navigation? + get() = Navigation.from(context) + fun attachHolder(holder: Holder) { holderRef = WeakReference(holder) } @@ -54,6 +68,18 @@ abstract class BaseWalletVM( this.onEach { action(it) }.launch() } + fun Flow.safeCollectFlow(action: suspend (T) -> Unit) { + this.flowOn(Dispatchers.IO).onEach { action(it) }.catch { + toast(it.bestMessage) + }.launch() + } + + fun Flow.safeCollectFlowAtPost(action: (T) -> Unit) { + this.safeCollectFlow { + post { action(it) } + } + } + fun detachHolder() { holderRef?.clear() holderRef = null @@ -76,4 +102,34 @@ abstract class BaseWalletVM( throw IllegalStateException("finish() must be called from UI thread") } } + + fun post(action: () -> Unit) { + uiHandler.post(action) + } + + fun postDelayed(delay: Long, runnable: Runnable) { + uiHandler.postDelayed(runnable, delay) + } + + fun cancelPost(runnable: Runnable) { + uiHandler.removeCallbacks(runnable) + } + + fun toast(@StringRes resId: Int) { + post { + context.showToast(resId) + } + } + + fun toast(text: String) { + post { + context.showToast(text) + } + } + + fun openScreen(screen: BaseFragment) { + post { + navigation?.add(screen) + } + } } \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/component/MainRecyclerView.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/component/MainRecyclerView.kt index bc8e61e2d..b8c4ce7d2 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/component/MainRecyclerView.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/component/MainRecyclerView.kt @@ -3,6 +3,7 @@ package com.tonapps.tonkeeper.ui.component import android.content.Context import android.graphics.Canvas import android.util.AttributeSet +import android.util.Log import android.view.WindowInsets import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/backup/check/BackupCheckScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/backup/check/BackupCheckScreen.kt index 35628b3e0..162e72dc3 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/backup/check/BackupCheckScreen.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/backup/check/BackupCheckScreen.kt @@ -1,8 +1,11 @@ package com.tonapps.tonkeeper.ui.screen.backup.check import android.os.Bundle +import android.util.Log import android.view.View import android.widget.Button +import androidx.core.view.updatePadding +import androidx.core.widget.NestedScrollView import androidx.lifecycle.lifecycleScope import com.tonapps.tonkeeper.koin.walletViewModel import com.tonapps.tonkeeper.ui.base.BaseWalletScreen @@ -17,7 +20,10 @@ import kotlinx.coroutines.flow.onEach import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import uikit.base.BaseFragment +import uikit.extensions.doKeyboardAnimation import uikit.extensions.pinToBottomInsets +import uikit.extensions.scrollDown +import uikit.extensions.scrollView import uikit.widget.HeaderView import uikit.widget.TextHeaderView import uikit.widget.WordInput @@ -35,6 +41,7 @@ class BackupCheckScreen(wallet: WalletEntity): WalletContextScreen(R.layout.frag private lateinit var button: Button private lateinit var wordInputs: List + private lateinit var scrollView: NestedScrollView override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -43,6 +50,8 @@ class BackupCheckScreen(wallet: WalletEntity): WalletContextScreen(R.layout.frag val textView = view.findViewById(R.id.text) textView.desciption = getString(Localization.backup_check_subtitle, indexes[0] + 1, indexes[1] + 1, indexes[2] + 1) + scrollView = view.findViewById(R.id.scroll) + wordInputs = listOf( view.findViewById(R.id.word_input_1), view.findViewById(R.id.word_input_2), @@ -66,17 +75,35 @@ class BackupCheckScreen(wallet: WalletEntity): WalletContextScreen(R.layout.frag } } wordInput.doOnTextChanged = { checkEnableButton() } - wordInput.doOnFocus = { checkWords() } + wordInput.doOnFocus = { focus -> + if (focus) { + updateScroll(wordInput) + } + checkWords() + } } button = view.findViewById(R.id.done) button.isEnabled = false - button.pinToBottomInsets() button.setOnClickListener { saveBackup() } + scrollView.doKeyboardAnimation { offset, progress, _ -> + scrollView.updatePadding(bottom = offset) + button.translationY = -offset.toFloat() + if (progress >= .9f || .1f >= progress) { + getCurrentFocus()?.let { updateScroll(it) } + } + } + wordInputs.first().focus(true) } + private fun updateScroll(view: View) { + scrollView.postOnAnimation { + scrollView.scrollView(view) + } + } + private fun saveBackup() { if (button.isEnabled) { viewModel.saveBackup(args.backupId) { finish() } diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/collectibles/CollectiblesViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/collectibles/CollectiblesViewModel.kt index 405216dbb..0c90d10d8 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/collectibles/CollectiblesViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/collectibles/CollectiblesViewModel.kt @@ -37,7 +37,7 @@ class CollectiblesViewModel( private val transactionManager: TransactionManager, ): BaseWalletVM(app) { - private val ltFlow = transactionManager.getEventsFlow(wallet).stateIn(viewModelScope, SharingStarted.Eagerly, 0L) + private val ltFlow = transactionManager.eventsFlow(wallet).stateIn(viewModelScope, SharingStarted.Eagerly, 0L) val uiListStateFlow = combine( networkMonitor.isOnlineFlow, diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/collectibles/list/holder/NftHolder.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/collectibles/list/holder/NftHolder.kt index ce850cc0f..4a9142b88 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/collectibles/list/holder/NftHolder.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/collectibles/list/holder/NftHolder.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatTextView import com.facebook.drawee.generic.RoundingParams +import com.facebook.imagepipeline.common.ResizeOptions import com.facebook.imagepipeline.postprocessors.BlurPostProcessor import com.facebook.imagepipeline.request.ImageRequest import com.facebook.imagepipeline.request.ImageRequestBuilder @@ -79,14 +80,11 @@ class NftHolder(parent: ViewGroup): Holder(parent, R.layout.view_colle } private fun loadImage(uri: Uri, blur: Boolean) { + val builder = ImageRequestBuilder.newBuilderWithSource(uri) + builder.resizeOptions = ResizeOptions.forSquareSize(320) if (blur) { - val request = ImageRequestBuilder.newBuilderWithSource(uri) - .setPostprocessor(BlurPostProcessor(25, context, 3)) - .build() - imageView.setImageRequest(request) - } else { - imageView.setImageURI(uri, null) + builder.setPostprocessor(BlurPostProcessor(25, context, 3)) } - + imageView.setImageRequest(builder.build()) } } \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsScreen.kt index 98c466280..afdda2bec 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsScreen.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsScreen.kt @@ -17,6 +17,8 @@ import com.tonapps.uikit.list.ListPaginationListener import com.tonapps.wallet.api.entity.TokenEntity import com.tonapps.wallet.data.account.entities.WalletEntity import com.tonapps.wallet.localization.Localization +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import uikit.drawable.BarDrawable @@ -46,6 +48,7 @@ class EventsScreen(wallet: WalletEntity) : MainScreen.Child(R.layout.fragment_ma super.onViewCreated(view, savedInstanceState) headerView = view.findViewById(R.id.header) headerView.title = getString(Localization.history) + headerView.setSubtitle(Localization.updating) headerView.setColor(requireContext().backgroundTransparentColor) containerView = view.findViewById(R.id.container) @@ -64,38 +67,41 @@ class EventsScreen(wallet: WalletEntity) : MainScreen.Child(R.layout.fragment_ma openQRCode() } } - collectFlow(viewModel.isUpdatingFlow) { updating -> - if (updating) { + + collectFlow(viewModel.uiStateFlow, ::applyState) + } + + private suspend fun applyState(state: EventsUiState) = withContext(Dispatchers.Main) { + if (state.isEmpty) { + setEmptyState() + } else { + setListState() + if (state.isLoading) { headerView.setSubtitle(Localization.updating) - } else { - headerView.setSubtitle(null) + } + legacyAdapter.submitList(state.items) { + if (!state.isLoading) { + headerView.setSubtitle(null) + } } } - collectFlow(viewModel.uiItemsFlow, ::setItems) } override fun scrollUp() { super.scrollUp() listView.scrollToPosition(0) + viewModel.refresh() } private fun openQRCode() { navigation?.add(QRScreen.newInstance(screenContext.wallet, TokenEntity.TON)) } - private fun setItems(items: List) { - if (items.isEmpty()) { - setEmptyState() - } else { - setListState() - legacyAdapter.submitList(items) - } - } - private fun setEmptyState() { if (emptyView.visibility == View.VISIBLE) { return } + headerView.setSubtitle(null) emptyView.visibility = View.VISIBLE containerView.visibility = View.GONE } diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsUiState.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsUiState.kt new file mode 100644 index 000000000..807491504 --- /dev/null +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsUiState.kt @@ -0,0 +1,12 @@ +package com.tonapps.tonkeeper.ui.screen.events + +import com.tonapps.tonkeeper.core.history.list.item.HistoryItem + +data class EventsUiState( + val items: List = emptyList(), + val isLoading: Boolean = true, +) { + + val isEmpty: Boolean + get() = items.isEmpty() && !isLoading +} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsViewModel.kt index 359fd37d6..4b04fe43a 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsViewModel.kt @@ -1,8 +1,10 @@ package com.tonapps.tonkeeper.ui.screen.events import android.app.Application +import android.util.Log import androidx.lifecycle.viewModelScope import com.tonapps.tonkeeper.api.AccountEventWrap +import com.tonapps.tonkeeper.core.entities.AssetsExtendedEntity import com.tonapps.tonkeeper.core.history.HistoryHelper import com.tonapps.tonkeeper.core.history.list.item.HistoryItem import com.tonapps.tonkeeper.manager.tx.TransactionManager @@ -15,17 +17,14 @@ import io.tonapi.models.AccountEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import uikit.extensions.collectFlow +import java.util.concurrent.atomic.AtomicBoolean +// TODO: Refactor this class class EventsViewModel( app: Application, private val wallet: WalletEntity, @@ -36,160 +35,101 @@ class EventsViewModel( private val transactionManager: TransactionManager, ): BaseWalletVM(app) { - private var accountEvents: List? = null + private val autoRefreshRunnable = Runnable { checkAutoRefresh() } - private val _uiItemsFlow = MutableStateFlow?>(null) - val uiItemsFlow = _uiItemsFlow.filterNotNull().shareIn(viewModelScope, SharingStarted.Eagerly, 1) + private var events: Array? = null + private val isLoading: AtomicBoolean = AtomicBoolean(true) - private val _isUpdatingFlow = MutableStateFlow(true) - val isUpdatingFlow = _isUpdatingFlow.asSharedFlow() + private val _uiStateFlow = MutableStateFlow(EventsUiState()) + val uiStateFlow = _uiStateFlow.stateIn(viewModelScope, SharingStarted.Eagerly, EventsUiState()) init { viewModelScope.launch(Dispatchers.IO) { - val cached = getCached() ?: return@launch - _uiItemsFlow.value = cached + setUiItems(getCached()) + submitEvents(cache(), true) + submitEvents(load(), false) } - eventsRepository.decryptedCommentFlow.onEach { - submitAccountEvents(emptyList()) - }.launchIn(viewModelScope) - - collectFlow(settingsRepository.hiddenBalancesFlow.drop(1)) { - val uiItems = _uiItemsFlow.value?.map { - if (it is HistoryItem.Event) { - it.copy(hiddenBalance = settingsRepository.hiddenBalances) - } else { - it - } - } ?: return@collectFlow - - _uiItemsFlow.value = uiItems.toList() - screenCacheSource.set(CACHE_NAME, wallet.id, uiItems) + eventsRepository.decryptedCommentFlow.collectFlow { + updateState() } - collectFlow(transactionManager.getEventsFlow(wallet).map { - AccountEventWrap( - event = it.body, - cached = false, - eventIds = it.eventIds - ) - }) { accountEvent -> - submitAccountEvents(listOf(accountEvent)) - if (!accountEvent.inProgress) { - refresh() - } + settingsRepository.hiddenBalancesFlow.drop(1).collectFlow { + updateState() } - collectFlow(eventsRepository.getFlow(wallet.accountId, wallet.testnet)) { eventsResult -> - submitAccountEvents(eventsResult.events.events.map { accountEvent -> - AccountEventWrap( - event = accountEvent, - cached = eventsResult.cache, - eventIds = listOf(accountEvent.eventId) - ) - }) - - _isUpdatingFlow.value = eventsResult.cache + transactionManager.eventsFlow(wallet).safeCollectFlowAtPost { event -> + if (event.pending) { + appendEvent(AccountEventWrap(event.body)) + } + refresh() } } - private suspend fun refresh() = withContext(Dispatchers.IO) { - _isUpdatingFlow.value = true - val events = eventsRepository.getRemote(wallet.accountId, wallet.testnet)?.events - if (events != null) { - submitAccountEvents(events.map { accountEvent -> - AccountEventWrap( - event = accountEvent, - cached = false, - eventIds = listOf(accountEvent.eventId) - ) - }, true) + private fun checkAutoRefresh() { + val hasPendingEvents = hasPendingEvents() + if (hasPendingEvents) { + refresh() } - _isUpdatingFlow.value = false } - private suspend fun submitAccountEvents( - newEvents: List, - clear: Boolean = false, - ) = withContext(Dispatchers.IO) { - - val currentEvents = if (clear) { - mutableListOf() - } else { - accountEvents?.toMutableList() ?: mutableListOf() - } - - for (newEvent in newEvents) { - currentEvents.removeIf { - newEvent.eventIds.contains(it.eventId) && it.cached && !newEvent.cached - } - currentEvents.add(newEvent) - } - - val events = currentEvents.map { - it.copy() - }.distinctBy { - it.eventId - }.sortedByDescending { - it.timestamp - } - - accountEvents = events.toList() - - createUiItems(events.map { it.event }) + private fun startAutoRefresh() { + stopAutoRefresh() + postDelayed(25000, autoRefreshRunnable) } - private suspend fun createUiItems(events: List) { - val uiItems = mapping(events) - _uiItemsFlow.value = uiItems - screenCacheSource.set(CACHE_NAME, wallet.id, uiItems.toList()) + private fun stopAutoRefresh() { + cancelPost(autoRefreshRunnable) } - private fun lastLt(): Long? { - val lt = accountEvents?.last()?.lt ?: return null - if (lt > 0) { - return lt + fun refresh() { + if (isLoading.get()) { + return + } + setLoading() + viewModelScope.launch(Dispatchers.IO) { + submitEvents(load(), false) } - return null } fun loadMore() { - if (_isUpdatingFlow.value) { + if (isLoading.get()) { return } - val lastLt = lastLt() ?: return - viewModelScope.launch { - uiItemsLoading(true) - val events = eventsRepository.getRemote(wallet.accountId, wallet.testnet, lastLt)?.events - if (events == null) { - uiItemsLoading(false) - return@launch - } - submitAccountEvents(events.map { - AccountEventWrap( - event = it, - cached = false, - eventIds = listOf(it.eventId) - ) - }) - - uiItemsLoading(false) + + val lastLt = getLastLt() ?: return + + setLoading() + viewModelScope.launch(Dispatchers.IO) { + val events = load(lastLt) + appendEvents(events) } } - private fun uiItemsLoading(loading: Boolean) { - val oldValues = _uiItemsFlow.value ?: emptyList() - _uiItemsFlow.value = if (loading) { - historyHelper.withLoadingItem(oldValues) - } else { - historyHelper.removeLoadingItem(oldValues) + private fun setLoading() { + isLoading.set(true) + + _uiStateFlow.update { + it.copy( + items = historyHelper.withLoadingItem(it.items), + isLoading = true + ) } - _isUpdatingFlow.value = loading } - private suspend fun mapping( - events: List - ): List { + private fun setUiItems(uiItems: List) { + val loading = isLoading.get() + _uiStateFlow.value = EventsUiState( + items = if (loading) { + historyHelper.withLoadingItem(uiItems) + } else { + uiItems + }, + isLoading = loading + ) + } + + private suspend fun mapping(events: List): List { val items = historyHelper.mapping( wallet = wallet, events = events.map { it.copy() }, @@ -199,14 +139,76 @@ class EventsViewModel( return historyHelper.groupByDate(items) } - private fun getCached(): List? { - val items: List = screenCacheSource.get(CACHE_NAME, wallet.id) { + private fun getCached(): List { + return screenCacheSource.get(CACHE_NAME, wallet.id) { HistoryItem.createFromParcel(it) } - if (items.isEmpty()) { + } + + private suspend fun updateState() = withContext(Dispatchers.IO) { + val events = getEvents() + val uiItems = mapping(events.map { it.event }) + setUiItems(uiItems) + screenCacheSource.set(CACHE_NAME, wallet.id, uiItems) + } + + private suspend fun submitEvents(newEvents: List, loading: Boolean) = withContext(Dispatchers.IO) { + events = newEvents.distinctBy { it.eventId } + .sortedByDescending { it.timestamp } + .toTypedArray() + + if (hasPendingEvents()) { + startAutoRefresh() + } else { + stopAutoRefresh() + } + + isLoading.set(loading) + updateState() + } + + private suspend fun getEvents(): MutableList = withContext(Dispatchers.IO) { + events?.map { it.copy() }?.toMutableList() ?: mutableListOf() + } + + private suspend fun appendEvents(newEvents: List) { + val list = getEvents() + newEvents.map { it.copy() } + submitEvents(list, false) + } + + private fun appendEvent(event: AccountEventWrap) { + viewModelScope.launch { + appendEvents(listOf(event.copy())) + } + } + + private fun getLastLt(): Long? { + val lt = events?.last { !it.inProgress }?.lt ?: return null + if (0 >= lt) { return null } - return items + return lt + } + + private fun hasPendingEvents(): Boolean { + return events?.firstOrNull { it.inProgress } != null + } + + private suspend fun load(beforeLt: Long? = null): List { + val list = eventsRepository.getRemote( + accountId = wallet.accountId, + testnet = wallet.testnet, + beforeLt = beforeLt + )?.events?.map(::AccountEventWrap) + return list ?: emptyList() + } + + private suspend fun cache(): List { + val list = eventsRepository.getLocal( + accountId = wallet.accountId, + testnet = wallet.testnet + )?.events?.map { AccountEventWrap.cached(it)} + return list ?: emptyList() } private companion object { diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/InitEvent.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/InitEvent.kt index 68f9a2a0e..e680a57a5 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/InitEvent.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/InitEvent.kt @@ -3,5 +3,4 @@ package com.tonapps.tonkeeper.ui.screen.init sealed class InitEvent { data class Loading(val loading: Boolean): InitEvent() data object Back: InitEvent() - data object Finish: InitEvent() } \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/InitScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/InitScreen.kt index 626288882..50fdf4cbb 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/InitScreen.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/InitScreen.kt @@ -84,7 +84,6 @@ class InitScreen: BaseWalletScreen(R.layout.fragment_init, S private fun onEvent(event: InitEvent) { when (event) { is InitEvent.Back -> popBackStack() - is InitEvent.Finish -> finish() is InitEvent.Loading -> setLoading(event.loading) } } diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/InitViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/InitViewModel.kt index 3f4885f75..f541f2111 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/InitViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/init/InitViewModel.kt @@ -453,19 +453,19 @@ class InitViewModel( } if (type == InitArgs.Type.Import || type == InitArgs.Type.Testnet) { - for (wallet in wallets) { - backupRepository.addBackup(wallet.id, BackupEntity.Source.LOCAL) + post { + backupRepository.addBackupsAsync(wallets.map { it.id }) } } if (savedState.enablePush) { - pushManager.wallets(wallets, PushManager.State.Enable) + post { + pushManager.walletsAsync(wallets, PushManager.State.Enable) + } } val selectedWalletId = wallets.minByOrNull { it.version }!!.id accountRepository.setSelectedWallet(selectedWalletId) - - _eventFlow.tryEmit(InitEvent.Finish) } catch (e: Throwable) { context.logError(e) setLoading(false) diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/nft/NftScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/nft/NftScreen.kt index b0e30ef73..2f5945535 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/nft/NftScreen.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/nft/NftScreen.kt @@ -134,6 +134,7 @@ class NftScreen(wallet: WalletEntity): WalletContextScreen(R.layout.fragment_nft for ((index, button) in nftEntity.metadata.buttons.take(5).withIndex()) { val buttonView = newNftButton(buttonsContainer, index == 0) buttonView.text = button.label + buttonView.isEnabled = !nftEntity.inSale buttonView.setOnClickListener { openButtonDApp(button.uri) } } } diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/phrase/PhraseScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/phrase/PhraseScreen.kt index 0a907d843..866bbc3bf 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/phrase/PhraseScreen.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/phrase/PhraseScreen.kt @@ -14,6 +14,7 @@ import com.tonapps.wallet.data.account.entities.WalletEntity import uikit.base.BaseFragment import uikit.extensions.pinToBottomInsets import org.koin.androidx.viewmodel.ext.android.viewModel +import uikit.extensions.doKeyboardAnimation import uikit.widget.HeaderView import uikit.widget.PhraseWords @@ -37,13 +38,11 @@ class PhraseScreen(wallet: WalletEntity): WalletContextScreen(R.layout.fragment_ wordsView.setWords(args.words) copyButton = view.findViewById(R.id.copy) - copyButton.pinToBottomInsets() copyButton.setOnClickListener { requireContext().copyToClipboard(args.words.joinToString(" ")) } checkButton = view.findViewById(R.id.check) - checkButton.pinToBottomInsets() checkButton.setOnClickListener { navigation?.add(BackupCheckScreen.newInstance(screenContext.wallet, args.words, args.backupId)) finish() @@ -54,6 +53,11 @@ class PhraseScreen(wallet: WalletEntity): WalletContextScreen(R.layout.fragment_ } else { copyButton.visibility = View.VISIBLE } + + view.doKeyboardAnimation { offset, _, _ -> + copyButton.translationY = -offset.toFloat() + checkButton.translationY = -offset.toFloat() + } } companion object { diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/purchase/web/PurchaseWebScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/purchase/web/PurchaseWebScreen.kt index c2911f966..b47be961f 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/purchase/web/PurchaseWebScreen.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/purchase/web/PurchaseWebScreen.kt @@ -1,8 +1,10 @@ package com.tonapps.tonkeeper.ui.screen.purchase.web +import android.content.Context import android.graphics.Bitmap import android.os.Bundle import android.view.View +import com.tonapps.extensions.activity import com.tonapps.extensions.getParcelableCompat import com.tonapps.tonkeeper.core.AnalyticsHelper import com.tonapps.tonkeeper.helper.BrowserHelper @@ -87,6 +89,11 @@ class PurchaseWebScreen(wallet: WalletEntity): WalletContextScreen(R.layout.frag companion object { private const val METHOD_KEY = "method" + fun open(context: Context, method: WalletPurchaseMethodEntity) { + val activity = context.activity ?: throw IllegalStateException("Activity not found") + open(activity, method) + } + fun open(activity: NavigationActivity, method: WalletPurchaseMethodEntity) { if (method.useCustomTabs) { BrowserHelper.open(activity, method.uri) diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootActivity.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootActivity.kt index 428553dc7..e9311e5b5 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootActivity.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootActivity.kt @@ -5,15 +5,16 @@ import android.content.res.Configuration import android.net.Uri import android.os.Bundle import android.os.Handler -import android.util.Log import android.view.View import androidx.biometric.BiometricPrompt +import androidx.core.net.toUri import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.lifecycle.lifecycleScope import com.tonapps.extensions.toUriOrNull import com.tonapps.tonkeeper.App +import com.tonapps.tonkeeper.deeplink.DeepLink import com.tonapps.tonkeeper.ui.screen.transaction.TransactionScreen import com.tonapps.tonkeeper.extensions.toast import com.tonapps.tonkeeper.helper.BrowserHelper @@ -23,13 +24,19 @@ import com.tonapps.tonkeeper.ui.base.WalletFragmentFactory import com.tonapps.tonkeeper.ui.screen.backup.main.BackupScreen import com.tonapps.tonkeeper.ui.screen.battery.BatteryScreen import com.tonapps.tonkeeper.ui.screen.browser.dapp.DAppScreen +import com.tonapps.tonkeeper.ui.screen.camera.CameraScreen import com.tonapps.tonkeeper.ui.screen.init.InitArgs import com.tonapps.tonkeeper.ui.screen.init.InitScreen import com.tonapps.tonkeeper.ui.screen.ledger.sign.LedgerSignScreen import com.tonapps.tonkeeper.ui.screen.main.MainScreen +import com.tonapps.tonkeeper.ui.screen.name.edit.EditNameScreen import com.tonapps.tonkeeper.ui.screen.purchase.main.PurchaseScreen import com.tonapps.tonkeeper.ui.screen.purchase.web.PurchaseWebScreen +import com.tonapps.tonkeeper.ui.screen.qr.QRScreen import com.tonapps.tonkeeper.ui.screen.send.main.SendScreen +import com.tonapps.tonkeeper.ui.screen.settings.currency.CurrencyScreen +import com.tonapps.tonkeeper.ui.screen.settings.language.LanguageScreen +import com.tonapps.tonkeeper.ui.screen.settings.main.SettingsScreen import com.tonapps.tonkeeper.ui.screen.staking.stake.StakingScreen import com.tonapps.tonkeeper.ui.screen.staking.viewer.StakeViewerScreen import com.tonapps.tonkeeper.ui.screen.start.StartScreen @@ -161,30 +168,15 @@ class RootActivity: BaseWalletActivity() { fun event(event: RootEvent) { when (event) { - is RootEvent.Toast -> toast(event.resId) is RootEvent.Singer -> add(InitScreen.newInstance(if (event.qr) InitArgs.Type.SignerQR else InitArgs.Type.Signer, event.publicKey, event.name)) is RootEvent.Ledger -> add(InitScreen.newInstance(type = InitArgs.Type.Ledger, ledgerConnectData = event.connectData, accounts = event.accounts)) - is RootEvent.Browser -> add(DAppScreen.newInstance(event.wallet, url = event.uri)) is RootEvent.Transfer -> openSend( targetAddress = event.address, tokenAddress = event.jettonAddress ?: TokenEntity.TON.address, - amountNano = event.amount?.toLongOrNull() ?: 0L, + amountNano = event.amount ?: 0L, text = event.text, wallet = event.wallet ) - is RootEvent.OpenSend -> openSend(event.wallet) - is RootEvent.Transaction -> this.navigation?.add(TransactionScreen.newInstance(event.event)) - is RootEvent.BuyOrSell -> { - if (event.method == null) { - add(PurchaseScreen.newInstance(event.wallet)) - } else { - PurchaseWebScreen.open(this, event.method) - } - } - is RootEvent.OpenBackups -> add(BackupScreen.newInstance(event.wallet)) - is RootEvent.Staking -> add(StakingScreen.newInstance(event.wallet)) - is RootEvent.StakingPool -> add(StakeViewerScreen.newInstance(event.wallet, event.poolAddress, "")) - is RootEvent.Battery -> add(BatteryScreen.newInstance(event.wallet, event.promocode)) else -> { } } } diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootEvent.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootEvent.kt index 43d51a394..572b4e5d8 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootEvent.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootEvent.kt @@ -10,8 +10,6 @@ import com.tonapps.wallet.data.purchase.entity.PurchaseMethodEntity import org.ton.api.pub.PublicKeyEd25519 sealed class RootEvent { - data class Toast(val resId: Int): RootEvent() - data class OpenTab( val link: String, val wallet: WalletEntity @@ -25,11 +23,6 @@ sealed class RootEvent { val to: String? ): RootEvent() - data class BuyOrSell( - val wallet: WalletEntity, - val method: WalletPurchaseMethodEntity? = null - ): RootEvent() - data class Singer( val publicKey: PublicKeyEd25519, val name: String?, @@ -41,35 +34,11 @@ sealed class RootEvent { val accounts: List ): RootEvent() - data class Browser( - val wallet: WalletEntity, - val uri: Uri - ): RootEvent() - data class Transfer( val wallet: WalletEntity, val address: String, - val amount: String?, + val amount: Long?, val text: String?, val jettonAddress: String? ): RootEvent() - - data class OpenSend( - val wallet: WalletEntity, - ): RootEvent() - - data class Transaction( - val event: HistoryItem.Event - ): RootEvent() - - data class Battery( - val wallet: WalletEntity, - val promocode: String? - ): RootEvent() - - data class OpenBackups(val wallet: WalletEntity): RootEvent() - - data class Staking(val wallet: WalletEntity): RootEvent() - - data class StakingPool(val wallet: WalletEntity, val poolAddress: String): RootEvent() } \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt index 9278633b7..4f1795683 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt @@ -15,19 +15,17 @@ import com.google.firebase.crashlytics.setCustomKeys import com.google.firebase.ktx.Firebase import com.tonapps.blockchain.ton.TonNetwork import com.tonapps.extensions.MutableEffectFlow -import com.tonapps.extensions.getQueryLong import com.tonapps.extensions.locale import com.tonapps.extensions.setLocales import com.tonapps.extensions.toUriOrNull import com.tonapps.ledger.ton.LedgerConnectData -import com.tonapps.tonkeeper.Environment import com.tonapps.tonkeeper.core.AnalyticsHelper -import com.tonapps.tonkeeper.core.deeplink.DeepLink import com.tonapps.tonkeeper.core.entities.WalletPurchaseMethodEntity import com.tonapps.tonkeeper.core.history.HistoryHelper import com.tonapps.tonkeeper.core.history.list.item.HistoryItem -import com.tonapps.tonkeeper.core.signer.SingerArgs import com.tonapps.tonkeeper.core.widget.Widget +import com.tonapps.tonkeeper.deeplink.DeepLink +import com.tonapps.tonkeeper.deeplink.DeepLinkRoute import com.tonapps.tonkeeper.extensions.safeExternalOpenUri import com.tonapps.tonkeeper.helper.ShortcutHelper import com.tonapps.tonkeeper.manager.push.FirebasePush @@ -35,14 +33,31 @@ import com.tonapps.tonkeeper.manager.tonconnect.TonConnectManager import com.tonapps.tonkeeper.manager.tonconnect.bridge.model.BridgeError import com.tonapps.tonkeeper.manager.tonconnect.bridge.model.BridgeEvent import com.tonapps.tonkeeper.ui.base.BaseWalletVM +import com.tonapps.tonkeeper.ui.screen.backup.main.BackupScreen +import com.tonapps.tonkeeper.ui.screen.battery.BatteryScreen +import com.tonapps.tonkeeper.ui.screen.browser.dapp.DAppScreen +import com.tonapps.tonkeeper.ui.screen.camera.CameraScreen import com.tonapps.tonkeeper.ui.screen.init.list.AccountItem -import com.tonapps.tonkeeper.ui.screen.main.MainScreen +import com.tonapps.tonkeeper.ui.screen.name.edit.EditNameScreen +import com.tonapps.tonkeeper.ui.screen.purchase.main.PurchaseScreen +import com.tonapps.tonkeeper.ui.screen.purchase.web.PurchaseWebScreen +import com.tonapps.tonkeeper.ui.screen.qr.QRScreen +import com.tonapps.tonkeeper.ui.screen.send.main.SendScreen import com.tonapps.tonkeeper.ui.screen.send.transaction.SendTransactionScreen +import com.tonapps.tonkeeper.ui.screen.settings.currency.CurrencyScreen +import com.tonapps.tonkeeper.ui.screen.settings.language.LanguageScreen +import com.tonapps.tonkeeper.ui.screen.settings.main.SettingsScreen +import com.tonapps.tonkeeper.ui.screen.staking.stake.StakingScreen +import com.tonapps.tonkeeper.ui.screen.staking.viewer.StakeViewerScreen +import com.tonapps.tonkeeper.ui.screen.transaction.TransactionScreen import com.tonapps.tonkeeper.ui.screen.wallet.main.WalletViewModel.Companion.getWalletScreen import com.tonapps.tonkeeper.ui.screen.wallet.main.list.Item import com.tonapps.tonkeeper.ui.screen.wallet.main.list.WalletAdapter +import com.tonapps.tonkeeper.ui.screen.wallet.manage.TokensManageScreen +import com.tonapps.tonkeeper.ui.screen.wallet.picker.PickerScreen import com.tonapps.tonkeeperx.R import com.tonapps.wallet.api.API +import com.tonapps.wallet.api.entity.TokenEntity import com.tonapps.wallet.data.account.entities.WalletEntity import com.tonapps.wallet.data.account.AccountRepository import com.tonapps.wallet.data.browser.BrowserRepository @@ -55,7 +70,7 @@ import com.tonapps.wallet.data.purchase.PurchaseRepository import com.tonapps.wallet.data.settings.SettingsRepository import com.tonapps.wallet.localization.Localization import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow @@ -70,9 +85,10 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.timeout import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import uikit.extensions.collectFlow +import kotlin.time.Duration.Companion.seconds class RootViewModel( app: Application, @@ -282,22 +298,6 @@ class RootViewModel( } }.take(1) - fun processDeepLink( - uri: Uri, - fromQR: Boolean, - refSource: Uri? - ): Boolean { - savedState.returnUri = null - if (TonConnectManager.isTonConnectDeepLink(uri)) { - savedState.returnUri = tonConnectManager.processDeeplink(context, uri, fromQR, refSource) - return true - } else if (DeepLink.isSupportedUri(uri)) { - resolveDeepLink(uri, fromQR, refSource) - return true - } - return false - } - fun processIntentExtras(bundle: Bundle) { val pushType = bundle.getString("type") ?: return hasWalletFlow.take(1).collectFlow { @@ -319,142 +319,172 @@ class RootViewModel( } val wallet = accountRepository.getWalletByAccountId(accountId) ?: return val openUrl = bundle.getString("link")?.toUriOrNull() ?: dappUrl - _eventFlow.tryEmit(RootEvent.Browser(wallet, openUrl)) + openScreen(DAppScreen.newInstance(wallet, url = openUrl)) } private suspend fun processDeepLinkPush(uri: Uri, bundle: Bundle) { val accountId = bundle.getString("account") ?: return val wallet = accountRepository.getWalletByAccountId(accountId) ?: return - resolveOther(DeepLink.fixDeepLink(uri), wallet) + val deeplink = DeepLink(uri, false, null) + processDeepLink(wallet, deeplink) } - private fun resolveDeepLink( + fun processDeepLink( uri: Uri, fromQR: Boolean, refSource: Uri? - ) { - if (uri.host == "signer") { - collectFlow(hasWalletFlow.take(1)) { - delay(1000) - resolveSignerLink(uri, fromQR) - } - } else { - collectFlow(accountRepository.selectedWalletFlow.take(1)) { wallet -> - resolveOther(DeepLink.fixDeepLink(uri), wallet) + ): Boolean { + savedState.returnUri = null + val deeplink = DeepLink(uri, fromQR, refSource) + postDelayed(1000) { + if (deeplink.route is DeepLinkRoute.Signer) { + processSignerDeepLink(deeplink.route, fromQR) + } else { + processDeepLink(deeplink) } } + return true } - private fun resolveSignerLink(uri: Uri, fromQR: Boolean) { - try { - val args = SingerArgs(uri) - _eventFlow.tryEmit(RootEvent.Singer(args.publicKeyEd25519, args.name, fromQR)) - } catch (e: Throwable) { - toast(Localization.invalid_link) + @OptIn(FlowPreview::class) + private fun processDeepLink(deeplink: DeepLink) { + accountRepository.selectedWalletFlow.take(1).timeout(5.seconds).collectFlow { wallet -> + processDeepLink(wallet, deeplink) } } - private suspend fun resolveOther(uri: Uri, wallet: WalletEntity) { - val path = uri.path - - if (MainScreen.isSupportedDeepLink(uri.toString())) { - _eventFlow.tryEmit(RootEvent.OpenTab(uri.toString(), wallet)) - } else if (path?.startsWith("/send") == true) { - _eventFlow.tryEmit(RootEvent.OpenSend(wallet)) - } else if (path?.startsWith("/staking") == true) { - _eventFlow.tryEmit(RootEvent.Staking(wallet)) - } else if (path?.startsWith("/pool/") == true) { - _eventFlow.tryEmit(RootEvent.StakingPool(wallet, uri.pathSegments.last())) - } else if (path?.startsWith("/action/") == true) { - val actionId = uri.pathSegments.last() - val accountAddress = uri.getQueryParameter("account") - if (accountAddress == null) { - showTransaction(actionId) + private suspend fun processDeepLink(wallet: WalletEntity, deeplink: DeepLink) { + val route = deeplink.route + if (route is DeepLinkRoute.TonConnect) { + savedState.returnUri = tonConnectManager.processDeeplink( + context = context, + uri = route.uri, + fromQR = deeplink.fromQR, + refSource = deeplink.referrer + ) + } else if (route is DeepLinkRoute.Tabs) { + _eventFlow.tryEmit(RootEvent.OpenTab(route.tabUri, wallet)) + } else if (route is DeepLinkRoute.Send) { + openScreen(SendScreen.newInstance(wallet)) + } else if (route is DeepLinkRoute.Staking) { + openScreen(StakingScreen.newInstance(wallet)) + } else if (route is DeepLinkRoute.StakingPool) { + openScreen(StakeViewerScreen.newInstance(wallet, route.poolAddress, "")) + } else if (route is DeepLinkRoute.AccountEvent) { + if (route.address == null) { + showTransaction(route.eventId) } else { - showTransaction(accountAddress, actionId) + showTransaction(route.address, route.eventId) } - } else if (path?.startsWith("/transfer/") == true) { - val exp = uri.getQueryLong("exp") ?: 0 - if (exp > 0 && exp < System.currentTimeMillis() / 1000) { - toast(Localization.expired_link) - return - } - - _eventFlow.tryEmit(RootEvent.Transfer( + } else if (route is DeepLinkRoute.Transfer) { + processTransferDeepLink(wallet, route) + } else if (route is DeepLinkRoute.PickWallet) { + accountRepository.setSelectedWallet(route.walletId) + } else if (route is DeepLinkRoute.Swap) { + _eventFlow.tryEmit(RootEvent.Swap( wallet = wallet, - address = uri.pathSegments.last(), - amount = uri.getQueryParameter("amount"), - text = uri.getQueryParameter("text"), - jettonAddress = uri.getQueryParameter("jetton"), + uri = api.config.swapUri, + address = wallet.address, + from = route.from, + to = route.to )) - } else if (path?.startsWith("/action/") == true) { - val account = uri.getQueryParameter("account") ?: return - val hash = uri.pathSegments.lastOrNull() ?: return - showTransaction(account, hash) - } else if (path?.startsWith("/pick/") == true) { - val walletId = uri.pathSegments.lastOrNull() ?: return - viewModelScope.launch { accountRepository.setSelectedWallet(walletId) } - } else if (path?.startsWith("/swap") == true) { - val ft = uri.getQueryParameter("ft") ?: "TON" - val tt = uri.getQueryParameter("tt") - _eventFlow.tryEmit(RootEvent.Swap(wallet, api.config.swapUri, wallet.address, ft, tt)) - } else if (path?.startsWith("/battery") == true) { - val promocode = uri.getQueryParameter("promocode") - _eventFlow.tryEmit(RootEvent.Battery(wallet, promocode)) - } else if (path?.startsWith("/buy-ton") == true || uri.path == "/exchange" || uri.path == "/exchange/") { - _eventFlow.tryEmit(RootEvent.BuyOrSell(wallet)) - } else if (path?.startsWith("/exchange") == true) { - val name = uri.pathSegments.lastOrNull() ?: return - val method = purchaseRepository.getMethod(name, wallet.testnet, settingsRepository.getLocale()) - val methodWrapped: WalletPurchaseMethodEntity? = if (method != null) { - WalletPurchaseMethodEntity( + } else if (route is DeepLinkRoute.Battery) { + openScreen(BatteryScreen.newInstance(wallet, route.promocode)) + } else if (route is DeepLinkRoute.Purchase) { + openScreen(PurchaseScreen.newInstance(wallet)) + } else if (route is DeepLinkRoute.Exchange) { + val method = purchaseRepository.getMethod( + id = route.methodName, + testnet = wallet.testnet, + locale = settingsRepository.getLocale() + ) + if (method == null) { + toast(Localization.payment_method_not_found) + } else { + PurchaseWebScreen.open(context, WalletPurchaseMethodEntity( method = method, wallet = wallet, currency = settingsRepository.currency.code, config = api.config - ) - } else { - null + )) } - _eventFlow.tryEmit(RootEvent.BuyOrSell(wallet, methodWrapped)) - } else if (path?.startsWith("/backups") == true) { - _eventFlow.tryEmit(RootEvent.OpenBackups(wallet)) - } else if (path?.startsWith("/dapp") == true) { - viewModelScope.launch(Dispatchers.IO) { - val dAppUrl = uri.toString() - .replace("https://app.tonkeeper.com/dapp/", "https://") - .toUri() - - val apps = browserRepository.getApps(settingsRepository.country, wallet.testnet, context.locale) ?: return@launch - apps.find { - it.url.host == dAppUrl.host - }?.let { - _eventFlow.tryEmit(RootEvent.Browser(wallet, dAppUrl)) - } + } else if (route is DeepLinkRoute.Backups) { + openScreen(BackupScreen.newInstance(wallet)) + } else if (route is DeepLinkRoute.Settings) { + openScreen(SettingsScreen.newInstance(wallet)) + } else if (route is DeepLinkRoute.DApp) { + val dAppUri = route.url.toUri() + val dApp = browserRepository.getApps( + country = settingsRepository.country, + testnet = wallet.testnet, + locale = context.locale + ).find { it.url.host == dAppUri.host } + if (dApp == null) { + toast(Localization.app_not_found) + } else { + openScreen(DAppScreen.newInstance(wallet, url = dAppUri)) } + } else if (route is DeepLinkRoute.SettingsSecurity) { + openScreen(SettingsScreen.newInstance(wallet)) + } else if (route is DeepLinkRoute.SettingsCurrency) { + openScreen(CurrencyScreen.newInstance()) + } else if (route is DeepLinkRoute.SettingsLanguage) { + openScreen(LanguageScreen.newInstance()) + } else if (route is DeepLinkRoute.SettingsNotifications) { + openScreen(SettingsScreen.newInstance(wallet)) + } else if (route is DeepLinkRoute.EditWalletLabel) { + openScreen(EditNameScreen.newInstance(wallet)) + } else if (route is DeepLinkRoute.Camera) { + openScreen(CameraScreen.newInstance()) + } else if (route is DeepLinkRoute.Receive) { + openScreen(QRScreen.newInstance(wallet, TokenEntity.TON)) + } else if (route is DeepLinkRoute.ManageAssets) { + openScreen(TokensManageScreen.newInstance(wallet)) + } else if (route is DeepLinkRoute.WalletPicker) { + openScreen(PickerScreen.newInstance()) } else { toast(Localization.invalid_link) } } - private fun showTransaction(hash: String) { - viewModelScope.launch(Dispatchers.IO) { - val wallet = selectedWalletFlow.firstOrNull() ?: return@launch - val item = historyHelper.getEvent(wallet, hash).filterIsInstance().firstOrNull() ?: return@launch - _eventFlow.tryEmit(RootEvent.Transaction(item)) + private fun processTransferDeepLink(wallet: WalletEntity, route: DeepLinkRoute.Transfer) { + if (route.isExpired) { + toast(Localization.expired_link) + return } + _eventFlow.tryEmit(RootEvent.Transfer( + wallet = wallet, + address = route.address, + amount = route.amount, + text = route.text, + jettonAddress = route.jettonAddress, + )) } - private fun showTransaction(accountId: String, hash: String) { - viewModelScope.launch(Dispatchers.IO) { - val wallet = accountRepository.getWalletByAccountId(accountId, false) ?: return@launch - val event = api.getTransactionEvents(wallet.accountId, wallet.testnet, hash) ?: return@launch - val item = historyHelper.mapping(wallet, event).find { it is HistoryItem.Event } as? HistoryItem.Event ?: return@launch - _eventFlow.tryEmit(RootEvent.Transaction(item)) - } + private fun processSignerDeepLink(route: DeepLinkRoute.Signer, fromQR: Boolean) { + _eventFlow.tryEmit(RootEvent.Singer( + publicKey = route.publicKey, + name = route.name, + qr = fromQR || !route.local + )) + } + + private suspend fun showTransaction(hash: String) { + val wallet = selectedWalletFlow.firstOrNull() ?: return + historyHelper.getEvent(wallet, hash) + .filterIsInstance() + .firstOrNull()?.let { + openScreen(TransactionScreen.newInstance(it)) + } } - private fun toast(resId: Int) { - _eventFlow.tryEmit(RootEvent.Toast(resId)) + private suspend fun showTransaction(accountId: String, hash: String) { + val wallet = accountRepository.getWalletByAccountId(accountId, false) ?: return + val event = api.getTransactionEvents(wallet.accountId, wallet.testnet, hash) ?: return + historyHelper.mapping(wallet, event) + .find { it is HistoryItem.Event }?.let { + openScreen(TransactionScreen.newInstance(it as HistoryItem.Event)) + } + } } \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/settings/main/SettingsViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/settings/main/SettingsViewModel.kt index 0b43fe629..18a117547 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/settings/main/SettingsViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/settings/main/SettingsViewModel.kt @@ -2,10 +2,14 @@ package com.tonapps.tonkeeper.ui.screen.settings.main import android.app.Application import android.os.Build +import android.util.Log +import androidx.core.net.toUri import androidx.lifecycle.viewModelScope import com.tonapps.blockchain.ton.contract.BaseWalletContract import com.tonapps.blockchain.ton.contract.WalletVersion import com.tonapps.blockchain.ton.extensions.toAccountId +import com.tonapps.extensions.appVersionCode +import com.tonapps.extensions.appVersionName import com.tonapps.tonkeeper.Environment import com.tonapps.tonkeeper.core.AnalyticsHelper import com.tonapps.tonkeeper.extensions.capitalized @@ -13,6 +17,7 @@ import com.tonapps.tonkeeper.manager.push.PushManager import com.tonapps.tonkeeper.manager.tonconnect.TonConnectManager import com.tonapps.tonkeeper.ui.base.BaseWalletVM import com.tonapps.tonkeeper.ui.screen.settings.main.list.Item +import com.tonapps.tonkeeperx.BuildConfig import com.tonapps.uikit.list.ListCell import com.tonapps.wallet.api.API import com.tonapps.wallet.data.account.entities.WalletEntity @@ -183,7 +188,7 @@ class SettingsViewModel( uiItems.add(Item.Space) uiItems.add(Item.FAQ(ListCell.Position.FIRST, api.config.faqUrl)) - uiItems.add(Item.Support(ListCell.Position.MIDDLE, api.config.directSupportUrl)) + uiItems.add(Item.Support(ListCell.Position.MIDDLE, getSupportUrl())) uiItems.add(Item.News(ListCell.Position.MIDDLE, api.config.tonkeeperNewsUrl)) uiItems.add(Item.Contact(ListCell.Position.MIDDLE, api.config.supportLink)) if (environment.isGooglePlayServicesAvailable) { @@ -202,4 +207,11 @@ class SettingsViewModel( _uiItemsFlow.value = uiItems } + + private fun getSupportUrl(): String { + val startParams = "android${Build.VERSION.SDK_INT}app${context.appVersionCode}" + val builder = api.config.directSupportUrl.toUri().buildUpon() + builder.appendQueryParameter("start", startParams) + return builder.toString() + } } \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/staking/withdraw/StakeWithdrawScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/staking/withdraw/StakeWithdrawScreen.kt new file mode 100644 index 000000000..699a4554b --- /dev/null +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/staking/withdraw/StakeWithdrawScreen.kt @@ -0,0 +1,144 @@ +package com.tonapps.tonkeeper.ui.screen.staking.withdraw + +import android.os.Bundle +import android.view.View +import android.widget.Button +import androidx.lifecycle.lifecycleScope +import com.tonapps.icu.CurrencyFormatter.withCustomSymbol +import com.tonapps.tonkeeper.extensions.getTitle +import com.tonapps.tonkeeper.koin.walletViewModel +import com.tonapps.tonkeeper.ui.base.WalletContextScreen +import com.tonapps.tonkeeper.ui.screen.send.main.SendException +import com.tonapps.tonkeeper.ui.screen.staking.unstake.UnStakeScreen +import com.tonapps.tonkeeper.ui.screen.staking.viewer.StakeViewerScreen +import com.tonapps.tonkeeper.view.TransactionDetailView +import com.tonapps.tonkeeperx.R +import com.tonapps.wallet.data.account.entities.WalletEntity +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.koin.core.parameter.parametersOf +import uikit.extensions.collectFlow +import uikit.widget.FrescoView +import uikit.widget.HeaderView +import uikit.widget.ProcessTaskView + +class StakeWithdrawScreen(wallet: WalletEntity): WalletContextScreen(R.layout.fragment_stake_withdraw, wallet) { + + override val viewModel: StakeWithdrawViewModel by walletViewModel { + parametersOf(requireArguments().getString(ARG_POOL_ADDRESS)) + } + + private lateinit var iconView: FrescoView + private lateinit var walletView: TransactionDetailView + private lateinit var recipientView: TransactionDetailView + private lateinit var amountView: TransactionDetailView + private lateinit var feeView: TransactionDetailView + private lateinit var taskView: ProcessTaskView + private lateinit var confirmButton: Button + private lateinit var buttonsView: View + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val headerView = view.findViewById(R.id.header) + headerView.doOnActionClick = { finish() } + + iconView = view.findViewById(R.id.icon) + + walletView = view.findViewById(R.id.line_wallet) + walletView.value = wallet.label.getTitle(requireContext(), walletView.valueView, 12) + + recipientView = view.findViewById(R.id.line_recipient) + + amountView = view.findViewById(R.id.line_amount) + feeView = view.findViewById(R.id.line_fee) + + taskView = view.findViewById(R.id.task) + + val cancelButton = view.findViewById