From e5fb7d9e85f6e8c3c09553dcaedad4d5907e22fd Mon Sep 17 00:00:00 2001 From: polstianka Date: Wed, 13 Nov 2024 09:46:24 -0500 Subject: [PATCH] bug fixeds for safe mode --- .../main/java/com/tonapps/wallet/api/API.kt | 34 +-- .../data/settings/SettingsRepository.kt | 1 + .../com/tonapps/tonkeeper/core/DevSettings.kt | 8 + .../tonkeeper/core/history/HistoryHelper.kt | 43 ++- .../com/tonapps/tonkeeper/extensions/Uri.kt | 9 + .../tonapps/tonkeeper/helper/BrowserHelper.kt | 2 + .../com/tonapps/tonkeeper/koin/KoinModule.kt | 3 +- .../tonkeeper/koin/viewModelWalletModule.kt | 2 + .../ui/base/InjectedTonConnectScreen.kt | 178 +++++++++++++ .../ui/component/TonConnectWebView.kt | 13 + .../refill/list/holder/BatteryHolder.kt | 2 - .../ui/screen/browser/dapp/DAppScreen.kt | 99 +------ .../ui/screen/browser/dapp/DAppViewModel.kt | 30 +-- .../browser/search/BrowserSearchScreen.kt | 8 +- .../browser/search/BrowserSearchViewModel.kt | 9 + .../tonkeeper/ui/screen/card/CardBridge.kt | 166 ++++++++++++ .../tonkeeper/ui/screen/card/CardScreen.kt | 106 ++++++++ .../tonkeeper/ui/screen/card/CardViewModel.kt | 38 +++ .../manage/CollectiblesManageViewModel.kt | 12 +- .../tonkeeper/ui/screen/dev/DevScreen.kt | 22 +- .../tonkeeper/ui/screen/dev/DevViewModel.kt | 10 + .../tonkeeper/ui/screen/nft/NftViewModel.kt | 2 +- .../tonkeeper/ui/screen/root/RootActivity.kt | 13 +- .../tonkeeper/ui/screen/root/RootViewModel.kt | 2 + .../ui/screen/settings/main/SettingsScreen.kt | 3 +- .../settings/security/SecurityScreen.kt | 10 + .../stories/safemode/SafeModeStoriesScreen.kt | 36 +++ .../{w5/stories => stories/w5}/StoryEntity.kt | 2 +- .../ui/screen/stories/w5/W5StoriesScreen.kt | 71 +++++ .../screen/stories/w5/W5StoriesViewModel.kt | 46 ++++ .../token/picker/TokenPickerViewModel.kt | 19 +- .../viewer/list/holder/W5BannerHolder.kt | 2 +- .../tonkeeper/ui/screen/w5/DeleteMe.kt | 4 - .../w5/stories/StoryProgressDrawable.kt | 45 ---- .../ui/screen/w5/stories/StoryProgressView.kt | 26 -- .../ui/screen/w5/stories/W5StoriesScreen.kt | 156 ----------- .../screen/w5/stories/W5StoriesViewModel.kt | 160 ------------ .../app/src/main/res/layout/fragment_card.xml | 12 + .../app/src/main/res/layout/fragment_dapp.xml | 2 +- .../app/src/main/res/layout/fragment_dev.xml | 16 ++ .../src/main/res/layout/fragment_security.xml | 3 +- .../main/res/layout/fragment_w5_stories.xml | 75 ------ apps/wallet/instance/main/build.gradle.kts | 2 +- .../src/main/res/values-bg/strings.xml | 10 +- .../src/main/res/values-es/strings.xml | 10 +- .../src/main/res/values-in/strings.xml | 10 +- .../src/main/res/values-ru/strings.xml | 10 +- .../src/main/res/values-tr/strings.xml | 10 +- .../src/main/res/values-uk/strings.xml | 10 +- .../src/main/res/values-uz/strings.xml | 10 +- .../src/main/res/values-zh/strings.xml | 10 +- .../src/main/res/values/strings.xml | 14 +- .../java/com/tonapps/extensions/Bundle.kt | 10 + .../main/java/com/tonapps/extensions/Uri.kt | 4 + .../java/uikit/extensions/CharSequence.kt | 5 +- .../src/main/java/uikit/extensions/Context.kt | 7 + .../uikit/widget/stories/BaseStoriesScreen.kt | 245 +++++++++++++++++- .../java/uikit/widget/stories/StoriesState.kt | 7 + .../widget/webview/bridge/BridgeWebView.kt | 2 +- .../src/main/res/layout/fragment_stories.xml | 42 ++- 60 files changed, 1263 insertions(+), 655 deletions(-) create mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/base/InjectedTonConnectScreen.kt create mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/component/TonConnectWebView.kt create mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/card/CardBridge.kt create mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/card/CardScreen.kt create mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/card/CardViewModel.kt create mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/stories/safemode/SafeModeStoriesScreen.kt rename apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/{w5/stories => stories/w5}/StoryEntity.kt (95%) create mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/stories/w5/W5StoriesScreen.kt create mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/stories/w5/W5StoriesViewModel.kt delete mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/DeleteMe.kt delete mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/StoryProgressDrawable.kt delete mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/StoryProgressView.kt delete mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/W5StoriesScreen.kt delete mode 100644 apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/W5StoriesViewModel.kt create mode 100644 apps/wallet/instance/app/src/main/res/layout/fragment_card.xml delete mode 100644 apps/wallet/instance/app/src/main/res/layout/fragment_w5_stories.xml create mode 100644 ui/uikit/core/src/main/java/uikit/widget/stories/StoriesState.kt 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 e2ebfea72..060ca8981 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 @@ -1,6 +1,7 @@ package com.tonapps.wallet.api import android.content.Context +import android.os.Build import android.util.ArrayMap import android.util.Log import com.squareup.moshi.JsonAdapter @@ -11,6 +12,7 @@ import com.tonapps.blockchain.ton.extensions.base64 import com.tonapps.blockchain.ton.extensions.hex import com.tonapps.blockchain.ton.extensions.isValidTonAddress import com.tonapps.blockchain.ton.extensions.toRawAddress +import com.tonapps.extensions.appVersionName import com.tonapps.extensions.locale import com.tonapps.extensions.toUriOrNull import com.tonapps.icu.Coins @@ -75,7 +77,9 @@ class API( private val scope: CoroutineScope ) { - val defaultHttpClient = baseOkHttpClientBuilder().build() + private val userAgent = "Tonkeeper/${context.appVersionName} (Linux; Android ${Build.VERSION.RELEASE}; ${Build.MODEL})" + + val defaultHttpClient = baseOkHttpClientBuilder(userAgent).build() private val internalApi = InternalApi(context, defaultHttpClient) private val configRepository = ConfigRepository(context, scope, internalApi) @@ -89,15 +93,12 @@ class API( private val tonAPIHttpClient: OkHttpClient by lazy { createTonAPIHttpClient( context = context, + userAgent = userAgent, tonApiV2Key = { config.tonApiV2Key }, - allowDomains = { config.domains } + allowDomains = { config.domains } ) } - private val batteryHttpClient: OkHttpClient by lazy { - createBatteryAPIHttpClient(context) - } - @Volatile private var cachedCountry: String? = null @@ -145,7 +146,7 @@ class API( } private val batteryApi by lazy { - SourceAPI(BatteryApi(config.batteryHost, batteryHttpClient), BatteryApi(config.batteryTestnetHost, batteryHttpClient)) + SourceAPI(BatteryApi(config.batteryHost, tonAPIHttpClient), BatteryApi(config.batteryTestnetHost, tonAPIHttpClient)) } private val emulationJSONAdapter: JsonAdapter by lazy { @@ -846,7 +847,7 @@ class API( private val socketFactoryTcpNoDelay = SSLSocketFactoryTcpNoDelay() - private fun baseOkHttpClientBuilder(): OkHttpClient.Builder { + private fun baseOkHttpClientBuilder(userAgent: String): OkHttpClient.Builder { return OkHttpClient().newBuilder() .retryOnConnectionFailure(true) .connectTimeout(5, TimeUnit.SECONDS) @@ -855,6 +856,12 @@ class API( .callTimeout(5, TimeUnit.SECONDS) .pingInterval(5, TimeUnit.SECONDS) .followSslRedirects(true) + .addInterceptor { chain -> + val request = chain.request().newBuilder() + .addHeader("User-Agent", "TonWallet") + .build() + chain.proceed(request) + } .followRedirects(true) // .sslSocketFactory(socketFactoryTcpNoDelay.sslSocketFactory, socketFactoryTcpNoDelay.trustManager) // .socketFactory(SocketFactoryTcpNoDelay()) @@ -862,23 +869,16 @@ class API( private fun createTonAPIHttpClient( context: Context, + userAgent: String, tonApiV2Key: () -> String, allowDomains: () -> List ): OkHttpClient { - return baseOkHttpClientBuilder() + return baseOkHttpClientBuilder(userAgent) .addInterceptor(AcceptLanguageInterceptor(context.locale)) .addInterceptor(AuthorizationInterceptor.bearer( token = tonApiV2Key, allowDomains = allowDomains )).build() } - - private fun createBatteryAPIHttpClient( - context: Context, - ): OkHttpClient { - return baseOkHttpClientBuilder() - .addInterceptor(AcceptLanguageInterceptor(context.locale)) - .build() - } } } \ No newline at end of file diff --git a/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/SettingsRepository.kt b/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/SettingsRepository.kt index aea2e3e85..19dbac2d4 100644 --- a/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/SettingsRepository.kt +++ b/apps/wallet/data/settings/src/main/java/com/tonapps/wallet/data/settings/SettingsRepository.kt @@ -232,6 +232,7 @@ class SettingsRepository( if (value != field) { prefs.edit().putBoolean(SAFE_MODE_KEY, value).apply() field = value + tokenPrefsFolder.notifyChanged() } } diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/DevSettings.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/DevSettings.kt index 5654663d3..4ba5b6d79 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/DevSettings.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/core/DevSettings.kt @@ -23,6 +23,14 @@ object DevSettings { } } + var ignoreSystemFontSize: Boolean = prefs.getBoolean("ignore_system_font_size", false) + set(value) { + if (field != value) { + field = value + prefs.edit().putBoolean("ignore_system_font_size", value).apply() + } + } + fun tonConnectLog(message: String, error: Boolean = false) { if (tonConnectLogs) { 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 423b4f50a..21c99f852 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 @@ -336,7 +336,7 @@ class HistoryHelper( positionExtra: Int = 0, ): List = withContext(Dispatchers.IO) { val items = mutableListOf() - + val safeMode = settingsRepository.safeMode for (event in events) { val pending = event.inProgress @@ -352,9 +352,11 @@ class HistoryHelper( val chunkItems = mutableListOf() for ((actionIndex, action) in actions.withIndex()) { + if (safeMode && event.isScam) { + continue + } val timestamp = if (removeDate) 0 else event.timestamp - val isScam = - event.isScam || settingsRepository.isSpamTransaction(wallet.id, event.eventId) + val isScam = event.isScam || settingsRepository.isSpamTransaction(wallet.id, event.eventId) val item = action( index = actionIndex, @@ -363,7 +365,8 @@ class HistoryHelper( action = action, timestamp = timestamp, isScam = isScam - ) + ) ?: continue + chunkItems.add( item.copy( pending = pending, @@ -404,18 +407,21 @@ class HistoryHelper( action: Action, timestamp: Long, isScam: Boolean, - ): HistoryItem.Event { - + ): HistoryItem.Event? { + val safeMode = settingsRepository.safeMode val simplePreview = action.simplePreview val date = DateHelper.formatTransactionTime(timestamp, settingsRepository.getLocale()) - val dateDetails = - DateHelper.formatTransactionDetailsTime(timestamp, settingsRepository.getLocale()) + val dateDetails = DateHelper.formatTransactionDetailsTime(timestamp, settingsRepository.getLocale()) if (action.jettonSwap != null) { val jettonSwap = action.jettonSwap!! val tokenIn = jettonSwap.tokenIn val tokenOut = jettonSwap.tokenOut + if ((!tokenIn.verified || !tokenOut.verified) && safeMode) { + return null + } + val amountIn = jettonSwap.amountCoinsIn val amountOut = jettonSwap.amountCoinsOut @@ -451,6 +457,11 @@ class HistoryHelper( } else if (action.jettonTransfer != null) { val jettonTransfer = action.jettonTransfer!! val token = jettonTransfer.jetton.address + + if (safeMode && jettonTransfer.jetton.verification != JettonVerificationType.whitelist) { + return null + } + val symbol = jettonTransfer.jetton.symbol val isOut = !wallet.isMyAddress(jettonTransfer.recipient?.address ?: "") @@ -629,6 +640,10 @@ class HistoryHelper( it.with(pref) } + if (safeMode && nftItem?.verified != true) { + return null + } + val isEncryptedComment = nftItemTransfer.encryptedComment != null val comment = HistoryItem.Event.Comment.create( @@ -708,6 +723,10 @@ class HistoryHelper( } else if (action.jettonMint != null) { val jettonMint = action.jettonMint!! + if (safeMode && jettonMint.jetton.verification != JettonVerificationType.whitelist) { + return null + } + val amount = jettonMint.parsedAmount val value = CurrencyFormatter.format(jettonMint.jetton.symbol, amount, 2) @@ -850,6 +869,10 @@ class HistoryHelper( } else if (action.nftPurchase != null) { val nftPurchase = action.nftPurchase!! + if (safeMode && !nftPurchase.nft.verified) { + return null + } + val amount = Coins.of(nftPurchase.amount.value.toLong()) val value = CurrencyFormatter.format(nftPurchase.amount.tokenName, amount, 2) @@ -884,6 +907,10 @@ class HistoryHelper( } else if (action.jettonBurn != null) { val jettonBurn = action.jettonBurn!! + if (safeMode && jettonBurn.jetton.verification != JettonVerificationType.whitelist) { + return null + } + val amount = jettonBurn.parsedAmount val value = CurrencyFormatter.format(jettonBurn.jetton.symbol, amount, 2) diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/Uri.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/Uri.kt index ce19386d6..497049a07 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/Uri.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/Uri.kt @@ -6,13 +6,22 @@ import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.net.Uri import androidx.annotation.ColorInt +import com.tonapps.extensions.containsQuery import com.tonapps.extensions.isLocal +import com.tonapps.extensions.query import uikit.extensions.drawable fun Uri.isTonSite(): Boolean { return this.host?.endsWith(".ton") ?: false } +fun Uri.withUtmSource(source: String = "tonkeeper"): Uri { + if (containsQuery("utm_source")) { + return this + } + return this.buildUpon().appendQueryParameter("utm_source", source).build() +} + fun Uri.normalizeTONSites(): Uri { if (!isTonSite()) { return this diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/helper/BrowserHelper.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/helper/BrowserHelper.kt index daffd3c5b..d0ba7553c 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/helper/BrowserHelper.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/helper/BrowserHelper.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri +import android.provider.Browser import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import com.tonapps.extensions.activity @@ -59,6 +60,7 @@ object BrowserHelper { .setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_LIGHT, colorSchemeParams) .setColorSchemeParams(CustomTabsIntent.COLOR_SCHEME_DARK, colorSchemeParams) .build() + intent.intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.packageName) try { intent.launchUrl(activity, uri) 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 286d525b2..01dc77b0b 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 @@ -28,9 +28,8 @@ 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.w5.stories.W5StoriesViewModel +import com.tonapps.tonkeeper.ui.screen.stories.w5.W5StoriesViewModel import com.tonapps.tonkeeper.ui.screen.tonconnect.TonConnectViewModel -import com.tonapps.tonkeeper.ui.screen.wallet.main.list.WalletAdapter import com.tonapps.tonkeeper.usecase.emulation.EmulationUseCase import com.tonapps.tonkeeper.usecase.sign.SignUseCase import com.tonapps.wallet.data.settings.SettingsRepository 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 4c1311e71..e7568f88c 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 @@ -19,6 +19,7 @@ import com.tonapps.tonkeeper.ui.screen.token.picker.TokenPickerViewModel import com.tonapps.tonkeeper.ui.screen.battery.settings.BatterySettingsViewModel import com.tonapps.tonkeeper.ui.screen.battery.refill.BatteryRefillViewModel import com.tonapps.tonkeeper.ui.screen.battery.recharge.BatteryRechargeViewModel +import com.tonapps.tonkeeper.ui.screen.card.CardViewModel import com.tonapps.tonkeeper.ui.screen.collectibles.manage.CollectiblesManageScreen import com.tonapps.tonkeeper.ui.screen.collectibles.manage.CollectiblesManageViewModel import com.tonapps.tonkeeper.ui.screen.send.contacts.main.SendContactsViewModel @@ -67,4 +68,5 @@ val viewModelWalletModule = module { viewModelOf(::EditContactViewModel) viewModelOf(::AppsViewModel) viewModelOf(::CollectiblesManageViewModel) + viewModelOf(::CardViewModel) } \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/base/InjectedTonConnectScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/base/InjectedTonConnectScreen.kt new file mode 100644 index 000000000..916c832b2 --- /dev/null +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/base/InjectedTonConnectScreen.kt @@ -0,0 +1,178 @@ +package com.tonapps.tonkeeper.ui.base + +import android.app.Application +import android.net.Uri +import android.util.Log +import android.webkit.WebResourceRequest +import androidx.annotation.LayoutRes +import androidx.core.net.toUri +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tonapps.extensions.appVersionName +import com.tonapps.extensions.bestMessage +import com.tonapps.extensions.filterList +import com.tonapps.tonkeeper.deeplink.DeepLink +import com.tonapps.tonkeeper.deeplink.DeepLinkRoute +import com.tonapps.tonkeeper.extensions.normalizeTONSites +import com.tonapps.tonkeeper.manager.tonconnect.ConnectRequest +import com.tonapps.tonkeeper.manager.tonconnect.TonConnect +import com.tonapps.tonkeeper.manager.tonconnect.TonConnectManager +import com.tonapps.tonkeeper.manager.tonconnect.bridge.BridgeException +import com.tonapps.tonkeeper.manager.tonconnect.bridge.JsonBuilder +import com.tonapps.tonkeeper.manager.tonconnect.bridge.model.BridgeError +import com.tonapps.tonkeeper.manager.tonconnect.bridge.model.BridgeEvent +import com.tonapps.tonkeeper.manager.tonconnect.bridge.model.BridgeMethod +import com.tonapps.tonkeeper.ui.component.TonConnectWebView +import com.tonapps.tonkeeper.ui.screen.root.RootViewModel +import com.tonapps.tonkeeper.ui.screen.send.transaction.SendTransactionScreen +import com.tonapps.wallet.api.API +import com.tonapps.wallet.data.account.entities.WalletEntity +import com.tonapps.wallet.data.core.entity.SignRequestEntity +import com.tonapps.wallet.data.dapps.entities.AppConnectEntity +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import okhttp3.Response +import org.json.JSONArray +import org.json.JSONObject +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.activityViewModel +import uikit.base.BaseFragment +import uikit.extensions.activity +import java.util.concurrent.CancellationException + +abstract class InjectedTonConnectScreen(@LayoutRes layoutId: Int, wallet: WalletEntity): WalletContextScreen(layoutId, wallet), BaseFragment.SwipeBack { + + private val tonConnectManager: TonConnectManager by inject() + private val api: API by inject() + private val rootViewModel: RootViewModel by activityViewModel() + + abstract var webView: TonConnectWebView + + abstract val startUri: Uri + + val deviceInfo: JSONObject by lazy { + JsonBuilder.device(wallet.maxMessages, requireContext().appVersionName) + } + + val installId: String + get() = rootViewModel.installId + + fun back() { + if (webView.canGoBack()) { + webView.goBack() + } else { + finish() + } + } + + override fun onBackPressed(): Boolean { + back() + return false + } + + fun overrideUrlLoading(request: WebResourceRequest): Boolean { + val refererUri = request.requestHeaders?.get("Referer")?.toUri() + val url = request.url.normalizeTONSites() + val scheme = url.scheme ?: "" + if (scheme == "https" && url.host != "app.tonkeeper.com") { + return false + } + val deeplink = DeepLink(url, false, refererUri) + return when (deeplink.route) { + is DeepLinkRoute.TonConnect -> { + rootViewModel.processTonConnectDeepLink(deeplink, null) + true + } + is DeepLinkRoute.Unknown -> { + navigation?.openURL(url.toString()) + true + } + else -> rootViewModel.processDeepLink(uri = url, fromQR = false, refSource = refererUri, internal = false, fromPackageName = null) + } + } + + suspend fun tonapiFetch( + url: String, + options: String + ) = api.tonapiFetch(url, options) + + suspend fun tonconnect( + version: Int, + request: ConnectRequest + ): JSONObject { + if (version != 2) { + return JsonBuilder.connectEventError(BridgeError.badRequest("Version $version is not supported")) + } + val activity = requireContext().activity ?: return JsonBuilder.connectEventError(BridgeError.unknown("internal client error")) + if (tonConnectManager.isScam(requireContext(), request.manifestUrl.toUri(), webView.url!!.toUri(), startUri)) { + return JsonBuilder.connectEventError(BridgeError.unknown("internal client error")) + } + + return tonConnectManager.launchConnectFlow( + activity = activity, + tonConnect = TonConnect.fromJsInject(request, webView.url?.toUri()), + wallet = wallet + ) + } + + suspend fun tonconnectSend(array: JSONArray): JSONObject { + val messages = BridgeEvent.Message.parse(array) + if (messages.size == 1) { + val message = messages.first() + val id = message.id + if (message.method != BridgeMethod.SEND_TRANSACTION) { + return JsonBuilder.responseError(id, BridgeError.methodNotSupported("Method \"${message.method}\" not supported.")) + } + val signRequests = message.params.map { SignRequestEntity(it, startUri) } + if (signRequests.size != 1) { + return JsonBuilder.responseError(id, BridgeError.badRequest("Request contains excess transactions. Required: 1, Provided: ${signRequests.size}")) + } + val signRequest = signRequests.first() + return try { + val boc = SendTransactionScreen.run(requireContext(), wallet, signRequest) + JsonBuilder.responseSendTransaction(id, boc) + } catch (e: CancellationException) { + JsonBuilder.responseError(id, BridgeError.userDeclinedTransaction()) + } catch (e: BridgeException) { + JsonBuilder.responseError(id, BridgeError.badRequest(e.bestMessage)) + } catch (e: Throwable) { + FirebaseCrashlytics.getInstance().recordException(e) + JsonBuilder.responseError(id, BridgeError.unknown(e.bestMessage)) + } + } else { + return JsonBuilder.responseError(0, BridgeError.badRequest("Request contains excess messages. Required: 1, Provided: ${messages.size}")) + } + } + + abstract class ViewModel( + app: Application, + private val wallet: WalletEntity, + private val tonConnectManager: TonConnectManager + ): BaseWalletVM(app) { + + abstract val url: Uri + + val connectionFlow = tonConnectManager.walletConnectionsFlow(wallet).filterList { connection -> + connection.type == AppConnectEntity.Type.Internal && connection.appUrl.host == url.host + }.map { + it.firstOrNull() + } + + fun disconnect() { + tonConnectManager.disconnect(wallet, url, AppConnectEntity.Type.Internal) + } + + suspend fun restoreConnection(): JSONObject { + val connection = loadConnection() + return if (connection == null) { + JsonBuilder.connectEventError(BridgeError.unknownApp()) + } else { + JsonBuilder.connectEventSuccess(wallet, null, null, context.appVersionName) + } + } + + private suspend fun loadConnection(): AppConnectEntity? { + return connectionFlow.firstOrNull() + } + } + +} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/component/TonConnectWebView.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/component/TonConnectWebView.kt new file mode 100644 index 000000000..626435361 --- /dev/null +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/component/TonConnectWebView.kt @@ -0,0 +1,13 @@ +package com.tonapps.tonkeeper.ui.component + +import android.content.Context +import android.util.AttributeSet +import uikit.widget.webview.bridge.BridgeWebView + +class TonConnectWebView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = android.R.attr.webViewStyle, +) : BridgeWebView(context, attrs, defStyle) { + +} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/refill/list/holder/BatteryHolder.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/refill/list/holder/BatteryHolder.kt index 4800a677d..44c82bc7a 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/refill/list/holder/BatteryHolder.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/battery/refill/list/holder/BatteryHolder.kt @@ -3,14 +3,12 @@ package com.tonapps.tonkeeper.ui.screen.battery.refill.list.holder import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.method.LinkMovementMethod -import android.util.Log import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.AppCompatTextView import com.tonapps.tonkeeper.ui.screen.battery.refill.list.Item import com.tonapps.tonkeeper.view.BatteryView import com.tonapps.tonkeeperx.R -import com.tonapps.uikit.color.accentBlueColor import com.tonapps.uikit.color.accentOrangeColor import com.tonapps.uikit.color.stateList import com.tonapps.uikit.color.textAccentColor diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/dapp/DAppScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/dapp/DAppScreen.kt index 5a6f054e7..4b93084ab 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/dapp/DAppScreen.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/dapp/DAppScreen.kt @@ -21,6 +21,7 @@ import com.tonapps.tonkeeper.deeplink.DeepLink import com.tonapps.tonkeeper.deeplink.DeepLinkRoute import com.tonapps.tonkeeper.extensions.copyToClipboard import com.tonapps.tonkeeper.extensions.normalizeTONSites +import com.tonapps.tonkeeper.extensions.withUtmSource import com.tonapps.tonkeeper.koin.walletViewModel import com.tonapps.tonkeeper.manager.tonconnect.ConnectRequest import com.tonapps.tonkeeper.manager.tonconnect.TonConnect @@ -31,7 +32,9 @@ import com.tonapps.tonkeeper.manager.tonconnect.bridge.model.BridgeError import com.tonapps.tonkeeper.manager.tonconnect.bridge.model.BridgeEvent import com.tonapps.tonkeeper.manager.tonconnect.bridge.model.BridgeMethod import com.tonapps.tonkeeper.popup.ActionSheet +import com.tonapps.tonkeeper.ui.base.InjectedTonConnectScreen import com.tonapps.tonkeeper.ui.base.WalletContextScreen +import com.tonapps.tonkeeper.ui.component.TonConnectWebView import com.tonapps.tonkeeper.ui.screen.root.RootViewModel import com.tonapps.tonkeeper.ui.screen.send.transaction.SendTransactionScreen import com.tonapps.tonkeeperx.R @@ -54,10 +57,7 @@ import uikit.widget.webview.WebViewFixed import uikit.widget.webview.bridge.BridgeWebView import java.util.concurrent.CancellationException -class DAppScreen(wallet: WalletEntity): WalletContextScreen(R.layout.fragment_dapp, wallet) { - - private val api: API by inject() - private val tonConnectManager: TonConnectManager by inject() +class DAppScreen(wallet: WalletEntity): InjectedTonConnectScreen(R.layout.fragment_dapp, wallet) { private lateinit var headerDrawable: HeaderDrawable private lateinit var headerView: View @@ -67,10 +67,12 @@ class DAppScreen(wallet: WalletEntity): WalletContextScreen(R.layout.fragment_da private lateinit var menuView: View private lateinit var closeView: View private lateinit var refreshView: SwipeRefreshLayout - private lateinit var webView: BridgeWebView + override lateinit var webView: TonConnectWebView private val args: DAppArgs by lazy { DAppArgs(requireArguments()) } - private val rootViewModel: RootViewModel by activityViewModel() + + override val startUri: Uri + get() = args.url override val viewModel: DAppViewModel by walletViewModel { parametersOf(args.url) @@ -81,19 +83,7 @@ class DAppScreen(wallet: WalletEntity): WalletContextScreen(R.layout.fragment_da private val webViewCallback = object : WebViewFixed.Callback() { override fun shouldOverrideUrlLoading(request: WebResourceRequest): Boolean { - val refererUri = request.requestHeaders?.get("Referer")?.toUri() - val url = request.url.normalizeTONSites() - val scheme = url.scheme ?: "" - if (scheme == "https") { - return false - } - val deeplink = DeepLink(url, false, refererUri) - if (deeplink.route is DeepLinkRoute.TonConnect) { - rootViewModel.processTonConnectDeepLink(deeplink, null) - } else if (deeplink.route is DeepLinkRoute.Unknown) { - navigation?.openURL(url.toString()) - } - return true + return overrideUrlLoading(request) } override fun onPageStarted(url: String, favicon: Bitmap?) { @@ -122,7 +112,7 @@ class DAppScreen(wallet: WalletEntity): WalletContextScreen(R.layout.fragment_da override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - AnalyticsHelper.trackEventClickDApp(args.url.toString(), rootViewModel.installId) + AnalyticsHelper.trackEventClickDApp(args.url.toString(), installId) } private fun applyHost(url: String) { @@ -161,14 +151,14 @@ class DAppScreen(wallet: WalletEntity): WalletContextScreen(R.layout.fragment_da webView.settings.loadWithOverviewMode = true webView.addCallback(webViewCallback) webView.jsBridge = DAppBridge( - deviceInfo = JsonBuilder.device(wallet.maxMessages, requireContext().appVersionName).toString(), - send = ::send, + deviceInfo = deviceInfo.toString(), + send = ::tonconnectSend, connect = ::tonconnect, restoreConnection = viewModel::restoreConnection, disconnect = { viewModel.disconnect() }, - tonapiFetch = api::tonapiFetch, + tonapiFetch = ::tonapiFetch, ) - webView.loadUrl(args.url) + webView.loadUrl(args.url.withUtmSource()) refreshView = view.findViewById(R.id.refresh) refreshView.setColorSchemeColors(requireContext().tabBarActiveIconColor) @@ -198,35 +188,6 @@ class DAppScreen(wallet: WalletEntity): WalletContextScreen(R.layout.fragment_da } } - private suspend fun send(array: JSONArray): JSONObject { - val messages = BridgeEvent.Message.parse(array) - if (messages.size == 1) { - val message = messages.first() - val id = message.id - if (message.method != BridgeMethod.SEND_TRANSACTION) { - return JsonBuilder.responseError(id, BridgeError.methodNotSupported("Method \"${message.method}\" not supported.")) - } - val signRequests = message.params.map { SignRequestEntity(it, args.url) } - if (signRequests.size != 1) { - return JsonBuilder.responseError(id, BridgeError.badRequest("Request contains excess transactions. Required: 1, Provided: ${signRequests.size}")) - } - val signRequest = signRequests.first() - return try { - val boc = SendTransactionScreen.run(requireContext(), wallet, signRequest) - JsonBuilder.responseSendTransaction(id, boc) - } catch (e: CancellationException) { - JsonBuilder.responseError(id, BridgeError.userDeclinedTransaction()) - } catch (e: BridgeException) { - JsonBuilder.responseError(id, BridgeError.badRequest(e.bestMessage)) - } catch (e: Throwable) { - FirebaseCrashlytics.getInstance().recordException(e) - JsonBuilder.responseError(id, BridgeError.unknown(e.bestMessage)) - } - } else { - return JsonBuilder.responseError(0, BridgeError.badRequest("Request contains excess messages. Required: 1, Provided: ${messages.size}")) - } - } - private fun setDefaultState() { menuView.setOnClickListener { openDefaultMenu(it) } } @@ -275,25 +236,6 @@ class DAppScreen(wallet: WalletEntity): WalletContextScreen(R.layout.fragment_da startActivity(shareIntent) } - private suspend fun tonconnect( - version: Int, - request: ConnectRequest - ): JSONObject { - if (version != 2) { - return JsonBuilder.connectEventError(BridgeError.badRequest("Version $version is not supported")) - } - val activity = requireContext().activity ?: return JsonBuilder.connectEventError(BridgeError.unknown("internal client error")) - if (tonConnectManager.isScam(requireContext(), request.manifestUrl.toUri(), webView.url!!.toUri(), args.url)) { - return JsonBuilder.connectEventError(BridgeError.unknown("internal client error")) - } - - return tonConnectManager.launchConnectFlow( - activity = activity, - tonConnect = TonConnect.fromJsInject(request, webView.url?.toUri()), - wallet = wallet - ) - } - override fun onResume() { super.onResume() webView.addCallback(webViewCallback) @@ -304,19 +246,6 @@ class DAppScreen(wallet: WalletEntity): WalletContextScreen(R.layout.fragment_da webView.removeCallback(webViewCallback) } - private fun back() { - if (webView.canGoBack()) { - webView.goBack() - } else { - finish() - } - } - - override fun onBackPressed(): Boolean { - back() - return false - } - override fun onDestroyView() { super.onDestroyView() webView.removeCallback(webViewCallback) diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/dapp/DAppViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/dapp/DAppViewModel.kt index c7981310a..f4067f381 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/dapp/DAppViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/dapp/DAppViewModel.kt @@ -9,6 +9,7 @@ import com.tonapps.tonkeeper.manager.tonconnect.TonConnectManager import com.tonapps.tonkeeper.manager.tonconnect.bridge.JsonBuilder import com.tonapps.tonkeeper.manager.tonconnect.bridge.model.BridgeError import com.tonapps.tonkeeper.ui.base.BaseWalletVM +import com.tonapps.tonkeeper.ui.base.InjectedTonConnectScreen import com.tonapps.tonkeeper.worker.DAppPushToggleWorker import com.tonapps.wallet.data.account.entities.WalletEntity import com.tonapps.wallet.data.dapps.entities.AppConnectEntity @@ -26,15 +27,9 @@ import kotlin.time.Duration.Companion.seconds class DAppViewModel( app: Application, private val wallet: WalletEntity, - private val url: Uri, - private val tonConnectManager: TonConnectManager -): BaseWalletVM(app) { - - val connectionFlow = tonConnectManager.walletConnectionsFlow(wallet).filterList { connection -> - connection.type == AppConnectEntity.Type.Internal && connection.appUrl.host == url.host - }.map { - it.firstOrNull() - } + private val tonConnectManager: TonConnectManager, + override val url: Uri, +): InjectedTonConnectScreen.ViewModel(app, wallet, tonConnectManager) { fun mute() { DAppPushToggleWorker.run( @@ -44,21 +39,4 @@ class DAppViewModel( enable = false ) } - - fun disconnect() { - tonConnectManager.disconnect(wallet, url, AppConnectEntity.Type.Internal) - } - - suspend fun restoreConnection(): JSONObject { - val connection = loadConnection() - return if (connection == null) { - JsonBuilder.connectEventError(BridgeError.unknownApp()) - } else { - JsonBuilder.connectEventSuccess(wallet, null, null, context.appVersionName) - } - } - - private suspend fun loadConnection(): AppConnectEntity? { - return connectionFlow.firstOrNull() - } } \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/search/BrowserSearchScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/search/BrowserSearchScreen.kt index 542d72820..2f9c1b933 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/search/BrowserSearchScreen.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/search/BrowserSearchScreen.kt @@ -95,8 +95,12 @@ class BrowserSearchScreen(wallet: WalletEntity): WalletContextScreen(R.layout.fr } private fun inputDone() { - BrowserSearchViewModel.parseIfUrl(searchInput.text.toString())?.let { - navigation?.add(DAppScreen.newInstance(wallet, url = it)) + val query = searchInput.text.toString() + val url = BrowserSearchViewModel.parseIfUrl(query) + if (url != null) { + navigation?.add(DAppScreen.newInstance(wallet, url = url)) + } else { + navigation?.add(DAppScreen.newInstance(wallet, url = viewModel.createSearchUrl(query))) } searchInput.hideKeyboard() } diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/search/BrowserSearchViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/search/BrowserSearchViewModel.kt index 342ce6685..7ed70e362 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/search/BrowserSearchViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/browser/search/BrowserSearchViewModel.kt @@ -87,6 +87,15 @@ class BrowserSearchViewModel( } } + fun createSearchUrl(query: String): Uri { + val searchEngine = settingsRepository.searchEngine + return if (searchEngine == SearchEngine.GOOGLE) { + Uri.parse("https://www.google.com/search?q=$query") + } else { + Uri.parse("https://duckduckgo.com/?q=$query") + } + } + private fun searchBy(query: String): List { val searchEngine = settingsRepository.searchEngine val result = if (searchEngine == SearchEngine.GOOGLE) { diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/card/CardBridge.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/card/CardBridge.kt new file mode 100644 index 000000000..0c115d5f4 --- /dev/null +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/card/CardBridge.kt @@ -0,0 +1,166 @@ +package com.tonapps.tonkeeper.ui.screen.card + +import com.tonapps.tonkeeper.manager.tonconnect.ConnectRequest +import okhttp3.Headers +import okhttp3.Response +import org.json.JSONArray +import org.json.JSONObject +import uikit.widget.webview.bridge.JsBridge +import uikit.widget.webview.bridge.message.BridgeMessage + +class CardBridge( + val deviceInfo: String, + val isWalletBrowser: Boolean = true, + val protocolVersion: Int = 2, + val send: suspend (array: JSONArray) -> JSONObject, + val connect: suspend (protocolVersion: Int, request: ConnectRequest) -> JSONObject, + val restoreConnection: suspend () -> JSONObject, + val disconnect: suspend () -> Unit, + val tonapiFetch: suspend (url: String, options: String) -> Response +): JsBridge("tonkeeper") { + + override val availableFunctions = arrayOf("send", "connect", "restoreConnection", "disconnect") + + init { + keys["deviceInfo"] = deviceInfo + keys["protocolVersion"] = protocolVersion + keys["isWalletBrowser"] = isWalletBrowser + } + + override suspend fun invokeFunction(name: String, args: JSONArray): Any? { + return when (name) { + "connect" -> connect(protocolVersion, ConnectRequest.parse(args.getJSONObject(1))).toString() + "send" -> send(args).toString() + "restoreConnection" -> restoreConnection().toString() + "disconnect" -> disconnect() + "tonapi.fetch" -> { + val response = tonapiFetch(args.getString(0), args.optString(1) ?: "") + webAPIResponse(response).toString() + } + else -> null + } + } + + private fun webAPIResponse(response: Response): JSONObject { + val body = response.body?.string() ?: "" + val json = JSONObject() + json.put("body", body) + json.put("ok", response.isSuccessful) + json.put("status", response.code) + json.put("statusText", response.message) + json.put("type", webAPIResponseType(response.code)) + json.put("headers", webAPIResponseHeaders(response.headers)) + json.put("redirected", response.isRedirect) + json.put("url", response.request.url.toString()) + return json + } + + private fun webAPIResponseHeaders(headers: Headers): JSONObject { + val json = JSONObject() + for (i in 0 until headers.size) { + json.put(headers.name(i), headers.value(i)) + } + return json + } + + private fun webAPIResponseType(code: Int): String { + return when (code) { + 0 -> "error" + else -> "cors" + } + } + + override fun jsInjection(): String { + val funcs = availableFunctions.joinToString(",") {""" + $it: (...args) => { + return new Promise((resolve, reject) => window.invokeRnFunc('${it}', args, resolve, reject)) + } + """}.replace("\n", "").replace(" ", "") + + return """ + (() => { + if (!window.${windowKey}) { + window.rnPromises = {}; + window.rnEventListeners = []; + window.invokeRnFunc = (name, args, resolve, reject) => { + const invocationId = btoa(Math.random()).substring(0, 12); + const timeoutMs = ${timeout}; + const timeoutId = timeoutMs ? setTimeout(() => reject(new Error('bridge timeout for function with name: '+name+'')), timeoutMs) : null; + window.rnPromises[invocationId] = { resolve, reject, timeoutId } + window.ReactNativeWebView.postMessage(JSON.stringify({ + type: '${BridgeMessage.Type.InvokeRnFunc.value}', + invocationId: invocationId, + name, + args, + })); + }; + + window.addEventListener('message', ({ data }) => { + try { + const message = data; + console.log('message bridge', JSON.stringify(message)); + if (message.type === '${BridgeMessage.Type.FunctionResponse.value}') { + const promise = window.rnPromises[message.invocationId]; + + if (!promise) { + return; + } + + if (promise.timeoutId) { + clearTimeout(promise.timeoutId); + } + + if (message.status === 'fulfilled') { + promise.resolve(JSON.parse(message.data)); + } else { + promise.reject(new Error(message.data)); + } + + delete window.rnPromises[message.invocationId]; + } + + if (message.type === '${BridgeMessage.Type.Event.value}') { + window.rnEventListeners.forEach((listener) => listener(message.event)); + } + } catch { } + }); + } + + const listen = (cb) => { + window.rnEventListeners.push(cb); + return () => { + const index = window.rnEventListeners.indexOf(cb); + if (index > -1) { + window.rnEventListeners.splice(index, 1); + } + }; + }; + + window.${windowKey} = { + tonconnect: Object.assign(${JSONObject(keys)},{ $funcs },{ listen }) + }; + + window.tonapi = { + fetch: async (url, options) => { + return new Promise((resolve, reject) => { + window.invokeRnFunc('tonapi.fetch', [url, options], (result) => { + try { + const headers = new Headers(result.headers); + const response = new Response(result.body, { + status: result.status, + statusText: result.statusText, + headers: headers + }); + resolve(response); + } catch (e) { + reject(e); + } + }, reject) + }); + } + }; + })(); + """ + } + +} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/card/CardScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/card/CardScreen.kt new file mode 100644 index 000000000..3fe834275 --- /dev/null +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/card/CardScreen.kt @@ -0,0 +1,106 @@ +package com.tonapps.tonkeeper.ui.screen.card + +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.webkit.WebResourceRequest +import androidx.core.net.toUri +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tonapps.extensions.appVersionName +import com.tonapps.extensions.bestMessage +import com.tonapps.tonkeeper.koin.walletViewModel +import com.tonapps.tonkeeper.manager.tonconnect.ConnectRequest +import com.tonapps.tonkeeper.manager.tonconnect.TonConnect +import com.tonapps.tonkeeper.manager.tonconnect.TonConnectManager +import com.tonapps.tonkeeper.manager.tonconnect.bridge.BridgeException +import com.tonapps.tonkeeper.manager.tonconnect.bridge.JsonBuilder +import com.tonapps.tonkeeper.manager.tonconnect.bridge.model.BridgeError +import com.tonapps.tonkeeper.manager.tonconnect.bridge.model.BridgeEvent +import com.tonapps.tonkeeper.manager.tonconnect.bridge.model.BridgeMethod +import com.tonapps.tonkeeper.ui.base.InjectedTonConnectScreen +import com.tonapps.tonkeeper.ui.base.WalletContextScreen +import com.tonapps.tonkeeper.ui.component.TonConnectWebView +import com.tonapps.tonkeeper.ui.screen.send.transaction.SendTransactionScreen +import com.tonapps.tonkeeperx.R +import com.tonapps.wallet.api.API +import com.tonapps.wallet.data.account.entities.WalletEntity +import com.tonapps.wallet.data.core.entity.SignRequestEntity +import org.json.JSONArray +import org.json.JSONObject +import org.koin.android.ext.android.inject +import uikit.base.BaseFragment +import uikit.extensions.activity +import uikit.widget.webview.WebViewFixed +import java.util.concurrent.CancellationException + +class CardScreen(wallet: WalletEntity): InjectedTonConnectScreen(R.layout.fragment_card, wallet), BaseFragment.SwipeBack { + + override val viewModel: CardViewModel by walletViewModel() + + override lateinit var webView: TonConnectWebView + + override val startUri: Uri + get() = viewModel.url + + private val webViewCallback = object : WebViewFixed.Callback() { + override fun shouldOverrideUrlLoading(request: WebResourceRequest): Boolean { + return overrideUrlLoading(request) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + webView = view.findViewById(R.id.webView) + webView.settings.useWideViewPort = true + webView.settings.loadWithOverviewMode = true + webView.addCallback(webViewCallback) + webView.jsBridge = CardBridge( + deviceInfo = deviceInfo.toString(), + send = ::tonconnectSend, + connect = ::tonconnect, + restoreConnection = viewModel::restoreConnection, + disconnect = { viewModel.disconnect() }, + tonapiFetch = ::tonapiFetch, + ) + webView.loadUrl(viewModel.url.toString()) + + ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets -> + val statusInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars()) + val bottomInsets = insets.getInsets(WindowInsetsCompat.Type.ime() or WindowInsetsCompat.Type.navigationBars()) + applyWebViewOffset(statusInsets.top, bottomInsets.bottom) + insets + } + } + + override fun onResume() { + super.onResume() + webView.addCallback(webViewCallback) + } + + override fun onPause() { + super.onPause() + webView.removeCallback(webViewCallback) + } + + override fun onDestroyView() { + super.onDestroyView() + webView.removeCallback(webViewCallback) + webView.destroy() + } + + private fun applyWebViewOffset(top: Int, bottom: Int) { + webView.updateLayoutParams { + topMargin = top + bottomMargin = bottom + } + } + + companion object { + + fun newInstance(wallet: WalletEntity) = CardScreen(wallet) + } +} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/card/CardViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/card/CardViewModel.kt new file mode 100644 index 000000000..b115ba5da --- /dev/null +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/card/CardViewModel.kt @@ -0,0 +1,38 @@ +package com.tonapps.tonkeeper.ui.screen.card + +import android.app.Application +import android.net.Uri +import com.tonapps.extensions.appVersionName +import com.tonapps.extensions.filterList +import com.tonapps.extensions.locale +import com.tonapps.tonkeeper.manager.tonconnect.TonConnectManager +import com.tonapps.tonkeeper.manager.tonconnect.bridge.JsonBuilder +import com.tonapps.tonkeeper.manager.tonconnect.bridge.model.BridgeError +import com.tonapps.tonkeeper.ui.base.BaseWalletVM +import com.tonapps.tonkeeper.ui.base.InjectedTonConnectScreen +import com.tonapps.wallet.data.account.entities.WalletEntity +import com.tonapps.wallet.data.dapps.entities.AppConnectEntity +import com.tonapps.wallet.data.settings.SettingsRepository +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import org.json.JSONObject + +class CardViewModel( + app: Application, + private val wallet: WalletEntity, + private val tonConnectManager: TonConnectManager, + private val settingsRepository: SettingsRepository, +): InjectedTonConnectScreen.ViewModel(app, wallet, tonConnectManager) { + + override val url: Uri by lazy { + val builder = Uri.parse("https://next.holders.io").buildUpon() + builder.appendQueryParameter("lang", context.locale.language) + builder.appendQueryParameter("currency", settingsRepository.currency.code) + builder.appendQueryParameter("theme", "holders") + builder.appendQueryParameter("theme-style", if (settingsRepository.theme.light) "light" else "dark") + builder.appendQueryParameter("utm_source", "tonkeeper") + builder.build() + } + + +} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/collectibles/manage/CollectiblesManageViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/collectibles/manage/CollectiblesManageViewModel.kt index 5dc836e45..1005384d4 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/collectibles/manage/CollectiblesManageViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/collectibles/manage/CollectiblesManageViewModel.kt @@ -35,6 +35,8 @@ class CollectiblesManageViewModel( private val settingsRepository: SettingsRepository, ): BaseWalletVM(app) { + private val safeMode = settingsRepository.safeMode + private val _showedAllFlow = MutableStateFlow(false) private val showedAllFlow = _showedAllFlow.asStateFlow() @@ -160,23 +162,21 @@ class CollectiblesManageViewModel( private suspend fun collectionItems(collectibles: List): List { val items = mutableListOf() for (nft in collectibles) { + if (safeMode && !nft.verified) { + continue + } val collection = nft.collection ?: continue val index = items.indexOfFirst { it.address.equalsAddress(collection.address) } if (index == -1) { val state = settingsRepository.getTokenPrefs(wallet.id, nft.address).state - val spam = if (state == State.TRUST) { - false - } else { - nft.trust == Trust.blacklist - } items.add(Item.Collection( address = collection.address, title = collection.name, imageUri = nft.thumbUri, count = 1, - spam = spam + spam = state == State.SPAM )) } else { items[index] = items[index].copy( diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/dev/DevScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/dev/DevScreen.kt index 5269ab3ba..d85fefd99 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/dev/DevScreen.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/dev/DevScreen.kt @@ -37,6 +37,7 @@ class DevScreen: BaseWalletScreen(R.layout.fragment_dev, Scr private lateinit var logCopy: Button private lateinit var importPasscodeView: View private lateinit var importDAppsView: View + private lateinit var systemFontSizeView: ItemSwitchView override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -55,7 +56,16 @@ class DevScreen: BaseWalletScreen(R.layout.fragment_dev, Scr blurView.doOnCheckedChanged = { isChecked, byUser -> if (byUser) { DevSettings.blurEnabled = isChecked - requireContext().showToast("Restart app to apply changes") + toastAfterChange() + } + } + + systemFontSizeView = view.findViewById(R.id.ignore_system_font_size) + systemFontSizeView.setChecked(DevSettings.ignoreSystemFontSize, false) + systemFontSizeView.doOnCheckedChanged = { isChecked, byUser -> + if (byUser) { + DevSettings.ignoreSystemFontSize = isChecked + toastAfterChange() } } @@ -64,7 +74,7 @@ class DevScreen: BaseWalletScreen(R.layout.fragment_dev, Scr tonConnectLogsView.doOnCheckedChanged = { isChecked, byUser -> if (byUser) { DevSettings.tonConnectLogs = isChecked - requireContext().showToast("Restart app to apply changes") + toastAfterChange() } } @@ -89,8 +99,16 @@ class DevScreen: BaseWalletScreen(R.layout.fragment_dev, Scr valuesFromLegacy() } + view.findViewById(R.id.card).setOnLongClickListener { + viewModel.openCard() + true + } + logCopy = view.findViewById(R.id.log_copy) + } + private fun toastAfterChange() { + requireContext().showToast("Restart app to apply changes") } private fun valuesFromLegacy() { diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/dev/DevViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/dev/DevViewModel.kt index 2af781558..62fcca7bb 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/dev/DevViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/dev/DevViewModel.kt @@ -7,10 +7,13 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tonapps.extensions.bestMessage import com.tonapps.tonkeeper.extensions.requestVault import com.tonapps.tonkeeper.ui.base.BaseWalletVM +import com.tonapps.tonkeeper.ui.screen.card.CardScreen import com.tonapps.wallet.data.account.AccountRepository import com.tonapps.wallet.data.dapps.DAppsRepository import com.tonapps.wallet.data.rn.RNLegacy import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.ton.mnemonic.Mnemonic @@ -32,6 +35,13 @@ class DevViewModel( }*/ } + fun openCard() { + accountRepository.selectedWalletFlow.take(1).onEach { + openScreen(CardScreen.newInstance(it)) + finish() + }.launch() + } + fun importApps(callback: (result: String) -> Unit) { viewModelScope.launch(Dispatchers.IO) { val lines = mutableListOf() diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/nft/NftViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/nft/NftViewModel.kt index d94c9bf3c..bde3cf0ce 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/nft/NftViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/nft/NftViewModel.kt @@ -46,7 +46,7 @@ class NftViewModel( fun hideCollection(callback: () -> Unit) { viewModelScope.launch(Dispatchers.IO) { val address = nft.collection?.address ?: nft.address - settingsRepository.setTokenState(wallet.id, address, TokenPrefsEntity.State.SPAM) + settingsRepository.setTokenHidden(wallet.id, address, true) withContext(Dispatchers.Main) { callback() } 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 21a7058c6..292aa621c 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 @@ -13,11 +13,14 @@ import androidx.biometric.BiometricPrompt import androidx.core.app.ActivityCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.core.view.postDelayed import androidx.core.view.updatePadding import com.tonapps.blockchain.ton.extensions.base64 import com.tonapps.extensions.currentTimeSeconds +import com.tonapps.extensions.print import com.tonapps.extensions.toUriOrNull import com.tonapps.tonkeeper.App +import com.tonapps.tonkeeper.core.DevSettings import com.tonapps.tonkeeper.deeplink.DeepLink import com.tonapps.tonkeeper.extensions.isDarkMode import com.tonapps.tonkeeper.extensions.toast @@ -25,6 +28,7 @@ import com.tonapps.tonkeeper.helper.BrowserHelper import com.tonapps.tonkeeper.ui.base.BaseWalletActivity import com.tonapps.tonkeeper.ui.base.QRCameraScreen import com.tonapps.tonkeeper.ui.base.WalletFragmentFactory +import com.tonapps.tonkeeper.ui.screen.card.CardScreen 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 @@ -57,6 +61,7 @@ import uikit.extensions.collectFlow import uikit.extensions.findFragment import uikit.extensions.runAnimation import uikit.extensions.withAlpha +import uikit.navigation.Navigation.Companion.navigation class RootActivity: BaseWalletActivity() { @@ -120,9 +125,11 @@ class RootActivity: BaseWalletActivity() { } override fun attachBaseContext(newBase: Context) { - /*val newConfig = Configuration(newBase.resources.configuration) - newConfig.fontScale = 1.0f - applyOverrideConfiguration(newConfig)*/ + if (DevSettings.ignoreSystemFontSize) { + val newConfig = Configuration(newBase.resources.configuration) + newConfig.fontScale = 1.0f + applyOverrideConfiguration(newConfig) + } super.attachBaseContext(newBase) } 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 b85e043ff..b3f7abba7 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 @@ -49,6 +49,7 @@ 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.card.CardScreen import com.tonapps.tonkeeper.ui.screen.init.list.AccountItem import com.tonapps.tonkeeper.ui.screen.name.edit.EditNameScreen import com.tonapps.tonkeeper.ui.screen.purchase.PurchaseScreen @@ -96,6 +97,7 @@ import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext import org.ton.cell.Cell import uikit.extensions.activity +import uikit.navigation.Navigation.Companion.navigation import java.util.concurrent.CancellationException class RootViewModel( diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/settings/main/SettingsScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/settings/main/SettingsScreen.kt index dd9d322ac..975f2602a 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/settings/main/SettingsScreen.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/settings/main/SettingsScreen.kt @@ -2,7 +2,6 @@ package com.tonapps.tonkeeper.ui.screen.settings.main import android.content.Intent import android.os.Bundle -import android.util.Log import android.view.View import androidx.core.net.toUri import com.google.android.play.core.review.ReviewInfo @@ -26,7 +25,7 @@ import com.tonapps.tonkeeper.ui.screen.settings.main.list.Adapter import com.tonapps.tonkeeper.ui.screen.settings.main.list.Item import com.tonapps.tonkeeper.ui.screen.settings.security.SecurityScreen import com.tonapps.tonkeeper.ui.screen.settings.theme.ThemeScreen -import com.tonapps.tonkeeper.ui.screen.w5.stories.W5StoriesScreen +import com.tonapps.tonkeeper.ui.screen.stories.w5.W5StoriesScreen import com.tonapps.uikit.icon.UIKitIcon import com.tonapps.wallet.data.account.entities.WalletEntity import com.tonapps.wallet.data.core.SearchEngine diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/settings/security/SecurityScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/settings/security/SecurityScreen.kt index 4c58522b4..59533b90a 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/settings/security/SecurityScreen.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/settings/security/SecurityScreen.kt @@ -2,16 +2,20 @@ package com.tonapps.tonkeeper.ui.screen.settings.security import android.os.Bundle import android.view.View +import androidx.appcompat.widget.AppCompatTextView import androidx.lifecycle.lifecycleScope import com.tonapps.tonkeeper.ui.base.BaseWalletScreen import com.tonapps.tonkeeper.ui.base.ScreenContext import com.tonapps.tonkeeper.ui.screen.settings.passcode.ChangePasscodeScreen +import com.tonapps.tonkeeper.ui.screen.stories.safemode.SafeModeStoriesScreen import com.tonapps.tonkeeperx.R import com.tonapps.wallet.data.passcode.PasscodeBiometric +import com.tonapps.wallet.localization.Localization import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import org.koin.androidx.viewmodel.ext.android.viewModel import uikit.base.BaseFragment +import uikit.extensions.getSpannable import uikit.navigation.Navigation.Companion.navigation import uikit.widget.HeaderView import uikit.widget.item.ItemIconView @@ -67,6 +71,12 @@ class SecurityScreen: BaseWalletScreen(R.layout.fragment_sec viewModel.safeMode = checked } } + + val safeModeDescriptionView = view.findViewById(R.id.safe_mode_description) + safeModeDescriptionView.text = requireContext().getSpannable(Localization.safe_mode_description) + safeModeDescriptionView.setOnClickListener { + navigation?.add(SafeModeStoriesScreen.newInstance()) + } } private fun enableBiometric(value: Boolean) { diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/stories/safemode/SafeModeStoriesScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/stories/safemode/SafeModeStoriesScreen.kt new file mode 100644 index 000000000..fdfd39b81 --- /dev/null +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/stories/safemode/SafeModeStoriesScreen.kt @@ -0,0 +1,36 @@ +package com.tonapps.tonkeeper.ui.screen.stories.safemode + +import android.os.Bundle +import android.view.View +import com.facebook.common.util.UriUtil +import com.tonapps.tonkeeperx.R +import com.tonapps.wallet.localization.Localization +import uikit.widget.stories.BaseStoriesScreen + +class SafeModeStoriesScreen: BaseStoriesScreen() { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val items = mutableListOf() + items.add(Item( + image = UriUtil.getUriForResourceId(R.drawable.safemode_stories_1), + title = getString(Localization.stories_safemode_1_title), + subtitle = getString(Localization.stories_safemode_1_subtitle) + )) + items.add(Item( + image = UriUtil.getUriForResourceId(R.drawable.safemode_stories_2), + title = getString(Localization.stories_safemode_2_title), + subtitle = getString(Localization.stories_safemode_2_subtitle) + )) + items.add(Item( + image = UriUtil.getUriForResourceId(R.drawable.safemode_stories_3), + title = getString(Localization.stories_safemode_3_title), + subtitle = getString(Localization.stories_safemode_3_subtitle) + )) + putItems(items) + } + + companion object { + fun newInstance() = SafeModeStoriesScreen() + } +} diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/StoryEntity.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/stories/w5/StoryEntity.kt similarity index 95% rename from apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/StoryEntity.kt rename to apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/stories/w5/StoryEntity.kt index 20c436788..2c7f48420 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/StoryEntity.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/stories/w5/StoryEntity.kt @@ -1,4 +1,4 @@ -package com.tonapps.tonkeeper.ui.screen.w5.stories +package com.tonapps.tonkeeper.ui.screen.stories.w5 import com.facebook.common.util.UriUtil import com.facebook.drawee.backends.pipeline.Fresco diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/stories/w5/W5StoriesScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/stories/w5/W5StoriesScreen.kt new file mode 100644 index 000000000..62e997250 --- /dev/null +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/stories/w5/W5StoriesScreen.kt @@ -0,0 +1,71 @@ +package com.tonapps.tonkeeper.ui.screen.stories.w5 + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.lifecycleScope +import com.facebook.common.util.UriUtil +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tonapps.tonkeeper.ui.screen.settings.main.SettingsScreen +import com.tonapps.tonkeeper.ui.screen.wallet.picker.PickerMode +import com.tonapps.tonkeeper.ui.screen.wallet.picker.PickerScreen +import com.tonapps.wallet.localization.Localization +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.koin.androidx.viewmodel.ext.android.viewModel +import uikit.navigation.Navigation +import uikit.widget.stories.BaseStoriesScreen + +class W5StoriesScreen: BaseStoriesScreen() { + + val viewModel: W5StoriesViewModel by viewModel() + + private val showAddButton: Boolean by lazy { requireArguments().getBoolean(ARG_ADD_BUTTON) } + + private val navigation: Navigation? + get() = context?.let { Navigation.from(it) } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val stories = mutableListOf() + for ((index, story) in StoryEntity.all.withIndex()) { + stories.add(Item( + image = UriUtil.getUriForResourceId(story.imageResId), + title = getString(story.titleResId), + subtitle = getString(story.descriptionResId), + button = if (showAddButton && index >= StoryEntity.all.size - 1) getString(Localization.w5_add_wallet) else null + )) + } + putItems(stories) + } + + override fun onStoryButton(index: Int) { + super.onStoryButton(index) + if (isLastStory) { + addWallet() + } + } + + private fun addWallet() { + viewModel.addWallet(requireContext()).catch { + FirebaseCrashlytics.getInstance().recordException(it) + }.onEach { walletId -> + navigation?.add(PickerScreen.newInstance(PickerMode.Focus(walletId))) + navigation?.removeByClass({ + finish() + }, SettingsScreen::class.java) + }.launchIn(lifecycleScope) + } + + companion object { + + private const val ARG_ADD_BUTTON = "add_button" + + fun newInstance(addButton: Boolean): W5StoriesScreen { + StoryEntity.prefetchImages() + val fragment = W5StoriesScreen() + fragment.putBooleanArg(ARG_ADD_BUTTON, addButton) + return fragment + } + } +} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/stories/w5/W5StoriesViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/stories/w5/W5StoriesViewModel.kt new file mode 100644 index 000000000..d91a8dfef --- /dev/null +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/stories/w5/W5StoriesViewModel.kt @@ -0,0 +1,46 @@ +package com.tonapps.tonkeeper.ui.screen.stories.w5 + +import android.app.Application +import android.content.Context +import com.tonapps.blockchain.ton.contract.WalletVersion +import com.tonapps.tonkeeper.ui.base.BaseWalletVM +import com.tonapps.wallet.data.account.AccountRepository +import com.tonapps.wallet.data.account.Wallet +import com.tonapps.wallet.data.backup.BackupRepository +import com.tonapps.wallet.data.backup.entities.BackupEntity +import com.tonapps.wallet.data.passcode.PasscodeManager +import com.tonapps.wallet.data.rn.RNLegacy +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.take + +class W5StoriesViewModel( + app: Application, + private val accountRepository: AccountRepository, + private val passcodeManager: PasscodeManager, + private val backupRepository: BackupRepository, + private val rnLegacy: RNLegacy, +): BaseWalletVM(app) { + + fun addWallet(context: Context) = accountRepository.selectedWalletFlow.take(1) + .map { wallet -> + val fixedLabel = wallet.label.name.replace(wallet.version.title, "") + " " + WalletVersion.V5R1.title + accountRepository.addWallet( + ids = listOf(AccountRepository.newWalletId()), + label = Wallet.NewLabel(listOf(fixedLabel), wallet.label.emoji, wallet.label.color), + publicKey = wallet.publicKey, + versions = listOf(WalletVersion.V5R1), + type = wallet.type + ).first() + }.map { wallet -> + val mnemonic = accountRepository.getMnemonic(wallet.id) ?: throw Exception("mnemonic not found") + val passcode = passcodeManager.requestValidPasscode(context) + rnLegacy.addMnemonics(passcode, listOf(wallet.id), mnemonic.toList()) + wallet + }.map { wallet -> + backupRepository.addBackup(wallet.id, BackupEntity.Source.LOCAL) + wallet.id + }.flowOn(Dispatchers.IO) + +} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/token/picker/TokenPickerViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/token/picker/TokenPickerViewModel.kt index e8e3d7893..487febcb3 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/token/picker/TokenPickerViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/token/picker/TokenPickerViewModel.kt @@ -14,6 +14,7 @@ import com.tonapps.wallet.data.account.AccountRepository import com.tonapps.wallet.data.account.entities.WalletEntity import com.tonapps.wallet.data.settings.SettingsRepository import com.tonapps.wallet.data.token.TokenRepository +import com.tonapps.wallet.data.token.entities.AccountTokenEntity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow @@ -33,6 +34,8 @@ class TokenPickerViewModel( private val tokenRepository: TokenRepository, ): BaseWalletVM(app) { + private val safeMode: Boolean = settingsRepository.safeMode + private val _selectedTokenFlow = MutableStateFlow(selectedToken) private val selectedTokenFlow = _selectedTokenFlow.asStateFlow().filterNotNull() @@ -44,12 +47,24 @@ class TokenPickerViewModel( it.balance.isTransferable } ?: emptyList() - if (allowedTokens.isNotEmpty()) { + val list = if (allowedTokens.isNotEmpty()) { tokens.filter { allowedTokens.contains(it.address) } } else { tokens } - } + + if (safeMode) { + val safeModeList = mutableListOf< AccountTokenEntity>() + for (token in list) { + if (token.verified) { + safeModeList.add(token) + } + } + safeModeList + } else { + list + } + }.flowOn(Dispatchers.IO) private val searchTokensFlow = combine(tokensFlow, queryFlow) { tokens, query -> tokens.filter { it.symbol.contains(query, ignoreCase = true) } diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/token/viewer/list/holder/W5BannerHolder.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/token/viewer/list/holder/W5BannerHolder.kt index 226319f08..50830e0e6 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/token/viewer/list/holder/W5BannerHolder.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/token/viewer/list/holder/W5BannerHolder.kt @@ -4,7 +4,7 @@ import android.view.View import android.view.ViewGroup import com.tonapps.tonkeeper.koin.settingsRepository import com.tonapps.tonkeeper.ui.screen.token.viewer.list.Item -import com.tonapps.tonkeeper.ui.screen.w5.stories.W5StoriesScreen +import com.tonapps.tonkeeper.ui.screen.stories.w5.W5StoriesScreen import com.tonapps.tonkeeperx.R import com.tonapps.wallet.data.settings.SettingsRepository import uikit.navigation.Navigation diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/DeleteMe.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/DeleteMe.kt deleted file mode 100644 index 3c54d3c26..000000000 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/DeleteMe.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.tonapps.tonkeeper.ui.screen.w5 - -class DeleteMe { -} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/StoryProgressDrawable.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/StoryProgressDrawable.kt deleted file mode 100644 index 33ecac85a..000000000 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/StoryProgressDrawable.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tonapps.tonkeeper.ui.screen.w5.stories - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Paint -import com.tonapps.uikit.color.iconPrimaryColor -import uikit.base.BaseDrawable -import uikit.extensions.dp -import uikit.extensions.withAlpha - -class StoryProgressDrawable(context: Context): BaseDrawable() { - - var progress: Float = 0f - set(value) { - field = value - invalidateSelf() - } - - private val radius = 2f.dp - private val backgroundColor = context.iconPrimaryColor.withAlpha(.24f) - private val progressColor = context.iconPrimaryColor - - private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = backgroundColor - style = Paint.Style.FILL - } - - private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = progressColor - style = Paint.Style.FILL - } - - override fun draw(canvas: Canvas) { - val left = bounds.left.toFloat() - val top = bounds.top.toFloat() - val right = bounds.right.toFloat() - val bottom = bounds.bottom.toFloat() - - canvas.drawRoundRect(left, top, right, bottom, radius, radius, backgroundPaint) - - if (progress > 1f) return - val progressWidth = (right - left) * progress - canvas.drawRoundRect(left, top, left + progressWidth, bottom, radius, radius, progressPaint) - } -} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/StoryProgressView.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/StoryProgressView.kt deleted file mode 100644 index b60d8a3ed..000000000 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/StoryProgressView.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tonapps.tonkeeper.ui.screen.w5.stories - -import android.content.Context -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.util.AttributeSet -import android.view.View - -class StoryProgressView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyle: Int = 0, -) : View(context, attrs, defStyle) { - - private val drawable = StoryProgressDrawable(context) - - var progress: Float - get() = drawable.progress - set(value) { - drawable.progress = value - } - - init { - background = drawable - } -} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/W5StoriesScreen.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/W5StoriesScreen.kt deleted file mode 100644 index e3349de5b..000000000 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/W5StoriesScreen.kt +++ /dev/null @@ -1,156 +0,0 @@ -package com.tonapps.tonkeeper.ui.screen.w5.stories - -import android.annotation.SuppressLint -import android.os.Bundle -import android.view.Gravity -import android.view.MotionEvent -import android.view.View -import android.widget.FrameLayout -import android.widget.LinearLayout -import androidx.appcompat.widget.AppCompatTextView -import androidx.appcompat.widget.LinearLayoutCompat -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.lifecycleScope -import com.tonapps.tonkeeper.ui.base.BaseWalletScreen -import com.tonapps.tonkeeper.ui.base.ScreenContext -import com.tonapps.tonkeeper.ui.screen.settings.main.SettingsScreen -import com.tonapps.tonkeeper.ui.screen.wallet.picker.PickerMode -import com.tonapps.tonkeeper.ui.screen.wallet.picker.PickerScreen -import com.tonapps.tonkeeperx.R -import com.tonapps.wallet.localization.Localization -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import org.koin.androidx.viewmodel.ext.android.viewModel -import uikit.base.BaseFragment -import uikit.extensions.collectFlow -import uikit.extensions.dp -import uikit.extensions.getViews -import uikit.extensions.round -import uikit.widget.FrescoView -import uikit.widget.RowLayout - -class W5StoriesScreen: BaseWalletScreen(R.layout.fragment_w5_stories, ScreenContext.None) { - - override val viewModel: W5StoriesViewModel by viewModel() - - private val showAddButton: Boolean by lazy { requireArguments().getBoolean(ARG_ADD_BUTTON) } - - private lateinit var contentView: FrameLayout - private lateinit var linesView: RowLayout - private lateinit var imageView: FrescoView - private lateinit var titleView: AppCompatTextView - private lateinit var descriptionView: AppCompatTextView - private lateinit var addButton: View - - @SuppressLint("ClickableViewAccessibility") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - contentView = view.findViewById(R.id.content) - contentView.round(20f.dp) - contentView.setOnTouchListener { v, event -> - onTouchEvent(event) - true - } - - linesView = view.findViewById(R.id.lines) - - view.findViewById(R.id.close).setOnClickListener { finish() } - - imageView = view.findViewById(R.id.image) - - titleView = view.findViewById(R.id.title) - descriptionView = view.findViewById(R.id.description) - - addButton = view.findViewById(R.id.add) - addButton.setOnClickListener { addWallet() } - - ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets -> - val statusBarOffset = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top - val navBarOffset = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom - view.setPadding(0, statusBarOffset, 0, navBarOffset) - insets - } - - collectFlow(viewModel.storyFlow, ::applyStory) - collectFlow(viewModel.progressFlow) { (index, progress) -> - setProgress(index, progress) - } - applyLines() - } - - private fun setProgress(targetIndex: Int, progress: Float) { - val views = linesView.getViews().map { it as StoryProgressView } - for ((index, view) in views.withIndex()) { - if (targetIndex > index) { - view.progress = 1f - } else if (targetIndex == index) { - view.progress = progress - } else { - view.progress = 0f - } - } - } - - private fun onTouchEvent(event: MotionEvent) { - when (event.action) { - MotionEvent.ACTION_DOWN -> { - viewModel.pause() - } - MotionEvent.ACTION_UP -> { - val screenWidth = resources.displayMetrics.widthPixels - val next = event.x > screenWidth / 2 - viewModel.resume(next) - } - } - } - - private fun applyLines() { - linesView.removeAllViews() - - for (i in 0 until viewModel.stories.size) { - val view = StoryProgressView(requireContext()) - val params = LinearLayoutCompat.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, 4.dp, 1f) - params.gravity = Gravity.CENTER_VERTICAL - params.setMargins(2.dp, 0, 2.dp, 0) - linesView.addView(view, params) - } - } - - private fun addWallet() { - viewModel.addWallet(requireContext()).catch { - - - }.onEach { walletId -> - navigation?.add(PickerScreen.newInstance(PickerMode.Focus(walletId))) - navigation?.removeByClass({ - finish() - }, SettingsScreen::class.java) - }.launchIn(lifecycleScope) - } - - private fun applyStory(story: StoryEntity) { - imageView.setLocalRes(story.imageResId) - titleView.setText(story.titleResId) - descriptionView.setText(story.descriptionResId) - addButton.visibility = if (story.showButton && showAddButton) { - View.VISIBLE - } else { - View.GONE - } - } - - companion object { - - private const val ARG_ADD_BUTTON = "add_button" - - fun newInstance(addButton: Boolean): W5StoriesScreen { - StoryEntity.prefetchImages() - val fragment = W5StoriesScreen() - fragment.putBooleanArg(ARG_ADD_BUTTON, addButton) - return fragment - } - } -} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/W5StoriesViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/W5StoriesViewModel.kt deleted file mode 100644 index df14ba604..000000000 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/w5/stories/W5StoriesViewModel.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.tonapps.tonkeeper.ui.screen.w5.stories - -import android.app.Application -import android.content.Context -import android.util.Log -import android.view.ViewConfiguration -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.facebook.drawee.backends.pipeline.Fresco -import com.facebook.imagepipeline.request.ImageRequest -import com.google.common.util.concurrent.AtomicDouble -import com.tonapps.blockchain.ton.contract.WalletVersion -import com.tonapps.extensions.MutableEffectFlow -import com.tonapps.tonkeeper.ui.base.BaseWalletVM -import com.tonapps.wallet.data.account.AccountRepository -import com.tonapps.wallet.data.account.Wallet -import com.tonapps.wallet.data.backup.BackupRepository -import com.tonapps.wallet.data.backup.entities.BackupEntity -import com.tonapps.wallet.data.passcode.PasscodeManager -import com.tonapps.wallet.data.rn.RNLegacy -import com.tonapps.wallet.localization.Localization -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch - -class W5StoriesViewModel( - app: Application, - private val accountRepository: AccountRepository, - private val passcodeManager: PasscodeManager, - private val backupRepository: BackupRepository, - private val rnLegacy: RNLegacy, -): BaseWalletVM(app) { - - val stories = StoryEntity.all - private val autoSwitchDuration = 5000L - private val progressDelay = 8L - - private val _storyFlow = MutableEffectFlow() - val storyFlow = _storyFlow.asSharedFlow() - - private val progress = AtomicDouble(0.0) - private val _progressFlow = MutableEffectFlow>() - val progressFlow = _progressFlow.asSharedFlow() - - private var isAutoSwitchPaused = false - private var currentIndex = 0 - private var timerJob: Job? = null - private var lastPauseTime = 0L - - private val isLastStory: Boolean - get() = currentIndex == stories.size - 1 - - private val isFirstStory: Boolean - get() = currentIndex == 0 - - init { - applyCurrentStory() - } - - fun pause() { - stopStoryTimer() - isAutoSwitchPaused = true - lastPauseTime = System.currentTimeMillis() - } - - fun resume(next: Boolean) { - isAutoSwitchPaused = false - val elapsedSincePause = System.currentTimeMillis() - lastPauseTime - if (120 >= elapsedSincePause) { - if (next && !isLastStory) { - nextStory() - } else if (!next && !isFirstStory) { - prevStory() - } else { - startStoryTimer() - } - } else { - startStoryTimer() - } - } - - fun nextStory() { - if (isLastStory) { - setProgress(1.0) - return - } - currentIndex = (currentIndex + 1) % stories.size - applyCurrentStory() - } - - fun prevStory() { - if (isFirstStory) { - return - } - currentIndex = if (currentIndex - 1 < 0) stories.size - 1 else currentIndex - 1 - applyCurrentStory() - } - - private fun applyCurrentStory() { - _storyFlow.tryEmit(stories[currentIndex]) - progress.set(0.0) - startStoryTimer() - } - - private fun startStoryTimer() { - stopStoryTimer() - timerJob = viewModelScope.launch { - val startFromProgress = progress.get() - var remainingTime = autoSwitchDuration * (1 - startFromProgress) - - while (isActive && !isAutoSwitchPaused && remainingTime > 0) { - delay(progressDelay) - remainingTime -= progressDelay - val progress = 1 - remainingTime / autoSwitchDuration - setProgress(progress) - } - setProgress(1.0) - nextStory() - } - } - - private fun setProgress(newProgress: Double) { - progress.set(newProgress) - _progressFlow.tryEmit(Pair(currentIndex, progress.toFloat())) - } - - private fun stopStoryTimer() { - timerJob?.cancel() - } - - fun addWallet(context: Context) = accountRepository.selectedWalletFlow.take(1) - .map { wallet -> - val fixedLabel = wallet.label.name.replace(wallet.version.title, "") + " " + WalletVersion.V5R1.title - accountRepository.addWallet( - ids = listOf(AccountRepository.newWalletId()), - label = Wallet.NewLabel(listOf(fixedLabel), wallet.label.emoji, wallet.label.color), - publicKey = wallet.publicKey, - versions = listOf(WalletVersion.V5R1), - type = wallet.type - ).first() - }.map { wallet -> - val mnemonic = accountRepository.getMnemonic(wallet.id) ?: throw Exception("mnemonic not found") - val passcode = passcodeManager.requestValidPasscode(context) - rnLegacy.addMnemonics(passcode, listOf(wallet.id), mnemonic.toList()) - wallet - }.map { wallet -> - backupRepository.addBackup(wallet.id, BackupEntity.Source.LOCAL) - wallet.id - }.flowOn(Dispatchers.IO) - -} \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/res/layout/fragment_card.xml b/apps/wallet/instance/app/src/main/res/layout/fragment_card.xml new file mode 100644 index 000000000..2838ab76f --- /dev/null +++ b/apps/wallet/instance/app/src/main/res/layout/fragment_card.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/res/layout/fragment_dapp.xml b/apps/wallet/instance/app/src/main/res/layout/fragment_dapp.xml index ba5e591e5..5f82cb73a 100644 --- a/apps/wallet/instance/app/src/main/res/layout/fragment_dapp.xml +++ b/apps/wallet/instance/app/src/main/res/layout/fragment_dapp.xml @@ -89,7 +89,7 @@ android:id="@+id/refresh" android:layout_width="match_parent" android:layout_height="match_parent"> - + + + + + android:textColor="?attr/textTertiaryColor"/> - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/wallet/instance/main/build.gradle.kts b/apps/wallet/instance/main/build.gradle.kts index fc04ecd01..a2bf066de 100644 --- a/apps/wallet/instance/main/build.gradle.kts +++ b/apps/wallet/instance/main/build.gradle.kts @@ -24,7 +24,7 @@ android { targetSdk = 34 versionCode = 600 - versionName = "5.0.15" // Format is "major.minor.patch" (e.g. "1.0.0") and only numbers are allowed + versionName = "5.0.16" // Format is "major.minor.patch" (e.g. "1.0.0") and only numbers are allowed testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/apps/wallet/localization/src/main/res/values-bg/strings.xml b/apps/wallet/localization/src/main/res/values-bg/strings.xml index faf6282bf..ade309017 100644 --- a/apps/wallet/localization/src/main/res/values-bg/strings.xml +++ b/apps/wallet/localization/src/main/res/values-bg/strings.xml @@ -29,7 +29,7 @@ Използвайте биометрия за потвърждаване на транзакции Резервирайте възстановителната фраза на портфейла Присъединете се към канала на Tonkeeper - В този режим са налични само проверени приложения, NFT и токени. Научете повече + В този режим са налични само проверени приложения, NFT и токени. Научете повече Контакти @@ -228,7 +228,7 @@ Портфейли Изберете вашата страна Създайте нов портфейл или добавете съществуващ - Добре дошли в Tonkeeper + Добре дошли в Tonkeeper Световно ниво на скорост TON е мрежа, проектирана за скорост и производителност. Таксите са значително по-ниски, отколкото на други блокчейн платформи, а транзакциите се потвърждават в рамките на секунди. @@ -238,6 +238,7 @@ Вградени абонаменти TON абонаментите са между потребителите и не познават граници, така че никой не може да ограничи доставчиците или клиентите. Почти нулеви такси и пълен контрол за вас. + За предотвратяване на неочаквани рискове. Tonkeeper Няма интернет връзка Създайте нов портфейл или добавете съществуващ @@ -527,4 +528,9 @@ Безопасен режим Отворете Настройки Връзките са достъпни само за проверени приложения + Само проверени приложения и услуги + За да избегнете фишинг и измами. + Само проверени NFT + За да избегнете спам и фалшиви артикули. + Само проверени токени diff --git a/apps/wallet/localization/src/main/res/values-es/strings.xml b/apps/wallet/localization/src/main/res/values-es/strings.xml index 321efa05c..8ec9abc0a 100644 --- a/apps/wallet/localization/src/main/res/values-es/strings.xml +++ b/apps/wallet/localization/src/main/res/values-es/strings.xml @@ -192,6 +192,7 @@ Tonkeeper almacena tus claves criptográficas en tu dispositivo sin requerir documentos, información personal, datos de contacto o KYC. Suscripciones integradas Las suscripciones de TON son peer-to-peer y sin fronteras para que nadie pueda restringir a los proveedores o clientes. Tarifas casi nulas y control total para ti. + Para prevenir riesgos inesperados. Tonkeeper Sin conexión a internet Crea una nueva billetera o añade una existente @@ -380,7 +381,7 @@ Permisos de Bluetooth Por favor, habilite los permisos de Bluetooth en su configuración para usar esta función Abrir Configuración - Bienvenido a Tonkeeper + Bienvenido a Tonkeeper Comentario requerido Biométrico habilitado Error de envío, inténtalo de nuevo más tarde @@ -480,6 +481,11 @@ Modo seguro Abrir configuración Las conexiones solo están disponibles para aplicaciones verificadas - En este modo, solo están disponibles las aplicaciones, los NFT y los tokens verificados. Más información + En este modo, solo están disponibles las aplicaciones, los NFT y los tokens verificados. Más información + Solo aplicaciones y servicios verificados + Para evitar el phishing y el fraude. + Solo NFT verificadas + Para evitar spam y artículos falsos. + Solo tokens verificados \ No newline at end of file diff --git a/apps/wallet/localization/src/main/res/values-in/strings.xml b/apps/wallet/localization/src/main/res/values-in/strings.xml index 26f465c71..517d9b7f1 100644 --- a/apps/wallet/localization/src/main/res/values-in/strings.xml +++ b/apps/wallet/localization/src/main/res/values-in/strings.xml @@ -192,6 +192,7 @@ Tonkeeper menyimpan kunci kriptografi Anda di perangkat Anda tanpa memerlukan dokumen, informasi pribadi, detail kontak, atau KYC. Langganan bawaan Langganan TON adalah peer-to-peer dan tanpa batas sehingga tidak ada yang dapat membatasi penyedia atau pelanggan. Biaya hampir nol dan kendali penuh untuk Anda. + Untuk mencegah risiko yang tidak terduga. Tonkeeper Tidak ada koneksi internet Buat dompet baru atau tambahkan yang sudah ada @@ -380,7 +381,7 @@ Izin Bluetooth Silakan aktifkan izin Bluetooth di pengaturan Anda untuk menggunakan fitur ini Buka Pengaturan - Selamat datang di Tonkeeper + Selamat datang di Tonkeeper Komentar yang diperlukan Biometrik diaktifkan Kesalahan pengiriman, coba lagi nanti @@ -475,6 +476,11 @@ Mode aman Buka Pengaturan Koneksi hanya tersedia untuk aplikasi yang terverifikasi - Dalam mode ini, hanya aplikasi, NFT, dan token terverifikasi yang tersedia. Pelajari lebih lanjut + Dalam mode ini, hanya aplikasi, NFT, dan token terverifikasi yang tersedia. Pelajari lebih lanjut + Hanya Aplikasi dan Layanan Terverifikasi + Untuk menghindari phishing dan penipuan. + Hanya NFT Terverifikasi + Untuk menghindari spam dan barang palsu. + Hanya Token Terverifikasi \ No newline at end of file diff --git a/apps/wallet/localization/src/main/res/values-ru/strings.xml b/apps/wallet/localization/src/main/res/values-ru/strings.xml index 49879de6b..c22968121 100644 --- a/apps/wallet/localization/src/main/res/values-ru/strings.xml +++ b/apps/wallet/localization/src/main/res/values-ru/strings.xml @@ -231,7 +231,7 @@ Идентификатор токена Безопасный режим Открыть настройки - В этом режиме доступны только проверенные сервисы, NFT и токены. Подробнее + В этом режиме доступны только проверенные сервисы, NFT и токены. Подробнее Подключение доступно только к проверенным приложениям Вы можете изменить это в настройках безопасности вашего кошелька. @@ -303,7 +303,7 @@ Выберите вашу страну Создайте новый кошелек или добавьте существующий - Приветствуем в Tonkeeper + Приветствуем в Tonkeeper Рекордная скорость TON — это сеть, созданная для скорости и пропускной способности. Комиссии значительно ниже, чем в других блокчейнах, а транзакции подтверждаются за считанные секунды. @@ -313,6 +313,7 @@ Встроенные подписки Подписки TON являются одноранговыми и не имеют границ, поэтому никто не может ограничивать поставщиков или клиентов. Практически нулевые комиссии и полный контроль для вас. + Для предотвращения непредвиденных рисков. Tonkeeper Создайте новый кошелек или добавьте существующий Создать новый кошелёк @@ -554,6 +555,11 @@ Скрытые Показать все Подробности токена + Только проверенные приложения и сервисы + Чтобы избежать фишинга и мошенничества. + Только проверенные NFT + Чтобы избежать спама и поддельных товаров. + Только проверенные токены \ No newline at end of file diff --git a/apps/wallet/localization/src/main/res/values-tr/strings.xml b/apps/wallet/localization/src/main/res/values-tr/strings.xml index 4d5fc6077..c81323752 100644 --- a/apps/wallet/localization/src/main/res/values-tr/strings.xml +++ b/apps/wallet/localization/src/main/res/values-tr/strings.xml @@ -192,6 +192,7 @@ Tonkeeper, belgeler, kişisel bilgiler, iletişim detayları veya KYC gerektirmeden kriptografik anahtarlarınızı cihazınızda saklar. Yerleşik abonelikler TON abonelikleri eşler arası ve sınırsızdır, bu nedenle hiç kimse sağlayıcıları veya müşterileri kısıtlayamaz. Neredeyse sıfır ücretler ve tam kontrol sizin için. + Beklenmeyen riskleri önlemek için. Tonkeeper İnternet bağlantısı yok Yeni bir cüzdan oluşturun veya mevcut bir cüzdan ekleyin @@ -380,7 +381,7 @@ Bluetooth İzinleri Lütfen bu özelliği kullanmak için ayarlardan Bluetooth izinlerini etkinleştirin Ayarları Aç - Tonkeeper\'a hoş geldiniz + Tonkeeper\'a hoş geldiniz Gerekli yorum Biyometrik etkin Gönderim hatası, daha sonra tekrar deneyin @@ -480,5 +481,10 @@ Güvenli mod Ayarları Aç Bağlantılar yalnızca doğrulanmış uygulamalar için kullanılabilir - Bu modda yalnızca doğrulanmış uygulamalar, NFT\'ler ve token\'lar kullanılabilir. Daha fazla bilgi edinin + Bu modda yalnızca doğrulanmış uygulamalar, NFT\'ler ve token\'lar kullanılabilir. Daha fazla bilgi edinin + Yalnızca Doğrulanmış Uygulamalar ve Hizmetler + Sahtekarlık ve dolandırıcılıktan korunmak için. + Sadece Doğrulanmış NFT\'ler + Spam ve sahte ürünlerden kaçınmak için. + Sadece Doğrulanmış Tokenlar \ No newline at end of file diff --git a/apps/wallet/localization/src/main/res/values-uk/strings.xml b/apps/wallet/localization/src/main/res/values-uk/strings.xml index feac2cada..70f1ce39c 100644 --- a/apps/wallet/localization/src/main/res/values-uk/strings.xml +++ b/apps/wallet/localization/src/main/res/values-uk/strings.xml @@ -192,6 +192,7 @@ Tonkeeper зберігає ваші криптографічні ключі на вашому пристрої без необхідності в документах, особистій інформації, контактних даних або KYC. Вбудовані підписки TON підписки є одноранговими та безкордонними, тому ніхто не може обмежити постачальників або клієнтів. Майже нульові комісії та повний контроль для вас. + Щоб запобігти несподіваним ризикам. Тонкіпер Немає підключення до інтернету Створіть новий гаманець або додайте існуючий @@ -380,7 +381,7 @@ Дозволи Bluetooth Будь ласка, увімкніть дозволи на використання Bluetooth у налаштуваннях, щоб скористатися цією функцією Відкрити Налаштування - Вітаємо в Tonkeeper + Вітаємо в Tonkeeper Обов\'язковий коментар Біометрія включена Помилка відправки, спробуйте пізніше @@ -490,5 +491,10 @@ Безпечний режим Відкрийте налаштування Підключення доступне лише для перевірених програм - У цьому режимі доступні лише перевірені програми, NFT і токени. Дізнайтесь більше + У цьому режимі доступні лише перевірені програми, NFT і токени. Дізнайтесь більше + Лише перевірені програми та служби + Щоб уникнути фішингу та шахрайства. + Тільки перевірені NFT + Щоб уникнути спаму та фейків. + Тільки перевірені токени \ No newline at end of file diff --git a/apps/wallet/localization/src/main/res/values-uz/strings.xml b/apps/wallet/localization/src/main/res/values-uz/strings.xml index ed1be95ec..e8ee06314 100644 --- a/apps/wallet/localization/src/main/res/values-uz/strings.xml +++ b/apps/wallet/localization/src/main/res/values-uz/strings.xml @@ -192,6 +192,7 @@ Tonkeeper sizning kriptografik kalitlaringizni qurilmangizda hujjatlar, shaxsiy ma\'lumotlar, aloqa ma\'lumotlari yoki KYC talab qilmasdan saqlaydi. Ichki obunalar TON obunalar peer-to-peer va chegarasiz bo\'lib, hech kim provayderlar yoki mijozlarni cheklay olmaydi. Deyarli nol to\'lovlar va to\'liq nazorat siz uchun. + Kutilmagan xavflarni oldini olish uchun. Tonkeeper Internet aloqasi yo\'q Yangi hamyon yarating yoki mavjudini qo\'shing @@ -380,7 +381,7 @@ Bluetooth ruxsatnomalari Iltimos, ushbu funksiyani ishlatish uchun sozlamalaringizda Bluetooth ruxsatlarini yoqing Sozlamalarni oching - Tonkeeperga xush kelibsiz + Tonkeeperga xush kelibsiz Sharh kerak Biometrik yoqilgan Yuborishda xatolik, keyinroq qayta urinib ko‘ring @@ -480,5 +481,10 @@ Xavfsiz rejim Sozlamalarni oching Ulanishlar faqat tasdiqlangan ilovalar uchun mavjud - Bu rejimda faqat tasdiqlangan ilovalar, NFTlar va tokenlar mavjud. Batafsil ma\'lumot + Bu rejimda faqat tasdiqlangan ilovalar, NFTlar va tokenlar mavjud. Batafsil ma\'lumot + Faqat tasdiqlangan ilovalar va xizmatlar + Firibgarlik va firibgarlikning oldini olish uchun. + Faqat tasdiqlangan NFTlar + Spam va soxta narsalarni oldini olish uchun. + Faqat tasdiqlangan tokenlar \ No newline at end of file diff --git a/apps/wallet/localization/src/main/res/values-zh/strings.xml b/apps/wallet/localization/src/main/res/values-zh/strings.xml index 534948a43..718a69e69 100644 --- a/apps/wallet/localization/src/main/res/values-zh/strings.xml +++ b/apps/wallet/localization/src/main/res/values-zh/strings.xml @@ -192,6 +192,7 @@ Tonkeeper在您的设备上存储您的加密密钥,无需文件、个人信息、联系方式或KYC。 内置订阅 TON 订阅是点对点和无国界的,因此没有人可以限制提供者或客户。几乎为零的费用和完全的控制权为您提供。 + 防范意外风险。 Tonkeeper 没有网络连接 创建新钱包或添加现有钱包 @@ -380,7 +381,7 @@ 蓝牙权限 请在设置中启用蓝牙权限以使用此功能 打开设置 - 欢迎来到 Tonkeeper + 欢迎来到 Tonkeeper 必填注释 已启用生物识别 发送错误,请稍后重试 @@ -475,5 +476,10 @@ 安全模式 打开“设置” 仅向经过验证的应用提供连接 - 在此模式下,仅经过验证的应用、NFT 和代币可用。了解详情 + 在此模式下,仅经过验证的应用、NFT 和代币可用。了解详情 + 仅限经过验证的应用和服务 + 避免网络钓鱼和欺诈。 + 仅限经过验证的 NFT + 以避免垃圾邮件和假冒物品。 + 仅限已验证的代币 \ No newline at end of file diff --git a/apps/wallet/localization/src/main/res/values/strings.xml b/apps/wallet/localization/src/main/res/values/strings.xml index 7ad70affa..d9d92330a 100644 --- a/apps/wallet/localization/src/main/res/values/strings.xml +++ b/apps/wallet/localization/src/main/res/values/strings.xml @@ -38,7 +38,7 @@ Token ID Safe mode Open Settings - In this mode, only verified apps, NFTs, and tokens are available. Learn more + In this mode, only verified apps, NFTs, and tokens are available. Learn more %1$d NFT %1$d NFTs @@ -258,7 +258,7 @@ Wallets Choose your country Create a new wallet or add an existing one - Welcome to Tonkeeper + Welcome to Tonkeeper World-class speed TON is a network designed for speed and throughput. Fees are significantly lower than on other blockchains, and transactions are confirmed in a matter of seconds. @@ -268,6 +268,16 @@ Built-in subscriptions TON subscriptions are peer-to-peer and borderless so that no one can restrict providers or customers. Near-zero fees and full control for you. + Only Verified Apps and Services + To avoid phishing and fraud. + + Only Verified NFTs + To avoid spam and fake items. + + Only Verified Tokens + To prevent unexpected risks. + + Tonkeeper No internet connection Create a new wallet or add an existing one diff --git a/lib/extensions/src/main/java/com/tonapps/extensions/Bundle.kt b/lib/extensions/src/main/java/com/tonapps/extensions/Bundle.kt index c0ea40cf1..3e1738b8c 100644 --- a/lib/extensions/src/main/java/com/tonapps/extensions/Bundle.kt +++ b/lib/extensions/src/main/java/com/tonapps/extensions/Bundle.kt @@ -28,3 +28,13 @@ inline fun Bundle.getParcelableCompat(key: String): R? { null } } + +fun Bundle.print(): String { + val sb = StringBuilder() + sb.append("Bundle {") + keySet().forEach { key -> + sb.append("\n $key: ${get(key)}") + } + sb.append("\n}") + return sb.toString() +} \ No newline at end of file diff --git a/lib/extensions/src/main/java/com/tonapps/extensions/Uri.kt b/lib/extensions/src/main/java/com/tonapps/extensions/Uri.kt index 969528eba..5e43a295b 100644 --- a/lib/extensions/src/main/java/com/tonapps/extensions/Uri.kt +++ b/lib/extensions/src/main/java/com/tonapps/extensions/Uri.kt @@ -52,6 +52,10 @@ val Uri.pathOrNull: String? val Uri.hostOrNull: String? get() = host?.ifBlank { null } +fun Uri.containsQuery(key: String): Boolean { + return query(key) != null +} + fun Uri.query(key: String): String? { return getQueryParameter(key)?.trim()?.ifBlank { null } } diff --git a/ui/uikit/core/src/main/java/uikit/extensions/CharSequence.kt b/ui/uikit/core/src/main/java/uikit/extensions/CharSequence.kt index 226495a1c..2c358a6ae 100644 --- a/ui/uikit/core/src/main/java/uikit/extensions/CharSequence.kt +++ b/ui/uikit/core/src/main/java/uikit/extensions/CharSequence.kt @@ -3,13 +3,14 @@ package uikit.extensions import android.content.Context import android.text.SpannableString import android.text.style.ForegroundColorSpan +import android.util.Log fun CharSequence.processAnnotation(context: Context): SpannableString { val spannableString = this as? SpannableString ?: SpannableString(this) val annotations = spannableString.getSpans(0, spannableString.length, android.text.Annotation::class.java) for (annotation in annotations) { - if (annotation.key == "colorRes") { - val color = context.getColorByIdentifier(annotation.value) + if (annotation.key == "colorAttr") { + val color = context.getColorByAttr(annotation.value) spannableString.setSpan(ForegroundColorSpan(color), spannableString.getSpanStart(annotation), spannableString.getSpanEnd(annotation), SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE) } } diff --git a/ui/uikit/core/src/main/java/uikit/extensions/Context.kt b/ui/uikit/core/src/main/java/uikit/extensions/Context.kt index b46236a21..f8f7b92cc 100644 --- a/ui/uikit/core/src/main/java/uikit/extensions/Context.kt +++ b/ui/uikit/core/src/main/java/uikit/extensions/Context.kt @@ -68,6 +68,13 @@ fun Context.getColorByIdentifier(name: String): Int { } } +fun Context.getColorByAttr(name: String): Int { + val a = theme.obtainStyledAttributes(intArrayOf(resources.getIdentifier(name, "attr", packageName))) + val color = a.getColor(0, 0) + a.recycle() + return color +} + val Context.activity: NavigationActivity? get() { var context = this diff --git a/ui/uikit/core/src/main/java/uikit/widget/stories/BaseStoriesScreen.kt b/ui/uikit/core/src/main/java/uikit/widget/stories/BaseStoriesScreen.kt index 927f94976..734d1f69c 100644 --- a/ui/uikit/core/src/main/java/uikit/widget/stories/BaseStoriesScreen.kt +++ b/ui/uikit/core/src/main/java/uikit/widget/stories/BaseStoriesScreen.kt @@ -3,24 +3,265 @@ package uikit.widget.stories import android.net.Uri import android.os.Bundle import android.os.Parcelable +import android.util.Log +import android.view.Gravity +import android.view.MotionEvent import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.LinearLayout +import androidx.appcompat.widget.AppCompatTextView +import androidx.appcompat.widget.LinearLayoutCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import uikit.R import uikit.base.BaseFragment +import uikit.extensions.dp +import uikit.extensions.getViews +import uikit.extensions.round +import uikit.widget.ColumnLayout import uikit.widget.FrescoView import uikit.widget.RowLayout +import java.util.concurrent.atomic.AtomicInteger open class BaseStoriesScreen: BaseFragment(R.layout.fragment_stories) { - data class Item(val image: Uri, val title: String, val subtitle: String) + data class Item( + val image: Uri, + val title: String? = null, + val subtitle: String? = null, + val button: String? = null + ) + private lateinit var contentView: View private lateinit var imageView: FrescoView private lateinit var linesView: RowLayout private lateinit var closeView: View + private lateinit var titleView: AppCompatTextView + private lateinit var subtitleView: AppCompatTextView + private lateinit var button: AppCompatTextView + + private val stories = mutableListOf() + private var state = StoriesState() + private var progress: Double = 0.0 + + private var timerJob: Job? = null + + val isLastStory: Boolean + get() = state.currentIndex == stories.size - 1 + + val isFirstStory: Boolean + get() = state.currentIndex == 0 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - imageView = view.findViewById(R.id.stories_image) + view.findViewById(R.id.stories_close).setOnClickListener { finish() } + + contentView = view.findViewById(R.id.stories_content) + contentView.round(20f.dp) + contentView.setOnTouchListener { v, event -> + onTouchEvent(event) + true + } + + imageView = view.findViewById(R.id.story_image) linesView = view.findViewById(R.id.stories_lines) closeView = view.findViewById(R.id.stories_close) + titleView = view.findViewById(R.id.story_title) + subtitleView = view.findViewById(R.id.story_subtitle) + button = view.findViewById(R.id.story_button) + button.setOnClickListener { onStoryButton(state.currentIndex) } + + ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets -> + val statusBarOffset = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top + val navBarOffset = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom + applyContentMargin(statusBarOffset, navBarOffset) + insets + } + } + + private fun applyContentMargin(top: Int, bottom: Int) { + contentView.updateLayoutParams { + topMargin = top + bottomMargin = bottom + } + } + + open fun onProgress(targetIndex: Int, progress: Float) { + val views = linesView.getViews().map { it as StoriesProgressView } + for ((index, view) in views.withIndex()) { + if (targetIndex > index) { + view.progress = 1f + } else if (targetIndex == index) { + view.progress = progress + } else { + view.progress = 0f + } + } + } + + open fun onStoryButton(index: Int) { + + } + + open fun onStoryItem(item: Item) { + setTitle(item.title) + setSubtitle(item.subtitle) + setButton(item.button) + imageView.setImageURI(item.image) + } + + private fun onTouchEvent(event: MotionEvent) { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + pause() + } + MotionEvent.ACTION_UP -> { + val screenWidth = resources.displayMetrics.widthPixels + val next = event.x > screenWidth / 2 + resume(next) + } + } + } + private fun setTitle(title: String?) { + if (!title.isNullOrBlank()) { + titleView.visibility = View.VISIBLE + titleView.text = title + } else { + titleView.visibility = View.GONE + } + } + + private fun setSubtitle(subtitle: String?) { + if (!subtitle.isNullOrBlank()) { + subtitleView.visibility = View.VISIBLE + subtitleView.text = subtitle + } else { + subtitleView.visibility = View.GONE + } + } + + private fun setButton(text: String?) { + if (!text.isNullOrBlank()) { + button.visibility = View.VISIBLE + button.text = text + } else { + button.visibility = View.GONE + } + } + + private fun setProgress(newProgress: Double) { + progress = newProgress + onProgress(state.currentIndex, newProgress.toFloat()) + } + + fun putItems(items: List) { + this.stories.clear() + this.stories.addAll(items) + applyLines() + applyCurrentStory() + } + + fun pause() { + stopStoryTimer() + state = state.copy( + isAutoSwitchPaused = true, + lastPauseTime = System.currentTimeMillis() + ) + } + + fun resume(next: Boolean) { + state = state.copy( + isAutoSwitchPaused = false + ) + val elapsedSincePause = System.currentTimeMillis() - state.lastPauseTime + if (120 >= elapsedSincePause) { + if (next && !isLastStory) { + nextStory() + } else if (!next && !isFirstStory) { + prevStory() + } else { + startStoryTimer() + } + } else { + startStoryTimer() + } + } + + private fun startStoryTimer() { + stopStoryTimer() + timerJob = lifecycleScope.launch { + val startFromProgress = progress + var remainingTime = autoSwitchDuration * (1 - startFromProgress) + + while (isActive && !state.isAutoSwitchPaused && remainingTime > 0) { + delay(progressDelay) + remainingTime -= progressDelay + val progress = 1 - remainingTime / autoSwitchDuration + setProgress(progress) + } + setProgress(1.0) + nextStory() + } + } + + fun prevStory() { + if (isFirstStory) { + return + } + state = state.copy( + currentIndex = if (state.currentIndex - 1 < 0) stories.size - 1 else state.currentIndex - 1 + ) + applyCurrentStory() } + + fun nextStory() { + if (isLastStory) { + setProgress(1.0) + return + } + state = state.copy( + currentIndex = (state.currentIndex + 1) % stories.size + ) + applyCurrentStory() + } + + private fun applyCurrentStory() { + if (state.currentIndex < 0 || state.currentIndex >= stories.size) { + state = state.copy(currentIndex = 0) + } + onStoryItem(stories[state.currentIndex]) + setProgress(0.0) + startStoryTimer() + } + + private fun stopStoryTimer() { + timerJob?.cancel() + } + + private fun applyLines() { + linesView.removeAllViews() + + for (i in 0 until stories.size) { + val view = StoriesProgressView(requireContext()) + val params = LinearLayoutCompat.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, 4.dp, 1f) + params.gravity = Gravity.CENTER_VERTICAL + params.setMargins(2.dp, 0, 2.dp, 0) + linesView.addView(view, params) + } + } + + companion object { + private val autoSwitchDuration = 5000L + private val progressDelay = 8L + } + } \ No newline at end of file diff --git a/ui/uikit/core/src/main/java/uikit/widget/stories/StoriesState.kt b/ui/uikit/core/src/main/java/uikit/widget/stories/StoriesState.kt new file mode 100644 index 000000000..71db285b8 --- /dev/null +++ b/ui/uikit/core/src/main/java/uikit/widget/stories/StoriesState.kt @@ -0,0 +1,7 @@ +package uikit.widget.stories + +data class StoriesState( + val currentIndex: Int = -1, + val isAutoSwitchPaused: Boolean = false, + val lastPauseTime: Long = 0, +) \ No newline at end of file diff --git a/ui/uikit/core/src/main/java/uikit/widget/webview/bridge/BridgeWebView.kt b/ui/uikit/core/src/main/java/uikit/widget/webview/bridge/BridgeWebView.kt index c7675af0d..28b3b2ec1 100644 --- a/ui/uikit/core/src/main/java/uikit/widget/webview/bridge/BridgeWebView.kt +++ b/ui/uikit/core/src/main/java/uikit/widget/webview/bridge/BridgeWebView.kt @@ -17,7 +17,7 @@ import uikit.widget.webview.bridge.message.BridgeMessage import uikit.widget.webview.bridge.message.FunctionInvokeBridgeMessage import uikit.widget.webview.bridge.message.FunctionResponseBridgeMessage -class BridgeWebView @JvmOverloads constructor( +open class BridgeWebView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = android.R.attr.webViewStyle, diff --git a/ui/uikit/core/src/main/res/layout/fragment_stories.xml b/ui/uikit/core/src/main/res/layout/fragment_stories.xml index e182d5eae..327105773 100644 --- a/ui/uikit/core/src/main/res/layout/fragment_stories.xml +++ b/ui/uikit/core/src/main/res/layout/fragment_stories.xml @@ -1,7 +1,8 @@ + android:layout_height="match_parent" + android:background="@color/constantBlack"> @@ -31,6 +32,43 @@ android:tint="@color/constantBlack" android:scaleType="centerInside"/> + + + + + + + + + + \ No newline at end of file