diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..60d81ac8c --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +# yarn pull_locales && git add packages/shared/i18n/locales diff --git a/package.json b/package.json index add26debb..738b52f27 100755 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ ] }, "scripts": { + "push_locales": "cd packages && cd shared && yarn push_locales", + "pull_locales": "cd packages && cd shared && yarn pull_locales", "postinstall": "patch-package", "pods": "cd packages && cd mobile && yarn pods", "icons": "cd packages && cd mobile && yarn icons", @@ -17,10 +19,14 @@ "run:android": "cd packages && cd mobile && yarn android", "build:android": "cd packages && cd mobile && yarn build:android", "build:android:apk": "cd packages && cd mobile && yarn build:android:apk", - "bump": "cd packages/mobile && yarn bump", + "bump": "yarn pull_locales && cd ../.. && cd packages/mobile && yarn bump", "rnm": "find . -name \"node_modules\" -type d -prune -exec rm -rf '{}' +", "fnm": "find . -name \"node_modules\" -type d -prune | xargs du -chs", - "adb": "adb reverse tcp:8081 tcp:8081" + "adb": "adb reverse tcp:8081 tcp:8081", + "prepare": "husky install" }, - "dependencies": {} + "dependencies": {}, + "devDependencies": { + "husky": "^8.0.0" + } } diff --git a/packages/@core-js/src/utils/AmountFormatter/FiatCurrencyConfig.ts b/packages/@core-js/src/utils/AmountFormatter/FiatCurrencyConfig.ts index 44e1c0791..a1bb3a16d 100644 --- a/packages/@core-js/src/utils/AmountFormatter/FiatCurrencyConfig.ts +++ b/packages/@core-js/src/utils/AmountFormatter/FiatCurrencyConfig.ts @@ -1,4 +1,5 @@ export enum FiatCurrencies { + Ton = 'ton', Usd = 'usd', Eur = 'eur', Rub = 'rub', @@ -29,6 +30,10 @@ export enum FiatCurrencies { } export const FiatCurrencySymbolsConfig = { + [FiatCurrencies.Ton]: { + symbol: 'TON', + side: 'end', + }, [FiatCurrencies.Usd]: { symbol: '$', side: 'start', @@ -79,7 +84,7 @@ export const FiatCurrencySymbolsConfig = { }, [FiatCurrencies.Brl]: { symbol: 'R$', - side: 'end', + side: 'start', }, [FiatCurrencies.Try]: { symbol: '₺', diff --git a/packages/mobile/.eslintrc.js b/packages/mobile/.eslintrc.js index 189699723..1a1c9e479 100644 --- a/packages/mobile/.eslintrc.js +++ b/packages/mobile/.eslintrc.js @@ -1,6 +1,10 @@ module.exports = { root: true, - extends: '@react-native-community', + extends: '@react-native', parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], + rules: { + 'react/react-in-jsx-scope': 'off', + 'react/jsx-uses-react': 'off', + 'react-hooks/exhaustive-deps': 'warn', + }, }; diff --git a/packages/mobile/Gemfile b/packages/mobile/Gemfile index ecb457e6a..36647872e 100644 --- a/packages/mobile/Gemfile +++ b/packages/mobile/Gemfile @@ -1,4 +1,5 @@ source 'https://rubygems.org' # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version -ruby File.read(File.join(__dir__, '.ruby-version')).strip -gem 'cocoapods', '~> 1.11', '>= 1.11.3' \ No newline at end of file +ruby ">= 2.6.10" +gem 'cocoapods', '~> 1.13' +gem 'activesupport', '>= 6.1.7.3', '< 7.1.0' \ No newline at end of file diff --git a/packages/mobile/Gemfile.lock b/packages/mobile/Gemfile.lock index 4dc374e20..ec07103c8 100644 --- a/packages/mobile/Gemfile.lock +++ b/packages/mobile/Gemfile.lock @@ -3,28 +3,27 @@ GEM specs: CFPropertyList (3.0.6) rexml - activesupport (6.1.7.2) + activesupport (7.0.8) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - zeitwerk (~> 2.3) - addressable (2.8.1) + addressable (2.8.5) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) atomos (0.1.3) claide (1.1.0) - cocoapods (1.11.3) + cocoapods (1.13.0) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.11.3) + cocoapods-core (= 1.13.0) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.4.0, < 2.0) + cocoapods-downloader (>= 1.6.0, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.4.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) cocoapods-try (>= 1.1.0, < 2.0) colored2 (~> 3.1) escape (~> 0.0.4) @@ -32,10 +31,10 @@ GEM gh_inspector (~> 1.0) molinillo (~> 0.8.0) nap (~> 1.0) - ruby-macho (>= 1.0, < 3.0) - xcodeproj (>= 1.21.0, < 2.0) - cocoapods-core (1.11.3) - activesupport (>= 5.0, < 7) + ruby-macho (>= 2.3.0, < 3.0) + xcodeproj (>= 1.23.0, < 2.0) + cocoapods-core (1.13.0) + activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) concurrent-ruby (~> 1.1) @@ -54,19 +53,19 @@ GEM netrc (~> 0.11) cocoapods-try (1.2.0) colored2 (3.1.2) - concurrent-ruby (1.2.0) + concurrent-ruby (1.2.2) escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - ffi (1.15.5) + ffi (1.16.1) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) httpclient (2.8.3) - i18n (1.12.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) json (2.6.3) - minitest (5.17.0) + minitest (5.16.3) molinillo (0.8.0) nanaimo (0.3.0) nap (1.1.0) @@ -78,24 +77,24 @@ GEM ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - xcodeproj (1.22.0) + xcodeproj (1.23.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) rexml (~> 3.2.4) - zeitwerk (2.6.7) PLATFORMS - arm64-darwin-22 + arm64-darwin-23 ruby DEPENDENCIES - cocoapods (~> 1.11, >= 1.11.3) + activesupport (>= 6.1.7.3, < 7.1.0) + cocoapods (~> 1.13) RUBY VERSION - ruby 2.7.6p219 + ruby 3.2.2p53 BUNDLED WITH - 2.4.10 + 2.4.20 diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index 93f84ff06..335e28461 100644 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -6,17 +6,7 @@ plugins { id "com.google.firebase.crashlytics" } -import com.android.build.OutputFile - def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() -def expoDebuggableVariants = ['debug'] -// Override `debuggableVariants` for expo-updates debugging -if (System.getenv('EX_UPDATES_NATIVE_DEBUG') == "1") { - react { - expoDebuggableVariants = [] - } -} - /** * This is the configuration block to customize your React Native Android app. @@ -26,17 +16,20 @@ react { entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim()) reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc" - debuggableVariants = expoDebuggableVariants + codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() + + // Use Expo CLI to bundle the app, this ensures the Metro config + // works correctly with Expo projects. + cliFile = new File(["node", "--print", "require.resolve('@expo/cli')"].execute(null, rootDir).text.trim()) + bundleCommand = "export:embed" /* Folders */ // The root of your project, i.e. where "package.json" lives. Default is '..' // root = file("../") // The folder where the react-native NPM package is. Default is ../node_modules/react-native // reactNativeDir = file("../node_modules/react-native") - // The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen - // codegenDir = file("../node_modules/react-native-codegen") - // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js - // cliFile = file("../node_modules/react-native/cli.js") + // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen + // codegenDir = file("../node_modules/@react-native/codegen") /* Variants */ // The list of variants to that are debuggable. For those we're going to @@ -47,9 +40,7 @@ react { /* Bundling */ // A list containing the node command and its flags. Default is just 'node'. // nodeExecutableAndArgs = ["node"] - // - // The command to run when bundling. By default is 'bundle' - // bundleCommand = "ram-bundle" + // // The path to the CLI configuration file. Default is empty. // bundleConfig = file(../rn-cli.config.js) @@ -72,19 +63,6 @@ react { // hermesFlags = ["-O", "-output-source-map"] } -// Override `hermesEnabled` by `expo.jsEngine` -ext { - hermesEnabled = (findProperty('expo.jsEngine') ?: "hermes") == "hermes" -} - -/** - * Set this to true to create four separate APKs instead of one, - * one for each native architecture. This is useful if you don't - * use App Bundles (https://developer.android.com/guide/app-bundle/) - * and want to have separate APKs to upload to the Play Store. - */ -def enableSeparateBuildPerCPUArchitecture = false - /** * Set this to true to Run Proguard on Release builds to minify the Java bytecode. */ @@ -103,16 +81,6 @@ def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInRelea */ def jscFlavor = 'org.webkit:android-jsc:+' -/** - * Private function to get the list of Native Architectures you want to build. - * This reads the value from reactNativeArchitectures in your gradle.properties - * file and works together with the --active-arch-only flag of react-native run-android. - */ -def reactNativeArchitectures() { - def value = project.getProperties().get("reactNativeArchitectures") - return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] -} - android { ndkVersion rootProject.ext.ndkVersion @@ -123,19 +91,11 @@ android { applicationId "com.ton_keeper" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 390 - versionName "3.4.3" + versionCode 392 + versionName "3.4.4" missingDimensionStrategy 'react-native-camera', 'general' } - splits { - abi { - reset() - enable enableSeparateBuildPerCPUArchitecture - universalApk false // If true, also generate a universal APK - include (*reactNativeArchitectures()) - } - } signingConfigs { release { if (project.hasProperty('TONKEEPER_UPLOAD_STORE_FILE')) { @@ -164,22 +124,6 @@ android { proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } - - // applicationVariants are e.g. debug, release - applicationVariants.all { variant -> - variant.outputs.each { output -> - // For each separate APK per architecture, set a unique version code as described here: - // https://developer.android.com/studio/build/configure-apk-splits.html - // Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc. - def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] - def abi = output.getFilter(OutputFile.ABI) - if (abi != null) { // null for the universal-debug, universal-release variants - output.versionCodeOverride = - defaultConfig.versionCode * 1000 + versionCodes.get(abi) - } - - } - } } // Apply static values from `gradle.properties` to the `android.packagingOptions` @@ -231,8 +175,6 @@ dependencies { } } - implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0") - debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { exclude group:'com.squareup.okhttp3', module:'okhttp' diff --git a/packages/mobile/android/app/src/main/AndroidManifest.xml b/packages/mobile/android/app/src/main/AndroidManifest.xml index 24d427b99..6a16d6121 100644 --- a/packages/mobile/android/app/src/main/AndroidManifest.xml +++ b/packages/mobile/android/app/src/main/AndroidManifest.xml @@ -46,7 +46,7 @@ onBackPressed + */ + @Override + public void invokeDefaultOnBackPressed() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (!moveTaskToBack(false)) { + // For non-root activities, use the default implementation to finish them. + super.invokeDefaultOnBackPressed(); + } + return; + } + + // Use the default back button implementation on Android S + // because it's doing more than {@link Activity#moveTaskToBack} in fact. + super.invokeDefaultOnBackPressed(); + } +} diff --git a/packages/mobile/android/app/src/main/java/com/ton_keeper/TonkeeperActivity.kt b/packages/mobile/android/app/src/main/java/com/ton_keeper/TonkeeperActivity.kt deleted file mode 100644 index 710ca5010..000000000 --- a/packages/mobile/android/app/src/main/java/com/ton_keeper/TonkeeperActivity.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.ton_keeper - -import android.content.Intent -import android.content.res.Configuration -import android.os.Build -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.WindowCompat -import com.facebook.react.ReactApplication -import com.facebook.react.ReactNativeHost -import com.facebook.react.bridge.Callback -import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler -import com.facebook.react.modules.core.PermissionAwareActivity -import com.facebook.react.modules.core.PermissionListener - -class TonkeeperActivity : AppCompatActivity(), - DefaultHardwareBackBtnHandler, - PermissionAwareActivity { - - private val reactNativeHost: ReactNativeHost - get() { - val app = application as ReactApplication - return app.reactNativeHost - } - - private var permissionListener: PermissionListener? = null - private var permissionsCallback: Callback? = null - - override fun onCreate(savedInstanceState: Bundle?) { - setTheme(R.style.AppTheme) - super.onCreate(null) - - setContentView(R.layout.activity_tonkeeper) - setRootFragment() - } - - override fun onResume() { - super.onResume() - if (permissionsCallback != null) { - permissionsCallback?.invoke() - permissionsCallback = null - } - } - - private fun setEdgeToEdge() { - WindowCompat.setDecorFitsSystemWindows(window, false) - } - - private fun setRootFragment() { - supportFragmentManager - .beginTransaction() - .replace(R.id.root, ReactComponent.Wallet.createFragment()) - .commit() - } - - override fun onBackPressed() { - if (reactNativeHost.hasInstance()) { - reactNativeHost.reactInstanceManager.onBackPressed() - return - } - super.onBackPressed() - } - - override fun invokeDefaultOnBackPressed() { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { - if (!moveTaskToBack(false)) { - super.onBackPressed() - } - return - } - super.onBackPressed() - } - - override fun onNewIntent(intent: Intent?) { - super.onNewIntent(intent) - if (reactNativeHost.hasInstance()) { - reactNativeHost.reactInstanceManager.onNewIntent(intent) - return - } - } - - override fun requestPermissions( - permissions: Array, - requestCode: Int, - listener: PermissionListener? - ) { - permissionListener = listener - requestPermissions(permissions, requestCode) - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - permissionsCallback = Callback { - val listener = permissionListener ?: return@Callback - val remove = listener.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (remove) permissionListener = null - } - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - if (reactNativeHost.hasInstance()) { - reactNativeHost.reactInstanceManager.onConfigurationChanged(this, newConfig) - } - } - - override fun onWindowFocusChanged(hasFocus: Boolean) { - super.onWindowFocusChanged(hasFocus) - if (reactNativeHost.hasInstance()) { - reactNativeHost.reactInstanceManager.onWindowFocusChange(hasFocus) - } - } -} \ No newline at end of file diff --git a/packages/mobile/android/build.gradle b/packages/mobile/android/build.gradle index 47aead383..776f800c7 100644 --- a/packages/mobile/android/build.gradle +++ b/packages/mobile/android/build.gradle @@ -5,10 +5,8 @@ buildscript { buildToolsVersion = findProperty('android.buildToolsVersion') ?: '33.0.0' minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '23') compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '33') - targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '33') - if (findProperty('android.kotlinVersion')) { - kotlinVersion = findProperty('android.kotlinVersion') - } + targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '33') + kotlinVersion = findProperty('android.kotlinVersion') ?: '1.8.10' frescoVersion = findProperty('expo.frescoVersion') ?: '2.5.0' // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. @@ -19,7 +17,7 @@ buildscript { mavenCentral() } dependencies { - classpath('com.android.tools.build:gradle:7.4.1') + classpath("com.android.tools.build:gradle:7.4.2") classpath('com.facebook.react:react-native-gradle-plugin') classpath('de.undercouch:gradle-download-task:5.0.1') classpath('com.google.gms:google-services:4.3.14') @@ -28,10 +26,10 @@ buildscript { } plugins { - id 'com.android.application' version '7.4.1' apply false - id 'com.android.library' version '7.4.1' apply false - id 'org.jetbrains.kotlin.android' version '1.7.20' apply false - id 'org.jetbrains.kotlin.jvm' version '1.7.20' apply false + id 'com.android.application' version '7.4.2' apply false + id 'com.android.library' version '7.4.2' apply false + id 'org.jetbrains.kotlin.android' version '1.8.10' apply false + id 'org.jetbrains.kotlin.jvm' version '1.8.10' apply false } allprojects { diff --git a/packages/mobile/android/devkit/build.gradle.kts b/packages/mobile/android/devkit/build.gradle.kts index 5f99d6921..f8a53ad68 100644 --- a/packages/mobile/android/devkit/build.gradle.kts +++ b/packages/mobile/android/devkit/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("com.android.application") version "7.4.1" apply false - id("com.android.library") version "7.4.1" apply false - id("org.jetbrains.kotlin.android") version "1.7.20" apply false - id("org.jetbrains.kotlin.jvm") version "1.7.20" apply false + id("com.android.application") version "7.4.2" apply false + id("com.android.library") version "7.4.2" apply false + id("org.jetbrains.kotlin.android") version "1.8.10" apply false + id("org.jetbrains.kotlin.jvm") version "1.8.10" apply false } diff --git a/packages/mobile/android/devkit/gradle/wrapper/gradle-wrapper.properties b/packages/mobile/android/devkit/gradle/wrapper/gradle-wrapper.properties index 00aea7273..4c9ec0d59 100644 --- a/packages/mobile/android/devkit/gradle/wrapper/gradle-wrapper.properties +++ b/packages/mobile/android/devkit/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ #Thu Nov 24 22:01:19 MSK 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip +networkTimeout=10000 distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/packages/mobile/android/devkit/sample/build.gradle.kts b/packages/mobile/android/devkit/sample/build.gradle.kts index 4b9cc34d9..173e10be3 100644 --- a/packages/mobile/android/devkit/sample/build.gradle.kts +++ b/packages/mobile/android/devkit/sample/build.gradle.kts @@ -34,7 +34,7 @@ android { compose = true } composeOptions { - kotlinCompilerExtensionVersion = "1.3.2" + kotlinCompilerExtensionVersion = "1.4.4" } } diff --git a/packages/mobile/android/gradle.properties b/packages/mobile/android/gradle.properties index 69be14f43..70eb1b545 100644 --- a/packages/mobile/android/gradle.properties +++ b/packages/mobile/android/gradle.properties @@ -21,11 +21,12 @@ org.gradle.jvmargs=-Xmx8192M -XX:MaxMetaspaceSize=512m # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true + +# Automatically convert third-party libraries to use AndroidX android.enableJetifier=true -android.kotlinVersion=1.7.20 # Version of flipper SDK to use with React Native -FLIPPER_VERSION=0.125.0 +FLIPPER_VERSION=0.182.0 # Use this property to specify which architecture you want to build. # You can also override it from the CLI using @@ -39,9 +40,10 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 # are providing them. newArchEnabled=false -# The hosted JavaScript engine -# Supported values: expo.jsEngine = "hermes" | "jsc" -expo.jsEngine=hermes +# Use this property to enable or disable the Hermes JS engine. +# If set to false, you will be using JSC instead. +hermesEnabled=true + # Enable GIF support in React Native images (~200 B increase) expo.gif.enabled=true # Enable webp support in React Native images (~85 KB increase) @@ -49,3 +51,6 @@ expo.webp.enabled=true # Enable animated webp support (~3.4 MB increase) # Disabled by default because iOS doesn't support animated webp expo.webp.animated=false + +# Enable network inspector +EX_DEV_CLIENT_NETWORK_INSPECTOR=true diff --git a/packages/mobile/android/gradle/wrapper/gradle-wrapper.properties b/packages/mobile/android/gradle/wrapper/gradle-wrapper.properties index 8fad3f5a9..6ec1567a0 100644 --- a/packages/mobile/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/mobile/android/gradlew b/packages/mobile/android/gradlew index 1b6c78733..dc7fe01a3 100755 --- a/packages/mobile/android/gradlew +++ b/packages/mobile/android/gradlew @@ -80,10 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' @@ -205,6 +205,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/packages/mobile/android/gradlew.bat b/packages/mobile/android/gradlew.bat index ac1b06f93..fb402c12a 100644 --- a/packages/mobile/android/gradlew.bat +++ b/packages/mobile/android/gradlew.bat @@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/packages/mobile/android/settings.gradle b/packages/mobile/android/settings.gradle index 7d650a628..296ba599c 100644 --- a/packages/mobile/android/settings.gradle +++ b/packages/mobile/android/settings.gradle @@ -31,4 +31,4 @@ useExpoModules() apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); applyNativeModulesSettingsGradle(settings) -includeBuild(new File(["node", "--print", "require.resolve('react-native-gradle-plugin/package.json')"].execute(null, rootDir).text.trim()).getParentFile()) +includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json')"].execute(null, rootDir).text.trim()).getParentFile()) diff --git a/packages/mobile/ios/Podfile b/packages/mobile/ios/Podfile index 9623e352a..5727ccf6f 100644 --- a/packages/mobile/ios/Podfile +++ b/packages/mobile/ios/Podfile @@ -34,8 +34,7 @@ elsif podfile_properties.key?('ios.flipper') then flipper_config = FlipperConfiguration.enabled(["Debug", "Release"], { 'Flipper' => podfile_properties['ios.flipper'] }) end end - - + target 'ton_keeper' do use_expo_modules! config = use_native_modules! @@ -56,17 +55,8 @@ target 'ton_keeper' do use_react_native!( :path => config[:reactNativePath], - # Hermes is now enabled by default. Disable by setting this flag to false. - # Upcoming versions of React Native may rely on get_default_flags(), but - # we make it explicit here to aid in the React Native upgrade process. :hermes_enabled => true, :fabric_enabled => false, - # Enables Flipper. - # - # Note that if you have use_frameworks! enabled, Flipper will not work and - # you should disable the next line. - # :flipper_configuration => flipper_config, - # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/.." ) @@ -80,10 +70,10 @@ target 'ton_keeper' do post_install do |installer| react_native_post_install( installer, - # Set `mac_catalyst_enabled` to `true` in order to apply patches - # necessary for Mac Catalyst builds + config[:reactNativePath], :mac_catalyst_enabled => false ) + __apply_Xcode_12_5_M1_post_install_workaround(installer) installer.target_installation_results.pod_target_installation_results .each do |pod_name, target_installation_result| diff --git a/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj b/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj index cbf2a38e4..9db8601b1 100644 --- a/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj @@ -130,6 +130,10 @@ 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 = ""; }; + 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 = ""; }; + 50AD049A2AD96AFA00E0648C /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hans"; path = "zh-Hans.lproj/PassCode.stringsdict"; sourceTree = ""; }; 66B33E2FC88F6DF6B498F962 /* Pods-ton_keeper-ton_keeperTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ton_keeper-ton_keeperTests.debug.xcconfig"; path = "Target Support Files/Pods-ton_keeper-ton_keeperTests/Pods-ton_keeper-ton_keeperTests.debug.xcconfig"; sourceTree = ""; }; 66D7306F36185132B3874D4C /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-ton_keeper-ton_keeperTests/ExpoModulesProvider.swift"; sourceTree = ""; }; 6AFA288D04A19A6FAE554C55 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-ton_keeper/ExpoModulesProvider.swift"; sourceTree = ""; }; @@ -751,6 +755,7 @@ buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "ton_keeperTests" */; buildPhases = ( 027EACABF3131444CA1290DC /* [CP] Check Pods Manifest.lock */, + DD2E2577A653816C1086F2C6 /* [Expo] Configure project */, 00E356EA1AD99517003FC87E /* Sources */, 00E356EB1AD99517003FC87E /* Frameworks */, 00E356EC1AD99517003FC87E /* Resources */, @@ -774,6 +779,7 @@ 8C4EC056529EFADE0053216C /* [CP] Check Pods Manifest.lock */, C4F5F2092927C96100D08D79 /* R.swift codegen */, FD10A7F022414F080027D42C /* Start Packager */, + 914665CBF9B9C82E180E82E9 /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, @@ -817,6 +823,8 @@ en, Base, ru, + tr, + "zh-Hans", ); mainGroup = 83CBB9F61A601CBA00E9B192; productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; @@ -959,6 +967,25 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + 914665CBF9B9C82E180E82E9 /* [Expo] Configure project */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "[Expo] Configure project"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-ton_keeper/expo-configure-project.sh\"\n"; + }; AAADAFD88EE0FE222F244A1B /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1031,6 +1058,25 @@ shellPath = /bin/sh; shellScript = "\"$PODS_ROOT/R.swift/rswift\" generate \"$SRCROOT/$PROJECT_NAME/Resources/Generated/R.generated.swift\"\n"; }; + DD2E2577A653816C1086F2C6 /* [Expo] Configure project */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "[Expo] Configure project"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-ton_keeper-ton_keeperTests/expo-configure-project.sh\"\n"; + }; F95E499949E50FBC1ECC1307 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1156,6 +1202,8 @@ children = ( C4380878292BD26A00FF5669 /* en */, C438087A292BD26C00FF5669 /* ru */, + 50AD04982AD96A5D00E0648C /* tr */, + 50AD049A2AD96AFA00E0648C /* zh-Hans */, ); name = PassCode.stringsdict; sourceTree = ""; @@ -1165,6 +1213,8 @@ children = ( C4F5F2062927C85B00D08D79 /* en */, C4F5F2082927C88200D08D79 /* ru */, + 50AD04972AD96A5C00E0648C /* tr */, + 50AD04992AD96AFA00E0648C /* zh-Hans */, ); name = PassCode.strings; sourceTree = ""; @@ -1234,7 +1284,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = ton_keeper/ton_keeper.entitlements; - CURRENT_PROJECT_VERSION = 390; + CURRENT_PROJECT_VERSION = 393; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = CT523DK2KC; ENABLE_BITCODE = NO; @@ -1244,7 +1294,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.4.3; + MARKETING_VERSION = 3.4.4; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1269,7 +1319,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = ton_keeper/ton_keeper.entitlements; - CURRENT_PROJECT_VERSION = 390; + CURRENT_PROJECT_VERSION = 393; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = CT523DK2KC; INFOPLIST_FILE = ton_keeper/SupportingFiles/Info.plist; @@ -1278,7 +1328,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.4.3; + MARKETING_VERSION = 3.4.4; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1336,6 +1386,7 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", + _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION, ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -1344,7 +1395,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, "$(inherited)", @@ -1356,6 +1407,9 @@ ); MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "$(inherited)"; + OTHER_CPLUSPLUSFLAGS = "$(inherited)"; + OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; }; @@ -1397,13 +1451,17 @@ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION, + ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, "$(inherited)", @@ -1414,6 +1472,9 @@ "\"$(inherited)\"", ); MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = "$(inherited)"; + OTHER_CPLUSPLUSFLAGS = "$(inherited)"; + OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/packages/mobile/ios/ton_keeper/Application/AppDelegate.mm b/packages/mobile/ios/ton_keeper/Application/AppDelegate.mm index aead1d134..00f08c4a3 100644 --- a/packages/mobile/ios/ton_keeper/Application/AppDelegate.mm +++ b/packages/mobile/ios/ton_keeper/Application/AppDelegate.mm @@ -38,16 +38,6 @@ - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge #endif } -/// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. -/// -/// @see: https://reactjs.org/blog/2022/03/29/react-v18.html -/// @note: This requires to be rendering on Fabric (i.e. on the New Architecture). -/// @return: `true` if the `concurrentRoot` feature is enabled. Otherwise, it returns `false`. -- (BOOL)concurrentRootEnabled -{ - return true; -} - // Linking API - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; diff --git a/packages/mobile/ios/ton_keeper/Resources/Localizable/tr.lproj/PassCode.strings b/packages/mobile/ios/ton_keeper/Resources/Localizable/tr.lproj/PassCode.strings new file mode 100644 index 000000000..a42a4775e --- /dev/null +++ b/packages/mobile/ios/ton_keeper/Resources/Localizable/tr.lproj/PassCode.strings @@ -0,0 +1,5 @@ +"enter" = "Enter passcode"; +"logout" = "Log out"; +"attempts.remaining" = "Attempts remaining: %@"; +"createNew" = "Create new passcode"; +"reenter" = "Re-enter passcode"; diff --git a/packages/mobile/ios/ton_keeper/Resources/Localizable/tr.lproj/PassCode.stringsdict b/packages/mobile/ios/ton_keeper/Resources/Localizable/tr.lproj/PassCode.stringsdict new file mode 100644 index 000000000..912057480 --- /dev/null +++ b/packages/mobile/ios/ton_keeper/Resources/Localizable/tr.lproj/PassCode.stringsdict @@ -0,0 +1,22 @@ + + + + + tryAfter + + NSStringLocalizedFormatKey + %#@number@ + number + + NSStringFormatValueTypeKey + ld + NSStringFormatSpecTypeKey + NSStringPluralRuleType + other + Try after %ld minutes + one + Try after %ld minute + + + + diff --git a/packages/mobile/ios/ton_keeper/Resources/Localizable/zh-Hans.lproj/PassCode.strings b/packages/mobile/ios/ton_keeper/Resources/Localizable/zh-Hans.lproj/PassCode.strings new file mode 100644 index 000000000..a42a4775e --- /dev/null +++ b/packages/mobile/ios/ton_keeper/Resources/Localizable/zh-Hans.lproj/PassCode.strings @@ -0,0 +1,5 @@ +"enter" = "Enter passcode"; +"logout" = "Log out"; +"attempts.remaining" = "Attempts remaining: %@"; +"createNew" = "Create new passcode"; +"reenter" = "Re-enter passcode"; diff --git a/packages/mobile/ios/ton_keeper/Resources/Localizable/zh-Hans.lproj/PassCode.stringsdict b/packages/mobile/ios/ton_keeper/Resources/Localizable/zh-Hans.lproj/PassCode.stringsdict new file mode 100644 index 000000000..912057480 --- /dev/null +++ b/packages/mobile/ios/ton_keeper/Resources/Localizable/zh-Hans.lproj/PassCode.stringsdict @@ -0,0 +1,22 @@ + + + + + tryAfter + + NSStringLocalizedFormatKey + %#@number@ + number + + NSStringFormatValueTypeKey + ld + NSStringFormatSpecTypeKey + NSStringPluralRuleType + other + Try after %ld minutes + one + Try after %ld minute + + + + diff --git a/packages/mobile/metro.config.js b/packages/mobile/metro.config.js index a8d7191b0..476187e84 100644 --- a/packages/mobile/metro.config.js +++ b/packages/mobile/metro.config.js @@ -24,7 +24,7 @@ config.resolver.nodeModulesPaths = [ 'react-native-svg-transformer', )), (config.resolver.assetExts = config.resolver.assetExts.filter((ext) => ext !== 'svg')); -config.resolver.sourceExts.push('svg', 'cjs'); +config.resolver.sourceExts.push('svg', 'cjs', 'mjs'); config.resolver.extraNodeModules = { stream: require.resolve('stream-browserify'), diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 9c6dccd18..963bd729f 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -57,15 +57,15 @@ "date-fns": "^2.30.0", "domain-from-partial-url": "^1.1.0", "dotenv": "^16.0.3", - "expo": "~48.0.15", - "expo-blur": "~12.2.2", - "expo-local-authentication": "~13.2.1", - "expo-modules-core": "~1.2.6", - "expo-secure-store": "~12.1.1", - "expo-splash-screen": "~0.18.1", + "expo": "~49.0.13", + "expo-blur": "~12.6.0", + "expo-local-authentication": "~13.6.0", + "expo-modules-core": "~1.5.11", + "expo-secure-store": "~12.5.0", + "expo-splash-screen": "~0.22.0", "http-browserify": "^1.7.0", "https-browserify": "^1.0.0", - "i18n-js": "^4.1.1", + "i18n-js": "^4.3.2", "int64-buffer": "^1.0.1", "isomorphic-webcrypto": "^2.3.8", "js-sha512": "^0.8.0", @@ -73,8 +73,8 @@ "jsonwebtoken": "^8.5.1", "linkify-it": "^3.0.2", "lodash": "^4.17.21", - "lottie-ios": "3.2.3", - "lottie-react-native": "5.1.3", + "lottie-ios": "4.3.0", + "lottie-react-native": "6.3.1", "memoize-one": "^6.0.0", "node-html-parser": "^6.1.4", "node-libs-browser": "^2.2.1", @@ -83,7 +83,7 @@ "qrcode": "^1.4.4", "query-string": "^7.0.1", "react": "18.2.0", - "react-native": "0.71.7", + "react-native": "0.72.6", "react-native-apk-install": "0.1.0", "react-native-camera": "^4.2.1", "react-native-config": "^1.5.1", @@ -110,6 +110,7 @@ "react-native-randombytes": "^3.6.1", "react-native-rate": "^1.2.6", "react-native-reanimated": "^3.5.2", + "react-native-reanimated-carousel": "^3.5.1", "react-native-restart": "^0.0.27", "react-native-safe-area-context": "4.5.0", "react-native-screens": "3.25.0", @@ -142,9 +143,9 @@ "devDependencies": { "@babel/core": "^7.20.0", "@babel/plugin-transform-flow-strip-types": "^7.22.5", - "@react-native-community/eslint-config": "^3.2.0", + "@react-native/eslint-config": "0.72.2", + "@tsconfig/react-native": "^3.0.0", "@types/bluebird": "^3.5.36", - "@types/i18n-js": "^3.8.2", "@types/jest": "^29.2.1", "@types/lodash": "^4.14.172", "@types/react": "~18.0.24", @@ -155,7 +156,7 @@ "eslint": "^8.19.0", "expo-yarn-workspaces": "^2.1.0", "jest": "^29.2.1", - "metro-react-native-babel-preset": "0.73.8", + "metro-react-native-babel-preset": "0.76.8", "patch-package": "^6.4.7", "postinstall-postinstall": "^2.1.0", "prettier": "^2.8.7", @@ -163,16 +164,8 @@ "react-test-renderer": "18.2.0", "typescript": "^4.9.4" }, - "jest": { - "preset": "react-native", - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "jsx", - "json", - "node" - ] + "engines": { + "node": ">=16" }, "expo": { "autolinking": { diff --git a/packages/mobile/src/core/CheckSecretWords/CheckSecretWords.tsx b/packages/mobile/src/core/CheckSecretWords/CheckSecretWords.tsx index 1534bb95c..17b300e9d 100644 --- a/packages/mobile/src/core/CheckSecretWords/CheckSecretWords.tsx +++ b/packages/mobile/src/core/CheckSecretWords/CheckSecretWords.tsx @@ -209,6 +209,7 @@ export const CheckSecretWords: FC = () => { {isConfigInputShown && ( ( ); +const countriesList = getCountries(); + +const AUTO_COUNTRY = { + ...countriesList.find((item) => item.code === getCountry())!, + code: 'AUTO', +}; + +const ALL_REGIONS = { + code: '*', + name: t('all_regions'), + flag: '🌍', +}; + const RenderItem = ({ item, isFirst, isLast, }: { - item: { code: string; name: string }; - isFirst: boolean; - isLast: boolean; + item: { code: string; name: string; flag: string }; + isFirst?: boolean; + isLast?: boolean; }) => { const setSelectedCountry = useMethodsToBuyStore( (state) => state.actions.setSelectedCountry, @@ -41,29 +56,56 @@ const RenderItem = ({ styles.containerListItem, ]; + const title = item.code === 'AUTO' ? t('choose_country.auto') : item.name; + let label: string | undefined; + + if (item.code === 'NOKYC') { + label = t('nokyc'); + } + + if (item.code === 'AUTO') { + label = item.name; + } + return ( + {item.flag} + + } onPress={handleSelectCountry} - title={item.name} + title={title} + label={ + label ? ( + + + {label} + + + ) : undefined + } /> ); }; -const countriesList = getCountries(); - export const ChooseCountry: React.FC = () => { const { bottom: bottomInset } = useSafeAreaInsets(); - const selectedCountry = useMethodsToBuyStore((state) => state.selectedCountry); - const listRef = useRef(); - const selectedCountryIndex = useMemo( - () => countriesList.findIndex((country) => country.code === selectedCountry), - [selectedCountry], - ); const [searchValue, setSearchValue] = React.useState(''); + const lastUsedCountriesCodes = useMethodsToBuyStore((state) => state.lastUsedCountries); + + const lastUsedCountries = useMemo( + () => + lastUsedCountriesCodes.map( + (code) => countriesList.find((country) => country.code === code)!, + ), + [lastUsedCountriesCodes], + ); + const filteredListBySearch = useMemo(() => { if (searchValue) { return countriesList.filter((country) => { @@ -74,11 +116,21 @@ export const ChooseCountry: React.FC = () => { return countriesList; }, [searchValue]); + const [searchFocused, setSearchFocused] = useState(false); + + const searchActive = searchFocused || searchValue.length > 0; + const searchNavBar = useCallback( (scrollY) => ( - + ), - [searchValue, setSearchValue], + [searchActive, searchValue], ); return ( @@ -87,16 +139,33 @@ export const ChooseCountry: React.FC = () => { - + {t('choose_country.empty_placeholder')} } - initialScrollIndex={selectedCountryIndex} + ListHeaderComponent={ + !searchActive ? ( + <> + + + + {lastUsedCountries.map((item, index) => ( + + + + + ))} + + + ) : null + } drawDistance={750} ItemSeparatorComponent={ListSeparatorItem} keyExtractor={(item) => item.code} - ref={listRef} contentContainerStyle={{ paddingBottom: bottomInset + 16 }} estimatedItemSize={CELL_SIZE} renderItem={({ item, index }) => ( @@ -134,4 +203,23 @@ const styles = Steezy.create(({ corners, colors }) => ({ separatorContainer: { marginHorizontal: 16, }, + flagContainer: { + width: 28, + height: 28, + alignItems: 'center', + justifyContent: 'center', + marginTop: isAndroid ? -2 : -4, + position: 'relative', + }, + flag: { + top: 0, + fontSize: isAndroid ? 22 : 28, + position: 'absolute', + }, + labelContainer: { + flex: 1, + }, + labelText: { + marginLeft: 8, + }, })); diff --git a/packages/mobile/src/core/ChooseCountry/components/SearchNavBar.tsx b/packages/mobile/src/core/ChooseCountry/components/SearchNavBar.tsx index 7e508ab05..b4a83ac9e 100644 --- a/packages/mobile/src/core/ChooseCountry/components/SearchNavBar.tsx +++ b/packages/mobile/src/core/ChooseCountry/components/SearchNavBar.tsx @@ -1,14 +1,24 @@ -import React from 'react'; +import React, { useCallback, useRef } from 'react'; import { t } from '@tonkeeper/shared/i18n'; -import { SearchInput, Steezy, Text, TouchableOpacity } from '@tonkeeper/uikit'; +import { + SearchInput, + Spacer, + Steezy, + Text, + TextInputRef, + TouchableOpacity, +} from '@tonkeeper/uikit'; import Animated, { SharedValue, useAnimatedStyle } from 'react-native-reanimated'; import { useTheme } from '$hooks/useTheme'; -import { goBack } from '$navigation/imperative'; +import { LayoutAnimation } from 'react-native'; +import { NavBar } from '$uikit'; export interface SearchNavBarProps { value: string; onChangeText: (value: string) => void; scrollY: SharedValue | undefined; + searchActive: boolean; + setSearchFocused: (value: boolean) => void; } export const SearchNavBar: React.FC = (props) => { @@ -20,15 +30,58 @@ export const SearchNavBar: React.FC = (props) => { }; }); + const { value, onChangeText, searchActive, setSearchFocused } = props; + + const inputRef = useRef(null); + + const handleFocus = useCallback(() => { + setSearchFocused(true); + LayoutAnimation.configureNext({ + ...LayoutAnimation.Presets.easeInEaseOut, + duration: 200, + }); + }, [setSearchFocused]); + + const handleBlur = useCallback(() => { + setSearchFocused(false); + LayoutAnimation.configureNext({ + ...LayoutAnimation.Presets.easeInEaseOut, + duration: 200, + }); + }, [setSearchFocused]); + + const handleCancelPress = useCallback(() => { + onChangeText(''); + inputRef.current?.blur(); + }, [onChangeText]); + return ( + {!searchActive ? ( + + {t('choose_country.title')} + + ) : ( + + )} - - - - {t('choose_country.cancel')} - - + + {searchActive ? ( + <> + + + + {t('choose_country.cancel')} + + + + ) : null} ); @@ -45,11 +98,14 @@ const styles = Steezy.create({ right: 0, marginBottom: 16, marginHorizontal: 16, - marginTop: 16, }, borderContainer: { zIndex: 2, borderBottomWidth: 0.5, borderBottomColor: 'transparent', }, + cancelContainer: { + padding: 16, + margin: -16, + }, }); diff --git a/packages/mobile/src/core/DAppsCategory/DAppsCategory.tsx b/packages/mobile/src/core/DAppsCategory/DAppsCategory.tsx new file mode 100644 index 000000000..cb3e7963a --- /dev/null +++ b/packages/mobile/src/core/DAppsCategory/DAppsCategory.tsx @@ -0,0 +1,69 @@ +import { BrowserStackRouteNames, openDAppsSearch } from '$navigation'; +import { useAppsListStore } from '$store'; +import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; +import React, { FC, memo, useCallback } from 'react'; +import type { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { Screen, Steezy, Text, View } from '@tonkeeper/uikit'; +import { shallow } from 'zustand/shallow'; +import { BrowserStackParamList } from '$navigation/BrowserStack/BrowserStack.interface'; +import { SearchButton } from '$core/DAppsExplore/components'; +import { List } from '$uikit/List/old/List'; +import { PopularAppCell } from '$core/DAppsExplore/components/PopularAppCell/PopularAppCell'; + +export type DAppsExploreProps = NativeStackScreenProps< + BrowserStackParamList, + BrowserStackRouteNames.Category +>; + +const DAppsCategoryComponent: FC = (props) => { + const { categoryId } = props.route.params; + + const tabBarHeight = useBottomTabBarHeight(); + + const category = useAppsListStore( + (s) => s.categories.find((item) => item.id === categoryId)!, + shallow, + ); + + const handleSearchPress = useCallback(() => { + openDAppsSearch(); + }, []); + + return ( + + + + + {category.apps.map((item, index) => ( + + ))} + + + + + + + ); +}; + +export const DAppsCategory = memo(DAppsCategoryComponent); + +const styles = Steezy.create(() => ({ + contentContainerStyle: { + paddingBottom: 0, + }, + searchBarContainer: { + padding: 16, + position: 'relative', + }, +})); diff --git a/packages/mobile/src/core/DAppsExplore/DAppsExplore.style.ts b/packages/mobile/src/core/DAppsExplore/DAppsExplore.style.ts deleted file mode 100644 index 25fe1e4c8..000000000 --- a/packages/mobile/src/core/DAppsExplore/DAppsExplore.style.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { IsTablet, TabletMaxWidth } from '$shared/constants'; -import styled, { css } from '$styled'; -import { ns } from '$utils'; -import Animated from 'react-native-reanimated'; - -export const NavBarSpacerHeight = IsTablet ? ns(8) : ns(28); - -export const Wrap = styled.View` - flex: 1; -`; - -export const ScrollViewContainer = styled.View<{ topInset: number }>` - flex: 1; - padding-top: ${({ topInset }) => topInset}px; -`; - -// Special width for tablets or big devices -export const Content = styled.View` - ${() => - IsTablet && - css` - width: ${TabletMaxWidth}px; - `} -`; - -export const ContentWrapper = styled.View` - ${() => - IsTablet && - css` - align-items: center; - `} -`; - -export const SearchBarContainer = styled.View<{ tabBarHeight: number }>` - margin-bottom: ${({ tabBarHeight }) => tabBarHeight}px; - padding: ${ns(16)}px ${ns(IsTablet ? 0 : 16)}px; - position: relative; -`; - -export const SearchBarDivider = styled(Animated.View)` - position: absolute; - left: 0; - right: 0; - top: -0.5px; - height: 0.5px; - background: ${({ theme }) => theme.colors.separatorCommon}; -`; - -export const TopTabsDivider = styled(Animated.View)` - position: absolute; - left: 0; - right: 0; - bottom: -0.5px; - height: 0.5px; - background: ${({ theme }) => theme.colors.separatorCommon}; -`; - -export const NavBarSmallSpacer = styled.View` - height: ${ns(20)}px; -`; - -export const NavBarSpacer = styled.View` - height: ${NavBarSpacerHeight}px; -`; \ No newline at end of file diff --git a/packages/mobile/src/core/DAppsExplore/DAppsExplore.tsx b/packages/mobile/src/core/DAppsExplore/DAppsExplore.tsx index 96f095f3d..766ebfbd1 100644 --- a/packages/mobile/src/core/DAppsExplore/DAppsExplore.tsx +++ b/packages/mobile/src/core/DAppsExplore/DAppsExplore.tsx @@ -1,285 +1,233 @@ -import { useTheme } from '$hooks/useTheme'; -import { DeeplinkOrigin, useDeeplinking } from '$libs/deeplinking'; -import { - openDAppsSearch, - openScanQR, - TabsStackRouteNames, -} from '$navigation'; -import { openRequireWalletModal } from '$core/ModalContainer/RequireWallet/RequireWallet'; -import { IsTablet, LargeNavBarHeight, TabletMaxWidth } from '$shared/constants'; -import { store, useAppsListStore } from '$store'; -import { Icon, LargeNavBar, NavBar } from '$uikit'; -import { useScrollHandler } from '$uikit/ScrollHandler/useScrollHandler'; -import { deviceWidth, ns } from '$utils'; +import { BrowserStackRouteNames, openChooseCountry, openDAppsSearch } from '$navigation'; +import { useAppsListStore, useConnectedAppsList } from '$store'; import { useFlags } from '$utils/flags'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; -import React, { FC, memo, useCallback, useEffect, useRef, useState } from 'react'; -import { LayoutChangeEvent, StyleSheet } from 'react-native'; -import { TouchableOpacity, ScrollView } from 'react-native-gesture-handler'; -import Animated, { - Extrapolation, - interpolate, - useAnimatedStyle, - useDerivedValue, -} from 'react-native-reanimated'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import React, { + FC, + memo, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; import { - ConnectedApps, - PopularApps, SearchButton, AboutDApps, - TopTabs, - TopTabsHeight, + FeaturedApps, + ConnectedApps, + AppsCategory, } from './components'; -import * as S from './DAppsExplore.style'; -import { NavBarSpacerHeight } from './DAppsExplore.style'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; -import { TabStackParamList } from '$navigation/MainStack/TabStack/TabStack.interface'; +import { + Button, + Screen, + SegmentedControl, + Steezy, + View, + isAndroid, + ns, +} from '@tonkeeper/uikit'; +import { shallow } from 'zustand/shallow'; +import { BrowserStackParamList } from '$navigation/BrowserStack/BrowserStack.interface'; import { t } from '@tonkeeper/shared/i18n'; - -const OFFSET = ns(16); - -const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView); +import { Text as RNText } from 'react-native'; +import { ScrollPositionContext } from '$uikit'; +import { useFocusEffect, useTabPress } from '@tonkeeper/router'; +import { useSelectedCountry } from '$store/zustand/methodsToBuy/useSelectedCountry'; export type DAppsExploreProps = NativeStackScreenProps< - TabStackParamList, - TabsStackRouteNames.Explore + BrowserStackParamList, + BrowserStackRouteNames.Explore >; -const DAppsExploreComponent: FC = (props) => { - const { initialCategory } = props.route?.params || {}; - const { setParams } = props.navigation; +const getSelectedCountryStyle = (selectedCountry: string) => { + if (selectedCountry === '*') { + return { + icon: ( + + 🌍 + + ), + type: 'emoji', + }; + } + if (selectedCountry === 'NOKYC') { + return { + icon: ( + + ☠️ + + ), + type: 'emoji', + }; + } + + return { title: selectedCountry, type: 'text' }; +}; +const DAppsExploreComponent: FC = (props) => { const flags = useFlags(['disable_dapps']); const tabBarHeight = useBottomTabBarHeight(); - const deeplinking = useDeeplinking(); - - const theme = useTheme(); - - const { categories } = useAppsListStore(); - const [connectedAppsHeight, setConnectedAppsHeight] = useState(0); - const [scrollViewHeight, setScrollViewHeight] = useState(0); - const [popularAppsHeight, setPopularAppsHeight] = useState(0); + const { navigation } = props; - const tabsSnapOffset = connectedAppsHeight + LargeNavBarHeight + 4; + const { changeEnd } = useContext(ScrollPositionContext); - const { top: topInset } = useSafeAreaInsets(); + const connectedApps = useConnectedAppsList(); + const selectedCountry = useSelectedCountry(); - const [activeCategory, setActiveCategory] = useState('featured'); - - useEffect(() => { - if (initialCategory) { - setActiveCategory(initialCategory); - setParams({ initialCategory: undefined }); - } - }, [initialCategory]); - - const { isSnapPointReached, scrollRef, scrollTop, scrollHandler } = useScrollHandler( - tabsSnapOffset, - true, + const featuredApps = useAppsListStore( + (s) => s.categories.find((category) => category.id === 'featured')?.apps || [], + shallow, ); - const handlePressOpenScanQR = React.useCallback(() => { - if (store.getState().wallet.wallet) { - openScanQR((str) => { - const resolver = deeplinking.getResolver(str, { - delay: 200, - origin: DeeplinkOrigin.QR_CODE, - }); - if (resolver) { - resolver(); + const filteredFeaturedApps = useMemo(() => { + return featuredApps.filter((app) => { + if (app.excludeCountries && app.excludeCountries.includes(selectedCountry)) { + return false; + } + if (app.includeCountries && !app.includeCountries.includes(selectedCountry)) { + return false; + } + + return true; + }); + }, [featuredApps, selectedCountry]); + + const categories = useAppsListStore((s) => s.categories || [], shallow); + + const filteredCategories = useMemo(() => { + return categories + .map((category) => ({ + ...category, + apps: category.apps.filter((app) => { + if (app.excludeCountries && app.excludeCountries.includes(selectedCountry)) { + return false; + } + if (app.includeCountries && !app.includeCountries.includes(selectedCountry)) { + return false; + } + return true; - } + }), + })) + .filter((category) => category.id !== 'featured' && category.apps.length > 0); + }, [categories, selectedCountry]); - return false; - }); - } else { - openRequireWalletModal(); - } - }, [deeplinking]); + const { + actions: { fetchPopularApps }, + } = useAppsListStore(); const handleSearchPress = useCallback(() => { openDAppsSearch(); }, []); - const handleScrollViewLayout = useCallback((event: LayoutChangeEvent) => { - setScrollViewHeight(event.nativeEvent.layout.height); - }, []); - - const handleConnectedAppsLayout = useCallback((event: LayoutChangeEvent) => { - setConnectedAppsHeight(event.nativeEvent.layout.height); - }, []); - - const contentHeight = popularAppsHeight + tabsSnapOffset + TopTabsHeight - OFFSET; + const selectedCountryStyle = getSelectedCountryStyle(selectedCountry); - const bottomDividerStyle = useAnimatedStyle(() => ({ - opacity: - contentHeight > scrollViewHeight && - scrollTop.value + scrollViewHeight < contentHeight - ? 1 - : 0, - })); + const [segmentIndex, setSegmentIndex] = useState(0); - const topTabsDividerStyle = useAnimatedStyle(() => ({ - opacity: scrollTop.value > tabsSnapOffset ? 1 : 0, - })); + const showConnected = segmentIndex === 1; - const navBarOpacity = useDerivedValue(() => - interpolate( - scrollTop.value, - [ - connectedAppsHeight + NavBarSpacerHeight, - connectedAppsHeight + NavBarSpacerHeight + LargeNavBarHeight / 3.5, - ], - [1, 0], - Extrapolation.CLAMP, - ), - ); - - const topTabsContainerStyle = useAnimatedStyle(() => ({ - backgroundColor: - navBarOpacity.value === 0 ? theme.colors.backgroundPrimary : 'transparent', - })); - - const tabScrollView = useRef(null); + useEffect(() => { + fetchPopularApps(); + }, [fetchPopularApps]); - const navBarRight = ( - - - + useFocusEffect( + useCallback(() => { + changeEnd(false); + }, [changeEnd]), ); - const isBigScreen = deviceWidth > TabletMaxWidth; + useTabPress(() => { + if (showConnected) { + setSegmentIndex(0); + } + }); return ( - - - 0 - ? scrollViewHeight + tabsSnapOffset - : undefined, - }} - scrollEventThrottle={16} - stickyHeaderIndices={[0, 3]} - // snapToOffsets={ - // isSnapPointReached || tabsSnapOffset > scrollViewHeight / 2 - // ? undefined - // : [tabsSnapOffset] - // } - > - {isBigScreen ? ( - - {t('browser.title')} - + + + } + > + setSegmentIndex(segment)} + index={segmentIndex} + items={[t('browser.explore'), t('browser.connected')]} + style={styles.segmentedControl} + indicatorStyle={styles.segmentedControlIndicator} + /> + + + + {flags.disable_dapps ? ( + ) : ( - - {t('browser.title')} - + <> + + {filteredCategories.map((category) => ( + + ))} + )} - - {!flags.disable_dapps ? ( - - - - - - ) : null} - {!flags.disable_dapps ? ( - - - - { - scrollRef.current?.scrollTo({ - y: Math.min(scrollTop.value, tabsSnapOffset), - animated: false, - }); - setActiveCategory(value); - }} - /> - - - - - ) : null} - {!flags.disable_dapps ? ( - - - setPopularAppsHeight(event.nativeEvent.layout.height) - } - > - { - if (step >= 2) { - tabScrollView.current?.scrollToEnd(); - } else { - tabScrollView.current?.scrollTo({ x: 0 }); - } - }} - activeCategory={activeCategory} - setActiveCategory={setActiveCategory} - /> - - - ) : null} - {flags.disable_dapps ? ( - - - - - - ) : null} - - - - - - - - - - - + + + + + + + + + ); }; export const DAppsExplore = memo(DAppsExploreComponent); -const styles = StyleSheet.create({ - scanButton: { - zIndex: 3, - marginRight: ns(2), +const styles = Steezy.create(({ colors }) => ({ + container: { + flex: 1, + }, + regionButton: { + marginRight: 16, + }, + segmentedControl: { + backgroundColor: 'transparent', + }, + segmentedControlIndicator: { + backgroundColor: colors.buttonSecondaryBackground, + }, + contentContainerStyle: { + paddingBottom: 0, + }, + hidden: { + height: 0, + overflow: 'hidden', + }, + searchBarContainer: { + padding: 16, + position: 'relative', }, -}); \ No newline at end of file +})); diff --git a/packages/mobile/src/core/DAppsExplore/components/AppsCategory/AppsCategory.tsx b/packages/mobile/src/core/DAppsExplore/components/AppsCategory/AppsCategory.tsx new file mode 100644 index 000000000..bd9d8a009 --- /dev/null +++ b/packages/mobile/src/core/DAppsExplore/components/AppsCategory/AppsCategory.tsx @@ -0,0 +1,111 @@ +import React, { FC, memo, useCallback, useMemo } from 'react'; +import { IAppCategory } from '$store'; +import { List } from '$uikit/List/old/List'; +import { PopularAppCell } from '../PopularAppCell/PopularAppCell'; +import { Spacer, Steezy, Text, TouchableOpacity, View, ns } from '@tonkeeper/uikit'; +import { t } from '@tonkeeper/shared/i18n'; +import { chunk } from 'lodash'; +import { useWindowDimensions } from 'react-native'; +import { ScrollView } from 'react-native-gesture-handler'; +import { useNavigation } from '@tonkeeper/router'; +import { BrowserStackRouteNames } from '$navigation'; +import { IsTablet } from '$shared/constants'; + +interface Props { + category: IAppCategory; +} + +const BUTTON_HIT_SLOP = { + top: 26, + bottom: 26, + left: 26, + right: 26, +}; + +const AppsCategoryComponent: FC = (props) => { + const { category } = props; + + const nav = useNavigation(); + + const chunks = useMemo(() => chunk(category.apps, 3), [category.apps]); + + const showAll = chunks.length > 1; + + const { width: windowWidth } = useWindowDimensions(); + + const tabletChunkWidth = + chunks.length > 2 ? (windowWidth - ns(56)) / 2 : (windowWidth - ns(40)) / 2; + + const chunkWidth = IsTablet ? tabletChunkWidth : windowWidth - ns(56); + + const singleChunkWidth = IsTablet ? (windowWidth - ns(40)) / 2 : windowWidth - ns(32); + + const handlePressAll = useCallback(() => { + nav.push(BrowserStackRouteNames.Category, { categoryId: category.id }); + }, [category.id, nav]); + + return ( + + + {category.title} + {showAll ? ( + + + {t('browser.apps_all')} + + + ) : null} + + + {chunks.map((apps, i) => ( + + {i > 0 ? : null} + 1 ? chunkWidth : singleChunkWidth }}> + + {apps.map((item, index) => ( + + ))} + + + + ))} + + + ); +}; + +const styles = Steezy.create(() => ({ + container: { + marginTop: 16, + }, + titleContainer: { + paddingHorizontal: 16, + paddingVertical: 14, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + contentContainerStyle: { + paddingHorizontal: 16, + }, +})); + +export const AppsCategory = memo(AppsCategoryComponent); diff --git a/packages/mobile/src/core/DAppsExplore/components/ConnectedApps/ConnectedApps.tsx b/packages/mobile/src/core/DAppsExplore/components/ConnectedApps/ConnectedApps.tsx index cc9aa065b..4fe69b61d 100644 --- a/packages/mobile/src/core/DAppsExplore/components/ConnectedApps/ConnectedApps.tsx +++ b/packages/mobile/src/core/DAppsExplore/components/ConnectedApps/ConnectedApps.tsx @@ -1,36 +1,68 @@ import React, { FC, memo } from 'react'; -import { useConnectedAppsList } from '$store'; +import { IConnectedApp, useConnectedAppsList } from '$store'; import { AppsList } from '../AppsList/AppsList'; -import { Alert } from 'react-native'; +import { Alert, useWindowDimensions } from 'react-native'; import { TonConnect } from '$tonconnect'; import { t } from '@tonkeeper/shared/i18n'; +import { ScreenHeaderHeight } from '@tonkeeper/uikit/src/containers/Screen/utils/constants'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Spacer, Steezy, Text, View, ns } from '@tonkeeper/uikit'; +import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; -const ConnectedAppsComponent: FC = () => { +interface Props { + connectedApps: IConnectedApp[]; +} - const connectedApps = useConnectedAppsList(); +const ConnectedAppsComponent: FC = (props) => { + const { connectedApps } = props; - if (connectedApps.length > 0) { + const { height: windowHeight } = useWindowDimensions(); + const safeArea = useSafeAreaInsets(); + const tabBarHeight = useBottomTabBarHeight(); + + const height = + windowHeight - ScreenHeaderHeight - safeArea.top - tabBarHeight - ns(48) - ns(16) * 2; + + if (connectedApps.length === 0) { return ( - - Alert.alert(t('browser.remove_alert.title', { name }), '', [ - { - text: t('cancel'), - style: 'cancel', - }, - { - text: t('browser.remove_alert.approve_button'), - style: 'destructive', - onPress: () => TonConnect.disconnect(url), - }, - ]) - } - /> + + + {t('browser.connected_empty_title')} + + + + {t('browser.connected_empty_text')} + + ); } - return null; + return ( + + Alert.alert(t('browser.remove_alert.title', { name }), '', [ + { + text: t('cancel'), + style: 'cancel', + }, + { + text: t('browser.remove_alert.approve_button'), + style: 'destructive', + onPress: () => TonConnect.disconnect(url), + }, + ]) + } + /> + ); }; +const styles = Steezy.create(() => ({ + emptyContainer: { + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 32, + }, +})); + export const ConnectedApps = memo(ConnectedAppsComponent); diff --git a/packages/mobile/src/core/DAppsExplore/components/FeaturedApps/FeaturedApps.tsx b/packages/mobile/src/core/DAppsExplore/components/FeaturedApps/FeaturedApps.tsx new file mode 100644 index 000000000..241ec6ae9 --- /dev/null +++ b/packages/mobile/src/core/DAppsExplore/components/FeaturedApps/FeaturedApps.tsx @@ -0,0 +1,126 @@ +import { openDAppBrowser } from '$navigation'; +import { getServerConfig } from '$shared/constants'; +import { IAppMetadata } from '$store'; +import { getRandomInt } from '$utils'; +import { trackEvent } from '$utils/stats'; +import { Picture, Spacer, Steezy, Text, View, ns } from '@tonkeeper/uikit'; +import { FC, memo, useCallback, useRef } from 'react'; +import { useWindowDimensions } from 'react-native'; +import { TouchableWithoutFeedback } from 'react-native-gesture-handler'; +import Carousel from 'react-native-reanimated-carousel'; + +interface Props { + items: IAppMetadata[]; +} + +interface ItemProps { + metadata: IAppMetadata; +} + +const CarouselItem = memo(({ metadata }) => { + const handlePress = useCallback(() => { + const { url, name } = metadata; + + trackEvent('click_dapp', { url, name }); + + openDAppBrowser(url); + }, [metadata]); + + return ( + + + + + + + + + {metadata.name} + + + {metadata.description} + + + + + + + ); +}); + +const FeaturedAppsComponent: FC = (props) => { + const { items } = props; + + const { width } = useWindowDimensions(); + + const initialIndex = useRef(getRandomInt(0, items.length - 1)).current; + + return ( + } + /> + ); +}; + +const styles = Steezy.create(({ colors, corners }) => ({ + wrapper: { + flex: 1, + paddingHorizontal: 4, + transform: [{ translateX: 12 }], + }, + container: { + flex: 1, + borderRadius: corners.medium, + overflow: 'hidden', + backgroundColor: colors.backgroundContent, + }, + content: { + width: '100%', + height: '100%', + justifyContent: 'flex-end', + paddingHorizontal: 16, + paddingVertical: 8, + position: 'relative', + }, + contentRow: { + flexDirection: 'row', + alignItems: 'center', + }, + icon: { + width: 44, + height: 44, + borderRadius: corners.small, + }, + textContainer: { + flex: 1, + justifyContent: 'center', + height: 60, + }, + description: { + opacity: 0.76, + }, +})); + +export const FeaturedApps = memo(FeaturedAppsComponent); diff --git a/packages/mobile/src/core/DAppsExplore/components/PopularAppCell/PopularAppCell.tsx b/packages/mobile/src/core/DAppsExplore/components/PopularAppCell/PopularAppCell.tsx index 51b57088d..95e92ca2c 100644 --- a/packages/mobile/src/core/DAppsExplore/components/PopularAppCell/PopularAppCell.tsx +++ b/packages/mobile/src/core/DAppsExplore/components/PopularAppCell/PopularAppCell.tsx @@ -1,8 +1,9 @@ import { openDAppBrowser } from '$navigation'; -import { Icon, Separator } from '$uikit'; +import { Icon } from '$uikit'; import { trackEvent } from '$utils/stats'; import React, { FC, memo, useCallback } from 'react'; import * as S from './popularAppCell.style'; +import { ListSeparator, View } from '@tonkeeper/uikit'; // const moreIconSource = require('./more.png'); @@ -43,9 +44,9 @@ const PopularAppCellComponent: FC = (props) => { - {separator ? : null} + {separator ? : null} ); }; -export const PopularAppCell = memo(PopularAppCellComponent); \ No newline at end of file +export const PopularAppCell = memo(PopularAppCellComponent); diff --git a/packages/mobile/src/core/DAppsExplore/components/PopularAppCell/popularAppCell.style.ts b/packages/mobile/src/core/DAppsExplore/components/PopularAppCell/popularAppCell.style.ts index 5e029b20c..e41d85f16 100644 --- a/packages/mobile/src/core/DAppsExplore/components/PopularAppCell/popularAppCell.style.ts +++ b/packages/mobile/src/core/DAppsExplore/components/PopularAppCell/popularAppCell.style.ts @@ -5,9 +5,7 @@ import FastImage from 'react-native-fast-image'; const ICON_SIZE = 44; -export const CellContainer = styled.View` - background: ${({ theme }) => theme.colors.backgroundSecondary}; -`; +export const CellContainer = styled.View``; export const Cell = styled(Highlight).attrs({ useRNGHComponent: true })` position: relative; @@ -16,7 +14,8 @@ export const Cell = styled(Highlight).attrs({ useRNGHComponent: true })` export const Container = styled.View` flex-direction: row; align-items: center; - padding: ${ns(16)}px; + padding: 0 ${ns(16)}px; + height: ${ns(76)}px; overflow: hidden; `; @@ -44,16 +43,16 @@ export const Content = styled.View` `; export const Title = styled(Text).attrs(() => ({ - variant: 'label1', + variant: 'label2', numberOfLines: 1, }))``; export const SubTitle = styled(Text).attrs(() => ({ - variant: 'body2', + variant: 'body3Alt', numberOfLines: 2, color: 'foregroundSecondary', }))``; export const ChervonContainer = styled.View` margin-left: ${ns(16)}px; -`; \ No newline at end of file +`; diff --git a/packages/mobile/src/core/DAppsExplore/components/PopularApps/PopularApps.style.ts b/packages/mobile/src/core/DAppsExplore/components/PopularApps/PopularApps.style.ts deleted file mode 100644 index 253377fae..000000000 --- a/packages/mobile/src/core/DAppsExplore/components/PopularApps/PopularApps.style.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IsTablet, TabletMaxWidth } from '$shared/constants'; -import styled, { css } from '$styled'; -import { ns } from '$utils'; - -export const Container = styled.View` - padding: 0 ${ns(IsTablet ? 0 : 16)}px; - margin-bottom: ${ns(16)}px; -`; - -// Special width for tablets or big devices -export const Content = styled.View` - ${() => - IsTablet && - css` - width: ${TabletMaxWidth}px; - `} -`; \ No newline at end of file diff --git a/packages/mobile/src/core/DAppsExplore/components/PopularApps/PopularApps.tsx b/packages/mobile/src/core/DAppsExplore/components/PopularApps/PopularApps.tsx deleted file mode 100644 index 0f66cf745..000000000 --- a/packages/mobile/src/core/DAppsExplore/components/PopularApps/PopularApps.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React, { FC, memo, useCallback, useEffect, useRef } from 'react'; -import { useAppsListStore } from '$store'; -import { openDAppBrowser } from '$navigation'; -import { List } from '$uikit/List/old/List'; -import { PopularAppCell } from '../PopularAppCell/PopularAppCell'; -import * as S from './PopularApps.style'; -import { StepView, StepViewItem, StepViewRef } from '$shared/components'; -import { t } from '@tonkeeper/shared/i18n'; - -interface Props { - activeCategory: string; - setActiveCategory: (activeCategory: string) => void; - onChangeStep: (index: number) => void; -} - -const PopularAppsComponent: FC = (props) => { - const { activeCategory, setActiveCategory, onChangeStep } = props; - - const stepViewRef = useRef(null); - - const { - categories, - moreEnabled, - moreUrl, - actions: { fetchPopularApps }, - } = useAppsListStore(); - - const handleChangeStep = useCallback( - (currentStepId: string | number, currentStepIndex: number) => { - setActiveCategory(currentStepId as string); - onChangeStep(currentStepIndex); - }, - [setActiveCategory], - ); - - useEffect(() => { - fetchPopularApps(); - }, [fetchPopularApps]); - - useEffect(() => { - stepViewRef.current?.go(activeCategory); - }, [activeCategory]); - - if (categories.length === 0) { - return null; - } - - return ( - <> - - {categories.map((category) => ( - - - - - {category.apps.map((item, index) => ( - - ))} - - - - - ))} - - {moreEnabled && moreUrl ? ( - - - - - - ) : null} - - ); -}; - -export const PopularApps = memo(PopularAppsComponent); \ No newline at end of file diff --git a/packages/mobile/src/core/DAppsExplore/components/index.ts b/packages/mobile/src/core/DAppsExplore/components/index.ts index ddc39018f..7de65a11f 100644 --- a/packages/mobile/src/core/DAppsExplore/components/index.ts +++ b/packages/mobile/src/core/DAppsExplore/components/index.ts @@ -3,4 +3,5 @@ export { SearchButton } from './SearchButton/SearchButton'; export { AboutDApps } from './AboutDApps/AboutDApps'; export { TopTabs } from './TopTabs/TopTabs'; export { TopTabsHeight } from './TopTabs/TopTabs.style'; -export { PopularApps } from './PopularApps/PopularApps'; +export { AppsCategory } from './AppsCategory/AppsCategory'; +export { FeaturedApps } from './FeaturedApps/FeaturedApps'; diff --git a/packages/mobile/src/core/DAppsSearch/hooks/useSearchSuggests.ts b/packages/mobile/src/core/DAppsSearch/hooks/useSearchSuggests.ts index 6cc42bfb3..77d625fe2 100644 --- a/packages/mobile/src/core/DAppsSearch/hooks/useSearchSuggests.ts +++ b/packages/mobile/src/core/DAppsSearch/hooks/useSearchSuggests.ts @@ -17,7 +17,8 @@ export const useSearchSuggests = (query: string) => { const searchSuggestsRef = useRef([]); const searchSuggests = useMemo(() => { - const trimmedQuery = query.trim().toLowerCase(); + const trimmedQuery = query.trim(); + const lowerCaseQuery = trimmedQuery.toLowerCase(); if (trimmedQuery.length === 0) { return []; @@ -38,23 +39,23 @@ export const useSearchSuggests = (query: string) => { app.name .toLowerCase() .split(' ') - .some((word) => word.startsWith(trimmedQuery)) || - getDomainFromURL(app.url).toLowerCase().startsWith(trimmedQuery), + .some((word) => word.startsWith(lowerCaseQuery)) || + getDomainFromURL(app.url).toLowerCase().startsWith(lowerCaseQuery), ) .sort((a, b) => { if ( - (a.name.toLowerCase().startsWith(trimmedQuery) && - !b.name.toLowerCase().startsWith(trimmedQuery)) || - (getDomainFromURL(a.url).toLowerCase().startsWith(trimmedQuery) && - !getDomainFromURL(b.url).toLowerCase().startsWith(trimmedQuery)) + (a.name.toLowerCase().startsWith(lowerCaseQuery) && + !b.name.toLowerCase().startsWith(lowerCaseQuery)) || + (getDomainFromURL(a.url).toLowerCase().startsWith(lowerCaseQuery) && + !getDomainFromURL(b.url).toLowerCase().startsWith(lowerCaseQuery)) ) { return -1; } if ( - (b.name.toLowerCase().startsWith(trimmedQuery) && - !a.name.toLowerCase().startsWith(trimmedQuery)) || - (getDomainFromURL(b.url).toLowerCase().startsWith(trimmedQuery) && - !getDomainFromURL(a.url).toLowerCase().startsWith(trimmedQuery)) + (b.name.toLowerCase().startsWith(lowerCaseQuery) && + !a.name.toLowerCase().startsWith(lowerCaseQuery)) || + (getDomainFromURL(b.url).toLowerCase().startsWith(lowerCaseQuery) && + !getDomainFromURL(a.url).toLowerCase().startsWith(lowerCaseQuery)) ) { return 1; } diff --git a/packages/mobile/src/core/NFT/ProgrammableButtons/ProgrammableButtons.tsx b/packages/mobile/src/core/NFT/ProgrammableButtons/ProgrammableButtons.tsx index 9e3223b6d..92fc8b597 100644 --- a/packages/mobile/src/core/NFT/ProgrammableButtons/ProgrammableButtons.tsx +++ b/packages/mobile/src/core/NFT/ProgrammableButtons/ProgrammableButtons.tsx @@ -113,14 +113,14 @@ const ProgrammableButtonsComponent = (props: ProgrammableButtonsProps) => { return ( - {buttons.map((button) => ( + {buttons.map((button, idx) => (