From 4427ce0bd856ee30310029d7c84ef72fecf650ff Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Mon, 25 Mar 2024 14:49:22 +0300 Subject: [PATCH 01/22] fix(mobile): IAP updates (#727) * fix(mobile): IAP updates * fix(mobile): strings * fix(mobile): IAP updates * fix(mobile): strings * Fix other locales for compatibility * bump(mobile): 4.1.0 * Update IAP feature flag * battery fixes * fix logger manager name --- .../src/BatteryAPI/BatteryGenerated.ts | 58 ++++++++++++-- packages/mobile/android/app/build.gradle | 2 +- .../ios/ton_keeper.xcodeproj/project.pbxproj | 8 +- packages/mobile/src/config/index.ts | 4 +- .../src/core/DevMenu/DevConfigScreen.tsx | 11 +++ .../src/core/RefillBattery/RefillBattery.tsx | 24 +++--- .../src/wallet/managers/BatteryManager.ts | 19 +++-- .../src/wallet/managers/TonProofManager.ts | 1 - .../RefillBattery/RefillBattery.tsx | 15 ++-- .../RefillBattery/RefillBatteryIAP.tsx | 75 +++++++++++++------ .../RefillBattery/RestorePurchases.tsx | 61 +++++++++++++++ .../shared/i18n/locales/tonkeeper/en.json | 27 +++---- .../shared/i18n/locales/tonkeeper/ru-RU.json | 19 ++--- .../shared/i18n/locales/tonkeeper/tr-TR.json | 16 +--- .../i18n/locales/tonkeeper/zh-Hans-CN.json | 16 +--- packages/shared/utils/battery.ts | 16 ++-- .../uikit/src/components/List/ListItem.tsx | 3 +- 17 files changed, 258 insertions(+), 117 deletions(-) create mode 100644 packages/shared/components/RefillBattery/RestorePurchases.tsx diff --git a/packages/@core-js/src/BatteryAPI/BatteryGenerated.ts b/packages/@core-js/src/BatteryAPI/BatteryGenerated.ts index 3bc6985c2..02ee15352 100644 --- a/packages/@core-js/src/BatteryAPI/BatteryGenerated.ts +++ b/packages/@core-js/src/BatteryAPI/BatteryGenerated.ts @@ -14,6 +14,12 @@ export interface Error { error: string; } +export interface Status { + pending_transactions: { + id: string; + }[]; +} + export interface Config { /** * when building a message to transfer an NFT or Jetton, use this address to send excess funds back to Battery Service. @@ -25,6 +31,8 @@ export interface Config { export interface Balance { /** @example "10.250" */ balance: string; + /** @example "usd" */ + units: BalanceUnitsEnum; } export interface Purchases { @@ -87,7 +95,7 @@ export interface PromoCodeBatteryPurchaseStatus { error?: { /** @example "Temporary error. Try again later." */ msg: string; - /** @example "promo-code-is-already-used" */ + /** @example "promo-code-not-found" */ code: PromoCodeBatteryPurchaseStatusCodeEnum; }; } @@ -114,6 +122,12 @@ export interface Transactions { }[]; } +/** @example "usd" */ +export enum BalanceUnitsEnum { + Usd = 'usd', + Ton = 'ton', +} + /** @example "android" */ export enum PurchasesTypeEnum { RegularPurchase = 'regular-purchase', @@ -139,10 +153,10 @@ export enum IOsBatteryPurchaseStatusCodeEnum { Unknown = 'unknown', } -/** @example "promo-code-is-already-used" */ +/** @example "promo-code-not-found" */ export enum PromoCodeBatteryPurchaseStatusCodeEnum { - PromoCodeIsAlreadyUsed = 'promo-code-is-already-used', PromoCodeNotFound = 'promo-code-not-found', + PromoExceededAttempts = 'promo-exceeded-attempts', TemporaryError = 'temporary-error', } @@ -153,6 +167,23 @@ export enum TransactionsStatusEnum { Failed = 'failed', } +export interface GetBalanceParams { + /** @default "usd" */ + units?: UnitsEnum; +} + +/** @default "usd" */ +export enum UnitsEnum { + Usd = 'usd', + Ton = 'ton', +} + +/** @default "usd" */ +export enum GetBalanceParams1UnitsEnum { + Usd = 'usd', + Ton = 'ton', +} + export interface GetPurchasesParams { /** * @max 1000 @@ -421,7 +452,21 @@ export class BatteryGenerated { } /** - * @description This method returns information about the battery service. + * @description This method returns information about the current status of Battery Service. + * + * @name GetStatus + * @request GET:/status + */ + getStatus = (params: RequestParams = {}) => + this.http.request({ + path: `/status`, + method: 'GET', + format: 'json', + ...params, + }); + + /** + * @description This method returns information about Battery Service. * * @name GetConfig * @request GET:/config @@ -435,15 +480,16 @@ export class BatteryGenerated { }); /** - * @description This method returns information about a battery + * @description This method returns information about a user's balance. * * @name GetBalance * @request GET:/balance */ - getBalance = (params: RequestParams = {}) => + getBalance = (query: GetBalanceParams, params: RequestParams = {}) => this.http.request({ path: `/balance`, method: 'GET', + query: query, format: 'json', ...params, }); diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index 8af57145e..22aeaee46 100644 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -92,7 +92,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 433 - versionName "4.1.0" + versionName "4.2.0" missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'store', 'play' } diff --git a/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj b/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj index 5bffe413f..f3ca23162 100644 --- a/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 203A6A1F2760BD6800817B71 /* Montserrat-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 203A6A0D2760BD6800817B71 /* Montserrat-Regular.ttf */; }; 5008E612299F9D1400D4850B /* SFMono-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 5008E610299F9D1400D4850B /* SFMono-Bold.otf */; }; 5008E614299F9D1400D4850B /* SFMono-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 5008E611299F9D1400D4850B /* SFMono-Medium.otf */; }; + 506008722B7F9EF10061C2CD /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 506008712B7F9EF10061C2CD /* StoreKit.framework */; }; 76F9B0131AB7FE11009E6D10 /* libPods-ton_keeper-ton_keeperTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB5E03C54A755139D3788F39 /* libPods-ton_keeper-ton_keeperTests.a */; }; 78B9AC5F426C0F28766DEC94 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D7306F36185132B3874D4C /* ExpoModulesProvider.swift */; }; 8D962335543EE7C6E1C6AB28 /* libPods-ton_keeper.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 39C1C20FA83478354251FB2A /* libPods-ton_keeper.a */; }; @@ -130,6 +131,7 @@ 39C1C20FA83478354251FB2A /* libPods-ton_keeper.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ton_keeper.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 5008E610299F9D1400D4850B /* SFMono-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SFMono-Bold.otf"; path = "../assets/fonts/SFMono-Bold.otf"; sourceTree = ""; }; 5008E611299F9D1400D4850B /* SFMono-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SFMono-Medium.otf"; path = "../assets/fonts/SFMono-Medium.otf"; sourceTree = ""; }; + 506008712B7F9EF10061C2CD /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; 50AD04972AD96A5C00E0648C /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/PassCode.strings; sourceTree = ""; }; 50AD04982AD96A5D00E0648C /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/PassCode.stringsdict; sourceTree = ""; }; 50AD04992AD96AFA00E0648C /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/PassCode.strings"; sourceTree = ""; }; @@ -225,6 +227,7 @@ buildActionMask = 2147483647; files = ( 8D962335543EE7C6E1C6AB28 /* libPods-ton_keeper.a in Frameworks */, + 506008722B7F9EF10061C2CD /* StoreKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -260,6 +263,7 @@ 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { isa = PBXGroup; children = ( + 506008712B7F9EF10061C2CD /* StoreKit.framework */, C4FB2EE22912AF2700CB35D0 /* OpenSSL.xcframework */, C4FB2EE32912AF2700CB35D0 /* TON.xcframework */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */, @@ -1294,7 +1298,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.1.0; + MARKETING_VERSION = 4.2.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1328,7 +1332,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.1.0; + MARKETING_VERSION = 4.2.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/packages/mobile/src/config/index.ts b/packages/mobile/src/config/index.ts index 1af3a7a0f..809ac2088 100644 --- a/packages/mobile/src/config/index.ts +++ b/packages/mobile/src/config/index.ts @@ -82,9 +82,9 @@ const defaultConfig: Partial = { tronapiTestnetHost: 'https://testnet-tron.tonkeeper.com', batteryHost: 'https://battery.tonkeeper.com', batteryTestnetHost: 'https://testnet-battery.tonkeeper.com', - batteryMeanFees: '0.08', + batteryMeanFees: '0.03', disable_battery: true, - disable_battery_iap_module: true, + disable_battery_iap_module: Platform.OS !== 'android', // Enable for iOS, disable for Android disable_battery_send: true, disable_show_unverified_token: false, disable_tonstakers: false, diff --git a/packages/mobile/src/core/DevMenu/DevConfigScreen.tsx b/packages/mobile/src/core/DevMenu/DevConfigScreen.tsx index 79a539a0b..2f2987b3c 100644 --- a/packages/mobile/src/core/DevMenu/DevConfigScreen.tsx +++ b/packages/mobile/src/core/DevMenu/DevConfigScreen.tsx @@ -90,6 +90,17 @@ export const DevConfigScreen = memo(() => { /> } /> + + } + /> diff --git a/packages/mobile/src/core/RefillBattery/RefillBattery.tsx b/packages/mobile/src/core/RefillBattery/RefillBattery.tsx index 4b1a2df04..56d89988d 100644 --- a/packages/mobile/src/core/RefillBattery/RefillBattery.tsx +++ b/packages/mobile/src/core/RefillBattery/RefillBattery.tsx @@ -1,17 +1,23 @@ import { memo } from 'react'; import { RefillBattery as RefillBatteryComponent } from '@tonkeeper/shared/components/RefillBattery/RefillBattery'; import { t } from '@tonkeeper/shared/i18n'; -import { ScrollHandler } from '$uikit'; +import { Screen, Steezy, View } from '@tonkeeper/uikit'; export const RefillBattery = memo(() => { return ( - - - + + + + + + + + ); }); + +const styles = Steezy.create({ + container: { + marginBottom: 16, + }, +}); diff --git a/packages/mobile/src/wallet/managers/BatteryManager.ts b/packages/mobile/src/wallet/managers/BatteryManager.ts index e6434cb68..357d48258 100644 --- a/packages/mobile/src/wallet/managers/BatteryManager.ts +++ b/packages/mobile/src/wallet/managers/BatteryManager.ts @@ -1,8 +1,9 @@ -import { BatteryAPI } from '@tonkeeper/core/src/BatteryAPI'; +import { BatteryAPI, UnitsEnum } from '@tonkeeper/core/src/BatteryAPI'; import { MessageConsequences } from '@tonkeeper/core/src/TonAPI'; import { Storage } from '@tonkeeper/core/src/declarations/Storage'; import { State } from '@tonkeeper/core/src/utils/State'; import { TonProofManager } from '$wallet/managers/TonProofManager'; +import { logger, NamespacedLogger } from '$logger'; export interface BatteryState { isLoading: boolean; @@ -15,6 +16,8 @@ export class BatteryManager { balance: undefined, }); + private logger: NamespacedLogger; + constructor( private persistPath: string, private tonProof: TonProofManager, @@ -26,6 +29,7 @@ export class BatteryManager { storage: this.storage, key: `${this.persistPath}/battery`, }); + this.logger = logger.extend('BatteryManager'); } public async fetchBalance() { @@ -34,11 +38,14 @@ export class BatteryManager { throw new Error('No proof token'); } this.state.set({ isLoading: true }); - const data = await this.batteryapi.getBalance({ - headers: { - 'X-TonConnect-Auth': this.tonProof.tonProofToken, + const data = await this.batteryapi.getBalance( + { units: UnitsEnum.Ton }, + { + headers: { + 'X-TonConnect-Auth': this.tonProof.tonProofToken, + }, }, - }); + ); this.state.set({ isLoading: false, balance: data.balance }); } catch (err) { this.state.set({ isLoading: false, balance: '0' }); @@ -108,7 +115,7 @@ export class BatteryManager { return data.transactions; } catch (err) { - console.log('[ios battery in-app purchase]', err); + this.logger.error(err); } } diff --git a/packages/mobile/src/wallet/managers/TonProofManager.ts b/packages/mobile/src/wallet/managers/TonProofManager.ts index 0355b0f27..f30506a7a 100644 --- a/packages/mobile/src/wallet/managers/TonProofManager.ts +++ b/packages/mobile/src/wallet/managers/TonProofManager.ts @@ -32,7 +32,6 @@ export class TonProofManager { const { token } = await this.tonapi.wallet.tonConnectProof(proof); this.tonProofToken = token; - await SecureStore.setItemAsync(`proof-${this.identifier}`, token); } catch (err) { console.log('TonProofManager.obtainProof', err); diff --git a/packages/shared/components/RefillBattery/RefillBattery.tsx b/packages/shared/components/RefillBattery/RefillBattery.tsx index 6251637b1..45d1846a9 100644 --- a/packages/shared/components/RefillBattery/RefillBattery.tsx +++ b/packages/shared/components/RefillBattery/RefillBattery.tsx @@ -12,6 +12,7 @@ import { RefillBatteryIAP } from './RefillBatteryIAP'; import { t } from '@tonkeeper/shared/i18n'; import { config } from '@tonkeeper/mobile/src/config'; import { RechargeByPromoButton } from './RechargeByPromoButton'; +import { RestorePurchases } from './RestorePurchases'; const iconNames: { [key: string]: IconNames } = { [BatteryState.Empty]: 'ic-empty-battery-128', @@ -36,20 +37,16 @@ export const RefillBattery = memo(() => { - {t(`battery.title.${batteryState.toLowerCase()}`)} + {t(`battery.title`)} {t( `battery.description.${ - batteryState === BatteryState.Empty - ? 'empty' - : availableNumOfTransactionsCount - ? 'other' - : 'less_10' + batteryState === BatteryState.Empty ? 'empty' : 'other' }`, { - cnt: availableNumOfTransactionsCount, + count: availableNumOfTransactionsCount.toNumber(), }, )} @@ -59,9 +56,7 @@ export const RefillBattery = memo(() => { {!isInAppPurchasesDisabled ? : null} - - {t('battery.packages.disclaimer')} - + ); diff --git a/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx b/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx index 58f0b81ad..c9cf0ff38 100644 --- a/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx +++ b/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx @@ -3,7 +3,6 @@ import { memo, useCallback, useEffect, useState } from 'react'; import { t } from '@tonkeeper/shared/i18n'; import { tk } from '@tonkeeper/mobile/src/wallet'; import { Platform } from 'react-native'; -import { goBack } from '@tonkeeper/mobile/src/navigation/imperative'; import { useIAP } from 'react-native-iap'; import { SkeletonLine } from '@tonkeeper/mobile/src/uikit/Skeleton/SkeletonLine'; @@ -11,23 +10,23 @@ const packages = [ { key: 'large', transactions: 700, - packageId: 'Battery700', + packageId: 'LargePack', }, { key: 'medium', transactions: 400, - packageId: 'Battery400', + packageId: 'MediumPack', }, { key: 'small', transactions: 100, - packageId: 'Battery100', + packageId: 'SmallPack', }, ]; export const RefillBatteryIAP = memo(() => { const [purchaseInProgress, setPurchaseInProgress] = useState(false); - const { products, getProducts, requestPurchase, connected } = useIAP(); + const { products, getProducts, requestPurchase, finishTransaction } = useIAP(); useEffect(() => { getProducts({ @@ -37,24 +36,51 @@ export const RefillBatteryIAP = memo(() => { const makePurchase = useCallback( (packageId: string) => async () => { - setPurchaseInProgress(true); - let product = await requestPurchase({ sku: packageId }); - if (!product) return; + try { + setPurchaseInProgress(true); + let requestedPurchase = await requestPurchase({ sku: packageId }); - if (Array.isArray(product)) product = product[0]; + if (!requestedPurchase) { + return; + } - if (Platform.OS === 'ios') { - if (!product.transactionId) return; - await tk.wallet.battery.makeIosPurchase([{ id: product.transactionId }]); - } else { - if (!product.purchaseToken) return; - await tk.wallet.battery.makeAndroidPurchase([ - { token: product.purchaseToken, product_id: product.productId }, - ]); + const purchasesArray = Array.isArray(requestedPurchase) + ? requestedPurchase + : [requestedPurchase]; + + if (Platform.OS === 'ios') { + const processedTransactions = await tk.wallet.battery.makeIosPurchase( + purchasesArray.map((purchase) => ({ id: purchase.transactionId })), + ); + + for (let purchase of purchasesArray) { + if ( + !processedTransactions || + !processedTransactions.find( + (processedTransaction) => + purchase.transactionId === processedTransaction.transaction_id, + ) + ) { + continue; + } + await finishTransaction({ purchase, isConsumable: true }); + } + } else if (Platform.OS === 'android') { + await tk.wallet.battery.makeAndroidPurchase( + purchasesArray.map((purchase) => ({ + token: purchase.purchaseToken, + product_id: purchase.productId, + })), + ); + } + + Toast.success(t('battery.refilled')); + setPurchaseInProgress(false); + } catch (e) { + console.log(e); + setPurchaseInProgress(false); + Toast.fail(e.message); } - goBack(); - Toast.success(t('battery.refilled')); - setPurchaseInProgress(false); }, [], ); @@ -68,10 +94,11 @@ export const RefillBatteryIAP = memo(() => { ); return ( - - + + {t(`battery.packages.title`, { price: product?.localizedPrice ?? '', cnt: item.transactions, @@ -105,9 +132,13 @@ export const RefillBatteryIAP = memo(() => { const styles = Steezy.create({ valueContainerStyle: { + flex: 1, justifyContent: 'center', }, titleContainer: { + flex: 2, + }, + priceContainer: { flexDirection: 'row', alignItems: 'center', }, diff --git a/packages/shared/components/RefillBattery/RestorePurchases.tsx b/packages/shared/components/RefillBattery/RestorePurchases.tsx new file mode 100644 index 000000000..3f91eb7b6 --- /dev/null +++ b/packages/shared/components/RefillBattery/RestorePurchases.tsx @@ -0,0 +1,61 @@ +import React, { memo, useCallback } from 'react'; +import { Text, Toast, TouchableOpacity } from '@tonkeeper/uikit'; +import { t } from '../../i18n'; +import { getPendingPurchasesIOS, finishTransaction } from 'react-native-iap'; +import { Platform } from 'react-native'; +import { tk } from '@tonkeeper/mobile/src/wallet'; + +export const RestorePurchases = memo(() => { + const handleRestorePurchases = useCallback(async () => { + try { + const purchases = await getPendingPurchasesIOS(); + + if (!purchases.length) { + return Toast.fail('Nothing to restore'); + } + + if (Platform.OS === 'ios') { + const processedTransactions = await tk.wallet.battery.makeIosPurchase( + purchases.map((purchase) => ({ id: purchase.transactionId })), + ); + + for (let purchase of purchases) { + if ( + !processedTransactions || + !processedTransactions.find( + (processedTransaction) => + purchase.transactionId === processedTransaction.transaction_id, + ) + ) { + continue; + } + await finishTransaction({ purchase, isConsumable: true }); + } + } else if (Platform.OS === 'android') { + await tk.wallet.battery.makeAndroidPurchase( + purchases.map((purchase) => ({ + token: purchase.purchaseToken, + product_id: purchase.productId, + })), + ); + } + Toast.success(t('battery.refilled')); + } catch (e) { + Toast.fail(e.message); + } + }, []); + + return ( + + {t('battery.packages.disclaimer')}{' '} + + {t('battery.packages.restore')} + + + ); +}); diff --git a/packages/shared/i18n/locales/tonkeeper/en.json b/packages/shared/i18n/locales/tonkeeper/en.json index 6021cb5ff..042ab60d5 100644 --- a/packages/shared/i18n/locales/tonkeeper/en.json +++ b/packages/shared/i18n/locales/tonkeeper/en.json @@ -124,19 +124,19 @@ "auth_failed": "Authentication failed", "balances_setup_wallet": "Set up wallet", "battery": { + "refilled": "Battery refilled", "screen_title": "Battery", "settings": "Battery", "ok": "OK", - "title": { - "empty": "Recharge your battery for gas fees", - "almost_empty": "Battery for gas fees is almost empty", - "medium": "Battery for gas fees is half full", - "full": "Battery for gas fees is full" - }, + "title": "Tonkeeper Battery", "description": { "empty": "Send tokens and NFTs, pay for staking actions with empty main balance.", - "other": "You have enough charge battery for more than %{cnt} transactions.", - "less_10": "You have enough charge battery for less than 10 transactions." + "other": { + "few": "Your battery has %{count} charges", + "many": "Your battery has %{count} charges", + "one": "Your battery has %{count} charge", + "other": "Your battery has %{count} charges" + } }, "promocode": { "button": "Recharge by Promo Code", @@ -146,13 +146,14 @@ "success": "Your battery is charged" }, "packages": { - "title": "{{cnt}} transactions for {{price}}", + "title": "Charges for {{price}}", "subtitle": { - "large": "Large pack", - "medium": "Medium pack", - "small": "Small pack" + "large": "Large Pack", + "medium": "Medium Pack", + "small": "Small Pack" }, - "disclaimer": "This is approximate transactions number. Some your transactions may cost more.", + "disclaimer": "One charge covers the average transaction fee. Some transactions may cost more.", + "restore": "Restore Purchases.", "buy": "Buy", "ok": "OK", "refilled": "Your battery is charged" diff --git a/packages/shared/i18n/locales/tonkeeper/ru-RU.json b/packages/shared/i18n/locales/tonkeeper/ru-RU.json index 3a2bbe300..5b7792838 100644 --- a/packages/shared/i18n/locales/tonkeeper/ru-RU.json +++ b/packages/shared/i18n/locales/tonkeeper/ru-RU.json @@ -1057,19 +1057,19 @@ "region_nokyc": "Нейтральные воды", "nokyc": "без KYC", "battery": { + "refilled": "Батарейка заряжена", "screen_title": "Батарейка", "settings": "Батарейка", "ok": "OK", - "title": { - "empty": "Перезарядите свою батарейку для оплаты комиссий в сети", - "almost_empty": "Батарейка для оплаты комиссий почти разряжена", - "medium": "Батарейка для оплаты комиссий заряжена наполовину", - "full": "Батарейка для оплаты комиссий полностью заряжена" - }, + "title": "Батарейка Tonkeeper", "description": { "empty": "Отправляйте токены и NFT, взаимодействуйте со стейкингом при нулевом балансе кошелька.", - "other": "У вас достаточно заряда для более чем {{cnt}} транзакций.", - "less_10": "У вас достаточно заряда для менее чем 10 транзакций." + "other": { + "few": "В вашей батарейке %{count} заряда", + "many": "В вашей батарейке %{count} зарядов", + "one": "В вашей батарейке %{count} заряд", + "other": "В вашей батарейке %{count} зарядов" + } }, "promocode": { "button": "Зарядить с помощью промокода", @@ -1085,7 +1085,8 @@ "medium": "Средний пакет", "small": "Малый пакет" }, - "disclaimer": "Указано примерное количество транзакций. Некоторые транзакции могут стоить дороже.", + "disclaimer": "Один заряд покрывает среднюю комиссию за транзакцию. Некоторые транзакции могут стоить дороже.", + "restore": "Восстановить покупки.", "buy": "Купить", "ok": "OK", "refilled": "Ваша батарейка заряжена" diff --git a/packages/shared/i18n/locales/tonkeeper/tr-TR.json b/packages/shared/i18n/locales/tonkeeper/tr-TR.json index 4de4f1935..882809852 100644 --- a/packages/shared/i18n/locales/tonkeeper/tr-TR.json +++ b/packages/shared/i18n/locales/tonkeeper/tr-TR.json @@ -126,8 +126,7 @@ "balances_setup_wallet" : "Cüzdanı ayarlayın", "battery" : { "description" : { - "empty" : "Ana bakiyeniz boş olsa bile token ve NFT gönderin, staking işlemleri gerçekleştirin.", - "other" : "%{cnt} işlem için bataryanızda yeterli şarj seviyesi mevcut." + "empty" : "Ana bakiyeniz boş olsa bile token ve NFT gönderin, staking işlemleri gerçekleştirin." }, "ok" : "TAMAM", "packages" : { @@ -139,8 +138,7 @@ "large" : "Büyük paket", "medium" : "Orta paket", "small" : "Küçük paket" - }, - "title" : "{{price}} karşılığı {{cnt}} işlem" + } }, "promocode" : { "apply" : "Uygula", @@ -150,13 +148,7 @@ "title" : "Promosyon Kodu" }, "screen_title" : "Batarya", - "settings" : "Batarya", - "title" : { - "almost_empty" : "Gas ücretleri için batarya neredeyse boş", - "empty" : "Gaz ücretleri için bataryanızı şarj edin", - "full" : "Gas ücretleri için batarya dolu", - "medium" : "Gas ücretleri için bataryanın yarısı dolu" - } + "settings" : "Batarya" }, "browser" : { "about_dapps_caption" : "Oturum açma ve ödemeler için Tonkeeper'ı kullanabileceğiniz uygulamaları ve hizmetleri keşfedin.", @@ -1060,4 +1052,4 @@ "wallet_swap" : "Takas", "wallet_title" : "Cüzdan", "yesterday" : "Dün" -} \ No newline at end of file +} diff --git a/packages/shared/i18n/locales/tonkeeper/zh-Hans-CN.json b/packages/shared/i18n/locales/tonkeeper/zh-Hans-CN.json index 167d08782..1cb9e2e4a 100644 --- a/packages/shared/i18n/locales/tonkeeper/zh-Hans-CN.json +++ b/packages/shared/i18n/locales/tonkeeper/zh-Hans-CN.json @@ -90,8 +90,7 @@ "balances_setup_wallet" : "注册钱包", "battery" : { "description" : { - "empty" : "发送代币和 NFT,用空的主账户支付质押操作费用。", - "other" : "您已有足够的电量以进行 %{cnt} 次交易" + "empty" : "发送代币和 NFT,用空的主账户支付质押操作费用。" }, "ok" : "确定", "packages" : { @@ -103,8 +102,7 @@ "large" : "大杯", "medium" : "中杯", "small" : "小杯" - }, - "title" : "{{cnt}} 笔交易,总共 {{price}}" + } }, "promocode" : { "apply" : "应用", @@ -114,13 +112,7 @@ "title" : "优惠码" }, "screen_title" : "电池", - "settings" : "电池", - "title" : { - "almost_empty" : "电池即将耗尽", - "empty" : "为你的电池充电", - "full" : "电池已充满", - "medium" : "电池已经充满一半" - } + "settings" : "电池" }, "browser" : { "about_dapps_caption" : "探索可以使用Tonkeeper进行登录和支付的应用和服务。", @@ -882,4 +874,4 @@ "wallet_toncommunity_chat_link" : "https://t.me/toncoin_chat", "wallet_toncommunity_link" : "https://t.me/toncoin", "yesterday" : "昨天" -} \ No newline at end of file +} diff --git a/packages/shared/utils/battery.ts b/packages/shared/utils/battery.ts index 44b3d5919..0aef5bbcf 100644 --- a/packages/shared/utils/battery.ts +++ b/packages/shared/utils/battery.ts @@ -9,13 +9,11 @@ export enum BatteryState { } const valuesForBatteryState = { - [BatteryState.Medium]: '2', - [BatteryState.AlmostEmpty]: '1', + [BatteryState.Medium]: '1', + [BatteryState.AlmostEmpty]: '0.4', [BatteryState.Empty]: '0.03', }; -export const MEAN_FEES = config.get('batteryMeanFees'); - export function getBatteryState(batteryBalance: string) { const balance = new BigNumber(batteryBalance); const medium = new BigNumber(valuesForBatteryState[BatteryState.Medium]); @@ -40,12 +38,8 @@ export function getBatteryState(batteryBalance: string) { export function calculateAvailableNumOfTransactions(batteryBalance: string) { const balance = new BigNumber(batteryBalance); - // return balance divided by mean fees rounded down to nearest 10 + // return balance divided by mean fees rounded down return balance - .div(MEAN_FEES) - .decimalPlaces(0, BigNumber.ROUND_DOWN) - .div(10) - .decimalPlaces(0, BigNumber.ROUND_DOWN) - .times(10) - .toNumber(); + .div(config.get('batteryMeanFees')) + .decimalPlaces(0, BigNumber.ROUND_DOWN); } diff --git a/packages/uikit/src/components/List/ListItem.tsx b/packages/uikit/src/components/List/ListItem.tsx index 01fbb995e..c5dfeb8fe 100644 --- a/packages/uikit/src/components/List/ListItem.tsx +++ b/packages/uikit/src/components/List/ListItem.tsx @@ -15,6 +15,7 @@ import { View } from '../View'; export interface ListItemProps { titleType?: 'primary' | 'secondary'; title?: string | React.ReactNode; + titleContainerStyle?: StyleProp; titleTextType?: TTextTypes; subtitle?: string | React.ReactNode; subtitleStyle?: StyleProp; @@ -113,7 +114,7 @@ export const ListItem = memo((props) => { )} - + {isString(props.title) ? ( Date: Tue, 26 Mar 2024 19:04:44 +0400 Subject: [PATCH 02/22] fix(mobile): fix interactions while closing modal (#778) --- packages/shared/modals/AddWalletModal.tsx | 17 ++++++++++++----- .../containers/Modal/SheetModal/SheetModal.tsx | 12 +++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/shared/modals/AddWalletModal.tsx b/packages/shared/modals/AddWalletModal.tsx index e1702b490..1ad6ee80f 100644 --- a/packages/shared/modals/AddWalletModal.tsx +++ b/packages/shared/modals/AddWalletModal.tsx @@ -16,6 +16,7 @@ import { tk } from '@tonkeeper/mobile/src/wallet'; import { useUnlockVault } from '@tonkeeper/mobile/src/core/ModalContainer/NFTOperations/useUnlockVault'; import { getLastEnteredPasscode } from '@tonkeeper/mobile/src/store/wallet/sagas'; import { config } from '@tonkeeper/mobile/src/config'; +import { InteractionManager } from 'react-native'; interface AddWalletModalProps { isTonConnect?: boolean; @@ -89,7 +90,9 @@ export const AddWalletModal = memo(({ isTonConnect }) => { { nav.goBack(); - nav.navigate('ImportWalletStack'); + InteractionManager.runAfterInteractions(() => { + nav.navigate('ImportWalletStack'); + }); }} leftContentStyle={styles.iconContainer} leftContent={} @@ -104,7 +107,9 @@ export const AddWalletModal = memo(({ isTonConnect }) => { { nav.goBack(); - nav.navigate('AddWatchOnlyStack'); + InteractionManager.runAfterInteractions(() => { + nav.navigate('AddWatchOnlyStack'); + }); }} leftContentStyle={styles.iconContainer} leftContent={} @@ -120,9 +125,11 @@ export const AddWalletModal = memo(({ isTonConnect }) => { { nav.goBack(); - nav.navigate('ImportWalletStack', { - screen: 'ImportWallet', - params: { testnet: true }, + InteractionManager.runAfterInteractions(() => { + nav.navigate('ImportWalletStack', { + screen: 'ImportWallet', + params: { testnet: true }, + }); }); }} leftContentStyle={styles.iconContainer} diff --git a/packages/uikit/src/containers/Modal/SheetModal/SheetModal.tsx b/packages/uikit/src/containers/Modal/SheetModal/SheetModal.tsx index d59fb605b..2e330b876 100644 --- a/packages/uikit/src/containers/Modal/SheetModal/SheetModal.tsx +++ b/packages/uikit/src/containers/Modal/SheetModal/SheetModal.tsx @@ -19,6 +19,7 @@ import { useTheme } from '../../../styles'; import { useSheetInternal } from '@tonkeeper/router'; import { Easing, ReduceMotion, useReducedMotion } from 'react-native-reanimated'; +import { Handle, InteractionManager } from 'react-native'; export type SheetModalRef = BottomSheetModal; @@ -64,10 +65,15 @@ export const SheetModal = memo( return initialState === 'closed' ? -1 : 0; }, []); + const interactionHandle = useRef(null); + useEffect(() => { delegateMethods({ present: () => bottomSheetRef.current?.snapToIndex(0), - close: () => bottomSheetRef.current?.close(), + close: () => { + interactionHandle.current = InteractionManager.createInteractionHandle(); + bottomSheetRef.current?.close(); + }, }); }, []); @@ -83,6 +89,10 @@ export const SheetModal = memo( ); const handleClose = useCallback(async () => { + if (interactionHandle.current !== null) { + InteractionManager.clearInteractionHandle(interactionHandle.current); + interactionHandle.current = null; + } if (ignoreOnClose) { return; } From fa08da8d3d51ae56951252d20bf6e91cd8995bc9 Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Tue, 2 Apr 2024 19:14:41 +0300 Subject: [PATCH 03/22] feature(mobile): battery polishing (#780) * feature(mobile): battery support for jetton deeplinks * fix(mobile): Go to previous step when funds are insufficient * fix(mobile): Battery messages queue * feature(mobile): Experimental battery support for sign-raw * polishing wip * wip * wip * Drop other locales --- .../@core-js/src/service/contractService.ts | 9 +- .../src/service/transactionService.ts | 73 ++++++++++-- packages/mobile/src/blockchain/wallet.ts | 25 ++-- .../SendScreenBatteryWidget.tsx | 47 ++++++++ .../SendScreenBatteryWidget/index.ts | 1 + packages/mobile/src/components/index.ts | 1 + packages/mobile/src/config/index.ts | 2 +- .../src/core/HideableAmount/ShowBalance.tsx | 7 +- .../NFTOperations/Modals/SignRawModal.tsx | 37 +++--- .../NFTOperations/TxRequest.types.ts | 1 + packages/mobile/src/core/NFTSend/NFTSend.tsx | 36 ++++-- .../src/core/RefillBattery/RefillBattery.tsx | 2 +- packages/mobile/src/core/Send/Send.tsx | 18 ++- .../mobile/src/core/Settings/Settings.tsx | 4 +- .../src/core/StakingSend/StakingSend.tsx | 7 ++ packages/mobile/src/core/Swap/Swap.tsx | 5 + .../navigation/AppStack/AppStack.interface.ts | 1 + packages/mobile/src/navigation/ModalStack.tsx | 4 + .../SettingsStack/SettingsStack.interface.ts | 1 - .../SettingsStack/SettingsStack.tsx | 4 - packages/mobile/src/navigation/helper.ts | 5 +- .../hooks/useDeeplinkingResolvers.ts | 3 +- .../mobile/src/navigation/navigationNames.ts | 2 +- .../src/store/zustand/batteryUI/index.ts | 2 + .../src/store/zustand/batteryUI/types.ts | 6 + .../zustand/batteryUI/useBatteryUIStore.ts | 27 +++++ .../mobile/src/tabs/Wallet/WalletScreen.tsx | 2 +- .../src/wallet/managers/BatteryManager.ts | 49 +++++++- .../components/BatteryIcon/BatteryIcon.tsx | 33 ++++-- .../BatterySupportedTransactions.tsx | 111 ++++++++++++++++++ .../BatterySupportedTransactions/index.ts | 1 + .../RefillBattery/RefillBattery.tsx | 56 ++++++--- .../RefillBattery/RefillBatteryIAP.tsx | 65 ++++++++-- .../RefillBatterySettingsWidget.tsx | 39 ++++++ .../RefillBattery/RestorePurchases.tsx | 11 +- packages/shared/hooks/index.ts | 1 + .../shared/hooks/useIsEnabledForBattery.ts | 12 ++ .../shared/i18n/locales/tonkeeper/en.json | 44 ++++++- .../shared/i18n/locales/tonkeeper/id.json | 37 +----- .../shared/i18n/locales/tonkeeper/ru-RU.json | 44 +++++-- .../shared/i18n/locales/tonkeeper/tr-TR.json | 26 ---- packages/shared/modals/RefillBatteryModal.tsx | 95 ++++++++++++--- packages/shared/utils/blockchain.ts | 16 ++- .../png/ic-almost-empty-battery-128@4x.png | Bin 11881 -> 11692 bytes .../png/ic-almost-empty-battery-34@4x.png | Bin 0 -> 4892 bytes .../assets/icons/png/ic-battery-100-44@4x.png | Bin 0 -> 17350 bytes .../assets/icons/png/ic-battery-25-44@4x.png | Bin 0 -> 3961 bytes .../assets/icons/png/ic-battery-50-44@4x.png | Bin 0 -> 8456 bytes .../png/ic-battery-almost-empty-24@4x.png | Bin 0 -> 3412 bytes .../icons/png/ic-empty-battery-128@4x.png | Bin 11506 -> 13234 bytes .../ic-empty-battery-accent-flash-34@4x.png | Bin 0 -> 6054 bytes .../png/ic-empty-battery-flash-34@4x.png | Bin 0 -> 6046 bytes .../icons/png/ic-full-battery-128@4x.png | Bin 11853 -> 11787 bytes .../icons/png/ic-full-battery-34@4x.png | Bin 0 -> 5041 bytes .../icons/png/ic-medium-battery-128@4x.png | Bin 0 -> 11996 bytes .../icons/png/ic-medium-battery-34@4x.png | Bin 0 -> 5088 bytes .../svg/128/ic-almost-empty-battery-128.svg | 4 +- .../icons/svg/128/ic-empty-battery-128.svg | 4 +- .../icons/svg/128/ic-full-battery-128.svg | 4 +- .../icons/svg/128/ic-medium-battery-128.svg | 4 + .../svg/24/ic-battery-almost-empty-24.svg | 11 ++ .../svg/34/ic-almost-empty-battery-34.svg | 4 + .../34/ic-empty-battery-accent-flash-34.svg | 4 + .../svg/34/ic-empty-battery-flash-34.svg | 4 + .../icons/svg/34/ic-full-battery-34.svg | 4 + .../icons/svg/34/ic-medium-battery-34.svg | 4 + .../assets/icons/svg/44/ic-battery-100-44.svg | 4 + .../assets/icons/svg/44/ic-battery-25-44.svg | 4 + .../assets/icons/svg/44/ic-battery-50-44.svg | 4 + packages/uikit/src/components/Icon/Icon.tsx | 2 +- .../uikit/src/components/Icon/Icon.types.ts | 30 +++++ .../src/components/Icon/IconList.native.ts | 10 ++ .../uikit/src/components/List/ListItem.tsx | 4 +- .../uikit/src/components/Text/TextStyles.ts | 5 + 74 files changed, 870 insertions(+), 212 deletions(-) create mode 100644 packages/mobile/src/components/SendScreenBatteryWidget/SendScreenBatteryWidget.tsx create mode 100644 packages/mobile/src/components/SendScreenBatteryWidget/index.ts create mode 100644 packages/mobile/src/store/zustand/batteryUI/index.ts create mode 100644 packages/mobile/src/store/zustand/batteryUI/types.ts create mode 100644 packages/mobile/src/store/zustand/batteryUI/useBatteryUIStore.ts create mode 100644 packages/shared/components/BatterySupportedTransactions/BatterySupportedTransactions.tsx create mode 100644 packages/shared/components/BatterySupportedTransactions/index.ts create mode 100644 packages/shared/components/RefillBattery/RefillBatterySettingsWidget.tsx create mode 100644 packages/shared/hooks/useIsEnabledForBattery.ts create mode 100644 packages/uikit/assets/icons/png/ic-almost-empty-battery-34@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-battery-100-44@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-battery-25-44@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-battery-50-44@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-battery-almost-empty-24@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-empty-battery-accent-flash-34@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-empty-battery-flash-34@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-full-battery-34@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-medium-battery-128@4x.png create mode 100644 packages/uikit/assets/icons/png/ic-medium-battery-34@4x.png create mode 100644 packages/uikit/assets/icons/svg/128/ic-medium-battery-128.svg create mode 100644 packages/uikit/assets/icons/svg/24/ic-battery-almost-empty-24.svg create mode 100644 packages/uikit/assets/icons/svg/34/ic-almost-empty-battery-34.svg create mode 100644 packages/uikit/assets/icons/svg/34/ic-empty-battery-accent-flash-34.svg create mode 100644 packages/uikit/assets/icons/svg/34/ic-empty-battery-flash-34.svg create mode 100644 packages/uikit/assets/icons/svg/34/ic-full-battery-34.svg create mode 100644 packages/uikit/assets/icons/svg/34/ic-medium-battery-34.svg create mode 100644 packages/uikit/assets/icons/svg/44/ic-battery-100-44.svg create mode 100644 packages/uikit/assets/icons/svg/44/ic-battery-25-44.svg create mode 100644 packages/uikit/assets/icons/svg/44/ic-battery-50-44.svg diff --git a/packages/@core-js/src/service/contractService.ts b/packages/@core-js/src/service/contractService.ts index ebdb48760..dd1d47ea0 100644 --- a/packages/@core-js/src/service/contractService.ts +++ b/packages/@core-js/src/service/contractService.ts @@ -8,6 +8,11 @@ import { import { WalletContractV3R1, WalletContractV3R2, WalletContractV4 } from '@ton/ton'; import nacl from 'tweetnacl'; +export enum OpCodes { + JETTON_TRANSFER = 0xf8a7ea5, + NFT_TRANSFER = 0x5fcc3d14, +} + export enum WalletVersion { v3R1 = 0, v3R2 = 1, @@ -89,7 +94,7 @@ export class ContractService { static createNftTransferBody(createNftTransferBodyParams: CreateNftTransferBodyParams) { return beginCell() - .storeUint(0x5fcc3d14, 32) + .storeUint(OpCodes.NFT_TRANSFER, 32) .storeUint( createNftTransferBodyParams.queryId || ContractService.getWalletQueryId(), 64, @@ -106,7 +111,7 @@ export class ContractService { createJettonTransferBodyParams: CreateJettonTransferBodyParams, ) { return beginCell() - .storeUint(0xf8a7ea5, 32) // request_transfer op + .storeUint(OpCodes.JETTON_TRANSFER, 32) .storeUint( createJettonTransferBodyParams.queryId || ContractService.getWalletQueryId(), 64, diff --git a/packages/@core-js/src/service/transactionService.ts b/packages/@core-js/src/service/transactionService.ts index c03ed1616..2762b95bc 100644 --- a/packages/@core-js/src/service/transactionService.ts +++ b/packages/@core-js/src/service/transactionService.ts @@ -10,7 +10,7 @@ import { loadStateInit, } from '@ton/core'; import { Address as AddressFormatter } from '../formatters/Address'; -import { WalletContract } from './contractService'; +import { OpCodes, WalletContract } from './contractService'; import { SignRawMessage } from '@tonkeeper/mobile/src/core/ModalContainer/NFTOperations/TxRequest.types'; export type AnyAddress = string | Address | AddressFormatter; @@ -71,16 +71,75 @@ export class TransactionService { return { code, data }; } - static parseSignRawMessages(messages: SignRawMessage[]) { - return messages.map((message) => - internal({ + static parseSignRawMessages( + messages: SignRawMessage[], + customExcessesAccount?: string | null, + ) { + return messages.map((message) => { + let payload = message.payload && Cell.fromBase64(message.payload); + + if (payload && customExcessesAccount) { + payload = TransactionService.rebuildBodyWithCustomExcessesAccount( + payload, + customExcessesAccount, + ); + } + + return internal({ to: message.address, value: BigInt(message.amount), - body: message.payload && Cell.fromBase64(message.payload), + body: payload, bounce: this.getBounceFlagFromAddress(message.address), init: TransactionService.parseStateInit(message.stateInit), - }), - ); + }); + }); + } + + static rebuildBodyWithCustomExcessesAccount( + payload: Cell, + customExcessesAccount: string, + ) { + const slice = payload.beginParse(); + const opCode = slice.loadUint(32); + let builder = beginCell(); + + switch (opCode) { + case OpCodes.NFT_TRANSFER: + builder = builder + .storeUint(OpCodes.NFT_TRANSFER, 32) + .storeUint(slice.loadUint(64), 64) + .storeAddress(slice.loadAddress()); + + slice.loadMaybeAddress(); + + while (slice.remainingRefs) { + builder = builder.storeRef(slice.loadRef()); + } + + return builder + .storeAddress(Address.parse(customExcessesAccount)) + .storeBits(slice.loadBits(slice.remainingBits)) + .endCell(); + case OpCodes.JETTON_TRANSFER: + builder = builder + .storeUint(OpCodes.JETTON_TRANSFER, 32) + .storeUint(slice.loadUint(64), 64) + .storeCoins(slice.loadCoins()) + .storeAddress(slice.loadAddress()); + + slice.loadMaybeAddress(); + + while (slice.remainingRefs) { + builder = builder.storeRef(slice.loadRef()); + } + + return builder + .storeAddress(Address.parse(customExcessesAccount)) + .storeBits(slice.loadBits(slice.remainingBits)) + .endCell(); + default: + return payload; + } } static createTransfer(contract, transferParams: TransferParams) { diff --git a/packages/mobile/src/blockchain/wallet.ts b/packages/mobile/src/blockchain/wallet.ts index b08c929fb..e9199ba5e 100644 --- a/packages/mobile/src/blockchain/wallet.ts +++ b/packages/mobile/src/blockchain/wallet.ts @@ -26,16 +26,14 @@ import { import { tk } from '$wallet'; import { Address, Cell, internal } from '@ton/core'; -import { - emulateWithBattery, - sendBocWithBattery, -} from '@tonkeeper/shared/utils/blockchain'; +import { emulateBoc, sendBoc } from '@tonkeeper/shared/utils/blockchain'; import { OperationEnum, TonAPI, TypeEnum } from '@tonkeeper/core/src/TonAPI'; import { setBalanceForEmulation } from '@tonkeeper/shared/utils/wallet'; import { WalletNetwork } from '$wallet/WalletTypes'; import { createTonApiInstance } from '$wallet/utils'; import { config } from '$config'; import { toNano } from '$utils'; +import { BatterySupportedTransaction } from '$wallet/managers/BatteryManager'; const TonWeb = require('tonweb'); @@ -212,10 +210,6 @@ export class TonWallet { } } - private async sendBoc(boc: string): Promise { - await sendBocWithBattery(boc); - } - async createSubscription( unlockedVault: UnlockedVault | Vault, beneficiaryAddress: string, @@ -327,8 +321,12 @@ export class TonWallet { return await this.vault.tonWallet.methods.isPluginInstalled(subscriptionAddress); } - private async calcFee(boc: string, params?): Promise<[BigNumber, boolean]> { - const { emulateResult, battery } = await emulateWithBattery(boc, params); + private async calcFee( + boc: string, + params?, + withRelayer = true, + ): Promise<[BigNumber, boolean]> { + const { emulateResult, battery } = await emulateBoc(boc, params, withRelayer); return [new BigNumber(emulateResult.event.extra).multipliedBy(-1), battery]; } @@ -422,6 +420,9 @@ export class TonWallet { let [feeNano, isBattery] = await this.calcFee( boc, [setBalanceForEmulation(toNano('2'))], // Emulate with higher balance to calculate fair amount to send + tk.wallet.battery.state.data.supportedTransactions[ + BatterySupportedTransaction.Jetton + ], ); return [Ton.fromNano(feeNano.toString()), isBattery]; @@ -509,7 +510,7 @@ export class TonWallet { } try { - await this.sendBoc(boc); + await sendBoc(boc, isBattery); } catch (e) { if (!store.getState().main.isTimeSynced) { throw new Error('wrong_time'); @@ -721,7 +722,7 @@ export class TonWallet { } try { - await this.sendBoc(boc); + await sendBoc(boc, false); } catch (e) { if (!store.getState().main.isTimeSynced) { throw new Error('wrong_time'); diff --git a/packages/mobile/src/components/SendScreenBatteryWidget/SendScreenBatteryWidget.tsx b/packages/mobile/src/components/SendScreenBatteryWidget/SendScreenBatteryWidget.tsx new file mode 100644 index 000000000..a5646b46e --- /dev/null +++ b/packages/mobile/src/components/SendScreenBatteryWidget/SendScreenBatteryWidget.tsx @@ -0,0 +1,47 @@ +import React, { memo, useMemo } from 'react'; +import { useBatteryState } from '@tonkeeper/shared/query/hooks/useBatteryState'; +import { Icon, Spacer, Steezy, Text, View } from '@tonkeeper/uikit'; +import { t } from '@tonkeeper/shared/i18n'; +import { BatteryState } from '@tonkeeper/shared/utils/battery'; + +export interface SendScreenBatteryWidgetProps { + isSendingWithBattery: boolean; +} + +export const SendScreenBatteryWidget = memo((props) => { + const batteryState = useBatteryState(); + + const content = useMemo(() => { + if (props.isSendingWithBattery && batteryState === BatteryState.AlmostEmpty) { + return ( + <> + + {t('battery.send_widget.battery')} + + + + + ); + } + return null; + }, [batteryState, props.isSendingWithBattery]); + + return {content}; +}); + +const styles = Steezy.create({ + container: { + paddingVertical: 8, + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center', + }, + iconSize: { + width: 15, + height: 24, + }, +}); diff --git a/packages/mobile/src/components/SendScreenBatteryWidget/index.ts b/packages/mobile/src/components/SendScreenBatteryWidget/index.ts new file mode 100644 index 000000000..797105082 --- /dev/null +++ b/packages/mobile/src/components/SendScreenBatteryWidget/index.ts @@ -0,0 +1 @@ +export * from './SendScreenBatteryWidget'; diff --git a/packages/mobile/src/components/index.ts b/packages/mobile/src/components/index.ts index cdb256533..d63c51d3d 100644 --- a/packages/mobile/src/components/index.ts +++ b/packages/mobile/src/components/index.ts @@ -1 +1,2 @@ export * from './CardsWidget'; +export * from './SendScreenBatteryWidget'; diff --git a/packages/mobile/src/config/index.ts b/packages/mobile/src/config/index.ts index 809ac2088..a295db0dd 100644 --- a/packages/mobile/src/config/index.ts +++ b/packages/mobile/src/config/index.ts @@ -82,7 +82,7 @@ const defaultConfig: Partial = { tronapiTestnetHost: 'https://testnet-tron.tonkeeper.com', batteryHost: 'https://battery.tonkeeper.com', batteryTestnetHost: 'https://testnet-battery.tonkeeper.com', - batteryMeanFees: '0.03', + batteryMeanFees: '0.0055', disable_battery: true, disable_battery_iap_module: Platform.OS !== 'android', // Enable for iOS, disable for Android disable_battery_send: true, diff --git a/packages/mobile/src/core/HideableAmount/ShowBalance.tsx b/packages/mobile/src/core/HideableAmount/ShowBalance.tsx index d409a0f56..abea7aa99 100644 --- a/packages/mobile/src/core/HideableAmount/ShowBalance.tsx +++ b/packages/mobile/src/core/HideableAmount/ShowBalance.tsx @@ -34,7 +34,7 @@ export const ShowBalance: React.FC<{ amount: string }> = ({ amount }) => { ) : ( - {amount} + {amount} )} @@ -43,9 +43,12 @@ export const ShowBalance: React.FC<{ amount: string }> = ({ amount }) => { const styles = Steezy.create(({ colors }) => ({ container: { - height: 36, + flexDirection: 'row', + height: 54, + alignItems: 'center', }, starsContainer: { + height: 40, backgroundColor: colors.backgroundSecondary, borderRadius: 100, }, diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx index 7342c3b02..861748f32 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx @@ -39,11 +39,10 @@ import { AnyActionItem, } from '$wallet/models/ActivityModel'; import { JettonTransferAction, NftItemTransferAction } from 'tonapi-sdk-js'; -import { - TokenDetailsParams, - TokenDetailsProps, -} from '../../../../components/TokenDetails/TokenDetails'; +import { TokenDetailsParams } from '../../../../components/TokenDetails/TokenDetails'; import { ModalStackRouteNames } from '$navigation'; +import { CanceledActionError } from '$core/Send/steps/ConfirmStep/ActionErrors'; +import { emulateBoc, sendBoc } from '@tonkeeper/shared/utils/blockchain'; interface SignRawModalProps { consequences?: MessageConsequences; @@ -82,7 +81,14 @@ export const SignRawModal = memo((props) => { const handleOpenTokenDetails = (tokenDetailsParams: TokenDetailsParams) => () => nav.navigate(ModalStackRouteNames.TokenDetails, tokenDetailsParams); + const handleConfirm = onConfirm(async ({ startLoading }) => { + const pendingTransactions = await tk.wallet.battery.getStatus(); + if (pendingTransactions.length) { + Toast.fail(t('transfer_pending_by_battery_error')); + await delay(200); + throw new CanceledActionError(); + } const vault = await unlockVault(wallet.identifier); const privateKey = await vault.getTonPrivateKey(); @@ -93,19 +99,18 @@ export const SignRawModal = memo((props) => { Buffer.from(vault.tonPublicKey), vault.workchain, ); + const boc = TransactionService.createTransfer(contract, { - messages: TransactionService.parseSignRawMessages(params.messages), + messages: TransactionService.parseSignRawMessages( + params.messages, + isBattery ? await tk.wallet.battery.getExcessesAccount() : undefined, + ), seqno: await getWalletSeqno(wallet), sendMode: 3, secretKey: Buffer.from(privateKey), }); - await wallet.tonapi.blockchain.sendBlockchainMessage( - { - boc, - }, - { format: 'text' }, - ); + await sendBoc(boc, isBattery); if (onSuccess) { trackEvent(Events.SendSuccess, { from: SendAnalyticsFrom.SignRaw }); @@ -118,7 +123,7 @@ export const SignRawModal = memo((props) => { return () => { onDismiss?.(); }; - }, []); + }, [onDismiss]); const actions = useMemo(() => { if (consequences) { @@ -319,9 +324,13 @@ export const openSignRawModal = async ( seqno: await getWalletSeqno(wallet), secretKey: Buffer.alloc(64), }); - consequences = await wallet.tonapi.wallet.emulateMessageToWallet({ + + const { emulateResult, battery } = await emulateBoc( boc, - }); + options.experimentalWithBattery, + ); + consequences = emulateResult; + isBattery = battery; if (!isBattery) { const totalAmount = calculateMessageTransferAmount(params.messages); diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/TxRequest.types.ts b/packages/mobile/src/core/ModalContainer/NFTOperations/TxRequest.types.ts index 6ad3871d4..200a82282 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/TxRequest.types.ts +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/TxRequest.types.ts @@ -135,6 +135,7 @@ export type TxResponseOptions = { }; export type TxRequestBody = { + experimentalWithBattery?: boolean; type: TxTypes; expires_sec?: number; response_options?: TxResponseOptions; diff --git a/packages/mobile/src/core/NFTSend/NFTSend.tsx b/packages/mobile/src/core/NFTSend/NFTSend.tsx index d7e44eaf7..79ae333fc 100644 --- a/packages/mobile/src/core/NFTSend/NFTSend.tsx +++ b/packages/mobile/src/core/NFTSend/NFTSend.tsx @@ -35,10 +35,7 @@ import { delay } from '$utils'; import { Toast } from '$store'; import axios from 'axios'; import { useUnlockVault } from '$core/ModalContainer/NFTOperations/useUnlockVault'; -import { - emulateWithBattery, - sendBocWithBattery, -} from '@tonkeeper/shared/utils/blockchain'; +import { emulateBoc, sendBoc } from '@tonkeeper/shared/utils/blockchain'; import { checkIsInsufficient, openInsufficientFundsModal, @@ -48,9 +45,10 @@ import { Keyboard } from 'react-native'; import nacl from 'tweetnacl'; import { useInstance } from '$hooks/useInstance'; import { AccountsApi, Configuration } from '@tonkeeper/core/src/legacy'; -import { useWallet } from '@tonkeeper/shared/hooks'; +import { useIsEnabledForBattery, useWallet } from '@tonkeeper/shared/hooks'; import { tk } from '$wallet'; import { config } from '$config'; +import { BatterySupportedTransaction } from '$wallet/managers/BatteryManager'; interface Props { route: RouteProp; @@ -63,6 +61,9 @@ export const NFTSend: FC = (props) => { }, } = props; + const isNFTSendEnabledWithRelayer = useIsEnabledForBattery( + BatterySupportedTransaction.NFT, + ); const wallet = useWallet(); const stepViewRef = useRef(null); @@ -155,9 +156,10 @@ export const NFTSend: FC = (props) => { secretKey: Buffer.alloc(64), }); - const response = await emulateWithBattery( + const response = await emulateBoc( boc, [setBalanceForEmulation(toNano('2'))], // Emulate with higher balance to calculate fair amount to send + isNFTSendEnabledWithRelayer, ); setConsequences(response.emulateResult); @@ -178,7 +180,17 @@ export const NFTSend: FC = (props) => { } finally { setPreparing(false); } - }, [comment, isCommentEncrypted, nftAddress, recipient, wallet]); + }, [ + comment, + isCommentEncrypted, + isNFTSendEnabledWithRelayer, + nftAddress, + recipient, + wallet.address.ton.raw, + wallet.config.version, + wallet.config.workchain, + wallet.pubkey, + ]); const accountsApi = useInstance(() => { const tonApiConfiguration = new Configuration({ @@ -237,6 +249,13 @@ export const NFTSend: FC = (props) => { try { setSending(true); + const pendingTransactions = await tk.wallet.battery.getStatus(); + if (pendingTransactions.length) { + Toast.fail(t('transfer_pending_by_battery_error')); + await delay(200); + throw new CanceledActionError(); + } + const vault = await unlockVault(); const privateKey = await vault.getTonPrivateKey(); @@ -262,6 +281,7 @@ export const NFTSend: FC = (props) => { const checkResult = await checkIsInsufficient(totalAmount.toString(), tk.wallet); if (!isBattery && checkResult.insufficient) { + stepViewRef.current?.go(NFTSendSteps.ADDRESS); openInsufficientFundsModal({ totalAmount: totalAmount.toString(), balance: checkResult.balance, @@ -299,7 +319,7 @@ export const NFTSend: FC = (props) => { secretKey: Buffer.from(privateKey), }); - await sendBocWithBattery(boc); + await sendBoc(boc, isBattery); } catch (e) { throw e; } finally { diff --git a/packages/mobile/src/core/RefillBattery/RefillBattery.tsx b/packages/mobile/src/core/RefillBattery/RefillBattery.tsx index 56d89988d..8ba7e8289 100644 --- a/packages/mobile/src/core/RefillBattery/RefillBattery.tsx +++ b/packages/mobile/src/core/RefillBattery/RefillBattery.tsx @@ -6,7 +6,7 @@ import { Screen, Steezy, View } from '@tonkeeper/uikit'; export const RefillBattery = memo(() => { return ( - + diff --git a/packages/mobile/src/core/Send/Send.tsx b/packages/mobile/src/core/Send/Send.tsx index 03b53a4de..fab73db27 100644 --- a/packages/mobile/src/core/Send/Send.tsx +++ b/packages/mobile/src/core/Send/Send.tsx @@ -5,7 +5,7 @@ import { StepView, StepViewItem, StepViewRef } from '$shared/components'; import { CryptoCurrencies, CryptoCurrency, Decimals } from '$shared/constants'; import { walletActions } from '$store/wallet'; import { NavBar, Text } from '$uikit'; -import { parseLocaleNumber } from '$utils'; +import { delay, parseLocaleNumber } from '$utils'; import React, { FC, useCallback, @@ -68,6 +68,7 @@ export const Send: FC = ({ route }) => { from, expiryTimestamp, redirectToActivity = true, + isBattery: initialIsBattery = false, } = route.params; const initialAddress = @@ -123,7 +124,7 @@ export const Send: FC = ({ route }) => { const [fee, setFee] = useState(initialFee); const [isInactive, setInactive] = useState(initialIsInactive); - const [isBattery, setBattery] = useState(false); + const [isBattery, setBattery] = useState(initialIsBattery); const [insufficientFundsParams, setInsufficientFundsParams] = useState(null); @@ -279,6 +280,13 @@ export const Send: FC = ({ route }) => { return onFail(new DismissedActionError()); } + const pendingTransactions = await tk.wallet.battery.getStatus(); + if (pendingTransactions.length) { + Toast.fail(t('transfer_pending_by_battery_error')); + await delay(200); + return onFail(new CanceledActionError()); + } + if (expiryTimestamp && expiryTimestamp < getTimeSec()) { Toast.fail(t('transfer_deeplink_expired_error')); @@ -286,6 +294,7 @@ export const Send: FC = ({ route }) => { } if (insufficientFundsParams) { + goToAmount(); openInsufficientFundsModal(insufficientFundsParams); return onFail(new CanceledActionError()); @@ -333,13 +342,13 @@ export const Send: FC = ({ route }) => { } }, [ - fee, recipient, expiryTimestamp, insufficientFundsParams, trcPayload.value, - isBattery, + goToAmount, dispatch, + fee, currencyAdditionalParams, currency, parsedAmount, @@ -349,6 +358,7 @@ export const Send: FC = ({ route }) => { tokenType, jettonWalletAddress, decimals, + isBattery, from, unlock, ], diff --git a/packages/mobile/src/core/Settings/Settings.tsx b/packages/mobile/src/core/Settings/Settings.tsx index 6f7bbc70e..881879a90 100644 --- a/packages/mobile/src/core/Settings/Settings.tsx +++ b/packages/mobile/src/core/Settings/Settings.tsx @@ -22,10 +22,10 @@ import { openLegalDocuments, openManageTokens, openNotifications, - openRefillBattery, openSecurity, openSelectLanguage, openSubscriptions, + openRefillBatteryModal, } from '$navigation'; import { walletActions } from '$store/wallet'; import { @@ -207,7 +207,7 @@ export const Settings: FC = () => { }, []); const handleBattery = useCallback(() => { - openRefillBattery(); + openRefillBatteryModal(); }, []); const handleDeleteAccount = useCallback(() => { diff --git a/packages/mobile/src/core/StakingSend/StakingSend.tsx b/packages/mobile/src/core/StakingSend/StakingSend.tsx index 826b9f396..134e99a7e 100644 --- a/packages/mobile/src/core/StakingSend/StakingSend.tsx +++ b/packages/mobile/src/core/StakingSend/StakingSend.tsx @@ -276,6 +276,13 @@ export const StakingSend: FC = (props) => { try { setSending(true); + const pendingTransactions = await tk.wallet.battery.getStatus(); + if (pendingTransactions.length) { + Toast.fail(t('transfer_pending_by_battery_error')); + await delay(200); + throw new CanceledActionError(); + } + const totalAmount = calculateMessageTransferAmount(messages.current); const checkResult = await checkIsInsufficient(totalAmount, tk.wallet); if (checkResult.insufficient) { diff --git a/packages/mobile/src/core/Swap/Swap.tsx b/packages/mobile/src/core/Swap/Swap.tsx index c5a4f817d..4d03b21cf 100644 --- a/packages/mobile/src/core/Swap/Swap.tsx +++ b/packages/mobile/src/core/Swap/Swap.tsx @@ -16,6 +16,7 @@ import { ShouldStartLoadRequest } from 'react-native-webview/lib/WebViewTypes'; import { Linking } from 'react-native'; import { useDeeplinking } from '$libs/deeplinking'; import DeviceInfo from 'react-native-device-info'; +import { BatterySupportedTransaction } from '$wallet/managers/BatteryManager'; interface Props { jettonAddress?: string; @@ -83,6 +84,10 @@ export const Swap: FC = (props) => { openSignRawModal( request, { + experimentalWithBattery: + tk.wallet.battery.state.data.supportedTransactions[ + BatterySupportedTransaction.Swap + ], expires_sec: valid_until, response_options: { broadcast: false, diff --git a/packages/mobile/src/navigation/AppStack/AppStack.interface.ts b/packages/mobile/src/navigation/AppStack/AppStack.interface.ts index b5155d1bc..2ddee9ba8 100644 --- a/packages/mobile/src/navigation/AppStack/AppStack.interface.ts +++ b/packages/mobile/src/navigation/AppStack/AppStack.interface.ts @@ -17,6 +17,7 @@ export type AppStackParamList = { nftAddress: string; }; [AppStackRouteNames.Send]: { + isBattery?: boolean; currency?: CryptoCurrency | string; address?: string; comment?: string; diff --git a/packages/mobile/src/navigation/ModalStack.tsx b/packages/mobile/src/navigation/ModalStack.tsx index 5e2bc1c36..18e5db475 100644 --- a/packages/mobile/src/navigation/ModalStack.tsx +++ b/packages/mobile/src/navigation/ModalStack.tsx @@ -59,6 +59,10 @@ export const ModalStack = React.memo(() => ( path={AppStackRouteNames.ReceiveInscription} /> + {/* */} diff --git a/packages/mobile/src/navigation/SettingsStack/SettingsStack.interface.ts b/packages/mobile/src/navigation/SettingsStack/SettingsStack.interface.ts index 968cc655e..a14e1b11f 100644 --- a/packages/mobile/src/navigation/SettingsStack/SettingsStack.interface.ts +++ b/packages/mobile/src/navigation/SettingsStack/SettingsStack.interface.ts @@ -9,7 +9,6 @@ export type SettingsStackParamList = { [SettingsStackRouteNames.FontLicense]: {}; [SettingsStackRouteNames.Notifications]: {}; [SettingsStackRouteNames.ChooseCurrency]: {}; - [SettingsStackRouteNames.RefillBattery]: {}; [SettingsStackRouteNames.Language]: {}; [SettingsStackRouteNames.Backup]: {}; }; diff --git a/packages/mobile/src/navigation/SettingsStack/SettingsStack.tsx b/packages/mobile/src/navigation/SettingsStack/SettingsStack.tsx index 857f2d617..5c75f65eb 100644 --- a/packages/mobile/src/navigation/SettingsStack/SettingsStack.tsx +++ b/packages/mobile/src/navigation/SettingsStack/SettingsStack.tsx @@ -40,10 +40,6 @@ export const SettingsStack: FC = () => { component={LegalDocuments} /> - { const options = { currency: query.jetton, + isBattery: details.isBattery, address, comment, amount, diff --git a/packages/mobile/src/navigation/navigationNames.ts b/packages/mobile/src/navigation/navigationNames.ts index fdbdfc507..b4858e154 100644 --- a/packages/mobile/src/navigation/navigationNames.ts +++ b/packages/mobile/src/navigation/navigationNames.ts @@ -6,6 +6,7 @@ export enum ModalStackRouteNames { export enum AppStackRouteNames { MainStack = 'MainStack', + RefillBattery = 'RefillBattery', Receive = 'Receive', Send = 'Send', ChooseCountry = 'ChooseCountry', @@ -78,7 +79,6 @@ export enum SettingsStackRouteNames { FontLicense = 'FontLicense', Notifications = 'Notifications', ChooseCurrency = 'ChooseCurrency', - RefillBattery = 'RefillBattery', Language = 'Language', Backup = 'Backup', } diff --git a/packages/mobile/src/store/zustand/batteryUI/index.ts b/packages/mobile/src/store/zustand/batteryUI/index.ts new file mode 100644 index 000000000..18de375cd --- /dev/null +++ b/packages/mobile/src/store/zustand/batteryUI/index.ts @@ -0,0 +1,2 @@ +export * from './useBatteryUIStore'; +export * from './types'; diff --git a/packages/mobile/src/store/zustand/batteryUI/types.ts b/packages/mobile/src/store/zustand/batteryUI/types.ts new file mode 100644 index 000000000..975247518 --- /dev/null +++ b/packages/mobile/src/store/zustand/batteryUI/types.ts @@ -0,0 +1,6 @@ +export interface IBatteryUIStore { + isViewedBatteryScreen: boolean; + actions: { + setIsViewedBatteryScreen: (isViewed: boolean) => void; + }; +} diff --git a/packages/mobile/src/store/zustand/batteryUI/useBatteryUIStore.ts b/packages/mobile/src/store/zustand/batteryUI/useBatteryUIStore.ts new file mode 100644 index 000000000..6b4dfab50 --- /dev/null +++ b/packages/mobile/src/store/zustand/batteryUI/useBatteryUIStore.ts @@ -0,0 +1,27 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { create } from 'zustand'; +import { createJSONStorage, persist } from 'zustand/middleware'; +import { IBatteryUIStore } from './types'; + +const initialState: Omit = { + isViewedBatteryScreen: false, +}; + +export const useBatteryUIStore = create( + persist( + (set) => ({ + ...initialState, + actions: { + setIsViewedBatteryScreen: (isViewedBatteryScreen) => { + set({ isViewedBatteryScreen }); + }, + }, + }), + { + name: 'battery-ui', + storage: createJSONStorage(() => AsyncStorage), + partialize: ({ isViewedBatteryScreen }) => + ({ isViewedBatteryScreen } as IBatteryUIStore), + }, + ), +); diff --git a/packages/mobile/src/tabs/Wallet/WalletScreen.tsx b/packages/mobile/src/tabs/Wallet/WalletScreen.tsx index e9511cc93..5b95de5c7 100644 --- a/packages/mobile/src/tabs/Wallet/WalletScreen.tsx +++ b/packages/mobile/src/tabs/Wallet/WalletScreen.tsx @@ -159,7 +159,7 @@ export const WalletScreen = memo(({ navigation }) => { - + diff --git a/packages/mobile/src/wallet/managers/BatteryManager.ts b/packages/mobile/src/wallet/managers/BatteryManager.ts index 357d48258..64a07f883 100644 --- a/packages/mobile/src/wallet/managers/BatteryManager.ts +++ b/packages/mobile/src/wallet/managers/BatteryManager.ts @@ -5,15 +5,27 @@ import { State } from '@tonkeeper/core/src/utils/State'; import { TonProofManager } from '$wallet/managers/TonProofManager'; import { logger, NamespacedLogger } from '$logger'; +export enum BatterySupportedTransaction { + NFT = 'nft', + Jetton = 'jetton', + Swap = 'swap', +} + export interface BatteryState { isLoading: boolean; balance?: string; + supportedTransactions: Record; } export class BatteryManager { public state = new State({ isLoading: false, balance: undefined, + supportedTransactions: { + [BatterySupportedTransaction.NFT]: true, + [BatterySupportedTransaction.Jetton]: true, + [BatterySupportedTransaction.Swap]: true, + }, }); private logger: NamespacedLogger; @@ -25,7 +37,10 @@ export class BatteryManager { private storage: Storage, ) { this.state.persist({ - partialize: ({ balance }) => ({ balance }), + partialize: ({ balance, supportedTransactions }) => ({ + balance, + supportedTransactions, + }), storage: this.storage, key: `${this.persistPath}/battery`, }); @@ -119,6 +134,19 @@ export class BatteryManager { } } + public async setSupportedTransaction( + transaction: BatterySupportedTransaction, + supported: boolean, + ) { + this.state.set((state) => ({ + ...state, + supportedTransactions: { + ...state.supportedTransactions, + [transaction]: supported, + }, + })); + } + public async makeAndroidPurchase(purchases: { token: string; product_id: string }[]) { try { if (!this.tonProof.tonProofToken) { @@ -142,6 +170,25 @@ export class BatteryManager { } } + public async getStatus() { + try { + if (!this.tonProof.tonProofToken) { + throw new Error('No proof token'); + } + + const data = await this.batteryapi.getStatus({ + headers: { + 'X-TonConnect-Auth': this.tonProof.tonProofToken, + }, + }); + + return data.pending_transactions; + } catch (err) { + logger.error('getStatus error'); + return []; + } + } + public async sendMessage(boc: string) { try { if (!this.tonProof.tonProofToken) { diff --git a/packages/shared/components/BatteryIcon/BatteryIcon.tsx b/packages/shared/components/BatteryIcon/BatteryIcon.tsx index 052931752..e4a407da8 100644 --- a/packages/shared/components/BatteryIcon/BatteryIcon.tsx +++ b/packages/shared/components/BatteryIcon/BatteryIcon.tsx @@ -1,28 +1,43 @@ import React, { memo } from 'react'; import { useBatteryBalance } from '../../query/hooks/useBatteryBalance'; -import { Icon, IconNames, TouchableOpacity } from '@tonkeeper/uikit'; +import { Icon, IconNames, Steezy, TouchableOpacity } from '@tonkeeper/uikit'; import { BatteryState, getBatteryState } from '../../utils/battery'; -import { openRefillBatteryModal } from '../../modals/RefillBatteryModal'; import { config } from '@tonkeeper/mobile/src/config'; +import { useBatteryUIStore } from '@tonkeeper/mobile/src/store/zustand/batteryUI'; +import { openRefillBatteryModal } from '@tonkeeper/mobile/src/navigation'; -const iconNames: { [key: string]: IconNames } = { - [BatteryState.Empty]: 'ic-empty-battery-28', - [BatteryState.AlmostEmpty]: 'ic-empty-battery-28', - [BatteryState.Medium]: 'ic-almost-empty-battery-28', - [BatteryState.Full]: 'ic-full-battery-28', +const iconNames: { [key: string]: ((isViewed: boolean) => IconNames) | IconNames } = { + [BatteryState.Empty]: (isViewed) => + isViewed ? 'ic-empty-battery-flash-34' : 'ic-empty-battery-accent-flash-34', + [BatteryState.AlmostEmpty]: 'ic-almost-empty-battery-34', + [BatteryState.Medium]: 'ic-medium-battery-34', + [BatteryState.Full]: 'ic-full-battery-34', }; const hitSlop = { top: 8, bottom: 8, right: 8, left: 8 }; export const BatteryIcon = memo(() => { const { balance } = useBatteryBalance(); - if (!balance || balance === '0' || config.get('disable_battery')) return null; + const isViewedBatteryScreen = useBatteryUIStore((state) => state.isViewedBatteryScreen); + if (config.get('disable_battery')) return null; const iconName = iconNames[getBatteryState(balance)]; return ( - + ); }); + +const styles = Steezy.create({ + iconSize: { + width: 20, + height: 34, + }, +}); diff --git a/packages/shared/components/BatterySupportedTransactions/BatterySupportedTransactions.tsx b/packages/shared/components/BatterySupportedTransactions/BatterySupportedTransactions.tsx new file mode 100644 index 000000000..cb6e24412 --- /dev/null +++ b/packages/shared/components/BatterySupportedTransactions/BatterySupportedTransactions.tsx @@ -0,0 +1,111 @@ +import React, { memo, useCallback } from 'react'; +import { List, Steezy, Switch, Text, View } from '@tonkeeper/uikit'; +import BigNumber from 'bignumber.js'; +import { config } from '@tonkeeper/mobile/src/config'; +import { t } from '@tonkeeper/shared/i18n'; +import { capitalizeFirstLetter } from '../../utils/date'; +import { useExternalState } from '../../hooks/useExternalState'; +import { tk } from '@tonkeeper/mobile/src/wallet'; +import { BatterySupportedTransaction } from '@tonkeeper/mobile/src/wallet/managers/BatteryManager'; + +export interface SupportedTransaction { + type: BatterySupportedTransaction; + name: string; + nameSingle: string; + meanPrice: string; +} + +export const supportedTransactions: SupportedTransaction[] = [ + { + type: BatterySupportedTransaction.Swap, + name: 'battery.transactions.types.swap', + nameSingle: 'battery.transactions.type.swap', + meanPrice: '0.22', + }, + { + type: BatterySupportedTransaction.NFT, + name: 'battery.transactions.types.nft', + nameSingle: 'battery.transactions.type.transfer', + meanPrice: '0.025', + }, + { + type: BatterySupportedTransaction.Jetton, + name: 'battery.transactions.types.jetton', + nameSingle: 'battery.transactions.type.transfer', + meanPrice: '0.055', + }, +]; + +const calculateChargesAmount = (transactionCost: string, chargeCost: string) => + new BigNumber(transactionCost).div(chargeCost).decimalPlaces(0).toNumber(); + +export interface BatterySupportedTransactionsProps { + editable?: boolean; +} + +export const BatterySupportedTransactions = memo( + (props) => { + const supportedTransactionsValues = useExternalState( + tk.wallet.battery.state, + (state) => state.supportedTransactions, + ); + + const handleSwitchSupport = useCallback( + (transactionType: BatterySupportedTransaction) => (supported: boolean) => { + tk.wallet.battery.setSupportedTransaction(transactionType, supported); + }, + [], + ); + + return ( + + {props.editable && ( + + + {t('battery.transactions.settings')} + + + {t('battery.transactions.description')} + + + )} + + {supportedTransactions.map((transaction) => ( + + handleSwitchSupport(transaction.type)( + !supportedTransactionsValues[transaction.type], + ) + } + key={transaction.type} + title={capitalizeFirstLetter(t(transaction.name))} + subtitle={t('battery.transactions.charges_per_action', { + count: calculateChargesAmount( + transaction.meanPrice, + config.get('batteryMeanFees'), + ), + transactionName: t(transaction.nameSingle), + })} + rightContent={ + props.editable && ( + + ) + } + /> + ))} + + + ); + }, +); + +const styles = Steezy.create({ + textContainer: { + paddingHorizontal: 32, + marginBottom: 32, + }, +}); diff --git a/packages/shared/components/BatterySupportedTransactions/index.ts b/packages/shared/components/BatterySupportedTransactions/index.ts new file mode 100644 index 000000000..79ce008b1 --- /dev/null +++ b/packages/shared/components/BatterySupportedTransactions/index.ts @@ -0,0 +1 @@ +export * from './BatterySupportedTransactions'; diff --git a/packages/shared/components/RefillBattery/RefillBattery.tsx b/packages/shared/components/RefillBattery/RefillBattery.tsx index 45d1846a9..edd12060e 100644 --- a/packages/shared/components/RefillBattery/RefillBattery.tsx +++ b/packages/shared/components/RefillBattery/RefillBattery.tsx @@ -6,40 +6,57 @@ import { import { memo } from 'react'; import { useBatteryBalance } from '../../query/hooks/useBatteryBalance'; -import { Icon, IconNames, Spacer, Steezy, Text, View } from '@tonkeeper/uikit'; -import { navigation, SheetActions } from '@tonkeeper/router'; +import { + Icon, + IconNames, + Spacer, + Steezy, + Text, + TouchableOpacity, + View, +} from '@tonkeeper/uikit'; import { RefillBatteryIAP } from './RefillBatteryIAP'; import { t } from '@tonkeeper/shared/i18n'; import { config } from '@tonkeeper/mobile/src/config'; import { RechargeByPromoButton } from './RechargeByPromoButton'; import { RestorePurchases } from './RestorePurchases'; +import { RefillBatterySettingsWidget } from './RefillBatterySettingsWidget'; +import Animated from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; const iconNames: { [key: string]: IconNames } = { [BatteryState.Empty]: 'ic-empty-battery-128', - [BatteryState.AlmostEmpty]: 'ic-empty-battery-128', - [BatteryState.Medium]: 'ic-almost-empty-battery-128', + [BatteryState.AlmostEmpty]: 'ic-almost-empty-battery-128', + [BatteryState.Medium]: 'ic-medium-battery-128', [BatteryState.Full]: 'ic-full-battery-128', }; -export const RefillBattery = memo(() => { +export interface RefillBatteryProps { + navigateToTransactions: () => void; +} + +export const RefillBattery = memo((props) => { const { balance } = useBatteryBalance(); const batteryState = getBatteryState(balance ?? '0'); const iconName = iconNames[batteryState]; const availableNumOfTransactionsCount = calculateAvailableNumOfTransactions( balance ?? '0', ); + const bottomInsets = useSafeAreaInsets().bottom; const isInAppPurchasesDisabled = config.get('disable_battery_iap_module'); return ( - <> + - + {t(`battery.title`)} - {t( `battery.description.${ @@ -50,29 +67,30 @@ export const RefillBattery = memo(() => { }, )} - + {batteryState === BatteryState.Empty && ( + + + {t('battery.transactions.supported')} + + + )} + + {batteryState !== BatteryState.Empty && ( + + )} {!isInAppPurchasesDisabled ? : null} - + ); }); -export function openRefillBatteryModal() { - navigation.push('SheetsProvider', { - $$action: SheetActions.ADD, - component: RefillBattery, - path: '/refill-battery', - }); -} - export const styles = Steezy.create({ contentContainer: { - paddingTop: 48, alignItems: 'center', paddingHorizontal: 32, }, diff --git a/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx b/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx index c9cf0ff38..24693f563 100644 --- a/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx +++ b/packages/shared/components/RefillBattery/RefillBatteryIAP.tsx @@ -1,25 +1,50 @@ -import { Button, List, Spacer, Steezy, Text, Toast, View } from '@tonkeeper/uikit'; +import { + Button, + Icon, + IconNames, + List, + Spacer, + Steezy, + Text, + Toast, + View, +} from '@tonkeeper/uikit'; import { memo, useCallback, useEffect, useState } from 'react'; import { t } from '@tonkeeper/shared/i18n'; import { tk } from '@tonkeeper/mobile/src/wallet'; import { Platform } from 'react-native'; import { useIAP } from 'react-native-iap'; import { SkeletonLine } from '@tonkeeper/mobile/src/uikit/Skeleton/SkeletonLine'; +import { useTokenPrice } from '@tonkeeper/mobile/src/hooks/useTokenPrice'; +import { CryptoCurrencies } from '@tonkeeper/mobile/src/shared/constants'; +import BigNumber from 'bignumber.js'; +import { config } from '@tonkeeper/mobile/src/config'; -const packages = [ +export interface InAppPackage { + icon: IconNames; + key: string; + // TODO: move to backend + userProceed: number; + packageId: string; +} + +const packages: InAppPackage[] = [ { + icon: 'ic-battery-100-44', key: 'large', - transactions: 700, + userProceed: 7.5, packageId: 'LargePack', }, { + icon: 'ic-battery-50-44', key: 'medium', - transactions: 400, + userProceed: 5, packageId: 'MediumPack', }, { + icon: 'ic-battery-25-44', key: 'small', - transactions: 100, + userProceed: 2.5, packageId: 'SmallPack', }, ]; @@ -27,6 +52,7 @@ const packages = [ export const RefillBatteryIAP = memo(() => { const [purchaseInProgress, setPurchaseInProgress] = useState(false); const { products, getProducts, requestPurchase, finishTransaction } = useIAP(); + const tonPriceInUsd = useTokenPrice(CryptoCurrencies.Ton).usd; useEffect(() => { getProducts({ @@ -94,21 +120,30 @@ export const RefillBatteryIAP = memo(() => { ); return ( + } titleContainerStyle={styles.titleContainer.static} title={ - {t(`battery.packages.title`, { - price: product?.localizedPrice ?? '', - cnt: item.transactions, - })} + {t(`battery.packages.title.${item.key}`)} - - {!product?.localizedPrice && } - {t(`battery.packages.subtitle.${item.key}`)} + {t(`battery.packages.subtitle`, { + count: new BigNumber(item.userProceed) + .div(tonPriceInUsd) + .div(config.get('batteryMeanFees')) + .decimalPlaces(0) + .toNumber(), + })} } @@ -119,7 +154,7 @@ export const RefillBatteryIAP = memo(() => { disabled={purchaseInProgress || !product} onPress={makePurchase(item.packageId)} size="small" - title={t('battery.packages.buy')} + title={product?.localizedPrice ?? 'Loading'} /> } /> @@ -142,4 +177,8 @@ const styles = Steezy.create({ flexDirection: 'row', alignItems: 'center', }, + listItemIcon: { + width: 26, + height: 44, + }, }); diff --git a/packages/shared/components/RefillBattery/RefillBatterySettingsWidget.tsx b/packages/shared/components/RefillBattery/RefillBatterySettingsWidget.tsx new file mode 100644 index 000000000..f2ea3dd45 --- /dev/null +++ b/packages/shared/components/RefillBattery/RefillBatterySettingsWidget.tsx @@ -0,0 +1,39 @@ +import React, { memo, useMemo } from 'react'; +import { List } from '@tonkeeper/uikit'; +import { t } from '@tonkeeper/shared/i18n'; +import { useExternalState } from '../../hooks/useExternalState'; +import { tk } from '@tonkeeper/mobile/src/wallet'; + +export interface RefillBatterySettingsWidgetProps { + onPress: () => void; +} + +export const RefillBatterySettingsWidget = memo( + (props) => { + const enabledTransactions = useExternalState( + tk.wallet.battery.state, + (state) => state.supportedTransactions, + ); + + const enabledTransactionsNames = useMemo(() => { + return Object.entries(enabledTransactions) + .filter(([, enabled]) => enabled) + .map(([type]) => t(`battery.transactions.types.${type}`)) + .join(', '); + }, [enabledTransactions]); + + return ( + + + + ); + }, +); diff --git a/packages/shared/components/RefillBattery/RestorePurchases.tsx b/packages/shared/components/RefillBattery/RestorePurchases.tsx index 3f91eb7b6..0e6a76854 100644 --- a/packages/shared/components/RefillBattery/RestorePurchases.tsx +++ b/packages/shared/components/RefillBattery/RestorePurchases.tsx @@ -1,5 +1,5 @@ import React, { memo, useCallback } from 'react'; -import { Text, Toast, TouchableOpacity } from '@tonkeeper/uikit'; +import { Steezy, Text, Toast, TouchableOpacity } from '@tonkeeper/uikit'; import { t } from '../../i18n'; import { getPendingPurchasesIOS, finishTransaction } from 'react-native-iap'; import { Platform } from 'react-native'; @@ -46,7 +46,7 @@ export const RestorePurchases = memo(() => { }, []); return ( - + {t('battery.packages.disclaimer')}{' '} { > {t('battery.packages.restore')} + . ); }); + +const styles = Steezy.create({ + text: { + paddingHorizontal: 16, + }, +}); diff --git a/packages/shared/hooks/index.ts b/packages/shared/hooks/index.ts index 3fb30d02a..faf7be6ea 100644 --- a/packages/shared/hooks/index.ts +++ b/packages/shared/hooks/index.ts @@ -11,3 +11,4 @@ export * from './useBalancesState'; export * from './useBiometrySettings'; export * from './useLockSettings'; export * from './useWalletSetup'; +export * from './useIsEnabledForBattery'; diff --git a/packages/shared/hooks/useIsEnabledForBattery.ts b/packages/shared/hooks/useIsEnabledForBattery.ts new file mode 100644 index 000000000..9501343a1 --- /dev/null +++ b/packages/shared/hooks/useIsEnabledForBattery.ts @@ -0,0 +1,12 @@ +import { BatterySupportedTransaction } from '@tonkeeper/mobile/src/wallet/managers/BatteryManager'; +import { useExternalState } from './useExternalState'; +import { tk } from '@tonkeeper/mobile/src/wallet'; + +export function useIsEnabledForBattery(transactionType: BatterySupportedTransaction) { + const enabledTransactions = useExternalState( + tk.wallet.battery.state, + (state) => state.supportedTransactions, + ); + + return enabledTransactions[transactionType]; +} diff --git a/packages/shared/i18n/locales/tonkeeper/en.json b/packages/shared/i18n/locales/tonkeeper/en.json index 042ab60d5..2bd7fd459 100644 --- a/packages/shared/i18n/locales/tonkeeper/en.json +++ b/packages/shared/i18n/locales/tonkeeper/en.json @@ -124,13 +124,39 @@ "auth_failed": "Authentication failed", "balances_setup_wallet": "Set up wallet", "battery": { + "send_widget": { + "battery": "Battery" + }, + "transactions": { + "types": { + "nft": "NFT transfers", + "swap": "swaps", + "jetton": "jetton transfers", + "ton": "TON transfers" + }, + "type": { + "swap": "swap", + "transfer": "transfer" + }, + "title": "Transactions", + "settings": "Battery Settings", + "will_be_paid": "Will be paid: %{enabledTransactions}", + "description": "Selected transactions will be paid by Tonkeeper Battery.", + "supported": "Supported transactions", + "charges_per_action": { + "few": "≈ %{count} charges per %{transactionName}", + "other": "≈ %{count} charges per %{transactionName}", + "one": "≈ %{count} charge per %{transactionName}", + "many": "≈ %{count} charges per %{transactionName}" + } + }, "refilled": "Battery refilled", "screen_title": "Battery", "settings": "Battery", "ok": "OK", "title": "Tonkeeper Battery", "description": { - "empty": "Send tokens and NFTs, pay for staking actions with empty main balance.", + "empty": "Swap, send tokens and NFTs.", "other": { "few": "Your battery has %{count} charges", "many": "Your battery has %{count} charges", @@ -146,14 +172,19 @@ "success": "Your battery is charged" }, "packages": { - "title": "Charges for {{price}}", + "title": { + "large": "Large", + "medium": "Medium", + "small": "Small" + }, "subtitle": { - "large": "Large Pack", - "medium": "Medium Pack", - "small": "Small Pack" + "few": "%{count} charges", + "other": "%{count} charges", + "one": "%{count} charge", + "many": "%{count} charges" }, "disclaimer": "One charge covers the average transaction fee. Some transactions may cost more.", - "restore": "Restore Purchases.", + "restore": "Restore Purchases", "buy": "Buy", "ok": "OK", "refilled": "Your battery is charged" @@ -921,6 +952,7 @@ "transaction_view_in_explorer": "View in explorer", "transaction_wallet_initialized_date": "%{date}", "transaction_your_bid": "Your bid", + "transfer_pending_by_battery_error": "Another transaction is handling by battery", "transfer_deeplink_unknown_jetton_error" : "Unknown token", "transfer_deeplink_address_error": "Incorrect recipient address", "transfer_deeplink_unknown_token": "Unknown token", diff --git a/packages/shared/i18n/locales/tonkeeper/id.json b/packages/shared/i18n/locales/tonkeeper/id.json index ebb3a35c1..19848e43e 100644 --- a/packages/shared/i18n/locales/tonkeeper/id.json +++ b/packages/shared/i18n/locales/tonkeeper/id.json @@ -131,41 +131,6 @@ }, "auth_failed": "Otentikasi gagal", "balances_setup_wallet": "Mengatur dompet", - "battery": { - "description": { - "empty": "Kirim token dan NFT, bayar untuk aksi staking dengan saldo utama kosong.", - "other": "Anda memiliki cukup daya baterai untuk lebih dari %{cnt} transaksi.", - "less_10": "Anda memiliki cukup daya baterai untuk kurang dari 10 transaksi." - }, - "ok": "BAIK", - "packages": { - "buy": "Beli", - "disclaimer": "Ini adalah perkiraan jumlah transaksi. Beberapa transaksi Anda mungkin berbiaya lebih banyak.", - "ok": "OKE", - "refilled": "Baterai Anda sudah terisi", - "subtitle": { - "large": "Paket besar", - "medium": "Paket Medium", - "small": "Paket kecil" - }, - "title": "{{cnt}} transaksi seharga {{price}}" - }, - "promocode": { - "apply": "Terapkan", - "button": "Isi ulang dengan Kode Promo", - "placeholder": "Kode", - "success": "Baterai Anda sudah terisi", - "title": "Kode Promo" - }, - "screen_title": "Baterai", - "settings": "Baterai", - "title": { - "almost_empty": "Baterai untuk biaya gas hampir habis", - "empty": "Isi ulang baterai Anda untuk biaya gas", - "full": "Baterai untuk biaya gas sudah penuh", - "medium": "Baterai untuk biaya gas setengah penuh" - } - }, "browser": { "about_dapps_caption": "Jelajahi aplikasi dan layanan di mana Anda dapat menggunakan Tonkeeper untuk masuk dan pembayaran.", "about_dapps_learn_more": "Pelajari lebih lanjut", @@ -1193,4 +1158,4 @@ "with_passcode": "Masukkan Kode Sandi", "with_biometry": "Lanjutkan dengan %{type}" } -} \ No newline at end of file +} diff --git a/packages/shared/i18n/locales/tonkeeper/ru-RU.json b/packages/shared/i18n/locales/tonkeeper/ru-RU.json index 5b7792838..ce2d191b7 100644 --- a/packages/shared/i18n/locales/tonkeeper/ru-RU.json +++ b/packages/shared/i18n/locales/tonkeeper/ru-RU.json @@ -1057,13 +1057,39 @@ "region_nokyc": "Нейтральные воды", "nokyc": "без KYC", "battery": { + "send_widget": { + "battery": "Батарейка" + }, + "transactions": { + "types": { + "nft": "отправка NFT", + "swap": "обмен", + "jetton": "отправка токенов", + "ton": "отправка TON" + }, + "type": { + "swap": "обмен", + "transfer": "отправку" + }, + "title": "Транзакции", + "settings": "Настройки батарейки", + "will_be_paid": "Будут оплачены: %{enabledTransactions}", + "description": "Выбранные транзакции будут оплачены батарейкой Tonkeeper.", + "supported": "Поддерживаемые транзакции", + "charges_per_action": { + "few": "≈ %{count} заряда за %{transactionName}", + "other": "≈ %{count} зарядов за %{transactionName}", + "one": "≈ %{count} заряд за %{transactionName}", + "many": "≈ %{count} зарядов за %{transactionName}" + } + }, "refilled": "Батарейка заряжена", "screen_title": "Батарейка", "settings": "Батарейка", "ok": "OK", "title": "Батарейка Tonkeeper", "description": { - "empty": "Отправляйте токены и NFT, взаимодействуйте со стейкингом при нулевом балансе кошелька.", + "empty": "Обменивайте и отправляйте токены", "other": { "few": "В вашей батарейке %{count} заряда", "many": "В вашей батарейке %{count} зарядов", @@ -1079,15 +1105,19 @@ "success": "Ваша батарейка заряжена" }, "packages": { - "title": "{{cnt}} транзакций за {{price}}", + "title": { + "large": "Большой", + "medium": "Средний", + "small": "Маленький" + }, "subtitle": { - "large": "Большой пакет", - "medium": "Средний пакет", - "small": "Малый пакет" + "few": "%{count} заряда", + "other": "%{count} зарядов", + "one": "%{count} заряд", + "many": "%{count} зарядов" }, "disclaimer": "Один заряд покрывает среднюю комиссию за транзакцию. Некоторые транзакции могут стоить дороже.", - "restore": "Восстановить покупки.", - "buy": "Купить", + "restore": "Восстановить покупки", "ok": "OK", "refilled": "Ваша батарейка заряжена" } diff --git a/packages/shared/i18n/locales/tonkeeper/tr-TR.json b/packages/shared/i18n/locales/tonkeeper/tr-TR.json index 882809852..05e4a15e0 100644 --- a/packages/shared/i18n/locales/tonkeeper/tr-TR.json +++ b/packages/shared/i18n/locales/tonkeeper/tr-TR.json @@ -124,32 +124,6 @@ }, "auth_failed" : "Kimlik doğrulama başarısız oldu", "balances_setup_wallet" : "Cüzdanı ayarlayın", - "battery" : { - "description" : { - "empty" : "Ana bakiyeniz boş olsa bile token ve NFT gönderin, staking işlemleri gerçekleştirin." - }, - "ok" : "TAMAM", - "packages" : { - "buy" : "Satın al", - "disclaimer" : "Bu yaklaşık işlem sayısıdır. Bazı işlemlerinizin maliyeti daha yüksek olabilir.", - "ok" : "TAMAM", - "refilled" : "Bataryanız şarj oldu", - "subtitle" : { - "large" : "Büyük paket", - "medium" : "Orta paket", - "small" : "Küçük paket" - } - }, - "promocode" : { - "apply" : "Uygula", - "button" : "Promosyon Kodu ile yükleme yapın", - "placeholder" : "Kod", - "success" : "Bataryanız şarj oldu", - "title" : "Promosyon Kodu" - }, - "screen_title" : "Batarya", - "settings" : "Batarya" - }, "browser" : { "about_dapps_caption" : "Oturum açma ve ödemeler için Tonkeeper'ı kullanabileceğiniz uygulamaları ve hizmetleri keşfedin.", "about_dapps_learn_more" : "Daha fazla bilgi edinin", diff --git a/packages/shared/modals/RefillBatteryModal.tsx b/packages/shared/modals/RefillBatteryModal.tsx index 48c5f5d16..38955c239 100644 --- a/packages/shared/modals/RefillBatteryModal.tsx +++ b/packages/shared/modals/RefillBatteryModal.tsx @@ -1,24 +1,83 @@ -import { memo } from 'react'; -import { Modal } from '@tonkeeper/uikit'; -import { navigation, SheetActions } from '@tonkeeper/router'; +import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { RefillBattery } from '../components/RefillBattery/RefillBattery'; +import { NavBar } from '@tonkeeper/mobile/src/uikit'; +import { + StepView, + StepViewItem, + StepViewRef, +} from '@tonkeeper/mobile/src/shared/components'; +import { BatterySupportedTransactions } from '../components/BatterySupportedTransactions'; +import { useBatteryUIStore } from '@tonkeeper/mobile/src/store/zustand/batteryUI'; +import { useBatteryState } from '../query/hooks/useBatteryState'; +import { BatteryState } from '../utils/battery'; +import { t } from '@tonkeeper/shared/i18n'; + +export enum RefillBatterySteps { + REFILL_BATTERY, + TRANSACTIONS, +} export const RefillBatteryModal = memo(() => { + const stepViewRef = useRef(null); + const setIsViewed = useBatteryUIStore( + (state) => state.actions.setIsViewedBatteryScreen, + ); + const batteryState = useBatteryState(); + const [currentStep, setCurrentStep] = useState<{ + id: RefillBatterySteps; + index: number; + }>({ + id: RefillBatterySteps.REFILL_BATTERY, + index: 0, + }); + + useEffect(() => { + setIsViewed(true); + }, [setIsViewed]); + + const handleNavigateToTransactions = useCallback(() => { + stepViewRef.current?.go(RefillBatterySteps.TRANSACTIONS); + }, []); + + const navbarTitle = useMemo(() => { + switch (currentStep.id) { + case RefillBatterySteps.TRANSACTIONS: + return batteryState === BatteryState.Empty + ? t('battery.transactions.title') + : null; + default: + return null; + } + }, [currentStep, batteryState]); + + const hideBackButton = currentStep.index === 0; + + const handleChangeStep = useCallback((id: string | number, index: number) => { + setCurrentStep({ id: id as RefillBatterySteps, index }); + }, []); + + const handleBack = useCallback(() => stepViewRef.current?.goBack(), []); + return ( - - - - - - - + <> + + {navbarTitle} + + + + + + + + + + ); }); - -export function openRefillBatteryModal() { - navigation.push('SheetsProvider', { - $$action: SheetActions.ADD, - component: RefillBatteryModal, - path: '/refill-battery', - }); -} diff --git a/packages/shared/utils/blockchain.ts b/packages/shared/utils/blockchain.ts index 71be2655e..35c3bbf30 100644 --- a/packages/shared/utils/blockchain.ts +++ b/packages/shared/utils/blockchain.ts @@ -2,9 +2,13 @@ import { config } from '@tonkeeper/mobile/src/config'; import { tk } from '@tonkeeper/mobile/src/wallet'; import { ContentType } from '@tonkeeper/core/src/TonAPI'; -export async function sendBocWithBattery(boc) { +export async function sendBoc(boc, attemptWithRelayer = true) { try { - if (config.get('disable_battery') || config.get('disable_battery_send')) { + if ( + !attemptWithRelayer || + config.get('disable_battery') || + config.get('disable_battery_send') + ) { throw new Error('Battery disabled'); } if ( @@ -24,9 +28,13 @@ export async function sendBocWithBattery(boc) { } } -export async function emulateWithBattery(boc, params?) { +export async function emulateBoc(boc, params?, attemptWithRelayer = true) { try { - if (config.get('disable_battery') || config.get('disable_battery_send')) { + if ( + !attemptWithRelayer || + config.get('disable_battery') || + config.get('disable_battery_send') + ) { throw new Error('Battery disabled'); } if ( diff --git a/packages/uikit/assets/icons/png/ic-almost-empty-battery-128@4x.png b/packages/uikit/assets/icons/png/ic-almost-empty-battery-128@4x.png index 51f717e489293e46759c08b2a4e96b8c751553e6..f44f5a2b19739b8424c629768a4d6ebaa0e4c17b 100644 GIT binary patch literal 11692 zcmeHtXCR#4v+&tfL!u@Uk|7n ztr9JWUA>q4Y<~ZHKfa&d_tSgVhh5JzGiT<^oH^6Nb?&O3p<$x|0GvUpsptX#hkn8V zH6`@5={v9weNkF#sH%Vy(qDRQPBZ|V0I8yM-}n6rJ|Mx&Cy2P#gI=j!P)Cu&of^`W zo<$r`oeYsO!xp`E~{nuBXkr(Lk z@H~AN`79j~`SPpjty?Is$$+394p>CYg|@??)awC96GaQtew?cZg+#G~iXr=>p)qG9 z#Ml3gkEU51nhda6`Sg|j*)AE6mgqY2j-OBVuFe|8jPZ6wdXGn+tZ^X^%-F>H+0iRv zB?o3>Q3Hpm!jr_ks8X4(Wqw%i1#*ziSshmx7(O1N9B0b8w}-K_z3*R?h3oXm!^}>W zyBMSG@A$xGUjiWc)hjMvCJz<-e7q=T@eOk9k2deg3mc2^YQH0Te#c}Weg}Xg%`#(Q z;$>9Tc-L`5meor)| zY91p9{m12APy5FBmNX`A7TIkLPR?tcNqo}th>$qrxU}~E#d}J(4vWA2ag4VX3DiL2ERuO zf@4KE5PR+|Y_unnSjUsgO*9vm`aSP9QtsHae4xn{a8RKXB+q}KyDoXP&) z#O7;E{WYRF-_KuB#(SL-3u)^`vS`BWWirHFYJj-w9}u9ScszP6s^+geQ9En-O{d`S zSIK;ur#u(YBJ{96ydA)=H2G;)C|nfCWO<~ z*GaF$dz(TpuTkL-<=9!sHQBonp-5dww(A=8oOH*k;$7o~iv$8{sFj^eD+~Zl4F4VD zV=QuE9^ZO)8?)9P&e4uN#mQm{2Ylb6-L`N7x$n`UdgGPY9hc=vu}CyG;^$X5kW9@k ztY@Y=vCp)&OFPTYk7$AvPH9r<{as(^h$EWUOvB*t(S#Pbt=dk>=3rhnCKZ&m)rv_? zLyKzJEj%v&DS_VJdOM4!*14Y#xd}(F=Ih ze(~BO;{wk~@?K8pWGE31MvpsqQ{vDZT7UKe!c5Gg(aeZ@Bo;gtYyEb5|8)9VzYWv{cvZ$2IU{%blj?wV0+|4t0R$~zG?t8}zkN@-{tA&j^vV^8m8s&6 z4MK+4cu^;oBK;+#l@^xw6`oO9=OHev61pFZc(paw;J~XF0N4m2OT~%oulSJ5g6jmB z02>@cUW<-fz$^dEnm<&eAf3Bn+bh7P+eVam(ikp&8vt?dZj8RYdQu9SgTlM9G4-7N zES_A4ha7M!`wU{&2^p14C~|Uf6$sO2`$>(7z-JyUiv%=1B8mZop00ZR^#0RI))TkK zry*g55+A@E7~~kDtWIej#^7^wE?U2bGIISc*-P?8& za}!r``{z3ENo7n51nUWO0tT?x=*AdlNfS!AS8!Bo!Pa zB?>S34SK~6-Q8%m;N0oglN26-ytQgPsW{vz~-M&nyb8mUzT&#vNn-BhIlY{hMDjIr^Qy#y=vl6qJ zK1*Lc8j)Wp9P1yn+|W?5bCUEGQAiCEbUzO|`SrMWTq&`sn(g2bj*=8zC=U(y)maaY zMxjSrW^i{Zwzswh*HJXSG%!$Q;!fb@k8j^PH}4;d6~Q>#O~}K}hzU4Gn@(PhRUe-8 zJJ3?>!g4jg1)-+zg9xwmFwMalcD356iyKBN_f4x?ZJhg?>jHOHmf3$`Ihw;#MEJU% zP}B;~IZ~O=^kI4Qzc0{7(oL$2EZC)?SaCr|4Gj&x6KA;S{)GN?r|k)u6UuvgQfEWN zczaG90lnB{_Pe@4V`Hi(+l6&?L4G?=t+_$E70YJ6T-~2zh4@;NzBd-KpuwszDD}6S z{?(JMavUn~*DyVJ2f52LUt|`SMmH&|6hs!AAzz#{VqSIpYj^vn_WBvP4J3l5u<0X@ zi0orlr2PV(R_9WkmbriH(6JBX_sG910R&$)7ArUt%dE(=m-b3^8vZs?{TQuWqRuh? z6@_=0=t|#X1Pm@~nAdl;h4;=Q&HQ%YDwfW_7U$4glb#_HZ_}&Rq!8prffMb9ALBM( zKUEYns@Bz@WZ5!39ILsg%~oyTIPhC7A<_H3 zT#WBn0D8p-Y0|{Fgo+Wn+eyXM(2;&!LbSO%GS* zqU`&d6XKnAP>1v1{siayzW;hy>?mv?s+fFIZd*Ad6*qtj8LSu8ck!=9@p^TwHZysp zEh4Se`;EelFzOPTLns1&<(`X?xGI5PO-3oqf?s}cQpV4uPs;xvdiut%oM6i6U;9$u z+Dc*@KA=hqjN6yj-A~U-L1m|)#^!{ZB2dI-*%^!`eiuoj1B<`2%nc z3gqC>SUYuZ@?)F)?xg7O+mGJ(v4T9W*5@XpDjOh)3e;}Ke`yZD6Lvj8byJ5P{B?S12Mp-(vbI^HZ0 zwG>=tYN#%gJAKg`ub(@ZM@BX|F}doR;m8P91-CXg%juUI{Hx4|rnFprcC^`DI1ZTX z_XUb6X#Gtvi&PfEJM;dC) zb+K;ni!0E(KiqdNzWPr{vbO=5^hQ@fa)7yD!QG3MXC(h&Cx6CmJkq~!6+JPLz8=6+ zyYNQuWBtbD(4@QD({h-mV+6r}sxE$~W|o6pQLMTnYYxNa(Lcq?-*r^QjxWnBf|dQ8 zeE@5a@D?`n$h8^H`R;&*ej#(upNqK|S(~IpkHvfO6Sb}z`#pB1=LOz8*D$QZXVvQu(X)H5UX)T`H7K)8Rw#XB8ya)P*t5& z$l&MPV-;o1D{v5;F%!Po6MHm#gc8)p#83DfbWQeDis-1BNEOs)-bYsR;u1U-3*Yhg zDRxxyC0&RIoJiw=lBG*Kr53upc}b}1wHsPQJShk?xc>M-r$yQ4vZL_*GQN)WvidTf zueRi#YUDs|i9jfPV|aHrBdet3^Wu6@=^LUS*@r&y}pH-5Bx3Sv3Vd4tAZn&C1U6&3Yi;-fT+ zbXE{V$v)h(?KfV|+!4gKMh7oLl!iSY!EAD{SgiPvXJdWRax80XU_L&&|KMQwsD0Qx zbN2!4rKxk!W<<>3x5>@gyUD>6R3V%uq&0*e3LDz^KAd{bl}+sV#6ntt(Zu&y(B7zE zJ2GoVTwq5h7gMv@O)S8rCecWug*hA8ZE7pxh{0a znMmt+uT?5G=jD})>=GL0!hS51`H52IciFZo-6UafL>9@%qW0a420Rz>fd$kFmmn=* z`)!GBSqY=qyg{d%(Z&uk5>>=oVQ;4n{UZg8A=Fjwc_eS z`ng#LVD4`yDTfqY>_V7Ke|TG=ciI&GQO6@;CJq8m~5>1j_ z@e+5X7t&+P+l@_>eD?7cco{Xi+@!HfwO6MYSybjcJS0CDc569-*U}BW8 z4pDzpZR#K0UgAWBE561mil^tZC}6H|SZv3d8lTl^)X4TGq+E2{i|9;hjA^Ao*eq?$ zGEq<}dL1O(*d@~o+nao5@M%y(P<)X>>nUJJdrj7=s4FXZlqRuAj^LxTk1LEiyUQtJ z5sQ>?U?UHGDYw@ULHqPo<&X<0e@K1@Y^++VeKJK%n01@7UX!A8qpfGT5r848HSFqZ z-Y)9g>GdL;0lLuBiG96>ls(l8IaJ~S!Z$uDz!55)7~aRcgvp_LE6!*;VB@qrf943o zPMv#CWVG+im=ZH5P-)iectTT&>il#LoU!byj$9)wwS7LX8AKjhNv?&`V=MK7^*)GO zn*Ty`7rdl~aWc{R8?cKmmWCUyP9LqzRM-tMgm%U*2~WmKvg!QVBoJty5}0poK$Nds zqsLaYt{li7?B5Z0qC$|JK0PkRvzG$nb;&OaG_X5krOpjh8g+%K+1$q7>x%-m))*HW zwlx3VI(QefRj0gY-wzC>Y4HQLj;6-y@FMt~C(u*+8gk1%`jRCI#7sgCS3KaMHxX7L zt=Dvy{(_wE@O(DC?GR1qi5vB5auB!?60?TH9q>06S_3kH(i zV`a|Em?H>ggJjK#*Ps3K8ZbQWmfsGfI4WFGO}tK)KCSskyJ5A@VfnqD1Vo1g#z`rq zAexWZx8w@~2|QL_QXXmX_qY=psR5&!?FL-q&uJ9AVfNbd=Z|(m!(L<MD1Vg-C_0=oRs9D z)DK1^z>{tsFx#XRz^5<<@x}?7r<=p+B%l9L0khx7Ip{Ay3I(wAsp@J2Z*`~S-@f(~ zz(JRV3CPUr=P^&XrQc)#uL^4=1@jTTM&zOI6V6JaWvOKC>;`E+^1%^5W0#boM;g1Y zfZ1j;ve4KTxHh}kAS0mZUP9^sch(&Mw0_Jgc@1O41p$RB5V%nxDeyh{;9I^wOw3Q$ zXk-|@Y)z3qMGYwIQ#+|y)6}_*w&0fQJnI=>;N3KcZxAUF{V~P|S6ukvoab|e1ZtA= z<@3zNu9MafI<O%;a&W;IySkUxoNMFB7f-<#JkWWD6pK;5)=~WiQfn z=UFm%%y)n1%b+ZjAwkFLTb0~z6Oirw9wM1htR6@URBuD#MN`OArKkEsECS8cfmSE0 za}QtwGH}G&b11pFnqzbw67E^InE`ke0rARy0SF=G{=yo|pE0hTVGsSq4bHx&pbTAG zV-o}j=Fg?%=5-oIe`)-zGFE=Uj|@Kt(oek|ZZrTkG}VtU=t08$)|`u+&W5%aA>mZu z^K7EQq&5Ji@IsAVZC}>(C!8weTI_86^PydpfFf9B+ zf|^nOwni&b-9TI~XTM|xS>!GLRNpItfV9d_3WcsbT}N9A@RAu)D+y6Dd`DCE2Mu80 zhxEb-UWqy|#p7^BFp$(cRz?e7C}ajS?|@Dqi+JT$#tf7T$GrJ>g}QX${wo|?MGhph z2cMxYor3|@!C^yQ06b`9{2B%z<<~VJJB`S}Sw>j;?k)N$C~Lp@oTR_M1P|ZAKu^Or z7zm|igi3Czjl7{xApLPfUjVD8$pL7Ha$kom5_)w-1qdkU%7H0XY5+pHr)M)D&H+$) z39*&s3z)yvW&ZaA01(gq1!5ve1HelL7=-h8h5!Bezx(jN_VEA9VQ}Y3MUy!Qk=hQ7 zP)!doa6uQz*Edv_S4PlhFa3{GP;LBsQND^mo(8^@{{#K**7P3*dVO&vJ5D`lb!;w~_aNt8)Vkq{ za1ghjbY;K}iq^FE%3ngePf)XoMi+aw2K;&q<22m_V^7@GX6gkOpUDNl-hJS4gerhx zV=X715ScXg++hWUgEL1|>nI^04PzOPxq($yeqI1d=@-^n*9O%ohlLLC<+^*p$Phld zV+VGAwfz$4-{WsZA|W|N-S&zBt5e^4SN{`X^*rVCs2#ewCzzGsQl>(nDrywPsn---!oEoBUT;%4JE0c($ zU~7#Z#E0A{wHu9%t{RMrI{yV8nh6PM(d$#w?cbV}4|aWOvz49WPRiPfq6TiV;>h{# z4+)ht(5(mO=CJyk_H{prQ+A8I!T!c|F+7 zttr+1+I?iB6P1E8IVtLbPJ_o-oRkZN<(K1C3y=ATm$i?+Y_F3F*)#ZEOU+v1#gCm{;#?v*~I0vO6sXBXOn2} zO7Gd16w*p^dz}pYNU{~}tVU&Ds)0uy%)I&e0MfvQMa{H}_0?DxLl9VoNg7nlX>yw& z~9KQsUnr<_7I)kZKn|-_Xn2F=_RYwmARP*@pFu^JypLLbP!j zR)Ro;|36j;S%P`TABp0Fiyb*g;}m`Qbe(a<;+<*&t5Ga%3YHIwT%l0PU4(;%)j}N} z&lrMija1-V%*tPaQAp&JO>7_Mzv3NY+ATNy4bUMbVA#jiw@Htfc5aJS^q5T3_^}hk|S=9Yc6w)UyA4 z7z@%?$xC{37Dm$D)UEinzS9Okx1!^gGq{FaeuQ*56J7GJrJDYJEBA5r96(bLJ3|vH z|HNio7jma)=>OuzV6_C9zUglo#Hx=oE>VbDDiN%caF9k!y(j@pF=B zxlMMUz4QD{D=GG@mOPczjFJ+z3A7Hj`{B!Jkm1E2#c_TJF;YkNH|*C9xnfgUmBt`G zf%n~|7lRef?nbWVE;mQ6*^ONkXa6fGCQecSRQ@9b5}p_^gg&SW^#9?jfk|IY8ES#j ziW^YG%**YX5q(1dBrt=}@GlLke;((dxmzn3{>HfOWMtB(kx8qxkA=j*4|tI>Jj#=l zLCM)rSUQl35?nYEuP>W2nJ@pQk$nv!&nNH71G@;D5$cZuaKt6POMnM5zhE&JIR<`- z%haqQj=!WCKeB)6L6ku0C>KvxMJiOQK4tOR`#jVn1=_BSFiWWVs;t}btil!YP~_CW zgIaw<_@Oehxw<4b0L+ZyXs3rORu=U>@+^|18td5Zq-VQHkRi8cN6H>#wR znlp-cTIiZvBd**gV5FkhxuY4cH?+AV?K88%Vnm|-Hx%jmlvZq8iC*qc?7HOPFN|G;`O>GuQ9J9gDp zA07rk`_ zipJyzHR9-ShUwuDY?KYP|Wn@U)@SrAU!rd-(J03laiYT>cmOvE9b7M{6`YwbLH zqe?2MG|pK?%Y4mNn$bJ4>GkDgv-0}h<=-g9;aeQ~Q3<@f?PL`_cCJ)!Q6R?xv+1it zk62q-xq;eDN~d}3rJo9|1_7zmW;GCJazqoM(z}*-so7V@2YoqB{(y!U{b;eFSA`1g zQ}8>c^oHfZgop^N87lCEuOtrcU`1s@!|`B@$xwkbA{5%JHT?<$BWc`4nZAi1ni zpQUp&zNaFy0-(z1^P1-jTF(FjW4s&9rTVKjhv7vNzB+}2T-QSzJ%b#!FYY(d)#aL9 z8hY|%o6$jp96HsmbgZaC!x!bf9+NFS)rXVH%iy+kA@Na7@QmV%p|$2GxBRV#IL645 zs9>B|?5q|iviJU${dMT`V&gQ9rI1g#d_v(jV=8OAG}(2toZCKUd(sDxf5AWwEwc&| z+%pjO+n{Ru`G8vg=9!EjlGtI1^}?leIsjF8()Vb~Rq>Rt4NYotfQ4K|^hLl8)gslP z83wKJAldv^W;dal_fW67OkqvXTaA#*##9b=^ZUN7`WK663nYq+UA_94s@6yqE%L-YZq3Zy0O9p|RkBdVnGa836#LodW^-uOyU@h2+Hd#J()pK5bvn zMbsaB=@OUQ-{naKi)Gq|Ap0hw;wq@pjx@^r3rhK-U-aI8QZ)17rjM|h-|H+|vW`FT z{Pu1u78Qc8Q&YtQ@f10JwZ}K(gSk7Z5qG8H`ua@93hTCxgAqMF z$wg5Kf$OOPLEp`i-9{6X3b@yM3~`jWR`a5P;8x8J9$QbcE0Ix29n$Liw-(eo*a9>r z)iR|9%DlfbwH=%3u98)0ZXXLLyUowz)EL2)@=D{B#CN5~ntdLuF$alEA&WyeX}LJ1 zg3K{=*ItRS`l5O~6i*8|W@Np)<92?RR~b{@<{e*ECmT2RBGGViTP0I^>~EVviqjG0 zARJFu`es=B@`c^)i)=*#0cZ`r2RZNqt0EhqakziwLc$TSN6t{KCYsuYnF>2WDy92c3<1=EsPoF zUR&!Oje-p#ez)r>>!338FTRE!u22({ry{~ezbE^3dq3TAbZJr#OJw+9oA2H;Q9Iw& z_{5`|R3S0zD6p3tu>J3W7==W<$m%k}9y*9c-4sQ42gB)9Z zZU0#C*zQH98Yg{}M^Acm(2M7X^L?j*@Su^QwI4{Yg$ksE>|9i&jc|Gzgn-|&r z9H_W}Z(Ma;LBb5uKeIT5ZT@6zsD4_Ibyy|(myB?*m4m_hd5l{}`Df9hh#WGAgVcCe zv-AX(t$aD{+%q=liW5^F zQ1?6#i$&>EPHu#-4c9gIccAk-=7paLP!gb4q|lPl+Ew(y3pOT(BR;>y6r*|swSygP zckS$K*8PK+HgjC)x+7+(7)y8%@{r6aGu(q7q)z`dqaT-E@Y~R^^Y_~xT=7na&@$2;En{erSBX}LVCh@u^CEp z_EiUy#LJHOYWKEw!`HC_&8;lk`MzBfQA4gGLB^~#gT$8?J7W$F2`EUS8l z<)JfA&*15jSfJu#8FbthxeJ0sgp;0itvniK)AR2d>bkhkZxzsx_1n1EDc8uqOatmV zTQN!b+1u6})0l5Q&4eZhhK>KuerRkFt0Y61qz^)o%eoal!e#0l^vt&;772BwndWQ( z&-trIxl;f$fvgiXTsVOy5^Jz!1lt_WvW-N?q&s_1Iat&Cf!tDndJoiMS&If#=3 zIcIy(y{>4cShqMwkBM3}wkYq2w&0kg6O~+c&8(C%6-8MQE%s%i1)7scdGC@&Tz*op z0CFKlFXcnCwubFS=hiKxy~3WMN!?|23_-KD{FOwAoC#Eti2gpf)ahNimAw9kopqe^ zA!_U)Ap+|Xkti>+gGc$B7JgGvh^f94JHpl0!n}xBGT)KJ$MYbUw)XqkU))E1jY=w$ zo1y&vbne@9N>C7Y^v=WSe>Of3mOGYpZ=W14*m5`*z1{1l`(bdkOS50{D^Fr#LaeT1 z_3XLqU{h-+E!)fChd1K8;zq*>!B3FBus~dvq^3@6 zD=B`t(`O3}mNa*p4x@Sci+){{ykS5NG-HHKtJk0M9ci!0o#LcN^{H045cZfIuhS1S zUgHLwKN@+@TUgIq+1kW1~@^PF&# z<&VRzlcm9`e}`5?06DI9`Ejk6cG6zNmTqf0^8Cx5bG_8pqZOni1*`Tah!ZukM7!R@ z2Qb7(77?SLu68Fp24ta~38mk&p+SV2<1oLSC%>lVg>9TnPF_5Q#tgcn7Q0aXH;BQZ zd^?q<#FRm)eO;)R{O+ zPoM@4m48P<%B1!UF4Mt#daxXppFR7V=C@fzz4|Ym%JGj3;ErJ*#Z$ zx%;30vQt7}mN2MmiyNf7Zen=YumhT4dJIi4d6Vws2j7b%H3~5S6}v)LLcLw`bpr`KU=C)XuLS_EP)+gD;Zz;I$axji9O<(eywEX)@l-KZ`=rb|S*aIpk@ZKQM z`qIeiYx?2w=esXB3#Vld^hou?t60?hqp}vl52z!HsI(8so z3EB%|JRTqWbLeJ{aZ;*XpIqB~bq>FYHNW8CrNmV3JtRhcCiZJXi5Sn~y(;#5A6jSq z9~7(dPV$ye>6N4uWJbt08_6d@t%~Q;P#Cy;=}&@V_E)3082itKM&a50y2GdSbRTUv zhUeY>cvreCF&{s-GVkQ&mt!L=b9m|L`t+-bD;hFsU%7T(WzD?ny=5dvHT~dnjCU4x zn!fE%B9-ZmGA4C@V{Cd&!LC?I`fY;Ch#rS{|0mO@xsILVbk%*v)?;b@o;tkp(~hob w!zQU01jK^yARR&x2n6vF5d;m=t8^?Ny$2Aa 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 I_5QQ}0 diff --git a/packages/uikit/assets/icons/png/ic-almost-empty-battery-34@4x.png b/packages/uikit/assets/icons/png/ic-almost-empty-battery-34@4x.png new file mode 100644 index 0000000000000000000000000000000000000000..f08f576e6ca5a7393717885f6005b5faa9c5a263 GIT binary patch literal 4892 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 qu!N|vbnNo-^25P{MT%GU9coVv8h_Y6v!W9G5| literal 0 HcmV?d00001 diff --git a/packages/uikit/assets/icons/png/ic-battery-25-44@4x.png b/packages/uikit/assets/icons/png/ic-battery-25-44@4x.png new file mode 100644 index 0000000000000000000000000000000000000000..a5b9e110b5647fae550ae549de44bcff6eb33312 GIT binary patch literal 3961 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 W00w*?dw=~)1GLn2)v8o1gZ>9v>6sV+ literal 0 HcmV?d00001 diff --git a/packages/uikit/assets/icons/png/ic-battery-50-44@4x.png b/packages/uikit/assets/icons/png/ic-battery-50-44@4x.png new file mode 100644 index 0000000000000000000000000000000000000000..794c11cf4c081b4a4748552c37333c369692664e GIT binary patch literal 8456 zcmd^l-YNyp4ZQHznHksoH=vuxj$#-GZXzvON9`R1`hxL5UQyv!qDj;007j*1)*~R@d~Br z1Z1V5q6m2W@A=+Qlmq}k0BVYIdOjJ4EB^HGYez&#FZ7a?!`TDWY0VzK- z3TWY-OLrbe1qSU98=21g`DV4uN%EJk+7k>U{_wy=9^bal@385;e(Bt08Cyf=#`Du? zP=|~TJ68_tWr&SeidAU87H(^#&q#>d>lpFqnHc(!$HU{4`=vq*?>f7&(p5*} z39>ahqYMJ~s}LHp1yFbw)|`}BYr)m#pC2*)AjjfTVw%JV)AwU{(WDEUbANaezbZEh^wcpe7vrZ3yi-6Roqd`u{7Tgm#pcaq)K-8&F#oeFrt(XTa=!f*{$dB3a=_?=vstl7g0icY zt{G8$uc!6*{>{{uUg1>eGj`b_HwBWNv7~CJt@rSmzEy0Y3?D3d>QWdqD)6|ZLySeO zFVpzyNMzV;tP2LFWyNU1@tnxt(s)`LjrMA0NHgW6Y&=>tgU+1A!=%clt^W;doO*2x?PR_iwsxMcB;=M(H_idR zOCH-2I{iCGD<|ACZOuuq5>+mFy)4Z4xUtyaEW%180wJnhaOti*kAwYM|E<*h34420 z(6{$pLb*wk)^QqLSjjoS3HUAHKEJ@%zF7Mhb%pjPx(CrKitHGPk--|9yg(?ta4}TVfgnKozokt=^-1++0}=q>Ir$qj?kO8 z7qjNnay7c#(($!9rtdChvuSnqEcI9jRT)2CNWac0DLa2Y{3Gnb@|lNi!GKPj2n6S9 zca%S&aPS&!#KI@a@r>)OsT2m~hN%!b2zdl!^;%-v9b7JnxOe0~%kqE=Dq{X-b>L&P zA}*B&$L-OtFH;zlM5jUy`Wk0Cc1}9WX|)~xwuZAzRGdFAjRx5;hNh(ZMM#r~UQz{5 zi$L0Jcg7{<3E1`g=u(t>O8kbr`3VPIqPq-L<0s$>SkJ>-&ymM2qetq zp?%;akdMjCobkfLE-7NYSv{d(F{1m4u>H6T=_jU+jp7B_0I_c)GxOe!6o27mho20R z8l?3J%3B=m#r%0ADjV6PV5wejAXv^W|(i*f@SW+^X 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 literal 0 HcmV?d00001 diff --git a/packages/uikit/assets/icons/png/ic-battery-almost-empty-24@4x.png b/packages/uikit/assets/icons/png/ic-battery-almost-empty-24@4x.png new file mode 100644 index 0000000000000000000000000000000000000000..3d3b26747e63845bf984299e09b9a8a1989e9096 GIT binary patch literal 3412 zcmdT{hcn!b^Zwj9oIB<88cq+9AWmF#AV#@R_GS2so`Lhe+4g)JE%+>trO zB5}DX78v5^;xX)5PTBVFn<1R8&Kri#O0U*^l&1P?j9jOSw=GM|2Pc(cyW*^6IH0*0 zZZPx#hXExD#>&K$?6wnf3M%{2r|9SR1Jm#HICOp|q`$X%fJ~ST+Ue*2MngwOmpe=S z!dz@o79jz`!j{|s`~vdoF363oqn16>rmF1Ir=jXi?|%mCD7uSEOXJ%``syho_<*>9*#(Vf^a?17p>=95ckor(K<9mnq6<4#{@;=9B^J^4q@kq}Sd>XicVxfHM8 zGRo-_p$fNuen_a=CVFW`P)i4XrN*cA9Pn~9vi%_GYuAChyDPo3 z$)p$@8yaxlTDf!r*>?mM4Kp}8+adckF{eNJ$B(XjFhNg&%`9wvttIk8AtYQ^v3aUF zq&-`8LAlXd?o2*2-h~GHsop~^xEX41WW9soIo9g-y@He5F_hKnx$YloK08MGLCb!I z{6%!`ZGANBTkND)Ol_w9W)7UxnRIHo%jl28e+B|@MGgG_rY3sCZZPKIkRfEh2$sF` z{9>g{x}s4OdQU40Z%(JR6-Kq}Ny(e=(j=l5(0P@wN41 zfvjc#rRbZNx{$T#E^#JTLFq74-1N0h6Q3vdDvRo#q|d9n&=i+5C*gopq3)aLMe}Lg zggmt{jK%(9$&vl){5;HF7hPo=d*)x{MOw7MU^-J2Ua_Z6jI%wX5((oLP)Czw`tI8m zZ)D=ew`GK(2>SwS>}=I5qT@HAh$lL8lg+;8Eh&weVGT(9GGsqCQUA$*45QbiUnjjr@oOlG!dTVhK_P2^E_`C$s8H4 zE0i3{v5RIaW|g82tB7oq|FRI)T5gl@RLz?i-xMu0Lf!UsYGS)^72imAq7&}-b2#gt z^2!p&wlJ|XC0fZ1y_teWbxphho1F}`K=#;OLF3P!HuF9k9>3)OLkm9mNa}81OR2|` zr9MV5ZNMD6A?xq60%IXy`U+IgXHC{FphWW!=Msc^U$;5C^Zi#tGRDL^v>mVe;$OZ?ao*vLDpP>5Sn9 zKvGt3%%u-?U!U~`sq7uEU6+ukaLJNAGs*zeyu*tJLs{3qM_*PEh{g<1KO3`j*T6Ui`A?{UOgAh^~^^GU-+%0DorJF#tmIqhhQu&p6w7 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 Z>Cdk}XI`&;@rSSw>dyV!IX4Y`{uf=S0mT3S literal 11506 zcmeHtXH=6-wC)>yHY0R>nHYN}B|HEhKd=vv<;ppM%c~ham3qw6+QM?K5pD*W=NkdenX~*LY z;Qa7GM&i|5e9=E0Q8if46)AX_#DsmOOBLp3=Jl24s|^heSMLW20+z89z)*Enm4ogj zdyc?{8d7>*H=pShI}AHxx%q$q++l&p6Mc2g7msh+PL$%Ngt6WSf;kXyKQ9Lx;-i|9 z_@ajo9xc$;iK1QyO;!7jwTr|u&hU}zNuhG_m@>Ry&)y?Ao7$33JXYhNKVJ+# zTl=s(kcI11^Ea?oXxiXZz^k61$$VM3eMF!8*_u3396_2gKopxaj}8Gixv2r6ytK06 zqktwpT&z~D5VZHSl+$&bGUgY`q6neS%|+AvK?s0-8%>WXX{*^gooL3zW%E`+2&c>N z#pp=p+f{(&6xc>Y&6$$-Yd#0^sTWjii)hPf?h#w)Kl=@VQjx6oAzm9dO$Cec2wgSSr01zT!XYT#b<~YZyJg3}X%u~84uyr-rON|Ed zKotT|^kg-8e>a;x>PVO@L@a$s8!K*RE#MR1Qlkna_ZRd|Zz6Z41l{W~4k zoVs=h+G``2lZIs>nC7PdU-r0)l$cjhH)aE7XGsbpZPwk~&+V|s=IHyuNke!_b=cF2 zJ}l8R(K*rX4k9H-$}Sywpf1Ivynz*UML?!Z) z0x;09wY6=v)QuLC8S?A462&Fl^<%g0>N zN?YOIUL5rpmE#QUhKhcy5Z_nYPcPIQr4D6N;hgmp|pCEWJx<@ z)7BmLwZt3ZP%T9_JHtoAq4YSMV`Lx25G8jBq#b5FQOVr9!SG_cls)mIFP!Ah0z%O>ix#Q%E3vEigRl;lL^c1q3G!L7P4smsmejdKhI}Q?lR#%*=na zfCxSXrT6?v)mV-97c=6ZxCHT`ZHaXFrE(H; z24$i3Isgb3<_A1YLl&RoIf{C4vs+@~tM5_mGA~U084VyOWn&Y>FF}mg%v{Q1cCiX( zz5@YtU=DsGlA5&&Q1zXAyT;>nflAO*dQqU#-JbdagFGq)%|g-vk&rJrq~y!)+N1*y23_1`t~S2B;RlFK`hNAs8f zUxBpQ#YF2dtHh7Bel|3^OWoScy)v6K-RW*y(;c0TvzIBWW#R7;FaKQP(=PfkeMZm; zl^1`N5aefl6Y-NY+o(Gb5=Pos2_rR5=h^ag=4aTX)KzF@Vx>@uHU3^URadl>3iTr6=y0Iln`wPAlx z8R3CZU`S_Q>fU;jnQ)-s{b@INEc`e#58JtC>J{{(fv`a{f~ixKyL7(wSybecdhZu2 zb;=lzrV)`X9RaP z!%1^q5O?SPZ*2#^n=0YAH*$fQmwz1&$HhRD)kxaj>aqgt`n@#1$;JHFO%vYJrn)@4 z{gmOiA)w9S9hW4oCo9?Spx`$zLmA^Fg4!n=KlP9hbF0aW>IJu*J^(Y-rv5%7=?^}} zYWXJ0sh$@~FtPDU>Lv<1GtPM52VC#%n)&@U@-aJpS=VHk&V8E2$a!F-?8_e7<8#W1 z5cF(hG)3@LdxtvDS-rVm(E=xpawiOh{P=sv>%@U-UFDtPplL3yQ*A1c=0A_M{6^~3 zo2*oARl^~!IsTtT@uTgHtU-dExr5n{g#fu9n3zp>_rVP}Qq8g$(B2bZJ?M9{cyo-C zi>j^V#6+R{$vB%t{K{H_#9IbRi+c^%6HAnHeLRo0TfB->e8=d4TR_~Rc=d6*n^~jj zKr-40FnG6;X1!wg_thH*S>{DGl%ajEoF= zT`UhyI5kiZdM23N##5{H7cP_FR6ckLrS z^+XOz_bG}Z?wyuI)2fV7N`Ur&Xl^+mL^r)^ROLPQiS=`CVz#ebpUmar-MZ55vUTIx zS;iTro84}tet9lamuQ{P)jyY;RVqBi2*G(1d@?Gw5Iqh0guoYzV@od6O4T*W(6b`3 z6mh{RF(uPdW9AXYNEg#_DF18p=(4a6EGpLhqLkjhaxlCo#UTo92 zK9I;5Q@{^xgc+x()3l7{0z)6359x375$0UmiCfjAG2lR_R{JSDsF$5+y6{eGaQNzq2% zzjm`)Z|KeNfht#7z2QJLX!g}%*N=q#HT)*=5rQdQfeyx1op;i85t)3qf@FXD`nJdTRK00K}rF`+d8AN ztmr-nFF$eoL;E064oa=VfCB6vcN0obX=^+=?gpJo@L0|9)g33~gpTY6!ma@0Wo2{A zs!G~R(k~NxO&XQJaOm2ooA7?;blq!#`GvWQ%ipC1H=ooZR$iPK4ZZH`>w7JzE}$)+ zsXVcF?-55=CQ<}$|NMQPe_rQgZ<(X7&7%Z-1ty@j=VRK9UXww$T|0qW`U7Iofn&oz zx=x(vew@^X$cU1ppr>@sQfI66ZY(RwrZQ?kOH_;1<`b&kRZQjYC%71V z=cN)(8DrSZI+FhEXiIEi>~mhdYD%vRu0|rG0Q$tsbUax$$@u1^SAQXz)p0e}IEQ<+W*=+NJ~~Or zhM?9}R`ZCykkoJrJxhDu@~ z&nYf%7((Yd3IO}4g`KARPNoF+L}FD^vYE`U5qEL6pqZ_kB}QY)Uu&0AwT$eltbz*A z116#Z);>h)?T=+2w^~147T++%N)1k{EXg_Bke-&sIzDdjH$|{y5BDIOo@e?IZ4=CR zG8VjR))g<|9}%1o44gc4u;}MbrgCuRjk<)s$UZmDz$>^*N8U`XJ5QFp%ms1%not>N z_qU@*8-vu8l%hB_!Ip0?&Sa|{QsJF`H@8h|dnIcgaY_)IN7)}Y%zo}7{7%@6=yP+t zsl~h>zsw@1vEa@NN{tb|K5|WW2UyWYF}#vXwTQv- zOnUTfSF5)A!WsGegn)Z(C-Rf>!fE5ZZ9Ih~9@8eoAC3BDvekq&vPAdw6Eg%YC)X{2 z+OsoMbqSpDh`a}gl-5Jdxh>tP$?0ij&`oRno^6ZLVpOhLdz3iEoUw4N3%SHrZ0?T3 z<%G>_MIQH^PbgC$O^eL7P46v)G#+6=KllUOLv}uv(}XosA`WfDkUMYmMwg0(ojN;v z(bH2WmB59TXPE0Ve0yu_^MH0GLi3_thjW*tg5P-E!{*r2wM$t0n%VdIIqoxCLICvw zK5dGB?Bz0Nxa$dyFYS1EYHBKeSqR8PAyx)K1V?84P1-W3|Ok=f6CS~=2xI~XD_+g>N&bS`5_5dem$x3U1YhYYL#e3Cy&MbYHu1wzqNM=N ziPygs9ZYMbpdDvwmfD^S^1h+a(@4RmDe^thN8P)XE@iY> zZ98ygTB0G>K2-+XFBc0ElWVUHY0T6LlLiXoz*BWCH};|n=djVqiQ>c+X$ux`<^I_j zUOwwqCk~|u*4)|BS5$v57FZm{M+bZQ_~Vv-+4VuNGeD%neez<)WgOu*`r@@n3O%OH z!27N*X-06xhfiHk0jxK^dM+^XDRLrPtXP6WvjLWu@DGXK^Xjg2(CEp0a98v#Zrv3E z+Fk_C&j=WwlUE8Bkuv6%ucs;XAnO5)k563#!vl(|^R06@WhOcBJu+_MBthZYce^D| z&lpBd9-8|AUA97BporY}MeT+Lzfe%4P#K35Y{D2L#nsXI52wp1Iy!E%1XBprWn7YP z)LOd=_HDX(^!n%aB{$_;ZvJn2yoB12yGPJ zn*@SayIQq^big7y7^J%G0w8;G&CMV9{qQ>JsUt-$zyg2+?vaspBYd%Y{`XB~5Z{5H zI_8HoKn(-MQq?a6H;@k_su;%XE^aeFY+}LzwVDM@HURD`3)`3U8?%$jaUY9AEO(`v zDZt5U-TNA^jW)l1mN(RB9usT4yTwTd`x++rEINu~dO zf%$HltH2x)Y$PK-Oqa|4J%_xK5b#G+`>0}+N(Ta*pSSry%`57+HBLhZt8nA4rJUi$ zO8~I9V*PiQh!If~Om8WedK9Xt)tk-$Os(Qo1l#=c)V%D}ypJ>6bV9`rm3V{M(F@a~ z5L$Hz?S&@FY}5ET%82_SG^s&R4JW7RU<$ZVTOdi188jt(jRIn`?yW9_Di@%Vj+#7G zZbF`5rmNjxd727@Rt-X%s!hi=we#WEHsmGyX$g`<+WEf&Ht`u14otFFW|0~A76%?7U^~YP(4^-kkJe8Me`l#tANYPB$e~uI z)UzMe0w>IXPJ+}f9_-2z{JbSGyCRL;L+#pmHXYik?221>n667D@DxHHujL zMg!RZ@4j%l>65t&FgZHdjo5%w*&kl8Q3D#kGPjv`e*qW@aQY%rj4bE3@(bRB>V(qi z5o@T)li#bjXZveZPQv#bz{FcmNHbbK1Q!Bqna|s;Yje~I23-E$-E`}(Nz9h}v!8ya z0_TWIMkYEo6K@g}x=k6uHXhoc2$d%{U@=iGn6spG3upls!824#dn_q%?+U=%oF}Lg z+js(+5HtZBp0_7SSpVNq2$So!V4exKtQ2H%H_!sKOy)!%+G`xtyzhJ*A&+bKp3wnu z*~>dvn}55*gt~xO$LUB3651sYT<4+7a1iK8Krk~x1i*QZ&4L88fChQ7{icHth`Tb} zXu%TfeUAbVeztK2oUgxyCCV*_5S2OjW5Dj8(hL4fOu);TUR`kh6ar5H1FdOUt|tDo zQl|mF4kJ8hqkkT&i7)49m_T@qSM^H(S<6E;F#!Iz4O&~Ulhd=kx;MZk1R%jw+lzvr z7Tn5|fN%~#H8;x~3kSvi(Lx>a-wo}*j28e@vH@^G13(-v8B9G5>i$2E|1%H&vk3n0 zHUn>{I*kSwxE&KgXZ{!ux@_`>&Wos&1P`14zHnz5D9QzXSe^$o&bb8sE5-NIh}^(; zasxmAwncvAd-H#2i?726fO3L8Py;Zxx0RCQM{#zTZ#o52H25Vug!XZ^e;>8~x|HpA zRKSBD8~tRr#RB{%xAl*2a14WJocqTk@9}+|0gtk3Vc(NjGkgNu$(i6SxGOW+^Ut=W zfyc}4=|b=*u|OyClKu1C8^KK44E$o>-_|War>94D&+VI&bzw_N(=R!SFGB&$RhQNd zNs5j|L(=%YWc5Iu5>M+|y%Iw>^x=RNJi28&uIee=k55r}5SP66)Oxt!%U)L1dTAgh z*XHvBY*2DChnK3_U!V0d%Bcm9YwF4){-h0GFbv!>kYNBXIdulyKGAvT%pVCM&+L*A zk$L*_@5Kl2)R1bsuI`lZi>y+dcQeUEe1E#)Q|=uml|f@|#t z>n%Wd?qaa_2gdJj6?X4kc7}LQB&cC)|Fm|MV>O6R-$W^|w)4T8KKvonO*PC5m?JEg zCpK;A={LZmPJxOYJS-(EsIzMQG!L=CUO_Vy86WZd_#lpkQpTa&H*NuUjzW~0j$dcR zFi0!-^Hu@uj1aXm=7dDs3*(_U-7&7?TQjDz^+slauz~cSoG{kjhhyAZo0q0oF6Iy- zKa5h$==?p;Q}C{cmqJqkt#$l(_3aX#7339E{?vXWPMw}e18E4+*ew3Rj!#k-^vZjb z{+Md@eQDQYZkKsgLTYCqM>f|O!2sw{e!*7wNiK)tIr`>!?G(b*-|RvYsU3k7;S@~d zxGM+MvhzScKYUA6+56MHtbs3Qw$dnGZRwtM@+I*-joGbAMwVdLmVUtdF6)I5w9EdS zW6;duy$7vZ6C#QcOGa(oB=r0yxM{%s_YDQ88AN+Qu%EuO!??E1OZI4eibUK=5u=a@ zFq*N6RHXeWT2Jdek|4cc|9PGrcJi&8(6wf3;6^3M*bylD!8)Ci9A$}lO>=QHi#F09 z`oht+F_e|<1|lAh+=5?=dDFoD28QVCo6aQL_JurJSql#$8@InA8tNni>0& z<+UZ>>c68`7de_0=>0Um#zV;i5d**)j*OSaL`;C|YoC|bC_TCj4Y%5~ce?scaxb0b zyhOgcHTMQ`$`SHXL`&rJpOq?^5EOn zm_`#c*rs-hp?BY5O>)4AB|X;oZbvwHo4s7U78H85;?dskZhG_h3RI3Iuy0xVI*nMA zKv}(%oUz-H1fo~qhNPZBl7wyqTf2)M@8AMA;c(O=x3k$X{=}K|B6j!B%jwD~^-0R$ zg-Z?8idS-i@eXBmf6*a(^jPmRHb5?AQbY!0vT_^4@&ve%UQVe8fLGFYjz*4fq3kMFr+0O{crS1iS|27#~eog6~+7dxZ6t+KBw*v3rRMgqLm{-VJHs|;wKfY)KhkMx;UDpGeeVuEHpHMz2`V;+|V=dz1PC5uTfS|(9FLoeU z0v|E-sAokHi%p{VH%b<(VUYENBgyed<3_#7rzZ`+xv=?aJMCMQ47s2szdha0FwUQ0 zhG@GRUl1c8KQYwoqy#;5cA6c8Xmp_R-Ha=%PO@ZBxpn%J?kf;cT0BzHnV;!VO_L&CShJ z7nAZbWw~&^PPRtU*-mrGT?ROt=CLrmKvf6j5Q9;*9Ry0nj$2h!7r4@yW0 zMV}aP+%1tH@-(AQyL|^8%_Nzig;TMO<4c4ZgXl3Eh>5)cGZjwyLQM$#IKSMhGkRcy z-+EG2ZM|dgwvvtOtH{4Nos*NPZS9e{+LHGp=Oa_}MKTE2a`wxUqwKu!Tz5`{eNoc% zc@R76mK|EoO5BTF=1kxlxypDj$?S)bHw&fEQ`t0+joDIYs;bQW{7I0CLQYm)hzeXzR()+5*x!wr}f- ztthIr7ffcjgJ1$Ae-C?Z{78_&e0XL`;75rB{W%u{l2F;2a}0!ryZ{hEosSG?T$`3? z^p*<80@MS;!^3hIZ;K+WGay3Vm9^bIVE7M0OI_KB@(Rxjis;*ZCSjVaJ?2w~U~akv zyNpSqm(4stC4i{rd>dvzKN>f=j{p&UGhrwYv%Y6Y+`GF-yv_Qh5cvg6W|%qZmHrrH zIZ@^0gW%?UNT&PrZJg_V=b6X!xv?)K5V(}5j!z26P&J*$)0Pk1CYAnCa4(Qa2t_f< zWE|{+=`Mogl#L;>Vg(1XCuwl(n?5%xQdwZ{k7{@`8g#7AWF%hV#1s*eTb-j|Vm30~2%@I6 zaiPO8C0fZ7suC6M_UY3O26-~^IsfeI~h4>!EVMd zPz^4sdJ@45s+W&00)21vF3=$FFbY-;~WuWcf#(rDLUN?<2YQ6gkIzjq*>XO=fq8Jp!1JxlTaVasIUTK6VaGHUkIpddvdFGGX-Idh)cbky50?D{rvbh>qn^8 z=S+!GLHbfw62bISvFW(qsbk*@nmfz9a!M@09MZLNNwS*n&u1)Hckzq6uSm**7`o_% z(=$xqHkc4AXYBUKpbN|*wVC|y+?Xpt2#|T+N7%&G;FogU(}famY6SY3^5}pr1{%&%a=w*l?8NlC*|9LL{&&8-K$_@E zK~8xze-+W;5_R_L$|_0!w4U=vpR|M2$~yR*WX4F<{;6pjN%)u^$8fDWg%KSjW6hCL zHwfly`Y2tm*H2K`Z5WmdT=rJ8_Z7JHYnl|D6Tm91bBCM+6D}}67qirVyhRk~c1qy0 zrjH)nsZUSt=}1l1jx$cQes8lm=w|mGq%`q2616>!j1p|%fmK$_BFAhZr({>0E*<+4 z<)O4>LbJURfjB|Jn=WQh!k%GB2#emX>6n-hWsBPwC$&+Pd!!#^PU2|$@F^aUOQPd zvTlzEI&n!d-wj#?xe=Czx4n@idu6O&6=dUiOpYg`0~E8~^o4v=g%sWeX>)(~_OT`1 zlnP!jS73axtlxQk?dUOr;jJPaEMr`$`Y7{a?~;s`ryb$~AhR|M3iG^W2e6z#__(}Y5_m>Q)D*YUv>A4IV*jeei{1=CUE)!eLLD)zz zPXdI9XaC_%B{jg^NDcF-t`JR>9Q%dZ$;v6`1&R0bvR{4M47H4kR0kBvSsIyeuO<40 z3lc;N8lb-{<=~a*qCCSA6sHvxvq|Ws^`c8 zaH66lvF``tR&b`i<%d+K_4uq)8&j5!o6?3493vRQ#$jL{&P~+Gr(H$SfFFuMsaDK6 zyTHnhEqGWdl&ASLGfILWy5N`M4f?^?9A)O$%W?498(^vqT`G6NUj85d`Ks`dDZCYe zdq{wsAC!I+wOF&{62I1a-IOVVrtt1`JVHHX0X%?yPp;**W*^)Kt}QN_410W&9sS*P zqKPyUw0njS$Ue1^yr$HN6)vi9oP58)s7iAu-~*7IVCAB|5EwlaRqjq0 z`tY&HXsjUFTHo3;vgV`F;-JaRhns`#_ zf4%2tLVVlf>06Q^u;DhSQ=@E}fbG(%a-Xgk(2)Rh^ZuWHvbj%!2=B5WLGu#3{k;Gta zwbf-ic4k(WGaaWheK?D@Gj_Ua*VLJIX7%Cd)Y@7CiC`5HLb;AW5;O<_d;ro~gYpOn zA@6(7{>>i=g5f6F=f2Ll=j_i~S%iD{;|u3|erNyo*=HYu2U)PBWU3jaTR|H``@RU~&w{WB*8lAi zphklHT?QNf+uXdVBM~~ChU*Qo;N}$*&9M9y7!Lx-iBq%Z82Ck6DDC?#EibgksZB>> z*exJYR8>_O{xQ?PW=dX9z^4GD#j*Kx0t%&JJ~V0CtiBU%_w53L9LM$@gDWG*yoJRx z$q>&1v?v+<5wMdb-`l#Y{*TG%FPXT`Aae^!O9at#0LCY|>r)`QyR~ui)+G0vP+WD1 z$zN1@HxaEv{Kgtq$O7=WDOb$?Z_5zq}x0WHC z@P{xz)hE}4-2!5S1o@tY^UMJNlR|#{kKH0-C8Z6LS6Fr*faP}U7=iM7WztvRd~*Q6)NVfeCA&pLOG+A~ zsHn)tgs<7HLlp)+QdL!zl2ZyPX^_*sX)6FswOfY;t~}UwuGnr7(U6h`0q{Ay1z1J0 zyw`3G(U5Wm83@%>Vz&;f5U3njlXCyxDJS^Nch4*UFu`seQII=-@s2#ZMOY=}3_?OI zuvdT|N1JHI5Ug2m?N=~| zo&Zo0Yg?MQ!ysC32=n3BcWik-+D4+iKR|hlO8<%>YYA8YZ4M_w0Ip}IFHN2{>+JEi zcUq!t#1aJsE5`Pad`s6M%_PNz*um(*WPr+Em}HQdr3#c?ByLG08dt#>S>?L_z>g zfp7|pUJ$y$=mXGAKwqQ^41|cNGl807hM98+@JIY^U=@BAfD8bcAY_0s0YEMim<=+} zK`hEzn>RI$R(4c_0L5B>M?;HyqMjxbx9okj?!ZWeMl?tuQ1*$CAFm;n#W{on%=ncdXxr>7 z#_X=Ht~R4?OM|MaD&t^VM?f-`0r&y|*GJzPAN7Z5;Yg2W3~P`;Am9)ACvF3HNi;nJ zSSuj^b5GNj9Z}TRD3V`LzEl!_m4GTlH{Gz^A8Nm~rKKel>C0i~K_<<}e;nY|QFOSU z1+U)MSpTDAZAaoZ<*SLz@uP?Te4_20b(5yg2@>%_0Fx{>aiwWwWuIt!r#{k`5gnIz zQ`yZB2*Toe5n}w*7jFN%;oPW6EP+74ANJ+`I}`rKVpT~@pNjOT=p_azDk}1w?oDq6 zaGiw?On|=D+O+vUEDF;kfV_gThe7xetjv@X4qP*BZ2r(O;w2H{!@i6MVa*)YcJ3gcD>R>P!KL5eTBFO2!j4D_#2Y|=4Ih@PBrp-?Sc+5hT8K?R(R$ctT&`XT+ zBZ5qLv9+o0+ZH<4*g$Jj-M5+WqJ=6S^T0<2S+JyJDwsc|)&}>G$zR35e22!_ur$oS z2GFHan3#+63va*T{NFz^NH|QlLv_?k;(Kn~(a)_M*xvAG2K}32alsr~@zGz-|3aW5 z#RksxiL{?s(5G<#GNbp$7}#QbR2UboBp1dY1jWk>e*N~2jY=J9oWp@&uoJ*@DrE_D z>-paWFt88^iZ|q#`N?PmYV1b{`h{ZAsrfhEHU~H#L6~kV9+4Ms<(_e2%Wj4GEBhBY9;wPV9+2e)Yh01iD0zKG|qsTf{I120WL(4 zIckl|J<-Y1I0Y#bk0B2P4N+87*Ds@bvL(vM;ID_>5bhQ=jgqeXrz%S19_^-k+ zi)x3^o~$RzHHl;J1xPB#(chbvJ42Ylr?}pf562=*<0zDlr68UvNSaE|xe(K_*feRJ z107c>C8Wp|M3k#imdHs-7rw52mS@=2{t~YCMVhJc!1V zkn$x)<4N=-Ml&Khf@nrWM-ZzNZDTLJ_Ekp^Rc@{AomITPtJeQvcUMVG=fSdd=WeiD zQxZ_02k|n+>pIH}Hg^-@P5@~D&6Yy^z;0bhKzJ!!Mn%c`t~)?jhfBA}SZar!WTVF) zBT%;f+%gb;hsgVWKY;0C5P$VCKx#(n|cIuNcNtt>;d zB}0?lXoFnDgo)ih1-MA1ED^LpyhLej=QkMmvTAu^98IJir=Sf2ptz>Hf|>rsN*yNo zcB1q+3C|m(q_%TDG5-oy-w{mW%>+qu6rMIn(b~=_VA=>mw$-|_zxS;~XmT2!HAvA@ zoZ#*5pLIX%J@ejqCX(fJMIRrS zV-U|pkb#T^--)FS0<=NgNVFNtP?*GlI7xCmo-s%)8Ou<7;qV*r(3A>z!XPnbEJHc5 zY4w0kZTB}B_*}C4CBmHqx|6%QLnSp`%>>#m z!A)#%)7F(a$CIrm(WIn7ifg(n2%OlAWpw-imNIxL0U(o5Qqy$=L_5H|lLhZsx+nX< znl+TtTbQSuK}u>n=R@+Zpt#3;l9>tSnE>u1N!XFseYT{gYbO|u!Vtms;~7mY4^hZ& zkyc14gDl_BIT?~RB;p=Rf*1#21qdsolo&s;yR)RG>kkC3>8H?!;Ht~d*e!50oOl*S zq-rgv36s`?ai!f_Vju^=9SnNf&uPa>)_47~bX~`_c8eN`lrhK|qx-)AEU{Zh97zY^ zK^Df|lJ#AWg4vOq38sWWN@}|w2H`=wbtH&15WZVn+w~*6g+#*f1}R?OSq$dK?beZK z2=uol>$+~VTgOlwZIGh1ol^wSGf1()bd(SaPWrsgXgJm&RcqPs`>2|MDRyf~76LTW zZVf|mtU=EBx*mZ?Hmf}kGurLeFce1`WO+^R9Dvr?tt06O+G@9ip*YeY2Ahuo7q7Cu;UEE!dMOOy@6t%FC+7(c8iFF;|v0@ z7v95iF(JXYf75N$XSa|@IM5*EspvuAZ`D<1@3vdVa5&B&4Bln8h9t7FepR-kXNetW zkT8XQ3E;HdIuZ{b3{n3Vo&G`q#~CEJ>hd!Tu@XS1-C7dJS(0+4UJnu_4HcQ&nT>n^ z^>%AX045RcslRJ%N^HdINP_@CLuJO1Efv{ikXQ)D(*PWQwy8kiZ`7~K-ek89RUB)O z^K7{*`?W2V*$-S6&YCR2D~R~V08(_14*GR{Ma~cHR$+ydxs6`En)(539{j= zd}6TxEoJ7#AlwKb%~p{k!DtS3WGcUI#3`hVL54E8PsfJA9~3>s8NPA7HxkSx1S;f# zEg&Y_D)IvF31C(50qV6|h*eV7AVV4a&)+`y0{|#q-#e2KUINgqAS?qgBk95zUCdZn zcUSfYN$)!jxW*unXsFCM0^kUMX8>S%O=r0PJquubk|Zz_eW9T;>-8k{7k3;#NR203 zD{?ji^smY4kAX)UDza*l)n6j;ltBQ11RqLDKSaN2Se5lyGWtp+o-+s-lakO&OS&)f z%TAw0SHX)0xhlauyl;kSyPq800O{z@A=UC#6Ck#R$sx{92=3DuKAcq7dfm&Hn@lTx_eAXRHQ z4Zy^a$`G(2G(PJqNs^ROxZWTq2eyP5_BITIw)LOQ`fP9+dG4jjD!ASt=t3A6b_B93 z%jjCE)7jy1y+H;Ke%sK`8Q3jh-nMb&#B(;tN*P>l5CEWoEgvp|*NW_{Teen=J!Oxg zl!1L12*0l0-MIhq%t!sFyV^l?6^J)(S(W3)-4hdm^Z}3n47-&ilEDF}BVQWma9CjY z0h~r71qLFBMiU7gK{O-5QUI(Yh-Mgw1>o%kF`5z4V-U@VsEIA8J z1hLSl#^FdQ)s7{D1z1NAufam`WG6@gQi~wUjcObZvs69VrG+twa-$lDV@Tzboz=%6 z>Wyj~jx-dAeV#GI>Aj=`q{6Sph1$|G-~ph+E= zSm|UJOzlEQQZfh=DY;IQ3aA}RN!%{jfYKNQ%+q4hq;V2T$I=j@U6?Zbu1aA7W>r;H zx%wGiFEDrvXR4GXgZ?D;H*Y--hDxfN{`R|PuZ&He#%|td>$nO7yDp8!hp)e~>7)Pv zCLB^~-JpcLb5@f){HF4IgAW0N8^{pU48jtHQR>n-1SzmssUYEP;G-ReNXZ~1uq;Yl z8iznYky1H%Q3L_7Td8$MK_Kv%tf-V}Y$q?TG@F5iN`(c#4h$N^9}2$=z*MM~)}PjY zXVeNcwnCXy0YGJ4k4fqM+kruYw6rvw1)#J)IY~SewL*=pFp<)+V_^55VDJp^@m&tL zDfL3QIe%eUykBvyNicH@%JTqTqEeQagXe$$$RN!02gSaKh29V@)i8)3S1v3rc;O?1 z$j&hh0MzykC6whAlvP^Lr?HXzMP;iQC{Zka%Ip|>sX-1LsO|?)t=uc&Cvz8;PFF6k zv4yz{OQ$pNglc&P{btY$0EX?A)?v=!cOXr}X6>An(VVxateAk!h;F0*-}pm#q@|_qgDC21 zEV5up$y75e9tYu`D5{$bl<#Y*Yl!sah!64R7nJ@J6Gi)K@;(AKO5o@Fn(DSfZ}}Z} z<`t9%gb;tpEbl|K54I6~y|r=k4~PAFwCeeb%AaBI=cDTU0)Jv)8xevq`IXjP^?!mk zj8$mf!s4sQXDlVgG6t3bm=Rlxc&4?n?u!`VSd6+2Kp+tC_owC7664b`iTN1ZL!g69 zbO_Av5_3BXagrfV87$72EYCss`VsE$N|Q!Uf2J?u4g}KI6*>?McE(Rv{JCby9b?kS zjQDeTpfM-V?B_i{Cf2h5prKKe_ zQX!>%A_@zuGP}5=20(dSq-pGCQ#OsQY;3F^^}&6m0ccy>8{yn>pR5+?y|V$D8@oP@ zZ7_c854YdFYga>`N^#{3<>bjX%;RnM)lQn;CJ49%E`QIR3t?t_Z(noa!ykO`Tb0FO z0HQp|d5g;L0Qe~YgYet6p^>Smb8nrDwn>co^Un03xP!nuHSu z@De0H5vwuI6Ze5;-r~|pY*G~wN&(yq=#6I*%!eTSA4_Z@gR5J2)}M%H;|b10)`+}C zjq?dqB*6s)%my$AKz3sLbuC>04l-~E;3h#vaBpM5USLhaUMpuys+w1@Vj4lthTyB1 zX$k`q2sD9-bBTEZfpQs02atn-Ki#_4q#HmVfKCGX7~IZ8r--SY!Ka9*ogj}x(jOuG z2lqDq{%EoVrp#PMVPREfr_r53GUVrw%u%T$D9tA+JSLI41pCfO?hmV!CBNuo;Sc4Q gS-mQ=m`>pT0X+A_smyS5Y5)KL07*qoM6N<$g1Q?RZ~y=R literal 0 HcmV?d00001 diff --git a/packages/uikit/assets/icons/png/ic-empty-battery-flash-34@4x.png b/packages/uikit/assets/icons/png/ic-empty-battery-flash-34@4x.png new file mode 100644 index 0000000000000000000000000000000000000000..f6b3fdfdce0cdc72487a0a5f4f5edcc6e37b34bb GIT binary patch literal 6046 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 literal 0 HcmV?d00001 diff --git a/packages/uikit/assets/icons/png/ic-medium-battery-34@4x.png b/packages/uikit/assets/icons/png/ic-medium-battery-34@4x.png new file mode 100644 index 0000000000000000000000000000000000000000..323eecba0498b93b827eefb2f7440639f0ffecd5 GIT binary patch literal 5088 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 literal 0 HcmV?d00001 diff --git a/packages/uikit/assets/icons/svg/128/ic-almost-empty-battery-128.svg b/packages/uikit/assets/icons/svg/128/ic-almost-empty-battery-128.svg index bf7e7b928..f693b5fd1 100644 --- a/packages/uikit/assets/icons/svg/128/ic-almost-empty-battery-128.svg +++ b/packages/uikit/assets/icons/svg/128/ic-almost-empty-battery-128.svg @@ -1,4 +1,4 @@ - - + + diff --git a/packages/uikit/assets/icons/svg/128/ic-empty-battery-128.svg b/packages/uikit/assets/icons/svg/128/ic-empty-battery-128.svg index 58901b892..412e0088e 100644 --- a/packages/uikit/assets/icons/svg/128/ic-empty-battery-128.svg +++ b/packages/uikit/assets/icons/svg/128/ic-empty-battery-128.svg @@ -1,4 +1,4 @@ - - + + diff --git a/packages/uikit/assets/icons/svg/128/ic-full-battery-128.svg b/packages/uikit/assets/icons/svg/128/ic-full-battery-128.svg index 74746ab11..df51d8f15 100644 --- a/packages/uikit/assets/icons/svg/128/ic-full-battery-128.svg +++ b/packages/uikit/assets/icons/svg/128/ic-full-battery-128.svg @@ -1,4 +1,4 @@ - - + + diff --git a/packages/uikit/assets/icons/svg/128/ic-medium-battery-128.svg b/packages/uikit/assets/icons/svg/128/ic-medium-battery-128.svg new file mode 100644 index 000000000..2aff92bd1 --- /dev/null +++ b/packages/uikit/assets/icons/svg/128/ic-medium-battery-128.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/uikit/assets/icons/svg/24/ic-battery-almost-empty-24.svg b/packages/uikit/assets/icons/svg/24/ic-battery-almost-empty-24.svg new file mode 100644 index 000000000..bcfedeb03 --- /dev/null +++ b/packages/uikit/assets/icons/svg/24/ic-battery-almost-empty-24.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/uikit/assets/icons/svg/34/ic-almost-empty-battery-34.svg b/packages/uikit/assets/icons/svg/34/ic-almost-empty-battery-34.svg new file mode 100644 index 000000000..2e42335b8 --- /dev/null +++ b/packages/uikit/assets/icons/svg/34/ic-almost-empty-battery-34.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/uikit/assets/icons/svg/34/ic-empty-battery-accent-flash-34.svg b/packages/uikit/assets/icons/svg/34/ic-empty-battery-accent-flash-34.svg new file mode 100644 index 000000000..9c5e912f8 --- /dev/null +++ b/packages/uikit/assets/icons/svg/34/ic-empty-battery-accent-flash-34.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/uikit/assets/icons/svg/34/ic-empty-battery-flash-34.svg b/packages/uikit/assets/icons/svg/34/ic-empty-battery-flash-34.svg new file mode 100644 index 000000000..bc4b6ab82 --- /dev/null +++ b/packages/uikit/assets/icons/svg/34/ic-empty-battery-flash-34.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/uikit/assets/icons/svg/34/ic-full-battery-34.svg b/packages/uikit/assets/icons/svg/34/ic-full-battery-34.svg new file mode 100644 index 000000000..a2daa7923 --- /dev/null +++ b/packages/uikit/assets/icons/svg/34/ic-full-battery-34.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/uikit/assets/icons/svg/34/ic-medium-battery-34.svg b/packages/uikit/assets/icons/svg/34/ic-medium-battery-34.svg new file mode 100644 index 000000000..5cd278e0d --- /dev/null +++ b/packages/uikit/assets/icons/svg/34/ic-medium-battery-34.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/uikit/assets/icons/svg/44/ic-battery-100-44.svg b/packages/uikit/assets/icons/svg/44/ic-battery-100-44.svg new file mode 100644 index 000000000..a6d78c3e9 --- /dev/null +++ b/packages/uikit/assets/icons/svg/44/ic-battery-100-44.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/uikit/assets/icons/svg/44/ic-battery-25-44.svg b/packages/uikit/assets/icons/svg/44/ic-battery-25-44.svg new file mode 100644 index 000000000..ed92b106f --- /dev/null +++ b/packages/uikit/assets/icons/svg/44/ic-battery-25-44.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/uikit/assets/icons/svg/44/ic-battery-50-44.svg b/packages/uikit/assets/icons/svg/44/ic-battery-50-44.svg new file mode 100644 index 000000000..295f26ff8 --- /dev/null +++ b/packages/uikit/assets/icons/svg/44/ic-battery-50-44.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/uikit/src/components/Icon/Icon.tsx b/packages/uikit/src/components/Icon/Icon.tsx index 9bc71bb4a..1df0c98f7 100644 --- a/packages/uikit/src/components/Icon/Icon.tsx +++ b/packages/uikit/src/components/Icon/Icon.tsx @@ -50,7 +50,7 @@ export const Icon = memo((props: IconProps) => { if (icon) { return ( - + ); diff --git a/packages/uikit/src/components/Icon/Icon.types.ts b/packages/uikit/src/components/Icon/Icon.types.ts index dc24385ab..214cc7aea 100644 --- a/packages/uikit/src/components/Icon/Icon.types.ts +++ b/packages/uikit/src/components/Icon/Icon.types.ts @@ -15,6 +15,7 @@ export type IconNames = | 'ic-full-battery-128' | 'ic-gear-128' | 'ic-lock-128' + | 'ic-medium-battery-128' | 'ic-notification-128' | 'ic-appearance-16' | 'ic-arrow-down-16' @@ -52,6 +53,7 @@ export type IconNames = | 'ic-xmark-circle-16' | 'ic-backspace-bold-24' | 'ic-backup-24' + | 'ic-battery-almost-empty-24' | 'ic-key-24' | 'ic-reset-24' | 'ic-ton-disabled-24' @@ -122,10 +124,18 @@ export type IconNames = | 'ic-xmark-outline-28' | 'ic-checkmark-circle-32' | 'ic-exclamationmark-circle-32' + | 'ic-almost-empty-battery-34' + | 'ic-empty-battery-accent-flash-34' + | 'ic-empty-battery-flash-34' + | 'ic-full-battery-34' + | 'ic-medium-battery-34' | 'ic-delete-36' | 'ic-exclamationmark-triangle-36' | 'ic-faceid-36' | 'ic-fingerprint-36' + | 'ic-battery-100-44' + | 'ic-battery-25-44' + | 'ic-battery-50-44' | 'ic-cards-stack-44' | 'ic-ton-old-wallets-44' | 'ic-logo-48' @@ -161,6 +171,7 @@ export const AllIcons = [ 'ic-full-battery-128', 'ic-gear-128', 'ic-lock-128', + 'ic-medium-battery-128', 'ic-notification-128', 'ic-appearance-16', 'ic-arrow-down-16', @@ -198,6 +209,7 @@ export const AllIcons = [ 'ic-xmark-circle-16', 'ic-backspace-bold-24', 'ic-backup-24', + 'ic-battery-almost-empty-24', 'ic-key-24', 'ic-reset-24', 'ic-ton-disabled-24', @@ -268,10 +280,18 @@ export const AllIcons = [ 'ic-xmark-outline-28', 'ic-checkmark-circle-32', 'ic-exclamationmark-circle-32', + 'ic-almost-empty-battery-34', + 'ic-empty-battery-accent-flash-34', + 'ic-empty-battery-flash-34', + 'ic-full-battery-34', + 'ic-medium-battery-34', 'ic-delete-36', 'ic-exclamationmark-triangle-36', 'ic-faceid-36', 'ic-fingerprint-36', + 'ic-battery-100-44', + 'ic-battery-25-44', + 'ic-battery-50-44', 'ic-cards-stack-44', 'ic-ton-old-wallets-44', 'ic-logo-48', @@ -308,6 +328,7 @@ export const IconSizes = { 'ic-full-battery-128': 128, 'ic-gear-128': 128, 'ic-lock-128': 128, + 'ic-medium-battery-128': 128, 'ic-notification-128': 128, 'ic-appearance-16': 16, 'ic-arrow-down-16': 16, @@ -345,6 +366,7 @@ export const IconSizes = { 'ic-xmark-circle-16': 16, 'ic-backspace-bold-24': 24, 'ic-backup-24': 24, + 'ic-battery-almost-empty-24': 24, 'ic-key-24': 24, 'ic-reset-24': 24, 'ic-ton-disabled-24': 24, @@ -415,10 +437,18 @@ export const IconSizes = { 'ic-xmark-outline-28': 28, 'ic-checkmark-circle-32': 32, 'ic-exclamationmark-circle-32': 32, + 'ic-almost-empty-battery-34': 34, + 'ic-empty-battery-accent-flash-34': 34, + 'ic-empty-battery-flash-34': 34, + 'ic-full-battery-34': 34, + 'ic-medium-battery-34': 34, 'ic-delete-36': 36, 'ic-exclamationmark-triangle-36': 36, 'ic-faceid-36': 36, 'ic-fingerprint-36': 36, + 'ic-battery-100-44': 100, + 'ic-battery-25-44': 25, + 'ic-battery-50-44': 50, 'ic-cards-stack-44': 44, 'ic-ton-old-wallets-44': 44, 'ic-logo-48': 48, diff --git a/packages/uikit/src/components/Icon/IconList.native.ts b/packages/uikit/src/components/Icon/IconList.native.ts index 54c933e42..e6219fc78 100644 --- a/packages/uikit/src/components/Icon/IconList.native.ts +++ b/packages/uikit/src/components/Icon/IconList.native.ts @@ -15,6 +15,7 @@ export const IconList = { 'ic-full-battery-128': require('../../../assets/icons/png/ic-full-battery-128.png'), 'ic-gear-128': require('../../../assets/icons/png/ic-gear-128.png'), 'ic-lock-128': require('../../../assets/icons/png/ic-lock-128.png'), + 'ic-medium-battery-128': require('../../../assets/icons/png/ic-medium-battery-128.png'), 'ic-notification-128': require('../../../assets/icons/png/ic-notification-128.png'), 'ic-appearance-16': require('../../../assets/icons/png/ic-appearance-16.png'), 'ic-arrow-down-16': require('../../../assets/icons/png/ic-arrow-down-16.png'), @@ -52,6 +53,7 @@ export const IconList = { 'ic-xmark-circle-16': require('../../../assets/icons/png/ic-xmark-circle-16.png'), 'ic-backspace-bold-24': require('../../../assets/icons/png/ic-backspace-bold-24.png'), 'ic-backup-24': require('../../../assets/icons/png/ic-backup-24.png'), + 'ic-battery-almost-empty-24': require('../../../assets/icons/png/ic-battery-almost-empty-24.png'), 'ic-key-24': require('../../../assets/icons/png/ic-key-24.png'), 'ic-reset-24': require('../../../assets/icons/png/ic-reset-24.png'), 'ic-ton-disabled-24': require('../../../assets/icons/png/ic-ton-disabled-24.png'), @@ -122,10 +124,18 @@ export const IconList = { 'ic-xmark-outline-28': require('../../../assets/icons/png/ic-xmark-outline-28.png'), 'ic-checkmark-circle-32': require('../../../assets/icons/png/ic-checkmark-circle-32.png'), 'ic-exclamationmark-circle-32': require('../../../assets/icons/png/ic-exclamationmark-circle-32.png'), + 'ic-almost-empty-battery-34': require('../../../assets/icons/png/ic-almost-empty-battery-34.png'), + 'ic-empty-battery-accent-flash-34': require('../../../assets/icons/png/ic-empty-battery-accent-flash-34.png'), + 'ic-empty-battery-flash-34': require('../../../assets/icons/png/ic-empty-battery-flash-34.png'), + 'ic-full-battery-34': require('../../../assets/icons/png/ic-full-battery-34.png'), + 'ic-medium-battery-34': require('../../../assets/icons/png/ic-medium-battery-34.png'), 'ic-delete-36': require('../../../assets/icons/png/ic-delete-36.png'), 'ic-exclamationmark-triangle-36': require('../../../assets/icons/png/ic-exclamationmark-triangle-36.png'), 'ic-faceid-36': require('../../../assets/icons/png/ic-faceid-36.png'), 'ic-fingerprint-36': require('../../../assets/icons/png/ic-fingerprint-36.png'), + 'ic-battery-100-44': require('../../../assets/icons/png/ic-battery-100-44.png'), + 'ic-battery-25-44': require('../../../assets/icons/png/ic-battery-25-44.png'), + 'ic-battery-50-44': require('../../../assets/icons/png/ic-battery-50-44.png'), 'ic-cards-stack-44': require('../../../assets/icons/png/ic-cards-stack-44.png'), 'ic-ton-old-wallets-44': require('../../../assets/icons/png/ic-ton-old-wallets-44.png'), 'ic-logo-48': require('../../../assets/icons/png/ic-logo-48.png'), diff --git a/packages/uikit/src/components/List/ListItem.tsx b/packages/uikit/src/components/List/ListItem.tsx index c5dfeb8fe..5ffa5d09a 100644 --- a/packages/uikit/src/components/List/ListItem.tsx +++ b/packages/uikit/src/components/List/ListItem.tsx @@ -38,6 +38,7 @@ export interface ListItemProps { rightContent?: React.ReactNode; valueMultiline?: boolean; onPress?: () => void; + disabled?: boolean; } function isString(str: T) { @@ -46,6 +47,7 @@ function isString(str: T) { export const ListItem = memo((props) => { const { + disabled, onPress, navigate, chevronColor = 'iconTertiary', @@ -89,7 +91,7 @@ export const ListItem = memo((props) => { return ( Date: Tue, 2 Apr 2024 19:14:50 +0300 Subject: [PATCH 04/22] feature(mobile): Support locked tokens (#782) --- .../@core-js/src/TonAPI/TonAPIGenerated.ts | 182 ++++++++++++------ packages/mobile/src/core/Jetton/Jetton.tsx | 34 +++- packages/mobile/src/store/models.ts | 1 + .../Wallet/components/WalletContentList.tsx | 37 +++- .../mobile/src/tabs/Wallet/hooks/useTokens.ts | 6 + .../src/wallet/managers/JettonsManager.ts | 2 - .../JettonBalanceModel/JettonBalanceModel.ts | 9 + .../shared/i18n/locales/tonkeeper/en.json | 1 + .../shared/i18n/locales/tonkeeper/ru-RU.json | 1 + 9 files changed, 205 insertions(+), 68 deletions(-) diff --git a/packages/@core-js/src/TonAPI/TonAPIGenerated.ts b/packages/@core-js/src/TonAPI/TonAPIGenerated.ts index a73a3cd2a..ceb0038ea 100644 --- a/packages/@core-js/src/TonAPI/TonAPIGenerated.ts +++ b/packages/@core-js/src/TonAPI/TonAPIGenerated.ts @@ -63,6 +63,13 @@ export interface BlockValueFlow { minted: BlockCurrencyCollection; } +export interface ServiceStatus { + /** @default true */ + rest_online: boolean; + /** @example 100 */ + indexing_latency: number; +} + export interface BlockchainBlock { /** @example 130 */ tx_quantity: number; @@ -296,7 +303,7 @@ export interface ComputePhase { */ gas_used?: number; /** - * @format uint32 + * @format int32 * @example 5 */ vm_steps?: number; @@ -811,8 +818,15 @@ export interface BlockchainRawAccount { last_transaction_lt: number; /** @example "088b436a846d92281734236967970612f87fbd64a2cd3573107948379e8e4161" */ last_transaction_hash?: string; + /** @example "088b436a846d92281734236967970612f87fbd64a2cd3573107948379e8e4161" */ + frozen_hash?: string; status: AccountStatus; storage: AccountStorageInfo; + libraries?: { + /** @example true */ + public: boolean; + root: string; + }[]; } export interface Account { @@ -1692,7 +1706,7 @@ export interface TraceID { /** @example "55e8809519cd3c49098c9ee45afdafcea7a894a74d0f628d94a115a50e045122" */ id: string; /** - * @format uint64 + * @format int64 * @example 1645544908 */ utime: number; @@ -1893,17 +1907,17 @@ export interface DecodedMessage { ext_in_msg_decoded?: { wallet_v3?: { /** - * @format uint32 + * @format int64 * @example 1 */ subwallet_id: number; /** - * @format uint32 + * @format int64 * @example 1 */ valid_until: number; /** - * @format uint32 + * @format int64 * @example 1 */ seqno: number; @@ -1911,22 +1925,22 @@ export interface DecodedMessage { }; wallet_v4?: { /** - * @format uint32 + * @format int64 * @example 1 */ subwallet_id: number; /** - * @format uint32 + * @format int64 * @example 1 */ valid_until: number; /** - * @format uint32 + * @format int64 * @example 1 */ seqno: number; /** - * @format int8 + * @format int32 * @example 1 */ op: number; @@ -1934,7 +1948,7 @@ export interface DecodedMessage { }; wallet_highload_v2?: { /** - * @format uint32 + * @format int64 * @example 1 */ subwallet_id: number; @@ -2211,23 +2225,20 @@ export interface AccountInfoByStateInit { } export interface Seqno { - /** @format uint32 */ + /** @format int32 */ seqno: number; } export interface BlockRaw { /** - * @format uint32 + * @format int32 * @example 4294967295 */ workchain: number; + /** @example 800000000000000 */ + shard: string; /** - * @format uint64 - * @example 9223372036854776000 - */ - shard: number; - /** - * @format uint32 + * @format int32 * @example 30699640 */ seqno: number; @@ -2239,7 +2250,7 @@ export interface BlockRaw { export interface InitStateRaw { /** - * @format uint32 + * @format int32 * @example 4294967295 */ workchain: number; @@ -2285,6 +2296,18 @@ export interface TokenRates { diff_30d?: Record; } +export interface MarketTonRates { + /** @example "OKX" */ + market: string; + /** @example 5.2 */ + usd_price: number; + /** + * @format int64 + * @example 1668436763 + */ + last_date_update: number; +} + /** @example "int_msg" */ export enum MessageMsgTypeEnum { IntMsg = 'int_msg', @@ -2372,9 +2395,10 @@ export enum JettonSwapActionDexEnum { } export enum NftPurchaseActionAuctionTypeEnum { + DNSTon = 'DNS.ton', DNSTg = 'DNS.tg', + NUMBERTg = 'NUMBER.tg', Getgems = 'getgems', - Basic = 'basic', } /** @example "ton20" */ @@ -2448,12 +2472,21 @@ export interface EmulateMessageToTraceParams { ignore_signature_check?: boolean; } +export interface EmulateMessageToAccountEventParams { + ignore_signature_check?: boolean; + /** + * account ID + * @example "0:97264395BD65A255A429B11326C84128B7D70FFED7949ABAE3036D506BA38621" + */ + accountId: string; +} + export interface GetAccountJettonsBalancesParams { /** * accept ton and all possible fiat currencies, separated by commas - * @example "ton,usd,rub" + * @example ["ton","usd","rub"] */ - currencies?: string; + currencies?: string[]; /** * account ID * @example "0:97264395BD65A255A429B11326C84128B7D70FFED7949ABAE3036D506BA38621" @@ -2604,8 +2637,8 @@ export interface GetAccountEventsParams { before_lt?: number; /** * @min 1 - * @max 1000 - * @example 100 + * @max 100 + * @example 20 */ limit: number; /** @@ -2926,14 +2959,16 @@ export interface GetStakingPoolsParams { export interface GetRatesParams { /** * accept ton and jetton master addresses, separated by commas - * @example "ton" + * @maxItems 100 + * @example ["ton"] */ - tokens: string; + tokens: string[]; /** * accept ton and all possible fiat currencies, separated by commas - * @example "ton,usd,rub" + * @maxItems 50 + * @example ["ton","usd","rub"] */ - currencies: string; + currencies: string[]; } export interface GetChartRatesParams { @@ -2963,7 +2998,7 @@ export interface GetChartRatesParams { export interface GetRawMasterchainInfoExtParams { /** * mode - * @format uint32 + * @format int32 * @example 0 */ mode: number; @@ -2972,7 +3007,7 @@ export interface GetRawMasterchainInfoExtParams { export interface GetRawBlockchainBlockHeaderParams { /** * mode - * @format uint32 + * @format int32 * @example 0 */ mode: number; @@ -2999,13 +3034,13 @@ export interface GetRawAccountStateParams { export interface GetRawShardInfoParams { /** * workchain - * @format uint32 + * @format int32 * @example 1 */ workchain: number; /** * shard - * @format uint64 + * @format int64 * @example 1 */ shard: number; @@ -3024,13 +3059,13 @@ export interface GetRawShardInfoParams { export interface GetRawTransactionsParams { /** * count - * @format uint32 + * @format int32 * @example 100 */ count: number; /** * lt - * @format uint64 + * @format int64 * @example 23814011000000 */ lt: number; @@ -3049,13 +3084,13 @@ export interface GetRawTransactionsParams { export interface GetRawListBlockTransactionsParams { /** * mode - * @format uint32 + * @format int32 * @example 0 */ mode: number; /** * count - * @format uint32 + * @format int32 * @example 100 */ count: number; @@ -3066,7 +3101,7 @@ export interface GetRawListBlockTransactionsParams { account_id?: string; /** * lt - * @format uint64 + * @format int64 * @example 23814011000000 */ lt?: number; @@ -3090,7 +3125,7 @@ export interface GetRawBlockProofParams { target_block?: string; /** * mode - * @format uint32 + * @format int32 * @example 0 */ mode: number; @@ -3099,7 +3134,7 @@ export interface GetRawBlockProofParams { export interface GetRawConfigParams { /** * mode - * @format uint32 + * @format int32 * @example 0 */ mode: number; @@ -3357,6 +3392,22 @@ export class TonAPIGenerated { this.http = http; } + status = { + /** + * @description Reduce indexing latency + * + * @tags Blockchain + * @name ReduceIndexingLatency + * @request GET:/v2/status + */ + reduceIndexingLatency: (params: RequestParams = {}) => + this.http.request({ + path: `/v2/status`, + method: 'GET', + format: 'json', + ...params, + }), + }; blockchain = { /** * @description Get blockchain block data @@ -3895,7 +3946,7 @@ export class TonAPIGenerated { */ timestamp: number; domain: { - /** @format uint32 */ + /** @format int32 */ length_bytes?: number; value: string; }; @@ -3945,7 +3996,7 @@ export class TonAPIGenerated { * @request POST:/v2/accounts/{account_id}/events/emulate */ emulateMessageToAccountEvent: ( - accountId: string, + { accountId, ...query }: EmulateMessageToAccountEventParams, data: { /** @example "te6ccgECBQEAARUAAkWIAWTtae+KgtbrX26Bep8JSq8lFLfGOoyGR/xwdjfvpvEaHg" */ boc: string; @@ -3955,6 +4006,7 @@ export class TonAPIGenerated { this.http.request({ path: `/v2/accounts/${accountId}/events/emulate`, method: 'POST', + query: query, body: data, format: 'json', ...params, @@ -4723,6 +4775,26 @@ export class TonAPIGenerated { format: 'json', ...params, }), + + /** + * @description Get the TON price from markets + * + * @tags Rates + * @name GetMarketsRates + * @request GET:/v2/rates/markets + */ + getMarketsRates: (params: RequestParams = {}) => + this.http.request< + { + markets: MarketTonRates[]; + }, + Error + >({ + path: `/v2/rates/markets`, + method: 'GET', + format: 'json', + ...params, + }), }; tonconnect = { /** @@ -4821,28 +4893,28 @@ export class TonAPIGenerated { this.http.request< { /** - * @format uint32 + * @format int32 * @example 0 */ mode: number; /** - * @format uint32 + * @format int32 * @example 257 */ version: number; /** - * @format uint64 + * @format int64 * @example 7 */ capabilities: number; last: BlockRaw; /** - * @format uint32 + * @format int32 * @example 1687938199 */ last_utime: number; /** - * @format uint32 + * @format int32 * @example 1687938204 */ now: number; @@ -4870,7 +4942,7 @@ export class TonAPIGenerated { this.http.request< { /** - * @format uint32 + * @format int32 * @example 1687146728 */ time: number; @@ -4946,7 +5018,7 @@ export class TonAPIGenerated { { id: BlockRaw; /** - * @format uint32 + * @format int32 * @example 0 */ mode: number; @@ -4978,7 +5050,7 @@ export class TonAPIGenerated { this.http.request< { /** - * @format uint32 + * @format int32 * @example 200 */ code: number; @@ -5117,7 +5189,7 @@ export class TonAPIGenerated { { id: BlockRaw; /** - * @format uint32 + * @format int32 * @example 100 */ req_count: number; @@ -5125,13 +5197,13 @@ export class TonAPIGenerated { incomplete: boolean; ids: { /** - * @format uint32 + * @format int32 * @example 0 */ mode: number; /** @example "131D0C65055F04E9C19D687B51BC70F952FD9CA6F02C2801D3B89964A779DF85" */ account?: string; - /** @format uint64 */ + /** @format int64 */ lt?: number; /** @example "131D0C65055F04E9C19D687B51BC70F952FD9CA6F02C2801D3B89964A779DF85" */ hash?: string; @@ -5185,9 +5257,9 @@ export class TonAPIGenerated { /** @example "131D0C65055F04E9C19D687B51BC70F952FD9CA6F02C2801D3B89964A779DF85" */ config_proof: string; signatures: { - /** @format uint32 */ + /** @format int64 */ validator_set_hash: number; - /** @format uint32 */ + /** @format int32 */ catchain_seqno: number; signatures: { /** @example "131D0C65055F04E9C19D687B51BC70F952FD9CA6F02C2801D3B89964A779DF85" */ @@ -5222,7 +5294,7 @@ export class TonAPIGenerated { this.http.request< { /** - * @format uint32 + * @format int32 * @example 0 */ mode: number; diff --git a/packages/mobile/src/core/Jetton/Jetton.tsx b/packages/mobile/src/core/Jetton/Jetton.tsx index 24c9634a5..975561c65 100644 --- a/packages/mobile/src/core/Jetton/Jetton.tsx +++ b/packages/mobile/src/core/Jetton/Jetton.tsx @@ -29,6 +29,7 @@ import { useWallet, useWalletCurrency } from '@tonkeeper/shared/hooks'; import { tk } from '$wallet'; import { Chart } from '$shared/components/Chart/new/Chart'; import { ChartPeriod } from '$store/zustand/chart'; +import { formatDate } from '@tonkeeper/shared/utils/date'; const unverifiedTokenHitSlop = { top: 4, left: 4, bottom: 4, right: 4 }; @@ -37,6 +38,10 @@ export const Jetton: React.FC = ({ route }) => { const jetton = useJetton(route.params.jettonAddress); const jettonActivityList = useJettonActivityList(jetton.jettonAddress); const jettonPrice = useTokenPrice(jetton.jettonAddress, jetton.balance); + const lockedJettonPrice = useTokenPrice( + jetton.jettonAddress, + jetton.lock?.amount.toString() ?? '0', + ); const wallet = useWallet(); const isWatchOnly = wallet && wallet.isWatchOnly; @@ -95,10 +100,33 @@ export const Jetton: React.FC = ({ route }) => { > {jettonPrice.formatted.totalFiat} + {jetton.lock ? ( + <> + + + {formatter.format(jetton.lock.amount, { + decimals: jetton.metadata.decimals, + currency: jetton.metadata.symbol, + currencySeparator: 'wide', + })} +  · + {' '} + {lockedJettonPrice.formatted.totalFiat} + + + {t('jetton_locked_till', { + date: formatDate(jetton.lock.till * 1000, 'd MMM yyyy'), + })} + + + ) : null} {jettonPrice.formatted.fiat ? ( - - {t('jetton_price')} {jettonPrice.formatted.fiat} - + <> + + + {t('jetton_price')} {jettonPrice.formatted.fiat} + + ) : null} {jetton.metadata.image ? ( diff --git a/packages/mobile/src/store/models.ts b/packages/mobile/src/store/models.ts index 5747245f3..4107ed66d 100644 --- a/packages/mobile/src/store/models.ts +++ b/packages/mobile/src/store/models.ts @@ -281,6 +281,7 @@ export interface JettonBalanceModel { currency: CryptoCurrency; metadata: JettonMetadata; balance: string; + lock?: { amount: string; till: number }; jettonAddress: string; walletAddress: string; verification: JettonVerification; diff --git a/packages/mobile/src/tabs/Wallet/components/WalletContentList.tsx b/packages/mobile/src/tabs/Wallet/components/WalletContentList.tsx index 41106cca4..a969cabfb 100644 --- a/packages/mobile/src/tabs/Wallet/components/WalletContentList.tsx +++ b/packages/mobile/src/tabs/Wallet/components/WalletContentList.tsx @@ -1,4 +1,4 @@ -import React, { memo, useMemo } from 'react'; +import React, { memo, ReactNode, useMemo } from 'react'; import { t } from '@tonkeeper/shared/i18n'; import { Screen, @@ -55,7 +55,7 @@ type TokenItem = { onPress?: () => void; title: string; subtitle?: string; - value?: string; + value?: string | ReactNode; subvalue?: string; rate?: Rate; picture?: string; @@ -142,11 +142,15 @@ const RenderItem = ({ item }: { item: Content }) => { } picture={item.picture} value={ - {` ${item.value}`} + typeof item.value === 'string' ? ( + {` ${item.value}`} + ) : ( + item.value + ) } subvalue={ item.subvalue && ( @@ -303,7 +307,24 @@ export const WalletContentList = memo( onPress: () => openJetton(item.address.rawAddress), picture: item.iconUrl, title: item.symbol, - value: item.quantity.formatted, + value: item.lock ? ( + + {item.quantity.formatted} + + {' '} + ·{' '} + + + {item.lock.formatted} + + + ) : ( + item.quantity.formatted + ), subvalue: item.rate.total, rate: item.rate.price ? { diff --git a/packages/mobile/src/tabs/Wallet/hooks/useTokens.ts b/packages/mobile/src/tabs/Wallet/hooks/useTokens.ts index f09e2e94d..9e8a8d87f 100644 --- a/packages/mobile/src/tabs/Wallet/hooks/useTokens.ts +++ b/packages/mobile/src/tabs/Wallet/hooks/useTokens.ts @@ -24,6 +24,9 @@ type TokenInfo = { value: string; formatted: string; }; + lock?: { + formatted: string; + }; price: TokenPrice; rate: { price: string | null; @@ -65,6 +68,9 @@ export const useTonkens = (): { value: item.balance, formatted: formatter.format(item.balance), }, + lock: item.lock && { + formatted: formatter.format(item.lock.amount), + }, price: rate, rate: { price: rate.formatted.fiat, diff --git a/packages/mobile/src/wallet/managers/JettonsManager.ts b/packages/mobile/src/wallet/managers/JettonsManager.ts index ffe8c6a20..f2b7cddee 100644 --- a/packages/mobile/src/wallet/managers/JettonsManager.ts +++ b/packages/mobile/src/wallet/managers/JettonsManager.ts @@ -7,8 +7,6 @@ import { JettonBalanceModel } from '../models/JettonBalanceModel'; import { Address } from '@tonkeeper/core/src/formatters/Address'; import { TokenApprovalManager } from './TokenApprovalManager'; import { TonPriceManager } from './TonPriceManager'; -import BigNumber from 'bignumber.js'; -import { AmountFormatter } from '@tonkeeper/core'; import { sortByPrice } from '@tonkeeper/core/src/utils/jettons'; export type JettonsState = { diff --git a/packages/mobile/src/wallet/models/JettonBalanceModel/JettonBalanceModel.ts b/packages/mobile/src/wallet/models/JettonBalanceModel/JettonBalanceModel.ts index 3574dde41..e2e9483d5 100644 --- a/packages/mobile/src/wallet/models/JettonBalanceModel/JettonBalanceModel.ts +++ b/packages/mobile/src/wallet/models/JettonBalanceModel/JettonBalanceModel.ts @@ -8,6 +8,7 @@ export class JettonBalanceModel { jettonAddress: string; walletAddress: string; verification: JettonVerification; + lock?: { amount: string; till: number }; constructor(jettonBalance: JettonBalance) { this.metadata = jettonBalance.jetton; @@ -15,6 +16,14 @@ export class JettonBalanceModel { jettonBalance.balance, jettonBalance.jetton.decimals, ); + // @ts-ignore will be implemented in API later + this.lock = jettonBalance.lock && { + amount: AmountFormatter.fromNanoStatic( + jettonBalance.lock.amount, + jettonBalance.jetton.decimals, + ), + till: jettonBalance.lock.till, + }; this.jettonAddress = new Address(jettonBalance.jetton.address).toFriendly(); this.walletAddress = new Address(jettonBalance.wallet_address.address).toFriendly(); this.verification = jettonBalance.jetton diff --git a/packages/shared/i18n/locales/tonkeeper/en.json b/packages/shared/i18n/locales/tonkeeper/en.json index 2bd7fd459..b439a3c77 100644 --- a/packages/shared/i18n/locales/tonkeeper/en.json +++ b/packages/shared/i18n/locales/tonkeeper/en.json @@ -402,6 +402,7 @@ "jetton_name": "%{name} Token", "jetton_open_explorer": "View details", "jetton_price": "Price:", + "jetton_locked_till": "Locked until %{date}", "jettons_list_title": "Tokens", "jettons_manage_tokens": "Manage tokens", "jettons_show_jettons": "Show tokens in wallet", diff --git a/packages/shared/i18n/locales/tonkeeper/ru-RU.json b/packages/shared/i18n/locales/tonkeeper/ru-RU.json index ce2d191b7..893848576 100644 --- a/packages/shared/i18n/locales/tonkeeper/ru-RU.json +++ b/packages/shared/i18n/locales/tonkeeper/ru-RU.json @@ -348,6 +348,7 @@ "jetton_name" : "%{name} Токен", "jetton_open_explorer" : "Подробнее", "jetton_price" : "Цена:", + "jetton_locked_till": "Заблокированы до %{date}", "jettons_list_title" : "Токены", "jettons_manage_tokens" : "Настроить токены", "jetton_token" : "Токен", From 14bdc8f6f57d37d93260ce33ffca1d470ad7724e Mon Sep 17 00:00:00 2001 From: Andrey Sorokin Date: Mon, 8 Apr 2024 15:25:30 +0500 Subject: [PATCH 05/22] Hotfix 4.1.2 (#787) --- packages/mobile/index.js | 15 +++--- .../RestakeBanner/RestakeBanner.tsx | 49 ++++++++++++------- .../DeleteAccountDone/DeleteAccountDone.tsx | 2 +- .../src/core/Notifications/Notification.tsx | 6 ++- .../mobile/src/core/Settings/Settings.tsx | 30 +++++++----- packages/mobile/src/core/Staking/Staking.tsx | 32 +++++++----- .../src/core/StakingSend/StakingSend.tsx | 38 ++++++++++---- .../src/store/zustand/notifications/types.ts | 9 ++++ .../notifications/useNotificationsStore.ts | 34 +++++++++++++ packages/mobile/src/wallet/AppVault.ts | 5 ++ packages/mobile/src/wallet/Tonkeeper.ts | 16 +++--- .../src/wallet/managers/StakingManager.ts | 23 --------- .../shared/i18n/locales/tonkeeper/en.json | 6 +-- .../shared/i18n/locales/tonkeeper/ru-RU.json | 6 +-- .../shared/i18n/locales/tonkeeper/tr-TR.json | 4 +- .../i18n/locales/tonkeeper/zh-Hans-CN.json | 4 +- 16 files changed, 178 insertions(+), 101 deletions(-) diff --git a/packages/mobile/index.js b/packages/mobile/index.js index 6b6d6dad9..d5b3354c7 100644 --- a/packages/mobile/index.js +++ b/packages/mobile/index.js @@ -22,7 +22,6 @@ import crashlytics from '@react-native-firebase/crashlytics'; import messaging from '@react-native-firebase/messaging'; import { withIAPContext } from 'react-native-iap'; import { startApp } from './src/index'; -import { tk } from './src/wallet'; LogBox.ignoreLogs([ 'Non-serializable values were found in the navigation state', @@ -47,15 +46,17 @@ async function handleDappMessage(remoteMessage) { ) { return null; } - + await useNotificationsStore.persist.rehydrate(); if (remoteMessage.data?.type === 'better_stake_option_found') { - tk.wallet.staking.toggleRestakeBanner( - true, - remoteMessage.data.stakingAddressToMigrateFrom, - ); + useNotificationsStore + .getState() + .actions.toggleRestakeBanner( + remoteMessage.data.account, + true, + remoteMessage.data.stakingAddressToMigrateFrom, + ); } - await useNotificationsStore.persist.rehydrate(); useNotificationsStore.getState().actions.addNotification( { ...remoteMessage.data, diff --git a/packages/mobile/src/components/RestakeBanner/RestakeBanner.tsx b/packages/mobile/src/components/RestakeBanner/RestakeBanner.tsx index 3ab76b3d2..302f802d7 100644 --- a/packages/mobile/src/components/RestakeBanner/RestakeBanner.tsx +++ b/packages/mobile/src/components/RestakeBanner/RestakeBanner.tsx @@ -26,6 +26,7 @@ import { IconsComposition } from './IconsComposition'; import { useFiatValue } from '$hooks/useFiatValue'; import { CryptoCurrencies } from '$shared/constants'; import { stakingFormatter } from '$utils/formatter'; +import { useNotificationsStore } from '$store'; export interface ExtendedPoolInfo extends PoolInfo { isWithdrawal: boolean; @@ -35,6 +36,7 @@ export interface ExtendedPoolInfo extends PoolInfo { export interface RestakeBannerProps { poolsList: ExtendedPoolInfo[]; migrateFrom: string; + bypassUnstakeStep?: boolean; } export enum RestakeSteps { @@ -52,11 +54,14 @@ export const RestakeBanner = memo((props) => { ) as ExtendedPoolInfo; }, [props.poolsList]); const { handleTopUpPress } = usePoolInfo(tonstakersPool); + const toggleRestakeBanner = useNotificationsStore( + (state) => state.actions.toggleRestakeBanner, + ); const handleCloseRestakeBanner = useCallback(() => { LayoutAnimation.easeInEaseOut(); - tk.wallet.staking.toggleRestakeBanner(false); - }, []); + toggleRestakeBanner(tk.wallet.address.ton.raw, false); + }, [toggleRestakeBanner]); const poolToWithdrawal = useMemo( () => @@ -68,8 +73,6 @@ export const RestakeBanner = memo((props) => { [poolToWithdrawal], ); - const bypassStakeStep = useStakingState((s) => s.bypassStakeStep, []); - const readyWithdraw = useFiatValue( CryptoCurrencies.Ton, stakingFormatter.fromNano(toWithdrawalStakingInfo?.ready_withdraw ?? '0'), @@ -79,13 +82,17 @@ export const RestakeBanner = memo((props) => { const handleWithdrawal = useCallback( (pool: ExtendedPoolInfo, withdrawAll?: boolean) => () => { + if (pool.implementation === PoolImplementationType.Tf) { + nav.push(AppStackRouteNames.StakingSend, { + poolAddress: pool.address, + transactionType: StakingTransactionType.WITHDRAWAL_CONFIRM, + }); + return; + } nav.push(AppStackRouteNames.StakingSend, { amount: withdrawAll && pool.balance, poolAddress: pool.address, - transactionType: - pool.implementation === PoolImplementationType.Tf - ? StakingTransactionType.WITHDRAWAL_CONFIRM - : StakingTransactionType.WITHDRAWAL, + transactionType: StakingTransactionType.WITHDRAWAL, }); }, [nav], @@ -107,7 +114,11 @@ export const RestakeBanner = memo((props) => { return RestakeSteps.DONE; } // Go to last step if pool to withdrawal is empty now (or if balance so small, or step is bypassed) - if (bypassStakeStep || Number(poolToWithdrawal?.balance) < 0.1) { + if ( + props.bypassUnstakeStep || + !poolToWithdrawal?.balance || + Number(poolToWithdrawal?.balance) < 0.1 + ) { return RestakeSteps.STAKE_INTO_TONSTAKERS; } // If user has pending withdrawal, render step with waiting @@ -116,7 +127,7 @@ export const RestakeBanner = memo((props) => { } return RestakeSteps.UNSTAKE; }, [ - bypassStakeStep, + props.bypassUnstakeStep, isWaitingForWithdrawal, poolToWithdrawal?.balance, readyWithdraw.amount, @@ -130,8 +141,8 @@ export const RestakeBanner = memo((props) => { }, [currentStepId, handleCloseRestakeBanner]); const { formattedDuration, isCooldown } = useStakingCycle( - poolToWithdrawal?.cycle_start, - poolToWithdrawal?.cycle_end, + poolToWithdrawal?.cycle_start ?? Date.now(), + poolToWithdrawal?.cycle_end ?? Date.now(), isWaitingForWithdrawal, ); @@ -195,12 +206,14 @@ export const RestakeBanner = memo((props) => { }), })} />, - + ); } return ( - + redirectToActivity={false} + ref={footerRef} + /> ); } @@ -287,37 +261,33 @@ export const CreateSubscription: FC = ({ ); } - // ToDo: Сделать верстку, когда будет дизайн - if (failed) { - return Failed; - } - return ( <> - - - - {info.merchantName} - - - - {info.productName} - - - - {info.status === 'created' && ( + + + {info.merchantName} + + + + {info.productName} + + {info.status === 'created' && ( + <> + - )} - + + )} + = ({ /> )} - - {isButtonShown && {renderButton()}} - + {isButtonShown && renderButton()} ); } return ( - + {renderContent()} diff --git a/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx b/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx index 80b68734a..d32fa2722 100644 --- a/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx +++ b/packages/mobile/src/core/ModalContainer/InsufficientFunds/InsufficientFunds.tsx @@ -1,6 +1,6 @@ import React, { memo, useCallback, useMemo } from 'react'; import { t } from '@tonkeeper/shared/i18n'; -import { Modal, Spacer } from '@tonkeeper/uikit'; +import { Modal, Spacer, WalletIcon } from '@tonkeeper/uikit'; import { openExploreTab, openRefillBatteryModal } from '$navigation'; import { SheetActions, useNavigation } from '@tonkeeper/router'; import { Button, Icon, Text } from '$uikit'; @@ -35,6 +35,7 @@ export interface InsufficientFundsParams { stakingFee?: string; fee?: string; isStakingDeposit?: boolean; + walletIdentifier?: string; } export const InsufficientFundsModal = memo((props) => { @@ -46,6 +47,7 @@ export const InsufficientFundsModal = memo((props) => { stakingFee, fee, isStakingDeposit, + walletIdentifier, } = props; const { balance: batteryBalance } = useBatteryBalance(); const nav = useNavigation(); @@ -120,6 +122,8 @@ export const InsufficientFundsModal = memo((props) => { ); }, [currency, fee, formattedAmount, formattedBalance, isStakingDeposit, stakingFee]); + const wallet = walletIdentifier ? tk.wallets.get(walletIdentifier)! : tk.wallet; + return ( @@ -128,7 +132,15 @@ export const InsufficientFundsModal = memo((props) => { - {t('txActions.signRaw.insufficientFunds.title')} + {tk.wallets.size > 1 ? ( + <> + {t('txActions.signRaw.insufficientFunds.title_multiwallet')}{' '} + {' '} + {wallet.config.name} + + ) : ( + t('txActions.signRaw.insufficientFunds.title') + )} {content} diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx index 071b32479..1a7db0601 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/Modals/SignRawModal.tsx @@ -6,7 +6,16 @@ import { calculateMessageTransferAmount, delay } from '$utils'; import { debugLog } from '$utils/debugLog'; import { t } from '@tonkeeper/shared/i18n'; import { Toast } from '$store'; -import { List, Modal, Spacer, Steezy, Text, View } from '@tonkeeper/uikit'; +import { + List, + Modal, + Spacer, + Steezy, + Text, + View, + WalletIcon, + isAndroid, +} from '@tonkeeper/uikit'; import { push } from '$navigation/imperative'; import { SheetActions, useNavigation } from '@tonkeeper/router'; import { @@ -219,9 +228,11 @@ export const SignRawModal = memo((props) => { {t('confirmSendModal.wallet')} - - {wallet.config.emoji} - + {wallet.config.name} @@ -342,6 +353,7 @@ export const openSignRawModal = async ( return openInsufficientFundsModal({ totalAmount, balance: checkResult.balance, + walletIdentifier, }); } } @@ -392,6 +404,7 @@ const styles = Steezy.create({ subtitleContainer: { flexDirection: 'row', gap: 4, + alignItems: 'center', }, withBatteryContainer: { paddingHorizontal: 32, @@ -399,4 +412,8 @@ const styles = Steezy.create({ actionsList: { marginBottom: 0, }, + emoji: { + fontSize: isAndroid ? 17 : 20, + marginTop: isAndroid ? -1 : 1, + }, }); diff --git a/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperationFooter.tsx b/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperationFooter.tsx index 936bab614..2f623c90f 100644 --- a/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperationFooter.tsx +++ b/packages/mobile/src/core/ModalContainer/NFTOperations/NFTOperationFooter.tsx @@ -131,6 +131,7 @@ interface ActionFooterProps { responseOptions?: TxResponseOptions; withCloseButton?: boolean; confirmTitle?: string; + secondary?: boolean; onPressConfirm: () => Promise; onCloseModal?: () => void; disabled?: boolean; @@ -205,6 +206,7 @@ export const ActionFooter = React.forwardRef props.onPressConfirm()} + mode={props.secondary ? 'secondary' : 'primary'} > {props.confirmTitle ?? t('nft_confirm_operation')} diff --git a/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.style.ts b/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.style.ts index 485ca184c..39e786921 100644 --- a/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.style.ts +++ b/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.style.ts @@ -98,3 +98,9 @@ export const Icon = styled(FastImage).attrs({ export const ItemSkeleton = styled.View` align-self: flex-end; `; + +export const WalletNameRow = styled.View` + flex-direction: row; + align-items: center; + justify-content: flex-end; +`; diff --git a/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.tsx b/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.tsx index ba0a1e898..b8eff459b 100644 --- a/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.tsx +++ b/packages/mobile/src/core/NFTSend/steps/ConfirmStep/ConfirmStep.tsx @@ -21,6 +21,7 @@ import { truncateDecimal } from '$utils'; import { BatteryState } from '@tonkeeper/shared/utils/battery'; import { useBatteryState } from '@tonkeeper/shared/query/hooks/useBatteryState'; import { tk } from '$wallet'; +import { Steezy, WalletIcon, isAndroid } from '@tonkeeper/uikit'; interface Props extends StepComponentProps { recipient: SendRecipient | null; @@ -106,9 +107,15 @@ const ConfirmStepComponent: FC = (props) => { {t('send_screen_steps.comfirm.wallet')} - - {tk.wallet.config.emoji} {tk.wallet.config.name} - + + + + {tk.wallet.config.name} + @@ -219,3 +226,10 @@ const ConfirmStepComponent: FC = (props) => { }; export const ConfirmStep = memo(ConfirmStepComponent); + +const styles = Steezy.create({ + emoji: { + fontSize: isAndroid ? 17 : 20, + marginTop: isAndroid ? -1 : 1, + }, +}); diff --git a/packages/mobile/src/core/Send/steps/AddressStep/AddressStep.tsx b/packages/mobile/src/core/Send/steps/AddressStep/AddressStep.tsx index 770caa4e4..cad9853d0 100644 --- a/packages/mobile/src/core/Send/steps/AddressStep/AddressStep.tsx +++ b/packages/mobile/src/core/Send/steps/AddressStep/AddressStep.tsx @@ -100,7 +100,7 @@ const AddressStepComponent: FC = (props) => { return new TonWeb.Address(resolvedDomain.wallet.address).toString( true, true, - true, + false, ) as string; } diff --git a/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.style.ts b/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.style.ts index 6477a0a47..96de5f44f 100644 --- a/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.style.ts +++ b/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.style.ts @@ -104,3 +104,9 @@ export const WarningRow = styled.View` export const WarningIcon = styled.View` margin-top: ${ns(2)}px; `; + +export const WalletNameRow = styled.View` + flex-direction: row; + align-items: center; + justify-content: flex-end; +`; diff --git a/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx b/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx index c9478a2ae..0c3d9b829 100644 --- a/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx +++ b/packages/mobile/src/core/Send/steps/ConfirmStep/ConfirmStep.tsx @@ -25,6 +25,7 @@ import { BatteryState } from '@tonkeeper/shared/utils/battery'; import { TokenType } from '$core/Send/Send.interface'; import { useBalancesState, useWallet } from '@tonkeeper/shared/hooks'; import { tk } from '$wallet'; +import { Steezy, WalletIcon, isAndroid } from '@tonkeeper/uikit'; const ConfirmStepComponent: FC = (props) => { const { @@ -226,9 +227,15 @@ const ConfirmStepComponent: FC = (props) => { {t('send_screen_steps.comfirm.wallet')} - - {tk.wallet.config.emoji} {tk.wallet.config.name} - + + + + {tk.wallet.config.name} + @@ -370,3 +377,10 @@ const ConfirmStepComponent: FC = (props) => { }; export const ConfirmStep = memo(ConfirmStepComponent); + +const styles = Steezy.create({ + emoji: { + fontSize: isAndroid ? 17 : 20, + marginTop: isAndroid ? -1 : 1, + }, +}); diff --git a/packages/mobile/src/core/Settings/Settings.tsx b/packages/mobile/src/core/Settings/Settings.tsx index da00ea3d4..b7bb8b85b 100644 --- a/packages/mobile/src/core/Settings/Settings.tsx +++ b/packages/mobile/src/core/Settings/Settings.tsx @@ -323,7 +323,7 @@ export const Settings: FC = () => { onPress={handleManageTokens} /> )} - {hasSubscriptions && ( + {!wallet.isWatchOnly && hasSubscriptions && ( = (props) => { {t('send_screen_steps.comfirm.wallet')} - - {tk.wallet.config.emoji} {tk.wallet.config.name} - + + + + {tk.wallet.config.name} + @@ -212,3 +219,10 @@ const ConfirmStepComponent: FC = (props) => { }; export const ConfirmStep = memo(ConfirmStepComponent); + +const styles = Steezy.create({ + emoji: { + fontSize: isAndroid ? 17 : 20, + marginTop: isAndroid ? -1 : 1, + }, +}); diff --git a/packages/mobile/src/navigation/MainStack/MainStack.tsx b/packages/mobile/src/navigation/MainStack/MainStack.tsx index c2a9f5106..dba6efb0a 100644 --- a/packages/mobile/src/navigation/MainStack/MainStack.tsx +++ b/packages/mobile/src/navigation/MainStack/MainStack.tsx @@ -157,6 +157,7 @@ export const MainStack: FC = () => { { {t('later')} } + hideBackButton /> diff --git a/packages/mobile/src/screens/ChooseWallets/ChooseWallets.tsx b/packages/mobile/src/screens/ChooseWallets/ChooseWallets.tsx index 59f73b1c1..71f0242b8 100644 --- a/packages/mobile/src/screens/ChooseWallets/ChooseWallets.tsx +++ b/packages/mobile/src/screens/ChooseWallets/ChooseWallets.tsx @@ -11,6 +11,7 @@ import { RouteProp } from '@react-navigation/native'; import { Address } from '@tonkeeper/shared/Address'; import { formatter } from '@tonkeeper/shared/formatter'; import { useImportWallet } from '$hooks/useImportWallet'; +import { DEFAULT_WALLET_VERSION } from '$wallet/constants'; export const ChooseWallets: FC<{ route: RouteProp; @@ -22,7 +23,10 @@ export const ChooseWallets: FC<{ const [selectedVersions, setSelectedVersions] = useState( walletsInfo - .filter((item) => item.balance > 0 || item.tokens) + .filter( + (item) => + item.balance > 0 || item.tokens || item.version === DEFAULT_WALLET_VERSION, + ) .map((item) => item.version), ); const [loading, setLoading] = useState(false); diff --git a/packages/mobile/src/screens/StartScreen/StartScreen.tsx b/packages/mobile/src/screens/StartScreen/StartScreen.tsx index 6431b3ca8..58cc6eecb 100644 --- a/packages/mobile/src/screens/StartScreen/StartScreen.tsx +++ b/packages/mobile/src/screens/StartScreen/StartScreen.tsx @@ -16,6 +16,9 @@ import { MainStackRouteNames } from '$navigation'; import { useDispatch } from 'react-redux'; import { walletActions } from '$store/wallet'; import { useNavigation } from '@tonkeeper/router'; +import { network } from '@tonkeeper/core'; +import { config } from '$config'; +import DeviceInfo from 'react-native-device-info'; const HEIGHT_RATIO = deviceHeight / 844; @@ -31,10 +34,30 @@ export const StartScreen = memo(() => { const logoShapesPosX = origShapesWidth / 2 - dimensions.width / 2; const logoShapesPosY = origShapesHeight / 2 - (origShapesHeight * ratioHeight) / 2; + const unsubscribeNotifications = useCallback(async () => { + // unsubscribe from all notifications, if app was reinstalled + try { + const deviceId = DeviceInfo.getUniqueId(); + const endpoint = `${config.get('tonapiIOEndpoint')}/unsubscribe`; + + await network.post(endpoint, { + params: { + device: deviceId, + }, + }); + } catch {} + }, []); + const handleCreatePress = useCallback(() => { dispatch(walletActions.generateVault()); + unsubscribeNotifications(); nav.navigate(MainStackRouteNames.CreateWalletStack); - }, [dispatch, nav]); + }, [dispatch, nav, unsubscribeNotifications]); + + const handleImportPress = useCallback(() => { + unsubscribeNotifications(); + nav.navigate(MainStackRouteNames.ImportWalletStack); + }, [nav, unsubscribeNotifications]); return ( @@ -79,7 +102,7 @@ export const StartScreen = memo(() => { - )} - - - ); -}; diff --git a/packages/mobile/src/core/NFTs/NFTItem/NFTItem.interface.ts b/packages/mobile/src/core/NFTs/NFTItem/NFTItem.interface.ts deleted file mode 100644 index 8dfa15c74..000000000 --- a/packages/mobile/src/core/NFTs/NFTItem/NFTItem.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { NFTModel } from '$store/models'; - -export interface NFTItemProps { - isLastInRow: boolean; - item: NFTModel; -}; diff --git a/packages/mobile/src/core/NFTs/NFTItem/NFTItem.style.ts b/packages/mobile/src/core/NFTs/NFTItem/NFTItem.style.ts deleted file mode 100644 index 526c2731c..000000000 --- a/packages/mobile/src/core/NFTs/NFTItem/NFTItem.style.ts +++ /dev/null @@ -1,150 +0,0 @@ -import styled, { RADIUS } from '$styled'; -import { ns } from '$utils'; -import FastImage from 'react-native-fast-image'; -import { Highlight } from '$uikit'; -import { Dimensions, StyleSheet } from 'react-native'; - -const deviceWidth = Dimensions.get('window').width; -export const NUM_OF_COLUMNS = Math.trunc(Math.max(2, Math.min(deviceWidth / 171, 3))); -const availableWidth = NUM_OF_COLUMNS === 2 ? (deviceWidth - ns(48)) / 2 : 171; // Padding and margin between NFTs - -export const Wrap = styled.View<{ withMargin: boolean }>` - width: ${availableWidth}px; - margin-right: ${({ withMargin }) => (withMargin ? ns(16) : 0)}px; - margin-bottom: ${ns(16)}px; -`; - -export const Background = styled.View` - background: ${({ theme }) => theme.colors.backgroundSecondary}; - border-radius: ${ns(RADIUS.normal)}px; - position: absolute; - z-index: 0; - top: 0; - left: 0; - right: 0; - bottom: 0; -`; - -export const TextWrap = styled.View` - z-index: 2; - padding: ${ns(10)}px ${ns(16)}px ${ns(12)}px ${ns(16)}px; -`; - -export const Pressable = styled(Highlight)` - border-radius: ${ns(RADIUS.normal)}px; -`; - -export const Image = styled(FastImage).attrs({ - resizeMode: 'cover', -})` - position: relative; - z-index: 2; - width: 100%; - height: ${ns(171)}px; - border-top-left-radius: ${ns(RADIUS.normal)}px; - border-top-right-radius: ${ns(RADIUS.normal)}px; - background: ${({ theme }) => theme.colors.backgroundTertiary}; -`; - -export const DNSBackground = styled.View` - position: relative; - z-index: 2; - width: 100%; - height: ${ns(171)}px; - border-top-left-radius: ${ns(RADIUS.normal)}px; - border-top-right-radius: ${ns(RADIUS.normal)}px; - background: ${({ theme }) => theme.colors.accentPrimary}; - padding-vertical: ${ns(16)}px; - padding-horizontal: ${ns(20)}px; -`; - -export const SmallImage = styled(FastImage).attrs({ - resizeMode: 'cover', -})` - z-index: 2; - flex: 1; - width: 100%; - height: 100%; - /* height: ${ns(114)}px; */ - border-top-left-radius: ${ns(RADIUS.normal)}px; - border-top-right-radius: ${ns(RADIUS.normal)}px; - background: ${({ theme }) => theme.colors.backgroundTertiary}; -`; - -export const SmallDNSBackground = styled.View` - position: relative; - z-index: 2; - width: 100%; - height: ${ns(114)}px; - border-top-left-radius: ${ns(RADIUS.normal)}px; - border-top-right-radius: ${ns(RADIUS.normal)}px; - background: ${({ theme }) => theme.colors.accentPrimary}; - padding-vertical: ${ns(16)}px; - padding-horizontal: ${ns(20)}px; -`; - -export const Badges = styled.View` - position: absolute; - bottom: ${8}px; - right: ${8}px; - flex-direction: row; - align-items: center; -`; - -export const FireBadge = styled.View` - position: absolute; - bottom: ${0}px; - right: ${0}px; - flex-direction: row; - align-items: center; -`; - -export const OnSaleBadge = styled.View` - position: absolute; - top: ${0}px; - right: ${0}px; - flex-direction: row; - align-items: center; -`; - -export const OnSaleBadgeIcon = styled.Image.attrs({ - source: require('$assets/sale_badge_32.png'), -})` - width: ${ns(32)}px; - height: ${ns(32)}px; -`; - -export const AppearanceBadge = styled.View` - width: ${ns(32)}px; - height: ${ns(32)}px; - border-radius: ${ns(32 / 2)}px; - background: ${({ theme }) => theme.colors.backgroundSecondary}; - align-items: center; - justify-content: center; -`; - -export const DNSBadge = styled.View` - width: ${ns(32)}px; - height: ${ns(32)}px; - border-radius: ${ns(32 / 2)}px; - background: rgba(255, 255, 255, 0.2); - align-items: center; - justify-content: center; -`; - -export const textStyles = StyleSheet.create({ - domainText: { - fontWeight: '700', - fontSize: 20, - lineHeight: 26, - }, - domainZoneText: { - opacity: 0.72, - }, -}); - -export const CollectionNameWrap = styled.View<{ withIcon?: boolean }>` - flex-direction: row; - align-items: center; - padding-right: ${({ withIcon }) => (!withIcon ? 0 : ns(12))}px; -`; diff --git a/packages/mobile/src/core/NFTs/NFTItem/NFTItem.tsx b/packages/mobile/src/core/NFTs/NFTItem/NFTItem.tsx deleted file mode 100644 index a2f4322d2..000000000 --- a/packages/mobile/src/core/NFTs/NFTItem/NFTItem.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React, { useCallback, useMemo } from 'react'; -import { NFTItemProps } from '$core/NFTs/NFTItem/NFTItem.interface'; -import * as S from './NFTItem.style'; -import { openNFT } from '$navigation'; -import { checkIsTonDiamondsNFT } from '$utils'; -import _ from 'lodash'; -import { Icon, Text } from '$uikit'; -import { t } from '@tonkeeper/shared/i18n'; -import { useFlags } from '$utils/flags'; -import { Address, DNS, KnownTLDs } from '@tonkeeper/core'; - -export const NFTItem: React.FC = ({ item, isLastInRow }) => { - const flags = useFlags(['disable_apperance']); - - const isTonDiamondsNft = checkIsTonDiamondsNFT(item); - const isOnSale = useMemo(() => !!item.sale, [item.sale]); - - const isTG = DNS.getTLD(item.dns || item.name) === KnownTLDs.TELEGRAM; - const isDNS = !!item.dns && !isTG; - - // eslint-disable-next-line react-hooks/exhaustive-deps - const handleOpenNftItem = useCallback( - _.throttle(() => openNFT({ currency: item.currency, address: item.address }), 1000), - [item], - ); - - const title = useMemo(() => { - if (isDNS) { - return item.dns; - } - - return item.name || Address.toShort(item.address); - }, [isDNS, item.dns, item.name, item.address]); - - const renderPicture = () => { - if (isDNS) { - return ( - - - {item.dns?.replace('.ton', '')} - - - {'.ton'} - - {isOnSale ? : null} - - - - - - - ); - } - - return ( - - {isOnSale ? : null} - - {isTonDiamondsNft && !flags.disable_apperance ? ( - - - - ) : null} - - - ); - }; - - return ( - - - - {renderPicture()} - - - {title} - - - - {isDNS - ? 'TON DNS' - : item?.collection - ? item.collection.name - : t('nft_single_nft')} - - {item.isApproved && ( - - )} - - - - - ); -}; diff --git a/packages/mobile/src/core/Notifications/Notification.tsx b/packages/mobile/src/core/Notifications/Notification.tsx index 15afa856b..033f94db8 100644 --- a/packages/mobile/src/core/Notifications/Notification.tsx +++ b/packages/mobile/src/core/Notifications/Notification.tsx @@ -192,7 +192,7 @@ export const Notification: React.FC = (props) => { }, [props.closeOtherSwipeable]); return ( - + ({ listStyle: { marginBottom: 0, }, - containerStyle: { - marginBottom: 8, - }, leftContentStyle: { alignItems: 'flex-start', alignSelf: 'flex-start', diff --git a/packages/mobile/src/core/Notifications/Notifications.tsx b/packages/mobile/src/core/Notifications/Notifications.tsx index 54676f1e7..a03c561e9 100644 --- a/packages/mobile/src/core/Notifications/Notifications.tsx +++ b/packages/mobile/src/core/Notifications/Notifications.tsx @@ -9,19 +9,19 @@ import { View, } from '$uikit'; import { ns } from '$utils'; -import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { CellSection } from '$shared/components'; import { t } from '@tonkeeper/shared/i18n'; import { useConnectedAppsList } from '$store'; import { Steezy } from '$styles'; import { SwitchDAppNotifications } from '$core/Notifications/SwitchDAppNotifications'; import { useNotificationsSwitch } from '$hooks/useNotificationsSwitch'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; export const Notifications: React.FC = () => { - const tabBarHeight = useBottomTabBarHeight(); const connectedApps = useConnectedAppsList(); const { isSubscribed, isDenied, openSettings, toggleNotifications } = useNotificationsSwitch(); + const insets = useSafeAreaInsets(); return ( @@ -29,7 +29,7 @@ export const Notifications: React.FC = () => { {isDenied && ( diff --git a/packages/mobile/src/core/Notifications/NotificationsActivity.tsx b/packages/mobile/src/core/Notifications/NotificationsActivity.tsx index b557a98da..0d79fb960 100644 --- a/packages/mobile/src/core/Notifications/NotificationsActivity.tsx +++ b/packages/mobile/src/core/Notifications/NotificationsActivity.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react'; -import { Button, Icon, Screen, Spacer, Text, View } from '$uikit'; +import { Button, Icon, Spacer, Text, View } from '$uikit'; import { Notification } from '$core/Notifications/Notification'; import { Steezy } from '$styles'; import { openNotifications } from '$navigation'; @@ -8,6 +8,7 @@ import { INotification, useDAppsNotifications } from '$store'; import { FlashList } from '@shopify/flash-list'; import { LayoutAnimation } from 'react-native'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; +import { Screen } from '@tonkeeper/uikit'; export enum ActivityListItem { Notification = 'Notification', @@ -51,7 +52,6 @@ export const NotificationsActivity: React.FC = () => { const list = useRef | null>(null); const closeOtherSwipeable = useRef void)>(null); const lastSwipeableId = useRef(null); - const tabBarHeight = useBottomTabBarHeight(); const handleOpenNotificationSettings = useCallback(() => { openNotifications(); @@ -137,10 +137,10 @@ export const NotificationsActivity: React.FC = () => { ) : ( item.id} renderItem={renderNotificationsItem} - contentContainerStyle={{ paddingBottom: tabBarHeight + 8 }} data={flashListData} ListEmptyComponent={ListEmpty} /> @@ -154,6 +154,9 @@ const styles = Steezy.create({ marginVertical: 14, marginHorizontal: 16, }, + gap8: { + gap: 8, + }, emptyContainer: { paddingHorizontal: 32, flex: 1, diff --git a/packages/mobile/src/core/Security/Security.tsx b/packages/mobile/src/core/Security/Security.tsx index 922d72fa9..8e704f244 100644 --- a/packages/mobile/src/core/Security/Security.tsx +++ b/packages/mobile/src/core/Security/Security.tsx @@ -1,6 +1,5 @@ import React, { FC, useCallback } from 'react'; import Animated from 'react-native-reanimated'; -import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import Clipboard from '@react-native-community/clipboard'; import * as S from './Security.style'; @@ -14,9 +13,10 @@ import { useBiometrySettings, useWallet } from '@tonkeeper/shared/hooks'; import { useNavigation } from '@tonkeeper/router'; import { vault } from '$wallet'; import { Haptics, Switch } from '@tonkeeper/uikit'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; export const Security: FC = () => { - const tabBarHeight = useBottomTabBarHeight(); + const insets = useSafeAreaInsets(); const wallet = useWallet(); const nav = useNavigation(); @@ -93,7 +93,7 @@ export const Security: FC = () => { showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingHorizontal: ns(16), - paddingBottom: tabBarHeight, + paddingBottom: insets.bottom + 16, }} scrollEventThrottle={16} > diff --git a/packages/mobile/src/core/Send/Send.tsx b/packages/mobile/src/core/Send/Send.tsx index dac0eb72b..a95c7797f 100644 --- a/packages/mobile/src/core/Send/Send.tsx +++ b/packages/mobile/src/core/Send/Send.tsx @@ -260,18 +260,20 @@ export const Send: FC = ({ route }) => { setPreparing(false); } }, [ - amount, - comment, - currency, - decimals, + recipient, dispatch, - isCommentEncrypted, + currencyAdditionalParams, + currency, + parsedAmount, + amount.all, + amount.value, tokenType, jettonWalletAddress, - parsedAmount, - recipient, - trcPayload, + decimals, + comment, + isCommentEncrypted, trcToken, + trcPayload, ]); const unlock = useUnlockVault(); diff --git a/packages/mobile/src/core/Settings/Settings.tsx b/packages/mobile/src/core/Settings/Settings.tsx index ac8dbfd59..b29cd28f1 100644 --- a/packages/mobile/src/core/Settings/Settings.tsx +++ b/packages/mobile/src/core/Settings/Settings.tsx @@ -3,13 +3,11 @@ import { useDispatch } from 'react-redux'; import Rate, { AndroidMarket } from 'react-native-rate'; import { Alert, Linking, Platform, View } from 'react-native'; import DeviceInfo from 'react-native-device-info'; -import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; -import Animated from 'react-native-reanimated'; import { TapGestureHandler } from 'react-native-gesture-handler'; import * as S from './Settings.style'; -import { Icon, PopupSelect, ScrollHandler, Spacer, Text } from '$uikit'; -import { Icon as NewIcon } from '@tonkeeper/uikit'; +import { Icon, PopupSelect, Spacer, Text } from '$uikit'; +import { Icon as NewIcon, Screen } from '@tonkeeper/uikit'; import { useShouldShowTokensButton } from '$hooks/useShouldShowTokensButton'; import { useNavigation } from '@tonkeeper/router'; import { List } from '@tonkeeper/uikit'; @@ -54,10 +52,12 @@ import { mapNewNftToOldNftData } from '$utils/mapNewNftToOldNftData'; import { WalletListItem } from '@tonkeeper/shared/components'; import { useSubscriptions } from '@tonkeeper/shared/hooks/useSubscriptions'; import { nativeLocaleNames } from '@tonkeeper/shared/i18n/translations'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; export const Settings: FC = () => { const animationRef = useRef(null); const devMenuHandlerRef = useRef(null); + const { bottom: paddingBottom } = useSafeAreaInsets(); const flags = useFlags([ 'disable_apperance', @@ -68,7 +68,6 @@ export const Settings: FC = () => { ]); const nav = useNavigation(); - const tabBarHeight = useBottomTabBarHeight(); const fiatCurrency = useWalletCurrency(); const dispatch = useDispatch(); @@ -235,13 +234,12 @@ export const Settings: FC = () => { return ( - - + + @@ -538,9 +536,8 @@ export const Settings: FC = () => { - - - + + ); }; diff --git a/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.style.ts b/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.style.ts index ce064bf96..9271de075 100644 --- a/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.style.ts +++ b/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.style.ts @@ -90,10 +90,10 @@ export const DetailsButtonContainer = styled.View` export const HeaderWrap = styled.View` align-items: center; - padding-horizontal: ${ns(16)}px; `; export const FlexRow = styled.View` + padding-horizontal: ${ns(12)}px; flex-direction: row; justify-content: space-between; margin-top: ${ns(16)}px; diff --git a/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx b/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx index e2717c095..c6db0f3d4 100644 --- a/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx +++ b/packages/mobile/src/core/StakingPoolDetails/StakingPoolDetails.tsx @@ -9,7 +9,6 @@ import { import { Button, Highlight, - IconButton, ScrollHandler, Spacer, StakedTonIcon, @@ -27,12 +26,11 @@ import { t } from '@tonkeeper/shared/i18n'; import { useFlag } from '$utils/flags'; import { formatter } from '@tonkeeper/shared/formatter'; import { IStakingLink, StakingLinkType } from './types'; -import { Icon, List, Steezy } from '@tonkeeper/uikit'; +import { ActionButtons, Icon, List, Steezy } from '@tonkeeper/uikit'; import { getLinkIcon, getLinkTitle, getSocialLinkType } from './utils'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Linking } from 'react-native'; import { PoolImplementationType } from '@tonkeeper/core/src/TonAPI'; -import BigNumber from 'bignumber.js'; import { ListItemRate } from '../../tabs/Wallet/components/ListItemRate'; import { useStakingState, useWallet, useWalletCurrency } from '@tonkeeper/shared/hooks'; import { StakingManager } from '$wallet/managers/StakingManager'; @@ -154,19 +152,6 @@ export const StakingPoolDetails: FC = (props) => { const isLiquidTF = pool.implementation === PoolImplementationType.LiquidTF; - const nextReward = useMemo(() => { - if (!stakingJetton || !pool.cycle_length) { - return; - } - - return formatter.format( - new BigNumber(balance.totalTon) - .multipliedBy(pool.apy / 100) - .dividedBy(31536000) - .multipliedBy(pool.cycle_length), - ); - }, [balance.totalTon, pool.apy, pool.cycle_length, stakingJetton]); - const stakingJettonView = useMemo( () => stakingJettonMetadata ? ( @@ -249,27 +234,24 @@ export const StakingPoolDetails: FC = (props) => { - - {!isWatchOnly ? ( - <> - - - - - - - - ) : null} + {hasPendingDeposit ? ( <> @@ -336,23 +318,11 @@ export const StakingPoolDetails: FC = (props) => { ) : null} - {/* {hasAnyBalance && stakingJetton && isLiquidTF ? ( - <> - - - - - - ) : null} */} - - {t('staking.details.about_pool')} - {stakingJettonMetadata && hasAnyBalance ? ( <> {stakingJettonView} - - + ) : null} diff --git a/packages/mobile/src/core/Wallet/ToncoinScreen.tsx b/packages/mobile/src/core/Wallet/ToncoinScreen.tsx index 1f7aec007..b52a3b131 100644 --- a/packages/mobile/src/core/Wallet/ToncoinScreen.tsx +++ b/packages/mobile/src/core/Wallet/ToncoinScreen.tsx @@ -6,17 +6,17 @@ import { Button, PopupMenu, PopupMenuItem } from '$uikit'; import { MainStackRouteNames, openDAppBrowser, openSend } from '$navigation'; import { openRequireWalletModal } from '$core/ModalContainer/RequireWallet/RequireWallet'; import { walletActions } from '$store/wallet'; -import { Linking, View } from 'react-native'; +import { View } from 'react-native'; import { delay, ns } from '$utils'; import { CryptoCurrencies, CryptoCurrency, Decimals } from '$shared/constants'; -import { i18n, t } from '@tonkeeper/shared/i18n'; +import { t } from '@tonkeeper/shared/i18n'; import { useNavigation } from '@tonkeeper/router'; import { Chart } from '$shared/components/Chart/new/Chart'; import { formatter } from '$utils/formatter'; import { Toast } from '$store'; import { useFlags } from '$utils/flags'; import { HideableAmount } from '$core/HideableAmount/HideableAmount'; -import { Icon, Screen, TonIcon, IconButton, IconButtonList } from '@tonkeeper/uikit'; +import { Icon, Screen, TonIcon, ActionButtons, Spacer } from '@tonkeeper/uikit'; import { ActivityList } from '@tonkeeper/shared/components'; import { useTonActivityList } from '@tonkeeper/shared/query/hooks/useTonActivityList'; @@ -114,22 +114,6 @@ const HeaderList = memo(() => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const handleOpenAction = useCallback(async (action: any) => { - try { - let shouldOpenInBrowser = action.openInBrowser; - if (action.scheme) { - shouldOpenInBrowser = await Linking.canOpenURL(action.scheme); - } - if (shouldOpenInBrowser) { - return Linking.openURL(action.url); - } - openDAppBrowser(action.url); - } catch (e) { - console.log(e); - openDAppBrowser(action.url); - } - }, []); - const { amount, tokenPrice } = useWalletInfo(); const handleReceive = useCallback(() => { @@ -147,13 +131,6 @@ const HeaderList = memo(() => { openSend({ currency: 'ton' }); }, [wallet]); - const handleOpenExchange = useCallback(() => { - if (!wallet) { - return openRequireWalletModal(); - } - nav.openModal('Exchange'); - }, [nav, wallet]); - const handlePressSwap = useCallback(() => { if (wallet) { nav.openModal('Swap'); @@ -196,39 +173,36 @@ const HeaderList = memo(() => { - - - - {!isWatchOnly ? ( - - ) : null} - - {!isWatchOnly ? ( - - ) : null} - {!flags.disable_swap && !isWatchOnly && ( - - )} - - - + + + + + {shouldShowChart && ( <> diff --git a/packages/mobile/src/core/Wallet/Wallet.style.ts b/packages/mobile/src/core/Wallet/Wallet.style.ts index 3a7d33303..f7de96af4 100644 --- a/packages/mobile/src/core/Wallet/Wallet.style.ts +++ b/packages/mobile/src/core/Wallet/Wallet.style.ts @@ -11,6 +11,10 @@ export const Header = styled.View` margin-horizontal: ${ns(-16)}px; `; +export const ActionsWrap = styled.View` + padding-horizontal: ${ns(16)}px; +`; + export const TokenInfoWrap = styled.View` align-items: center; padding-horizontal: ${ns(28)}px; @@ -24,7 +28,6 @@ export const FlexRow = styled.View` flex-direction: row; justify-content: space-between; margin-top: ${ns(16)}px; - margin-bottom: ${ns(28)}px; `; export const ExploreButtons = styled.View` diff --git a/packages/mobile/src/hooks/useHaveNfts.ts b/packages/mobile/src/hooks/useHaveNfts.ts new file mode 100644 index 000000000..375cd6cce --- /dev/null +++ b/packages/mobile/src/hooks/useHaveNfts.ts @@ -0,0 +1,6 @@ +import { useApprovedNfts } from '$hooks/useApprovedNfts'; + +export function useHaveNfts(): boolean { + const nfts = useApprovedNfts(); + return nfts.enabled.length > 0; +} diff --git a/packages/mobile/src/modals/ExchangeModal.tsx b/packages/mobile/src/modals/ExchangeModal.tsx index 711c1c9fb..7f1653d21 100644 --- a/packages/mobile/src/modals/ExchangeModal.tsx +++ b/packages/mobile/src/modals/ExchangeModal.tsx @@ -13,6 +13,7 @@ import { openChooseCountry } from '$navigation'; import { useSelectedCountry } from '$store/zustand/methodsToBuy/useSelectedCountry'; import { CountryButton } from '@tonkeeper/shared/components'; import { config } from '$config'; +import { useWallet } from '@tonkeeper/shared/hooks'; export const ExchangeModal = () => { const [showAll, setShowAll] = React.useState(false); @@ -65,6 +66,9 @@ export const ExchangeModal = () => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); }, [showAll]); + const wallet = useWallet(); + const watchOnly = wallet && wallet.isWatchOnly; + const [segmentIndex, setSegmentIndex] = useState(0); const isLoading = buy.length === 0 && sell.length === 0; @@ -79,11 +83,15 @@ export const ExchangeModal = () => { } title={ - setSegmentIndex(segment)} - index={segmentIndex} - items={[t('exchange_modal.buy'), t('exchange_modal.sell')]} - /> + watchOnly ? ( + categories && categories[0] && categories[0].title + ) : ( + setSegmentIndex(segment)} + index={segmentIndex} + items={[t('exchange_modal.buy'), t('exchange_modal.sell')]} + /> + ) } /> @@ -97,9 +105,11 @@ export const ExchangeModal = () => { {categories.map((category, cIndex) => ( {cIndex > 0 ? : null} - - {category.title} - + {!(watchOnly && cIndex === 0) ? ( + + {category.title} + + ) : null} {category.items.map((item, idx, arr) => ( (); @@ -116,6 +117,7 @@ export const MainStack: FC = () => { name={MainStackRouteNames.AddWatchOnlyStack} component={AddWatchOnlyStack} /> + diff --git a/packages/mobile/src/navigation/MainStack/TabStack/BackupIndicator.tsx b/packages/mobile/src/navigation/MainStack/TabStack/BackupIndicator.tsx deleted file mode 100644 index f86d928fc..000000000 --- a/packages/mobile/src/navigation/MainStack/TabStack/BackupIndicator.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { TabBarBadgeIndicator } from '$navigation/MainStack/TabStack/TabBarBadgeIndicator'; -import { useWalletSetup } from '@tonkeeper/shared/hooks'; - -export const BackupIndicator: React.FC = () => { - const { lastBackupAt } = useWalletSetup(); - - const isVisible = lastBackupAt === null; - - return ; -}; diff --git a/packages/mobile/src/navigation/MainStack/TabStack/TabStack.interface.ts b/packages/mobile/src/navigation/MainStack/TabStack/TabStack.interface.ts index 0f5b3acee..fc3f178f4 100644 --- a/packages/mobile/src/navigation/MainStack/TabStack/TabStack.interface.ts +++ b/packages/mobile/src/navigation/MainStack/TabStack/TabStack.interface.ts @@ -3,7 +3,6 @@ import { TabsStackRouteNames } from '$navigation'; export type TabStackParamList = { [TabsStackRouteNames.Balances]: {}; [TabsStackRouteNames.Activity]: {}; - [TabsStackRouteNames.NFT]: {}; [TabsStackRouteNames.BrowserStack]: {}; - [TabsStackRouteNames.SettingsStack]: {}; + [TabsStackRouteNames.Collectibles]: {}; }; diff --git a/packages/mobile/src/navigation/MainStack/TabStack/TabStack.tsx b/packages/mobile/src/navigation/MainStack/TabStack/TabStack.tsx index b7d40916d..812f77c1a 100644 --- a/packages/mobile/src/navigation/MainStack/TabStack/TabStack.tsx +++ b/packages/mobile/src/navigation/MainStack/TabStack/TabStack.tsx @@ -5,12 +5,11 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { StyleSheet } from 'react-native'; import { TabsStackRouteNames } from '$navigation'; import { TabStackParamList } from './TabStack.interface'; -import { Icon, ScrollPositionContext, View } from '$uikit'; +import { ScrollPositionContext, View } from '$uikit'; import { usePreloadChart } from '$hooks/usePreloadChart'; import { useTheme } from '$hooks/useTheme'; import { isAndroid, nfs, ns } from '$utils'; import { t } from '@tonkeeper/shared/i18n'; -import { SettingsStack } from '$navigation/SettingsStack/SettingsStack'; import { TabBarBadgeIndicator } from './TabBarBadgeIndicator'; import { WalletScreen } from '../../../tabs/Wallet/WalletScreen'; import Animated from 'react-native-reanimated'; @@ -18,13 +17,15 @@ import { FONT } from '$styled'; import { useCheckForUpdates } from '$hooks/useCheckForUpdates'; import { useLoadExpiringDomains } from '$store/zustand/domains/useExpiringDomains'; import { ActivityStack } from '$navigation/ActivityStack/ActivityStack'; -import { BackupIndicator } from '$navigation/MainStack/TabStack/BackupIndicator'; import { useFetchMethodsToBuy } from '$store/zustand/methodsToBuy/useMethodsToBuyStore'; import { trackEvent } from '$utils/stats'; import { useRemoteBridge } from '$tonconnect'; import { BrowserStack } from '$navigation/BrowserStack/BrowserStack'; import { useWallet } from '@tonkeeper/shared/hooks'; import { useDAppsNotifications } from '$store'; +import { Icon } from '@tonkeeper/uikit'; +import { Collectibles } from '$core/Colectibles/Collectibles'; +import { useHaveNfts } from '$hooks/useHaveNfts'; const Tab = createBottomTabNavigator(); @@ -33,6 +34,7 @@ export const TabStack: FC = () => { const safeArea = useSafeAreaInsets(); const theme = useTheme(); const { shouldShowRedDot, removeRedDot } = useDAppsNotifications(); + const haveNfts = useHaveNfts(); useRemoteBridge(); useLoadExpiringDomains(); @@ -40,10 +42,15 @@ export const TabStack: FC = () => { usePreloadChart(); useCheckForUpdates(); - const tabBarStyle = { height: ns(64) + (safeArea.bottom > 0 ? ns(20) : 0) }; + const tabBarStyle = useMemo( + () => ({ + height: ns(64) + (safeArea.bottom > 0 ? ns(20) : 0), + }), + [safeArea.bottom], + ); const containerTabStyle = useMemo( () => [tabBarStyle, styles.tabBarContainer, bottomSeparatorStyle], - [safeArea.bottom, bottomSeparatorStyle, tabBarStyle], + [bottomSeparatorStyle, tabBarStyle], ); const wallet = useWallet(); @@ -102,7 +109,7 @@ export const TabStack: FC = () => { component={ActivityStack} name={TabsStackRouteNames.Activity} listeners={{ - tabPress: (e) => { + tabPress: () => { removeRedDot(); }, }} @@ -125,25 +132,22 @@ export const TabStack: FC = () => { tabBarIcon: ({ color }) => , }} listeners={{ - tabPress: (e) => { + tabPress: () => { trackEvent('browser_open'); }, }} /> ) : null} - ( - - - {!isWatchOnly ? : null} - - ), - }} - /> + {haveNfts ? ( + , + }} + /> + ) : null} ); }; diff --git a/packages/mobile/src/navigation/navigationNames.ts b/packages/mobile/src/navigation/navigationNames.ts index b4858e154..1029c89bc 100644 --- a/packages/mobile/src/navigation/navigationNames.ts +++ b/packages/mobile/src/navigation/navigationNames.ts @@ -55,13 +55,13 @@ export enum MainStackRouteNames { Backup = 'Backup', BackupPhrase = 'BackupPhrase', BackupCheckPhrase = 'BackupCheckPhrase', + Settings = 'SettingsStack', } export enum TabsStackRouteNames { Balances = 'Balances', - NFT = 'TabNFT', BrowserStack = 'BrowserStack', - SettingsStack = 'SettingsStack', + Collectibles = 'Collectibles', Activity = 'Activity', } diff --git a/packages/mobile/src/shared/components/StakingListCell/StakingListCell.tsx b/packages/mobile/src/shared/components/StakingListCell/StakingListCell.tsx index b09f39571..e9cb1dbcb 100644 --- a/packages/mobile/src/shared/components/StakingListCell/StakingListCell.tsx +++ b/packages/mobile/src/shared/components/StakingListCell/StakingListCell.tsx @@ -10,7 +10,7 @@ import * as S from './StakingListCell.style'; import { HideableAmount } from '$core/HideableAmount/HideableAmount'; import { JettonBalanceModel } from '$store/models'; import { t } from '@tonkeeper/shared/i18n'; -import { Icon, ListSeparator, Pressable, isAndroid, useTheme } from '@tonkeeper/uikit'; +import { Icon, Pressable, isAndroid, useTheme } from '@tonkeeper/uikit'; import { useBackgroundHighlighted } from '@tonkeeper/shared/hooks/useBackgroundHighlighted'; import Animated from 'react-native-reanimated'; @@ -22,7 +22,6 @@ interface Props { stakingJetton?: JettonBalanceModel; icon?: ReactNode; iconSource?: Source | ImageRequireSource | null; - separator?: boolean; isWidget?: boolean; isWidgetAccent?: boolean; isBuyTon?: boolean; @@ -42,7 +41,6 @@ const StakingListCellComponent: FC = (props) => { stakingJetton, icon, iconSource, - separator, id, isWidget, isWidgetAccent, @@ -151,7 +149,6 @@ const StakingListCellComponent: FC = (props) => { ) : null} - {separator ? : null} ); }; diff --git a/packages/mobile/src/store/wallet/sagas.ts b/packages/mobile/src/store/wallet/sagas.ts index 6b016c1e7..e91f47ddb 100644 --- a/packages/mobile/src/store/wallet/sagas.ts +++ b/packages/mobile/src/store/wallet/sagas.ts @@ -38,6 +38,7 @@ import { InscriptionAdditionalParams, TokenType } from '$core/Send/Send.interfac import { WalletConfig, WalletContractVersion, WalletNetwork } from '$wallet/WalletTypes'; import { v4 as uuidv4 } from 'uuid'; import { vault as multiWalletVault } from '$wallet'; +import RNRestart from 'react-native-restart'; function* generateVaultWorker() { try { @@ -377,6 +378,7 @@ function* cleanWalletWorker(action: CleanWalletAction) { }); yield call([tk, 'removeAllWallets']); + RNRestart.restart(); } else { yield call( useConnectedAppsStore.getState().actions.unsubscribeFromAllNotifications, @@ -389,12 +391,14 @@ function* cleanWalletWorker(action: CleanWalletAction) { yield call([tk, 'removeWallet'], tk.wallet.identifier); + yield call(trackEvent, 'reset_wallet'); + if (tk.wallets.size > 0) { navigate(TabsStackRouteNames.Balances); + } else { + RNRestart.restart(); } } - - yield call(trackEvent, 'reset_wallet'); } catch (e) { e && debugLog(e.message); yield put(Toast.fail, e.message); diff --git a/packages/mobile/src/tabs/Activity/ActivityScreen.tsx b/packages/mobile/src/tabs/Activity/ActivityScreen.tsx index bbdf97dcb..4acd7c2fe 100644 --- a/packages/mobile/src/tabs/Activity/ActivityScreen.tsx +++ b/packages/mobile/src/tabs/Activity/ActivityScreen.tsx @@ -111,33 +111,37 @@ export const ActivityScreen = memo(() => { const renderNotificationsHeader = notifications.length ? ( - {notifications.slice(0, Math.min(newNotificationsCount, 2)).map((notification) => ( - - ))} - - - - - } - rightContent={ - newNotificationsCount - 2 > 0 ? ( - - {newNotificationsCount - 2} + + {notifications + .slice(0, Math.min(newNotificationsCount, 2)) + .map((notification) => ( + + ))} + + + - ) : null - } - onPress={handleOpenNotificationsScreen} - title={t('notifications.notifications')} - subtitle={t('notifications.from_connected')} - chevron - /> - + } + rightContent={ + newNotificationsCount - 2 > 0 ? ( + + {newNotificationsCount - 2} + + ) : null + } + onPress={handleOpenNotificationsScreen} + title={t('notifications.notifications')} + subtitle={t('notifications.from_connected')} + chevron + /> + + ) : undefined; @@ -196,4 +200,7 @@ const styles = Steezy.create(({ colors }) => ({ listStyle: { marginBottom: 8, }, + gap8: { + gap: 8, + }, })); diff --git a/packages/mobile/src/tabs/Wallet/WalletScreen.tsx b/packages/mobile/src/tabs/Wallet/WalletScreen.tsx index 672cd5d4b..76ca7709e 100644 --- a/packages/mobile/src/tabs/Wallet/WalletScreen.tsx +++ b/packages/mobile/src/tabs/Wallet/WalletScreen.tsx @@ -1,80 +1,47 @@ import { memo, useCallback, useEffect, useMemo } from 'react'; -import { i18n, t } from '@tonkeeper/shared/i18n'; -import { - Button, - IconButton, - IconButtonList, - Screen, - Text, - List, - View, - PagerView, - Spacer, - copyText, - Haptics, -} from '@tonkeeper/uikit'; +import { t } from '@tonkeeper/shared/i18n'; +import { Screen, Text, View, Spacer, copyText, Haptics, Icon } from '@tonkeeper/uikit'; import { InternalNotification, Tag } from '$uikit'; import { useNavigation } from '@tonkeeper/router'; -import { ScanQRButton } from '../../components/ScanQRButton'; -import { RefreshControl, useWindowDimensions } from 'react-native'; -import { NFTCardItem } from './NFTCardItem'; import { useDispatch } from 'react-redux'; -import { ns } from '$utils'; import { useIsFocused } from '@react-navigation/native'; import { useBalance } from './hooks/useBalance'; -import { ListItemRate } from './components/ListItemRate'; -import { TonIcon } from '@tonkeeper/uikit'; -import { CryptoCurrencies, TabletMaxWidth } from '$shared/constants'; -import { useBottomTabBarHeight } from '$hooks/useBottomTabBarHeight'; +import { TabletMaxWidth } from '$shared/constants'; import { useInternalNotifications } from './hooks/useInternalNotifications'; import { mainActions } from '$store/main'; -import { useTonkens } from './hooks/useTokens'; -import { useApprovedNfts } from '$hooks/useApprovedNfts'; -import { useTheme } from '$hooks/useTheme'; -import { useTokenPrice } from '$hooks/useTokenPrice'; import { Steezy } from '$styles'; -import { WalletContentList } from './components/WalletContentList'; -import { useFlags } from '$utils/flags'; import { useUpdatesStore } from '$store/zustand/updates/useUpdatesStore'; import { UpdatesCell } from '$core/ApprovalCell/Updates/UpdatesCell'; import { UpdateState } from '$store/zustand/updates/types'; import { ShowBalance } from '$core/HideableAmount/ShowBalance'; -import { Events, SendAnalyticsFrom } from '$store/models'; -import { openRequireWalletModal } from '$core/ModalContainer/RequireWallet/RequireWallet'; -import { openWallet } from '$core/Wallet/ToncoinScreen'; -import { trackEvent } from '$utils/stats'; import { ExpiringDomainCell } from './components/ExpiringDomainCell'; import { BatteryIcon } from '@tonkeeper/shared/components/BatteryIcon/BatteryIcon'; import { useNetInfo } from '@react-native-community/netinfo'; import { format } from 'date-fns'; import { getLocale } from '$utils/date'; import { TouchableOpacity } from 'react-native-gesture-handler'; -import { useWallet, useWalletCurrency, useWalletStatus } from '@tonkeeper/shared/hooks'; +import { useWallet, useWalletStatus } from '@tonkeeper/shared/hooks'; import { WalletSelector } from './components/WalletSelector'; -import { useInscriptionBalances } from '$hooks/useInscriptionBalances'; +import { MainStackRouteNames } from '$navigation'; +import { WalletActionButtons } from './components/WalletActionButtons/WalletActionButtons'; +import { WalletContentList } from './components/WalletContentList'; +import { usePreparedWalletContent } from './content-providers/utils/usePreparedWalletContent'; +import { FinishSetupList } from './components/FinishSetupList'; +import { BackupIndicator } from './components/Tabs/BackupIndicator'; export const WalletScreen = memo(({ navigation }) => { - const flags = useFlags(['disable_swap']); - const tabBarHeight = useBottomTabBarHeight(); const dispatch = useDispatch(); - const theme = useTheme(); const nav = useNavigation(); - const tokens = useTonkens(); - const { enabled: inscriptions } = useInscriptionBalances(); - const { enabled: nfts } = useApprovedNfts(); const wallet = useWallet(); const shouldUpdate = useUpdatesStore((state) => state.update.state) !== UpdateState.NOT_STARTED; - const balance = useBalance(tokens.total.fiat); - const tonPrice = useTokenPrice(CryptoCurrencies.Ton); - const currency = useWalletCurrency(); + + const preparedContent = usePreparedWalletContent(); + const balance = useBalance(preparedContent); const { isReloading: isRefreshing, updatedAt: walletUpdatedAt } = useWalletStatus(); const isFocused = useIsFocused(); - - const tronBalances = undefined; - const notifications = useInternalNotifications(); const { isConnected } = useNetInfo(); @@ -87,40 +54,7 @@ export const WalletScreen = memo(({ navigation }) => { return () => clearTimeout(timer); }, [dispatch]); - const handlePressSwap = useCallback(() => { - if (wallet) { - nav.openModal('Swap'); - } else { - openRequireWalletModal(); - } - }, [nav, wallet]); - - const handlePressBuy = useCallback(() => { - if (wallet) { - nav.openModal('Exchange'); - } else { - openRequireWalletModal(); - } - }, [nav, wallet]); - - const handlePressSend = useCallback(() => { - if (wallet) { - trackEvent(Events.SendOpen, { from: SendAnalyticsFrom.WalletScreen }); - nav.go('Send', { from: SendAnalyticsFrom.WalletScreen }); - } else { - openRequireWalletModal(); - } - }, [nav, wallet]); - - const handlePressRecevie = useCallback(() => { - if (wallet) { - nav.go('ReceiveModal'); - } else { - openRequireWalletModal(); - } - }, [nav, wallet]); - - const handleCreateWallet = () => nav.navigate('/add-wallet'); + const handleNavigateToSettingsStack = () => nav.navigate(MainStackRouteNames.Settings); const handleRefresh = useCallback(() => { if (!wallet) { @@ -141,9 +75,14 @@ export const WalletScreen = memo(({ navigation }) => { const isWatchOnly = wallet && wallet.isWatchOnly; + const ListFooter = useMemo( + () => (isWatchOnly ? null : ), + [isWatchOnly], + ); + const ListHeader = useMemo( () => ( - + {notifications.map((notification, i) => ( { {shouldUpdate && } - - + + {!isWatchOnly && } {wallet && isConnected !== false ? ( @@ -198,37 +137,7 @@ export const WalletScreen = memo(({ navigation }) => { ) : null} - - {!isWatchOnly ? ( - - ) : null} - - {!isWatchOnly ? ( - - ) : null} - {!flags.disable_swap && !isWatchOnly && ( - - )} - + {wallet && !wallet.isWatchOnly && ( <> @@ -237,12 +146,7 @@ export const WalletScreen = memo(({ navigation }) => { ), [ - balance.total.fiat, - flags.disable_swap, - handlePressBuy, - handlePressRecevie, - handlePressSend, - handlePressSwap, + balance, isConnected, isWatchOnly, notifications, @@ -252,134 +156,34 @@ export const WalletScreen = memo(({ navigation }) => { ], ); - // TODO: rewrite - const dimensions = useWindowDimensions(); - const mockupCardSize = { - width: ns(114), - height: ns(166), - }; - - const numColumn = 3; - const indent = ns(8); - const heightRatio = mockupCardSize.height / mockupCardSize.width; - - const nftCardSize = useMemo(() => { - const width = dimensions.width / numColumn - indent; - const height = width * heightRatio; - - return { width, height }; - }, [dimensions.width]); - - const isPagerView = - nfts.length && - tokens.list.length + inscriptions.length >= 2 && - inscriptions.length + tokens.list.length + nfts.length + 1 > 10; - if (!wallet) { - return ( - - } - hideBackButton - /> - - {ListHeader} - - openWallet(CryptoCurrencies.Ton)} - leftContent={} - chevron - subtitle={ - - } - /> - - - {!wallet && ( - - -