diff --git a/.gitignore b/.gitignore index 137ce2f8..9cb6864f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ MyTonWallet-opera.zip .vscode *.iml dev/perf/screenshot* +dev/locales/input.yaml +dev/locales/output.yaml .DS_store .DS_Store test-results diff --git a/capacitor.config.ts b/capacitor.config.ts index aca1515a..495c4ba7 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -1,4 +1,5 @@ import type { CapacitorConfig } from '@capacitor/cli'; +import type { KeyboardResize } from '@capacitor/keyboard'; const { APP_ENV = 'production' } = process.env; @@ -10,6 +11,7 @@ const COMMON_PLUGINS = [ '@capacitor/clipboard', '@capacitor/filesystem', '@capacitor/haptics', + '@capacitor/keyboard', '@capacitor/push-notifications', '@capacitor/share', '@capgo/capacitor-native-biometric', @@ -29,10 +31,6 @@ const IOS_PLUGINS = [ '@sina_kh/mtw-capacitor-splash-screen', ]; -const ANDROID_PLUGINS = [ - '@capacitor/keyboard', -]; - const config: CapacitorConfig = { appId: 'org.mytonwallet.app', appName: 'MyTonWallet', @@ -43,7 +41,7 @@ const config: CapacitorConfig = { }, android: { path: 'mobile/android', - includePlugins: COMMON_PLUGINS.concat(ANDROID_PLUGINS), + includePlugins: COMMON_PLUGINS, webContentsDebuggingEnabled: APP_ENV !== 'production', }, ios: { @@ -62,6 +60,11 @@ const config: CapacitorConfig = { PushNotifications: { presentationOptions: [], }, + Keyboard: { + // Needed to disable the automatic focus scrolling on iOS. The scroll is controlled manually by focusScroll.ts + // for a better focus scroll control. + resize: 'none' as KeyboardResize, + }, }, }; diff --git a/changelogs/3.2.6.txt b/changelogs/3.2.6.txt new file mode 100644 index 00000000..619f4cd5 --- /dev/null +++ b/changelogs/3.2.6.txt @@ -0,0 +1 @@ +Bug fixes and performance improvements diff --git a/dev/locales/README.md b/dev/locales/README.md new file mode 100644 index 00000000..ec9e5ab8 --- /dev/null +++ b/dev/locales/README.md @@ -0,0 +1,25 @@ +# i18n helpers + +Scripts for managing i18n translation keys and YAML files. + +## Features +1. **Extract Missing Translations**: Scans `src/` for `lang('key')` patterns and identifies missing translations in `src/i18n/`. +2. **Update Locales**: Merges new translations from `dev/locales/input.yaml` into YAML files in `src/i18n/`. + +## Usage + +### Extract Missing Translations +1. Run `npm run i18n:findMissing` + +2. Missing keys are written to `output.yaml`. + +### Update Locales +1. Add translations to `dev/locales/input.yaml`: + ```yaml + en: + $key: "English text" + de: + $key: "German text" + ``` + +2. Run `npm run i18n:update` diff --git a/dev/locales/config.js b/dev/locales/config.js new file mode 100644 index 00000000..562c6818 --- /dev/null +++ b/dev/locales/config.js @@ -0,0 +1,3 @@ +const i18nDir = './src/i18n'; + +module.exports = { i18nDir }; diff --git a/dev/locales/findMissingKeys.js b/dev/locales/findMissingKeys.js new file mode 100644 index 00000000..e36194b7 --- /dev/null +++ b/dev/locales/findMissingKeys.js @@ -0,0 +1,79 @@ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); +const { i18nDir } = require('./config'); + +const outputFilePath = path.resolve(__dirname, 'output.yaml'); +const srcDir = './src'; + +function extractLangKeys(dir) { + const langKeys = new Set(); + + function traverse(directory) { + const files = fs.readdirSync(directory); + + files.forEach((file) => { + const fullPath = path.join(directory, file); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + traverse(fullPath); + } else if (file.endsWith('.ts') || file.endsWith('.tsx') || file.endsWith('.js') || file.endsWith('.jsx')) { + const content = fs.readFileSync(fullPath, 'utf8'); + + const matches = content.matchAll(/lang\(\s*(['"`])((?:\\.|[^\\])*?)\1(?:,|\))/gs); + + for (const match of matches) { + let key = match[2]; + + key = key.replace(/\\'/g, "'").replace(/\\"/g, '"').replace(/\\n/g, '\n'); + + langKeys.add(key); + } + } + }); + } + + traverse(dir); + return langKeys; +} + +function findMissingTranslations(langKeys, i18nDir) { + const missingTranslations = {}; + + const localeFiles = fs.readdirSync(i18nDir).filter((file) => file.endsWith('.yaml')); + + localeFiles.forEach((file) => { + const lang = path.basename(file, '.yaml'); + const filePath = path.join(i18nDir, file); + const translations = yaml.load(fs.readFileSync(filePath, 'utf8')) || {}; + + langKeys.forEach((key) => { + if (!translations[key]) { + if (!missingTranslations[lang]) { + missingTranslations[lang] = {}; + } + missingTranslations[lang][key] = key; + } + }); + }); + + return missingTranslations; +} + +function writeMissingTranslations(missingTranslations, outputFilePath) { + const yamlContent = yaml.dump(missingTranslations, { noRefs: true, indent: 2 }); + fs.writeFileSync(outputFilePath, yamlContent, 'utf8'); + console.log(`Missing translations written to ${outputFilePath}`); +} + +(function main() { + console.log('Extracting lang keys from source code...'); + const langKeys = extractLangKeys(srcDir); + + console.log('Checking for missing translations...'); + const missingTranslations = findMissingTranslations(langKeys, i18nDir); + + console.log('Writing missing translations to output file...'); + writeMissingTranslations(missingTranslations, outputFilePath); +})(); diff --git a/dev/locales/updateLocales.js b/dev/locales/updateLocales.js new file mode 100644 index 00000000..1d6a74a2 --- /dev/null +++ b/dev/locales/updateLocales.js @@ -0,0 +1,46 @@ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); +const { i18nDir } = require('./config'); + +const inputFilePath = path.resolve(__dirname, 'input.yaml'); + +function updateYamlFiles() { + try { + const inputContent = fs.readFileSync(inputFilePath, 'utf8'); + const inputData = yaml.load(inputContent); + + for (const lang in inputData) { + const langData = inputData[lang]; + const yamlFilePath = path.join(i18nDir, `${lang}.yaml`); + + let existingData = {}; + + if (fs.existsSync(yamlFilePath)) { + const existingContent = fs.readFileSync(yamlFilePath, 'utf8'); + existingData = yaml.load(existingContent) || {}; + } + + const updatedData = { + ...existingData, + ...langData, + }; + + const updatedYaml = yaml.dump(updatedData, { + noRefs: true, + indent: 2, + lineWidth: -1, + quotingType: '"', + }); + fs.writeFileSync(yamlFilePath, updatedYaml, 'utf8'); + + console.log(`Updated: ${yamlFilePath}`); + } + + console.log('All YAML files have been updated.'); + } catch (error) { + console.error('Error updating YAML files:', error); + } +} + +updateYamlFiles(); diff --git a/mobile/android/app/capacitor.build.gradle b/mobile/android/app/capacitor.build.gradle index 6b5f33b4..f18dbeb0 100644 --- a/mobile/android/app/capacitor.build.gradle +++ b/mobile/android/app/capacitor.build.gradle @@ -16,6 +16,7 @@ dependencies { implementation project(':capacitor-clipboard') implementation project(':capacitor-filesystem') implementation project(':capacitor-haptics') + implementation project(':capacitor-keyboard') implementation project(':capacitor-push-notifications') implementation project(':capacitor-share') implementation project(':capgo-capacitor-native-biometric') @@ -28,7 +29,6 @@ dependencies { implementation project(':mtw-capacitor-usb-hid') implementation project(':native-bottom-sheet') implementation project(':native-dialog') - implementation project(':capacitor-keyboard') } diff --git a/mobile/android/capacitor.settings.gradle b/mobile/android/capacitor.settings.gradle index 10c42b68..b9fd1a4b 100644 --- a/mobile/android/capacitor.settings.gradle +++ b/mobile/android/capacitor.settings.gradle @@ -23,6 +23,9 @@ project(':capacitor-filesystem').projectDir = new File('../../node_modules/@capa include ':capacitor-haptics' project(':capacitor-haptics').projectDir = new File('../../node_modules/@capacitor/haptics/android') +include ':capacitor-keyboard' +project(':capacitor-keyboard').projectDir = new File('../../node_modules/@capacitor/keyboard/android') + include ':capacitor-push-notifications' project(':capacitor-push-notifications').projectDir = new File('../../node_modules/@capacitor/push-notifications/android') @@ -58,6 +61,3 @@ project(':native-bottom-sheet').projectDir = new File('../plugins/native-bottom- include ':native-dialog' project(':native-dialog').projectDir = new File('../plugins/native-dialog/android') - -include ':capacitor-keyboard' -project(':capacitor-keyboard').projectDir = new File('../../node_modules/@capacitor/keyboard/android') diff --git a/mobile/ios/App/Podfile b/mobile/ios/App/Podfile index e74ae4a5..376ec6a8 100644 --- a/mobile/ios/App/Podfile +++ b/mobile/ios/App/Podfile @@ -18,6 +18,7 @@ def capacitor_pods pod 'CapacitorClipboard', :path => '../../../node_modules/@capacitor/clipboard' pod 'CapacitorFilesystem', :path => '../../../node_modules/@capacitor/filesystem' pod 'CapacitorHaptics', :path => '../../../node_modules/@capacitor/haptics' + pod 'CapacitorKeyboard', :path => '../../../node_modules/@capacitor/keyboard' pod 'CapacitorPushNotifications', :path => '../../../node_modules/@capacitor/push-notifications' pod 'CapacitorShare', :path => '../../../node_modules/@capacitor/share' pod 'CapgoCapacitorNativeBiometric', :path => '../../../node_modules/@capgo/capacitor-native-biometric' diff --git a/mobile/ios/App/Podfile.lock b/mobile/ios/App/Podfile.lock index 1dcf561b..f8232df1 100644 --- a/mobile/ios/App/Podfile.lock +++ b/mobile/ios/App/Podfile.lock @@ -14,6 +14,8 @@ PODS: - Capacitor - CapacitorHaptics (6.0.1): - Capacitor + - CapacitorKeyboard (6.0.3): + - Capacitor - CapacitorMlkitBarcodeScanning (6.2.0): - Capacitor - GoogleMLKit/BarcodeScanning (= 5.0.0) @@ -152,6 +154,7 @@ DEPENDENCIES: - "CapacitorCordova (from `../../../node_modules/@capacitor/ios`)" - "CapacitorFilesystem (from `../../../node_modules/@capacitor/filesystem`)" - "CapacitorHaptics (from `../../../node_modules/@capacitor/haptics`)" + - "CapacitorKeyboard (from `../../../node_modules/@capacitor/keyboard`)" - "CapacitorMlkitBarcodeScanning (from `../../../node_modules/@capacitor-mlkit/barcode-scanning`)" - CapacitorNativeSettings (from `../../../node_modules/capacitor-native-settings`) - CapacitorPluginSafeArea (from `../../../node_modules/capacitor-plugin-safe-area`) @@ -207,6 +210,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/@capacitor/filesystem" CapacitorHaptics: :path: "../../../node_modules/@capacitor/haptics" + CapacitorKeyboard: + :path: "../../../node_modules/@capacitor/keyboard" CapacitorMlkitBarcodeScanning: :path: "../../../node_modules/@capacitor-mlkit/barcode-scanning" CapacitorNativeSettings: @@ -255,6 +260,7 @@ SPEC CHECKSUMS: CapacitorCordova: f48c89f96c319101cd2f0ce8a2b7449b5fb8b3dd CapacitorFilesystem: 37fb3aa5c945b4539ab11c74a5c57925a302bf24 CapacitorHaptics: fe689ade56ef20ec9b041a753c6da70c5d8ec9a9 + CapacitorKeyboard: 460c6f9ec5e52c84f2742d5ce2e67bbc7ab0ebb0 CapacitorMlkitBarcodeScanning: 178fb57424ec688b6a2fceee506ecc1ea00d1c8d CapacitorNativeSettings: 1ce5585ff07b161616cd0a795702637316677af2 CapacitorPluginSafeArea: e1eca7f70974f0e270d96f70cd0a5f51523164b1 @@ -289,6 +295,6 @@ SPEC CHECKSUMS: SinaKhMtwCapacitorStatusBar: e3fa73038e8dbac071751e1942eab65fc6c39a5f SwiftKeychainWrapper: 807ba1d63c33a7d0613288512399cd1eda1e470c -PODFILE CHECKSUM: ad14d80bb92e306162432517de945467c60b7527 +PODFILE CHECKSUM: fa64166a091f354ce503fdaf4d4b36e1cc75b934 COCOAPODS: 1.16.2 diff --git a/package-lock.json b/package-lock.json index d14643b0..9a2d15f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mytonwallet", - "version": "3.2.5", + "version": "3.2.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mytonwallet", - "version": "3.2.5", + "version": "3.2.6", "license": "GPL-3.0-or-later", "dependencies": { "@awesome-cordova-plugins/core": "6.9.0", diff --git a/package.json b/package.json index ad72760a..956b5665 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mytonwallet", - "version": "3.2.5", + "version": "3.2.6", "description": "The most feature-rich web wallet and browser extension for TON – with support of multi-accounts, tokens (jettons), NFT, TON DNS, TON Sites, TON Proxy, and TON Magic.", "main": "index.js", "scripts": { @@ -43,7 +43,9 @@ "giveaways:build": "webpack --config ./webpack-giveaways.config.ts && bash ./deploy/copy_to_dist.sh dist-giveaways", "giveaways:build:dev": "APP_ENV=development webpack --mode development --config ./webpack-giveaways.config.ts && bash ./deploy/copy_to_dist.sh dist-giveaways", "giveaways:dev": "APP_ENV=development webpack serve --mode development --port 4322 --config ./webpack-giveaways.config.ts", - "resolve-stacktrace": "node ./dev/resolveStackTrace.js" + "resolve-stacktrace": "node ./dev/resolveStackTrace.js", + "i18n:update": "node ./dev/locales/updateLocales.js", + "i18n:find-missing": "node ./dev/locales/findMissingKeys.js" }, "engines": { "node": "^22", diff --git a/public/static-sites/.gitignore b/public/static-sites/.gitignore index 749a535b..57ceaa32 100644 --- a/public/static-sites/.gitignore +++ b/public/static-sites/.gitignore @@ -1,3 +1,5 @@ index.css bg +images +common.js !_common/* diff --git a/public/static-sites/_common/common.js b/public/static-sites/_common/common.js new file mode 100644 index 00000000..0cf1b177 --- /dev/null +++ b/public/static-sites/_common/common.js @@ -0,0 +1,32 @@ +function getPlatform() { + const { + userAgent, + platform, + } = window.navigator; + + const iosPlatforms = ['iPhone', 'iPad', 'iPod']; + if ( + iosPlatforms.indexOf(platform) !== -1 + // For new IPads with M1 chip and IPadOS platform returns "MacIntel" + || (platform === 'MacIntel' && ('maxTouchPoints' in navigator && navigator.maxTouchPoints > 2)) + ) { + return 'iOS'; + } + + const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']; + if (macosPlatforms.indexOf(platform) !== -1) return 'macOS'; + + const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']; + if (windowsPlatforms.indexOf(platform) !== -1) return 'Windows'; + + if (/Android/.test(userAgent)) return 'Android'; + + if (/Linux/.test(platform)) return 'Linux'; + + return undefined; +} + +export let platform = getPlatform(); + +export const IS_DESKTOP = ['Windows', 'Linux', 'macOS'].includes(platform); +export const IS_MOBILE = !IS_DESKTOP; diff --git a/public/static-sites/_common/images/MacBook-mockup-1.5x.webp b/public/static-sites/_common/images/MacBook-mockup-1.5x.webp new file mode 100644 index 00000000..7efe7e19 Binary files /dev/null and b/public/static-sites/_common/images/MacBook-mockup-1.5x.webp differ diff --git a/public/static-sites/_common/images/MacBook-mockup-1x.webp b/public/static-sites/_common/images/MacBook-mockup-1x.webp new file mode 100644 index 00000000..32916899 Binary files /dev/null and b/public/static-sites/_common/images/MacBook-mockup-1x.webp differ diff --git a/public/static-sites/_common/images/MacBook-mockup-2x.webp b/public/static-sites/_common/images/MacBook-mockup-2x.webp new file mode 100644 index 00000000..c14367d3 Binary files /dev/null and b/public/static-sites/_common/images/MacBook-mockup-2x.webp differ diff --git a/public/static-sites/_common/images/QR-code-1.5x.webp b/public/static-sites/_common/images/QR-code-1.5x.webp new file mode 100644 index 00000000..aeb05474 Binary files /dev/null and b/public/static-sites/_common/images/QR-code-1.5x.webp differ diff --git a/public/static-sites/_common/images/QR-code-1x.webp b/public/static-sites/_common/images/QR-code-1x.webp new file mode 100644 index 00000000..22a6d240 Binary files /dev/null and b/public/static-sites/_common/images/QR-code-1x.webp differ diff --git a/public/static-sites/_common/images/QR-code-2x.webp b/public/static-sites/_common/images/QR-code-2x.webp new file mode 100644 index 00000000..d1bff43f Binary files /dev/null and b/public/static-sites/_common/images/QR-code-2x.webp differ diff --git a/public/static-sites/_common/index.css b/public/static-sites/_common/index.css index c6619a6e..49039834 100644 --- a/public/static-sites/_common/index.css +++ b/public/static-sites/_common/index.css @@ -57,6 +57,10 @@ body { } } +.hidden { + display: none !important; +} + .container { display: flex; flex-direction: column; @@ -74,10 +78,18 @@ body { box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } +.container.desktop { + max-width: unset; +} + .container.small-padding { padding: 1rem; } +.container.no-flex { + display: block; +} + .logo-container { display: flex; gap: 5px; @@ -133,7 +145,10 @@ body { color: #444; } -.info { +info-block { + display: block; + + box-sizing: border-box; margin-top: 28px; margin-bottom: 36px; @@ -141,6 +156,16 @@ body { color: #444444; } +/* All except last one */ +info-block:not(.with-gap > info-block):not(:last-of-type) { + margin-bottom: 0; +} + +/* All except first one */ +info-block:not(.with-gap > info-block) + info-block { + margin-top: 0; +} + .download-container { display: flex; gap: 20px; @@ -231,6 +256,11 @@ a:hover { border-radius: 1.25rem; } +.qr-code.without-margin { + margin: 0 !important; + margin-top: -16px !important; +} + .special-section { margin-top: 3rem; @@ -248,3 +278,62 @@ a:hover { margin-bottom: -1rem; margin-left: 0.625rem; } + +.with-separator { + position: relative; +} + +.subheader { + display: block; + + font-size: 20px; + font-weight: 700; + line-height: 1; + color: #222222; +} + +.with-separator::after { + content: ""; + + position: absolute; + top: calc(50% - 240px / 2); + left: 50%; + + display: block; + + width: 1px; + height: 240px; + + background-color: #CECECF; +} + +.column { + display: flex; + flex-direction: column; + gap: 16px; + align-items: center; + + min-width: 288px; + margin-top: 32px; + margin-bottom: 32px !important; + /* margin-bottom: 16px !important; */ +} + +.instruction { + margin-top: 0; + margin-bottom: 32px; +} + +.column > info-block:last-of-type { + margin-bottom: 0 !important; +} + +.columns { + display: flex; + gap: 32px; +} + +.dapp-open-block { + margin-top: -16px !important; + margin-bottom: 4px !important; +} diff --git a/public/static-sites/connect/desktop.html b/public/static-sites/connect/desktop.html new file mode 100644 index 00000000..e864aca4 --- /dev/null +++ b/public/static-sites/connect/desktop.html @@ -0,0 +1,69 @@ + + +
+ + +
+ Scan QR to open the app
on your mobile device:
+
+ 1. Launch the app and log into
your wallet.
+
+ 2. Once you have done it, proceed
by clicking the button below.
+
+ Once you connected your wallet, go back to dapp. +
+ + Return to dapp + +1. Launch the app and log into your wallet.
- Install MyTonWallet -
2. Once you launched MyTonWallet,
proceed by clicking the button below.
3. Once you connected your wallet,
go back to dapp.