diff --git a/index.html b/index.html index ade591734..3146b8bdc 100644 --- a/index.html +++ b/index.html @@ -22,13 +22,16 @@ var dark = t === 'Dark' || (t !== 'Light' && window.matchMedia('(prefers-color-scheme: dark)').matches) if (dark) { document.querySelector('meta[name="theme-color"]').setAttribute('content', '#101010') - document.documentElement.classList.add('ion-palette-dark') + document.documentElement.classList.add('palette-dark') } } catch (e) { if (location.hostname === 'localhost') console.warn('Theme bootstrap failed', e) } - + diff --git a/package.json b/package.json index 90725fe94..1b92c3374 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "@arkade-os/boltz-swap": "0.3.20", "@arkade-os/sdk": "0.4.19", "@branta-ops/branta": "0.0.9", - "@ionic/react": "^8.5.6", "@lendasat/lendasat-wallet-bridge": "^0.0.90", "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1", @@ -27,7 +26,8 @@ "qr": "^0.5.2", "qr-scanner": "^1.4.2", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-spring-bottom-sheet": "^3.4.1" }, "scripts": { "prepare": "husky install", diff --git a/playwright.config.ts b/playwright.config.ts index c38951366..d5f86abaf 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -30,7 +30,7 @@ export default defineConfig({ }, { name: 'Google Chrome', - use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + use: { ...devices['Desktop Chrome'], channel: 'chrome', viewport: { width: 1920, height: 1080 } }, }, ], }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1356bfd87..a35531f37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,6 @@ importers: '@branta-ops/branta': specifier: 0.0.9 version: 0.0.9 - '@ionic/react': - specifier: ^8.5.6 - version: 8.7.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@lendasat/lendasat-wallet-bridge': specifier: ^0.0.90 version: 0.0.90 @@ -80,6 +77,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-spring-bottom-sheet: + specifier: ^3.4.1 + version: 3.4.1(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: '@eslint/compat': specifier: ^1.3.2 @@ -585,15 +585,6 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead - '@ionic/core@8.7.8': - resolution: {integrity: sha512-GLWb/lz3kocpzTZTeQQ5xxoWz4CKHD6zpnbwJknTKsncebohAaw2KTe7uOw5toKQEDdohTseFuSGoDDBoRQ1Ug==} - - '@ionic/react@8.7.8': - resolution: {integrity: sha512-QRxGXcSkfmwVIFxdHI776bqiHpqT1FwwVNASBRPCD8RNCIT9NTZIvgNdJ2FokBZjHRfgk4QuYOcQntrbPmK0Hg==} - peerDependencies: - react: '>=16.8.6' - react-dom: '>=16.8.6' - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -610,6 +601,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + '@lendasat/lendasat-wallet-bridge@0.0.90': resolution: {integrity: sha512-VfgM5vUx81vVWlggtOReUkaC8VVOFVavIMixQM0G/ZI0L/nMi10fmkIBvXOVI4Jmsu9A5z6wHSUrERxD3a/Juw==} @@ -675,6 +669,18 @@ packages: engines: {node: '>=18'} hasBin: true + '@reach/portal@0.13.2': + resolution: {integrity: sha512-g74BnCdtuTGthzzHn2cWW+bcyIYb0iIE/yRsm89i8oNzNgpopbkh9UY8TPbhNlys52h7U60s4kpRTmcq+JqsTA==} + peerDependencies: + react: ^16.8.0 || 17.x + react-dom: ^16.8.0 || 17.x + + '@reach/utils@0.13.2': + resolution: {integrity: sha512-3ir6cN60zvUrwjOJu7C6jec/samqAeyAB12ZADK+qjnmQPdzSYldrFWwDVV5H0WkhbYXR3uh+eImu13hCetNPQ==} + peerDependencies: + react: ^16.8.0 || 17.x + react-dom: ^16.8.0 || 17.x + '@rolldown/pluginutils@1.0.0-beta.43': resolution: {integrity: sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==} @@ -692,21 +698,11 @@ packages: cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.34.9': - resolution: {integrity: sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==} - cpu: [arm64] - os: [darwin] - '@rollup/rollup-darwin-arm64@4.52.5': resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.34.9': - resolution: {integrity: sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==} - cpu: [x64] - os: [darwin] - '@rollup/rollup-darwin-x64@4.52.5': resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==} cpu: [x64] @@ -732,21 +728,11 @@ packages: cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.34.9': - resolution: {integrity: sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==} - cpu: [arm64] - os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.52.5': resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.34.9': - resolution: {integrity: sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==} - cpu: [arm64] - os: [linux] - '@rollup/rollup-linux-arm64-musl@4.52.5': resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} cpu: [arm64] @@ -777,21 +763,11 @@ packages: cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.34.9': - resolution: {integrity: sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==} - cpu: [x64] - os: [linux] - '@rollup/rollup-linux-x64-gnu@4.52.5': resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.34.9': - resolution: {integrity: sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==} - cpu: [x64] - os: [linux] - '@rollup/rollup-linux-x64-musl@4.52.5': resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} cpu: [x64] @@ -802,11 +778,6 @@ packages: cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.34.9': - resolution: {integrity: sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==} - cpu: [arm64] - os: [win32] - '@rollup/rollup-win32-arm64-msvc@4.52.5': resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==} cpu: [arm64] @@ -822,11 +793,6 @@ packages: cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.34.9': - resolution: {integrity: sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==} - cpu: [x64] - os: [win32] - '@rollup/rollup-win32-x64-msvc@4.52.5': resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==} cpu: [x64] @@ -892,16 +858,6 @@ packages: peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x - '@stencil/core@4.38.0': - resolution: {integrity: sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==} - engines: {node: '>=16.0.0', npm: '>=7.10.0'} - hasBin: true - - '@stencil/core@4.38.2': - resolution: {integrity: sha512-opyjA+DYAtaKmaSnuC8Bb/PH7nuO+1GhVn6amsN8XT+TlT9biptlcpz4YWETwYZ+XxtX+nLdxWbW0TVafrqsvQ==} - engines: {node: '>=16.0.0', npm: '>=7.10.0'} - hasBin: true - '@tanstack/react-virtual@3.13.19': resolution: {integrity: sha512-KzwmU1IbE0IvCZSm6OXkS+kRdrgW2c2P3Ho3NC+zZXWK6oObv/L+lcV/2VuJ+snVESRlMJ+w/fg4WXI/JzoNGQ==} peerDependencies: @@ -993,6 +949,9 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/warning@3.0.4': + resolution: {integrity: sha512-CqN8MnISMwQbLJXO3doBAV4Yw9hx9/Pyr2rZ78+NfaCnhyRA/nKrpyk6E7mKw17ZOaQdLpK9GiUjrqLzBlN3sg==} + '@typescript-eslint/eslint-plugin@8.46.2': resolution: {integrity: sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1096,6 +1055,18 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@xstate/react@1.6.3': + resolution: {integrity: sha512-NCUReRHPGvvCvj2yLZUTfR0qVp6+apc8G83oXSjN4rl89ZjyujiKrTff55bze/HrsvCsP/sUJASf2n0nzMF1KQ==} + peerDependencies: + '@xstate/fsm': ^1.0.0 + react: ^16.8.0 || ^17.0.0 + xstate: ^4.11.0 + peerDependenciesMeta: + '@xstate/fsm': + optional: true + xstate: + optional: true + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1208,6 +1179,9 @@ packages: resolution: {integrity: sha512-O1htyufFTYy3EO0JkHg2CLykdXEtV2ssqw47Gq9A0WByp662xpJnMEB9m43LZjsSDjIAOozWRExlFQk2hlV1XQ==} engines: {node: '>=4.5.0'} + body-scroll-lock@3.1.5: + resolution: {integrity: sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1610,6 +1584,9 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + focus-trap@6.9.4: + resolution: {integrity: sha512-v2NTsZe2FF59Y+sDykKY+XjqZ0cPfhq/hikWVL88BqLivnNiEffAsac6rP6H45ff9wG9LL5ToiDqrLEP9GX9mw==} + follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -1797,9 +1774,6 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - ionicons@8.0.13: - resolution: {integrity: sha512-2QQVyG2P4wszne79jemMjWYLp0DBbDhr4/yFroPCxvPP1wtMxgdIV3l5n+XZ5E9mgoXU79w7yTWpm2XzJsISxQ==} - is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2248,6 +2222,23 @@ packages: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} + react-spring-bottom-sheet@3.4.1: + resolution: {integrity: sha512-yDFqiPMm/fjefjnOe6Q9zxccbCl6HMUKsK5bWgfGHJIj4zmXVKio5d4icQvmOLuwpuCA2pwv4J6nGWS6fUZidQ==} + peerDependencies: + react: ^16.14.0 || 17 || 18 + + react-spring@8.0.27: + resolution: {integrity: sha512-nDpWBe3ZVezukNRandTeLSPcwwTMjNVu1IDq9qA/AMiUqHuRN4BeSWvKr3eIxxg1vtiYiOLy4FqdfCP5IoP77g==} + peerDependencies: + react: '>= 16.8.0' + react-dom: '>= 16.8.0' + + react-use-gesture@8.0.1: + resolution: {integrity: sha512-CXzUNkulUdgouaAlvAsC5ZVo0fi9KGSBSk81WrE4kOIcJccpANe9zZkAYr5YZZhqpicIFxitsrGVS4wmoMun9A==} + deprecated: This package is no longer maintained. Please use @use-gesture/react instead + peerDependencies: + react: '>= 16.8.0' + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -2443,6 +2434,9 @@ packages: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} + tabbable@5.3.3: + resolution: {integrity: sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -2552,6 +2546,25 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-subscription@1.12.0: + resolution: {integrity: sha512-MxN8IbiDehaybVEyPcEDxSKdhUyIw9hr+nPvJMj+XTW7kKnb8tqqY82jROTD29Y2mqHAizPWNB0aHNjOzuyN1A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + varuint-bitcoin@2.0.0: resolution: {integrity: sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==} @@ -2650,6 +2663,9 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -2718,6 +2734,9 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xstate@4.38.3: + resolution: {integrity: sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -3072,20 +3091,6 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} - '@ionic/core@8.7.8': - dependencies: - '@stencil/core': 4.38.0 - ionicons: 8.0.13 - tslib: 2.8.1 - - '@ionic/react@8.7.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@ionic/core': 8.7.8 - ionicons: 8.0.13 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - tslib: 2.8.1 - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3105,6 +3110,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@juggle/resize-observer@3.4.0': {} + '@lendasat/lendasat-wallet-bridge@0.0.90': {} '@marcbachmann/cel-js@7.3.1': {} @@ -3155,6 +3162,21 @@ snapshots: dependencies: playwright: 1.56.1 + '@reach/portal@0.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@reach/utils': 0.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + + '@reach/utils@0.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@types/warning': 3.0.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + warning: 4.0.3 + '@rolldown/pluginutils@1.0.0-beta.43': {} '@rollup/pluginutils@4.2.1': @@ -3168,15 +3190,9 @@ snapshots: '@rollup/rollup-android-arm64@4.52.5': optional: true - '@rollup/rollup-darwin-arm64@4.34.9': - optional: true - '@rollup/rollup-darwin-arm64@4.52.5': optional: true - '@rollup/rollup-darwin-x64@4.34.9': - optional: true - '@rollup/rollup-darwin-x64@4.52.5': optional: true @@ -3192,15 +3208,9 @@ snapshots: '@rollup/rollup-linux-arm-musleabihf@4.52.5': optional: true - '@rollup/rollup-linux-arm64-gnu@4.34.9': - optional: true - '@rollup/rollup-linux-arm64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-arm64-musl@4.34.9': - optional: true - '@rollup/rollup-linux-arm64-musl@4.52.5': optional: true @@ -3219,24 +3229,15 @@ snapshots: '@rollup/rollup-linux-s390x-gnu@4.52.5': optional: true - '@rollup/rollup-linux-x64-gnu@4.34.9': - optional: true - '@rollup/rollup-linux-x64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-x64-musl@4.34.9': - optional: true - '@rollup/rollup-linux-x64-musl@4.52.5': optional: true '@rollup/rollup-openharmony-arm64@4.52.5': optional: true - '@rollup/rollup-win32-arm64-msvc@4.34.9': - optional: true - '@rollup/rollup-win32-arm64-msvc@4.52.5': optional: true @@ -3246,9 +3247,6 @@ snapshots: '@rollup/rollup-win32-x64-gnu@4.52.5': optional: true - '@rollup/rollup-win32-x64-msvc@4.34.9': - optional: true - '@rollup/rollup-win32-x64-msvc@4.52.5': optional: true @@ -3333,28 +3331,6 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 18.3.1 - '@stencil/core@4.38.0': - optionalDependencies: - '@rollup/rollup-darwin-arm64': 4.34.9 - '@rollup/rollup-darwin-x64': 4.34.9 - '@rollup/rollup-linux-arm64-gnu': 4.34.9 - '@rollup/rollup-linux-arm64-musl': 4.34.9 - '@rollup/rollup-linux-x64-gnu': 4.34.9 - '@rollup/rollup-linux-x64-musl': 4.34.9 - '@rollup/rollup-win32-arm64-msvc': 4.34.9 - '@rollup/rollup-win32-x64-msvc': 4.34.9 - - '@stencil/core@4.38.2': - optionalDependencies: - '@rollup/rollup-darwin-arm64': 4.34.9 - '@rollup/rollup-darwin-x64': 4.34.9 - '@rollup/rollup-linux-arm64-gnu': 4.34.9 - '@rollup/rollup-linux-arm64-musl': 4.34.9 - '@rollup/rollup-linux-x64-gnu': 4.34.9 - '@rollup/rollup-linux-x64-musl': 4.34.9 - '@rollup/rollup-win32-arm64-msvc': 4.34.9 - '@rollup/rollup-win32-x64-msvc': 4.34.9 - '@tanstack/react-virtual@3.13.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/virtual-core': 3.13.19 @@ -3458,6 +3434,8 @@ snapshots: '@types/trusted-types@2.0.7': optional: true + '@types/warning@3.0.4': {} + '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -3611,6 +3589,16 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + '@xstate/react@1.6.3(@types/react@18.3.26)(react@18.3.1)(xstate@4.38.3)': + dependencies: + react: 18.3.1 + use-isomorphic-layout-effect: 1.2.1(@types/react@18.3.26)(react@18.3.1) + use-subscription: 1.12.0(react@18.3.1) + optionalDependencies: + xstate: 4.38.3 + transitivePeerDependencies: + - '@types/react' + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -3739,6 +3727,8 @@ snapshots: bip68@1.0.4: {} + body-scroll-lock@3.1.5: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -4286,6 +4276,10 @@ snapshots: flatted@3.3.3: {} + focus-trap@6.9.4: + dependencies: + tabbable: 5.3.3 + follow-redirects@1.15.11(debug@4.4.3): optionalDependencies: debug: 4.4.3 @@ -4468,10 +4462,6 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 - ionicons@8.0.13: - dependencies: - '@stencil/core': 4.38.2 - is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -4918,6 +4908,33 @@ snapshots: react-refresh@0.18.0: {} + react-spring-bottom-sheet@3.4.1(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@juggle/resize-observer': 3.4.0 + '@reach/portal': 0.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@xstate/react': 1.6.3(@types/react@18.3.26)(react@18.3.1)(xstate@4.38.3) + body-scroll-lock: 3.1.5 + focus-trap: 6.9.4 + react: 18.3.1 + react-spring: 8.0.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-use-gesture: 8.0.1(react@18.3.1) + xstate: 4.38.3 + transitivePeerDependencies: + - '@types/react' + - '@xstate/fsm' + - react-dom + + react-spring@8.0.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.2 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-use-gesture@8.0.1(react@18.3.1): + dependencies: + react: 18.3.1 + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -5185,6 +5202,8 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 + tabbable@5.3.3: {} + text-table@0.2.0: {} tinybench@2.9.0: {} @@ -5297,6 +5316,21 @@ snapshots: dependencies: punycode: 2.3.1 + use-isomorphic-layout-effect@1.2.1(@types/react@18.3.26)(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.26 + + use-subscription@1.12.0(react@18.3.1): + dependencies: + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) + + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + varuint-bitcoin@2.0.0: dependencies: uint8array-tools: 0.0.8 @@ -5401,6 +5435,10 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + webidl-conversions@7.0.0: {} whatwg-encoding@3.1.1: @@ -5474,6 +5512,8 @@ snapshots: xmlchars@2.2.0: {} + xstate@4.38.3: {} + yallist@3.1.1: {} yocto-queue@0.1.0: {} diff --git a/src/App.tsx b/src/App.tsx index f585a4941..d26e47ca6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,22 +1,5 @@ -import '@ionic/react/css/core.css' -/* Basic CSS for apps built with Ionic */ -import '@ionic/react/css/normalize.css' -import '@ionic/react/css/structure.css' -import '@ionic/react/css/typography.css' - -/* Optional CSS utils that can be commented out */ -import '@ionic/react/css/padding.css' -import '@ionic/react/css/float-elements.css' -import '@ionic/react/css/text-alignment.css' -import '@ionic/react/css/text-transformation.css' -import '@ionic/react/css/flex-utils.css' -import '@ionic/react/css/display.css' - -import '@ionic/react/css/palettes/dark.class.css' - import { AnimatePresence } from 'framer-motion' import { ConfigContext } from './providers/config' -import { IonApp, IonPage, IonTab, IonTabBar, IonTabButton, IonTabs, setupIonicReact } from '@ionic/react' import { NavigationContext, pageComponent, Pages, Tabs, type NavigationDirection } from './providers/navigation' import { useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { isInAppBrowser } from './lib/browser' @@ -29,21 +12,14 @@ import { AspContext } from './providers/asp' import { hapticLight } from './lib/haptics' import { setBootAnimActive as syncBootAnimFlag } from './lib/logoAnchor' import { PageTransition } from './components/PageTransition' -import SettingsIcon from './icons/Settings' import BootError from './components/BootError' import LoadingLogo from './components/LoadingLogo' import PillNavbarOverlay from './components/PillNavbarOverlay' -import FlexCol from './components/FlexCol' -import WalletIcon from './icons/Wallet' -import AppsIcon from './icons/Apps' -import Focusable from './components/Focusable' import { useReducedMotion } from './hooks/useReducedMotion' import { useLoadingStatus } from './hooks/useLoadingStatus' import { defaultPassword } from './lib/constants' import { consoleError } from './lib/logs' -setupIonicReact() - const PASSWORDLESS_AUTO_RELOAD_KEY = 'passwordless-auto-reload-attempted' export const appReloader = { reload: () => window.location.reload(), @@ -66,29 +42,6 @@ function PageAnimWrapper({ ) } -const animClass = 'tab-anim-pop' - -function AnimatedTabIcon({ children, animating }: { children: React.ReactNode; animating: boolean }) { - const ref = useRef(null) - - useEffect(() => { - if (!animating || !ref.current) return - const el = ref.current - el.classList.remove(animClass) - void el.offsetWidth // Force reflow so removing + re-adding the class triggers the animation - el.classList.add(animClass) - const handleEnd = () => el.classList.remove(animClass) - el.addEventListener('animationend', handleEnd) - return () => el.removeEventListener('animationend', handleEnd) - }, [animating]) - - return ( -
- {children} -
- ) -} - export default function App() { const { aspInfo } = useContext(AspContext) const { configLoaded } = useContext(ConfigContext) @@ -101,7 +54,6 @@ export default function App() { const isIAB = useMemo(() => isInAppBrowser(), []) const [isCapable, setIsCapable] = useState(false) const [jsCapabilitiesChecked, setJsCapabilitiesChecked] = useState(false) - const [animatingTab, setAnimatingTab] = useState(null) const [bootAnimActive, setBootAnimActive] = useState(false) // Syncs the external store before React re-renders, so Wallet reads // the correct value on the same frame LoadingLogo unmounts. @@ -112,10 +64,6 @@ export default function App() { const [bootAnimDone, setBootAnimDone] = useState(false) const [bootExitMode, setBootExitMode] = useState<'fly-to-target' | 'fly-up'>('fly-up') - // refs for the tabs to be able to programmatically activate them - const appsRef = useRef(null) - const walletRef = useRef(null) - const settingsRef = useRef(null) const passwordlessBootAttempted = useRef(false) const passwordlessReloadTimer = useRef>() @@ -158,52 +106,17 @@ export default function App() { if (authState === 'locked') return navigate(Pages.Unlock) }, [walletLoaded, wallet.pubkey, authState, initInfo, aspInfo.unreachable, jsCapabilitiesChecked, isCapable]) - // for some reason you need to manually set the active tab - // if you are coming from a page in a different tab - useEffect(() => { - switch (tab) { - case Tabs.Wallet: - walletRef.current?.setActive() - walletRef.current?.classList.remove('tab-hidden') - appsRef.current?.classList.add('tab-hidden') - settingsRef.current?.classList.add('tab-hidden') - break - case Tabs.Apps: - appsRef.current?.setActive() - appsRef.current?.classList.remove('tab-hidden') - walletRef.current?.classList.add('tab-hidden') - settingsRef.current?.classList.add('tab-hidden') - break - case Tabs.Settings: - settingsRef.current?.setActive() - settingsRef.current?.classList.remove('tab-hidden') - walletRef.current?.classList.add('tab-hidden') - appsRef.current?.classList.add('tab-hidden') - break - default: - break - } - }, [tab]) - - const triggerTabAnim = useCallback((tabName: string) => { - setAnimatingTab(null) - requestAnimationFrame(() => setAnimatingTab(tabName)) - }, []) - const handleWallet = () => { - triggerTabAnim('wallet') hapticLight() navigate(Pages.Wallet) } const handleApps = () => { - triggerTabAnim('apps') hapticLight() navigate(Pages.Apps) } const handleSettings = () => { - triggerTabAnim('settings') hapticLight() setOption(SettingsOptions.Menu) navigate(Pages.Settings) @@ -299,88 +212,12 @@ export default function App() { const showNavbar = page === screen && (screen === Pages.Wallet || screen === Pages.Apps || isSettingsRoot) return ( - - - {tab === Tabs.None ? ( -
- - - {comp} - - -
- ) : ( - <> - - -
- - {tab === Tabs.Wallet && ( - - {comp} - - )} - -
-
- -
- - {tab === Tabs.Apps && ( - - {comp} - - )} - -
-
- -
- - {tab === Tabs.Settings && ( - - {comp} - - )} - -
-
- - - - - - - - Wallet - - - - - - - - - - Apps - - - - - - - - - - Settings - - - - -
- - )} -
+
+ + + {comp} + + {tab !== Tabs.None && !bootAnimActive && ( ) ) : null} - +
) } diff --git a/src/components/AssetCard.tsx b/src/components/AssetCard.tsx index 13e3ca3d5..758179ec9 100644 --- a/src/components/AssetCard.tsx +++ b/src/components/AssetCard.tsx @@ -9,17 +9,27 @@ import { prettyNumber } from '../lib/format' interface AssetCardProps { assetId: string balance: number + darkPurple?: boolean decimals?: number icon?: string name?: string ticker?: string onClick?: () => void } -export default function AssetCard({ assetId, balance, decimals, icon, name, ticker, onClick }: AssetCardProps) { +export default function AssetCard({ + assetId, + balance, + darkPurple, + decimals, + icon, + name, + ticker, + onClick, +}: AssetCardProps) { const assetName = name || truncatedAssetId(assetId) || 'Asset name' const tokenTick = ticker ? ticker : 'TKN' return ( - + @@ -30,7 +40,9 @@ export default function AssetCard({ assetId, balance, decimals, icon, name, tick - {prettyNumber(centsToUnits(balance, decimals ?? 8))} + + {prettyNumber(centsToUnits(balance, decimals ?? 8))} + ) diff --git a/src/components/BootError.tsx b/src/components/BootError.tsx index fabc18f41..394bdb32e 100644 --- a/src/components/BootError.tsx +++ b/src/components/BootError.tsx @@ -22,7 +22,7 @@ export default function BootError() { style={{ position: 'fixed', inset: 0, - background: 'var(--ion-background-color, #fff)', + background: 'var(--background-color, #fff)', zIndex: 9, display: 'flex', alignItems: 'center', diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 76e49d066..9291fe958 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,4 +1,3 @@ -import { IonButton } from '@ionic/react' import { ReactElement, ReactNode, useCallback, useState } from 'react' import FlexRow from './FlexRow' import ArrowIcon from '../icons/Arrow' @@ -8,8 +7,10 @@ import PasteIcon from '../icons/Paste' import XIcon from '../icons/X' interface ButtonProps { + ariaLabel?: string children?: ReactNode clear?: boolean + copy?: boolean disabled?: boolean fancy?: boolean icon?: ReactElement @@ -20,11 +21,14 @@ interface ButtonProps { outline?: boolean red?: boolean secondary?: boolean + testId?: string } export default function Button({ + ariaLabel, children, clear, + copy, disabled, fancy, icon, @@ -35,11 +39,12 @@ export default function Button({ outline, red, secondary, + testId, }: ButtonProps) { const [pressed, setPressed] = useState(false) - const variant = red ? 'red' : secondary ? 'secondary' : clear ? 'clear' : outline ? 'outline' : 'dark' - const className = `${variant}${pressed ? ' pressed' : ''}` + const variant = red ? 'red' : secondary ? 'secondary' : clear ? 'clear' : outline ? 'outline' : copy ? 'copy' : 'dark' + const className = `button ${variant}${pressed ? ' pressed' : ''}` const handlePressStart = useCallback(() => { if (disabled || loading) return @@ -59,13 +64,15 @@ export default function Button({ ) return ( - : null)} )} - + ) } diff --git a/src/components/ButtonsOnBottom.tsx b/src/components/ButtonsOnBottom.tsx index c1e4793c8..39579e8e9 100644 --- a/src/components/ButtonsOnBottom.tsx +++ b/src/components/ButtonsOnBottom.tsx @@ -1,27 +1,16 @@ -import { IonFooter } from '@ionic/react' import { ReactNode } from 'react' import FlexCol from './FlexCol' interface ButtonsOnBottomProps { - bordered?: boolean children: ReactNode } -export default function ButtonsOnBottom({ bordered, children }: ButtonsOnBottomProps) { - const borderStyle = { - backgroundColor: 'var(--dark10)', - marginTop: '1rem', - width: '100%', - } - +export default function ButtonsOnBottom({ children }: ButtonsOnBottomProps) { return ( - <> - {bordered ?
: null} - - - {children} - - - +
+ + {children} + +
) } diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index bf8f3fdbd..6aa002f10 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -1,6 +1,7 @@ -import { IonCheckbox } from '@ionic/react' import FlexRow from './FlexRow' import { hapticLight } from '../lib/haptics' +import { useState } from 'react' +import Text from './Text' interface CheckboxProps { onChange: () => void @@ -8,11 +9,16 @@ interface CheckboxProps { } export default function Checkbox({ onChange, text }: CheckboxProps) { + const [checked, setChecked] = useState(false) + const handleChange = () => { + setChecked(!checked) hapticLight() onChange() } + const style: React.CSSProperties = { + display: 'block', border: '1px solid var(--dark50)', borderRadius: '0.5rem', margin: '0 2px', @@ -20,12 +26,33 @@ export default function Checkbox({ onChange, text }: CheckboxProps) { width: '100%', } return ( -
- - - {text} - +
+ + + {text}
) } + +const BoxIcon = ({ checked }: { checked: boolean }) => { + const color = checked ? 'var(--red)' : 'var(--dark50)' + const svgStyle: React.CSSProperties = { + background: color, + borderColor: color, + borderRadius: '6px', + padding: '2px', + } + const pathStyle: React.CSSProperties = { + fill: 'none', + strokeWidth: 2, + stroke: '#fff', + strokeDasharray: 30, + strokeDashoffset: 0, + } + return ( + + ) +} diff --git a/src/components/Content.tsx b/src/components/Content.tsx index de3510141..d8e8b1b7b 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,4 +1,3 @@ -import { IonContent } from '@ionic/react' import { ReactNode } from 'react' import Refresher from './Refresher' @@ -8,10 +7,11 @@ interface ContentProps { } export default function Content({ children, noFade }: ContentProps) { + const className = noFade ? 'content no-content-fade' : 'content' return ( - +
{children}
- +
) } diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index f302c8c3b..7698029e4 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -1,13 +1,12 @@ import { Component, type ErrorInfo, type ReactNode } from 'react' +import ButtonsOnBottom from './ButtonsOnBottom' +import Text, { TextSecondary } from './Text' +import CenterScreen from './CenterScreen' import * as Sentry from '@sentry/react' +import Content from './Content' import Button from './Button' -import { IonApp, IonPage } from '@ionic/react' import Padded from './Padded' -import Content from './Content' import Header from './Header' -import ButtonsOnBottom from './ButtonsOnBottom' -import Text, { TextSecondary } from './Text' -import CenterScreen from './CenterScreen' interface Props { children: ReactNode @@ -39,8 +38,8 @@ export default class ErrorBoundary extends Component { render() { if (this.state.hasError) { return ( - - +
+
@@ -54,8 +53,8 @@ export default class ErrorBoundary extends Component {
+
) } diff --git a/src/components/Grid.tsx b/src/components/Grid.tsx new file mode 100644 index 000000000..555ecd1a9 --- /dev/null +++ b/src/components/Grid.tsx @@ -0,0 +1,3 @@ +export default function Grid({ children }: { children: React.ReactNode }) { + return
{children}
+} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 83a242ff7..22bf3374e 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,4 +1,3 @@ -import { IonHeader, IonTitle } from '@ionic/react' import React, { useContext } from 'react' import { NavigationContext } from '../providers/navigation' import BackIcon from '../icons/Back' @@ -18,7 +17,7 @@ interface HeaderProps { text: string } -export default function Header({ auxAriaLabel, auxFunc, auxText, back, text, auxIcon, heading = true }: HeaderProps) { +export default function Header({ auxAriaLabel, auxFunc, auxText, back, text, auxIcon }: HeaderProps) { const { goBack } = useContext(NavigationContext) const handleBack = back @@ -46,7 +45,7 @@ export default function Header({ auxAriaLabel, auxFunc, auxText, back, text, aux } return ( - +
{handleBack ? ( @@ -59,14 +58,7 @@ export default function Header({ auxAriaLabel, auxFunc, auxText, back, text, aux '\u00A0' )}
- - {text} - +

{text}

{auxText || auxIcon ? ( @@ -77,6 +69,6 @@ export default function Header({ auxAriaLabel, auxFunc, auxText, back, text, aux )}
- +
) } diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 5ecf995e3..62a8bbf1e 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -1,6 +1,5 @@ import InputContainer from './InputContainer' -import { useRef, useEffect } from 'react' -import { IonInput } from '@ionic/react' +import { useRef, useEffect, ChangeEventHandler } from 'react' interface InputProps { focus?: boolean @@ -36,35 +35,36 @@ export default function Input({ value, }: InputProps) { const firstRun = useRef(true) - const input = useRef(null) + const input = useRef(null) useEffect(() => { if (focus && firstRun.current) { firstRun.current = false - input.current?.setFocus() + input.current?.focus() } }) - const handleInput = (ev: Event) => { - const v = (ev.target as HTMLInputElement).value + const handleChange: ChangeEventHandler = (ev) => { + const v = ev.currentTarget.value onChange(type === 'number' ? Number(v) : v) } return ( - ev.key === 'Enter' && onEnter && onEnter()} - placeholder={placeholder} ref={input} step={step} type={type} value={value} + className='input' data-testid={testId} + name={name ?? testId} + maxLength={maxLength} + onChange={handleChange} + placeholder={placeholder} + onKeyUp={(ev) => ev.key === 'Enter' && onEnter && onEnter()} /> ) diff --git a/src/components/InputAmount.tsx b/src/components/InputAmount.tsx index c080fe9fa..81ff6320a 100644 --- a/src/components/InputAmount.tsx +++ b/src/components/InputAmount.tsx @@ -1,5 +1,4 @@ -import { useContext, useEffect, useRef, useState } from 'react' -import { IonInput, IonText } from '@ionic/react' +import { ChangeEventHandler, useContext, useEffect, useRef, useState } from 'react' import { FiatContext } from '../providers/fiat' import InputContainer from './InputContainer' import { ConfigContext } from '../providers/config' @@ -9,6 +8,7 @@ import { LimitsContext } from '../providers/limits' import Focusable from './Focusable' import { unitsToCents } from '../lib/assets' import { AssetOption } from '../lib/types' +import { TextSecondary } from './Text' interface InputAmountProps { asset?: AssetOption @@ -52,11 +52,11 @@ export default function InputAmount({ const [error, setError] = useState('') const [otherValue, setOtherValue] = useState('') - const input = useRef(null) + const input = useRef(null) // focus input when focus prop changes useEffect(() => { - if (focus && input.current) input.current.setFocus() + if (focus && input.current) input.current.focus() }, [focus]) useEffect(() => { @@ -65,8 +65,8 @@ export default function InputAmount({ setError(sats ? (sats < 0 ? 'Invalid amount' : '') : '') }, [sats]) - const handleInput = (ev: Event) => { - const value = Number((ev.target as HTMLInputElement).value) + const handleInput: ChangeEventHandler = (ev) => { + const value = Number(ev.currentTarget.value) if (Number.isNaN(value)) return onSats(asset?.assetId ? unitsToCents(value, asset.decimals) : useFiat ? fromFiat(value) : value) } @@ -96,40 +96,37 @@ export default function InputAmount({ : '' return ( - <> - - ev.key === 'Enter' && onEnter && onEnter()} - readonly={readOnly} + + - {onMax && !disabled && !readOnly ? ( - - - Max - - - ) : null} - - + onFocus={onFocus} + className='input' + inputMode='decimal' + value={value || ''} + disabled={disabled} + readOnly={readOnly} + onChange={handleInput} + onKeyUp={(ev) => ev.key === 'Enter' && onEnter && onEnter()} + /> + {rightLabel} + + {onMax && !disabled && !readOnly ? ( + +

+ Max +

+
+ ) : null} + ) } diff --git a/src/components/InputContainer.tsx b/src/components/InputContainer.tsx index c43b843d1..7dd030cc3 100644 --- a/src/components/InputContainer.tsx +++ b/src/components/InputContainer.tsx @@ -47,7 +47,7 @@ export default function InputContainer({ {label || right ? : null} - {children} + {children} {bottomLeft || bottomRight ? : null} diff --git a/src/components/InputPassword.tsx b/src/components/InputPassword.tsx index e14a3b5ad..fcfcac750 100644 --- a/src/components/InputPassword.tsx +++ b/src/components/InputPassword.tsx @@ -1,7 +1,7 @@ +import { useRef, useEffect, useState } from 'react' import InputContainer from './InputContainer' -import { IonInput, IonInputPasswordToggle } from '@ionic/react' import { StrengthLabel } from './Strength' -import { useRef, useEffect } from 'react' +import PasswordIcon from '../icons/Password' interface InputPasswordProps { focus?: boolean @@ -13,26 +13,33 @@ interface InputPasswordProps { } export default function InputPassword({ focus, label, onChange, onEnter, strength, placeholder }: InputPasswordProps) { - const right = strength ? : undefined + const [visible, setVisible] = useState(false) - const input = useRef(null) + const input = useRef(null) // focus input when focus prop changes useEffect(() => { - if (focus && input.current) input.current.setFocus() + if (focus && input.current) input.current.focus() }, [focus, input.current]) + const right = strength ? : undefined + return ( - ev.key === 'Enter' && onEnter && onEnter()} - placeholder={placeholder} - ref={input} - type='password' - > - - + +
setVisible(!visible)} style={{ cursor: 'pointer', padding: '12px' }}> + +
) } diff --git a/src/components/InputWithScanner.tsx b/src/components/InputWithScanner.tsx index 6dbaeae81..e6ac1e67f 100644 --- a/src/components/InputWithScanner.tsx +++ b/src/components/InputWithScanner.tsx @@ -1,9 +1,9 @@ -import { IonInput, IonText } from '@ionic/react' import InputContainer from './InputContainer' -import { useRef, useEffect } from 'react' +import { useRef, useEffect, ChangeEventHandler } from 'react' import { hapticLight } from '../lib/haptics' import Paste from './Paste' import { ClearButtonOnInput, ScanButtonOnInput } from './Button' +import FlexRow from './FlexRow' interface InputWithScannerProps { error?: string @@ -30,14 +30,14 @@ export default function InputWithScanner({ validator, value, }: InputWithScannerProps) { - const input = useRef(null) + const input = useRef(null) useEffect(() => { - if (focus && input.current) input.current.setFocus() + if (focus && input.current) input.current.focus() }, [focus, input.current]) - const handleInput = (ev: Event) => { - onChange((ev.target as HTMLInputElement).value) + const handleChange: ChangeEventHandler = (ev) => { + onChange(ev.currentTarget.value) } const handlePaste = (data: string) => { @@ -58,25 +58,27 @@ export default function InputWithScanner({ return ( - ev.key === 'Enter' && onEnter && onEnter()} - > - + ) } diff --git a/src/components/Keyboard.tsx b/src/components/Keyboard.tsx index b2d92d4e6..775e26d40 100644 --- a/src/components/Keyboard.tsx +++ b/src/components/Keyboard.tsx @@ -1,4 +1,3 @@ -import { IonCol, IonGrid, IonRow } from '@ionic/react' import Header from './Header' import Content from './Content' import { useContext, useEffect, useState } from 'react' @@ -159,15 +158,22 @@ export default function Keyboard({ asset, back, hideBalance, onSave, value }: Ke const gridStyle = { borderTop: '1px solid var(--dark50)', marginTop: '0.5rem', - textAlign: 'center', width: '100%', } const rowStyle = { + display: 'flex', fontSize: '1.5rem', padding: '1rem', } + const keyStyle = { + flex: 1, + display: 'flex', + justifyContent: 'center', + cursor: 'pointer', + } + const keys = [ ['1', '2', '3'], ['4', '5', '6'], @@ -198,17 +204,17 @@ export default function Keyboard({ asset, back, hideBalance, onSave, value }: Ke )} - +
{keys.map((row) => ( - +
{row.map((key) => ( - handleKeyPress(key)}> +
handleKeyPress(key)}>

{key === 'x' ? <>← : key}

- +
))} - +
))} - +
+ + + + + ) } diff --git a/src/components/Select.tsx b/src/components/Select.tsx index b1fb7f277..1b92e9b9c 100644 --- a/src/components/Select.tsx +++ b/src/components/Select.tsx @@ -46,7 +46,7 @@ export default function Select({ labels, onChange, options, selected }: SelectPr {labels?.[index] ?? option} {option === selected && }
- {index < options.length - 1 &&
} + {index < options.length - 1 &&
}
))} diff --git a/src/components/SheetModal.tsx b/src/components/SheetModal.tsx index 1c4f2a63f..e47622900 100644 --- a/src/components/SheetModal.tsx +++ b/src/components/SheetModal.tsx @@ -1,5 +1,6 @@ -import { IonModal } from '@ionic/react' import { hapticLight } from '../lib/haptics' +import { BottomSheet } from 'react-spring-bottom-sheet' +import 'react-spring-bottom-sheet/dist/style.css' interface SheetModalProps { children?: React.ReactNode @@ -14,16 +15,11 @@ export default function SheetModal({ children, isOpen, onClose }: SheetModalProp } return ( - +
-
-
-
-
- {children} -
+
{children}
- + ) } @@ -42,16 +38,3 @@ const innerStyleWithSafeArea: React.CSSProperties = { ...innerStyle, paddingBottom: 'calc(2rem + env(safe-area-inset-bottom, 0px))', } - -const handleAreaStyle: React.CSSProperties = { - padding: '12px 0 20px', - cursor: 'grab', -} - -const handleStyle: React.CSSProperties = { - backgroundColor: 'var(--dark20)', - borderRadius: '100px', - height: '5px', - margin: '0 auto', - width: '40px', -} diff --git a/src/components/Strength.tsx b/src/components/Strength.tsx index 47b722ebb..d5f43dafe 100644 --- a/src/components/Strength.tsx +++ b/src/components/Strength.tsx @@ -1,4 +1,3 @@ -import { IonGrid, IonRow, IonCol, IonProgressBar } from '@ionic/react' import Text from './Text' import FlexRow from './FlexRow' @@ -32,35 +31,20 @@ export const StrengthLabel = ({ strength }: { strength: number }): JSX.Element = ) -export default function StrengthBars({ strength }: { strength: number }) { +export function StrengthBars({ strength }: { strength: number }) { const style = (col: number): React.CSSProperties => ({ backgroundColor: col < strength ? `var(--${getColor(strength)})` : '', - border: '1px solid var(--dark20)', - height: '0.5rem', + height: '4px', width: '100%', }) return ( - - - -
- - -
- - -
- - -
- - - +
+ + {[0, 1, 2, 3].map((col) => ( +
+ ))} + +
) } - -export function StrengthProgress({ strength }: { strength: number }) { - const color = getColor(strength) - return -} diff --git a/src/components/Text.tsx b/src/components/Text.tsx index 78ec4edb5..4efe3b953 100644 --- a/src/components/Text.tsx +++ b/src/components/Text.tsx @@ -1,4 +1,3 @@ -import { IonText } from '@ionic/react' import { ReactNode } from 'react' import { copyToClipboard } from '../lib/clipboard' import { useToast } from './Toast' @@ -80,11 +79,9 @@ export default function Text({ } return ( - -

- {children} -

-
+

+ {children} +

) } diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx index 589dd0f64..2fbd3a75a 100644 --- a/src/components/Toggle.tsx +++ b/src/components/Toggle.tsx @@ -1,5 +1,4 @@ import Text from './Text' -import { IonToggle } from '@ionic/react' import FlexRow from './FlexRow' import FlexCol from './FlexCol' import Focusable from './Focusable' @@ -14,17 +13,22 @@ interface ToggleProps { } export default function Toggle({ checked, onClick, text, subtext, testId }: ToggleProps) { - const handleClick = () => { + const handleChange = () => { hapticLight() onClick() } return ( - + {text} - - + +
+ +
{subtext ? ( diff --git a/src/components/Warning.tsx b/src/components/Warning.tsx index aa04a066e..1b1b8b4a9 100644 --- a/src/components/Warning.tsx +++ b/src/components/Warning.tsx @@ -15,11 +15,11 @@ export default function WarningBox({ green, red, text }: WarningProps) { const color = red || green ? 'white' : 'var(--orange)' const style: React.CSSProperties = { + color, + width: '100%', backgroundColor, borderRadius: '0.5rem', - color, padding: '0.75rem 1rem', - width: '100%', } return ( diff --git a/src/icons/Add.tsx b/src/icons/Add.tsx index dbd5a3cad..14f224115 100644 --- a/src/icons/Add.tsx +++ b/src/icons/Add.tsx @@ -3,7 +3,7 @@ export default function AddIcon({ reversed }: { reversed?: boolean }) { diff --git a/src/icons/Logo.tsx b/src/icons/Logo.tsx index 7f78a774b..3a4bc1e02 100644 --- a/src/icons/Logo.tsx +++ b/src/icons/Logo.tsx @@ -35,7 +35,7 @@ export default function LogoIcon({ small }: { small?: boolean }) { handleClick() } }} - style={{ cursor: 'pointer', width: size, height: size, padding: 18, margin: -18, boxSizing: 'content-box' }} + style={{ cursor: 'pointer', width: size, height: size, padding: 18, margin: -18 }} > {/* Original SVG paths — visible when settled, fading in during revert */} diff --git a/src/icons/Password.tsx b/src/icons/Password.tsx new file mode 100644 index 000000000..3ebcba54c --- /dev/null +++ b/src/icons/Password.tsx @@ -0,0 +1,26 @@ +export default function PasswordIcon({ visible = false }: { visible?: boolean }) { + if (visible) { + return ( + + ) + } else { + return ( + + ) + } +} diff --git a/src/icons/Share.tsx b/src/icons/Share.tsx index 2197b51c8..1bc9989c1 100644 --- a/src/icons/Share.tsx +++ b/src/icons/Share.tsx @@ -3,7 +3,7 @@ export default function ShareIcon({ reversed }: { reversed?: boolean }) { diff --git a/src/icons/Spinner.tsx b/src/icons/Spinner.tsx new file mode 100644 index 000000000..f983eaa62 --- /dev/null +++ b/src/icons/Spinner.tsx @@ -0,0 +1,47 @@ +export default function SpinnerIcon() { + return ( + + + + + + + + + + + + + + ) +} diff --git a/src/index.css b/src/index.css index 7742fd556..b9bd3cea9 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,86 @@ +:root { + --black: #040404; + --cream: #391998; + --dark80: rgba(34, 36, 40, 0.8); + --dark70: rgba(34, 36, 40, 0.7); + --dark50: rgba(34, 36, 40, 0.5); + --dark30: rgba(34, 36, 40, 0.3); + --dark20: rgba(34, 36, 40, 0.2); + --dark15: rgba(34, 36, 40, 0.15); + --dark10: rgba(34, 36, 40, 0.1); + --dark05: rgba(34, 36, 40, 0.05); + --green: #60b18a; + --greenbg: #054a19; + --grey: #747474; + --liz: #d5c6ff; + --logo-color: #391998; + --magenta: #8563e4; + --orange: #ff8e24; + --orangebg: #ff8e2418; + --purple: #391998; + --purple10: #39199815; + --purple20: #39199833; + --purplebg: #24154f; + --purpletext: #7043f4; + --btn-shadow-color: #1a0b4a; + --red: #a51515; + --redbg: #68222b; + --white: #fbfbfb; + --yellow: #e2c542; + --yellowoutlier: #debb20; + --fade-rgb: 255, 255, 255; + --background-color: #fff; + --background-focused: var(--purple20); + --danger: #a51515; + --warning: #c3730a; + --success: #60b18a; + --heading-font: 'TT Firs Neue', 'Geist', sans-serif; + --animation-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + --bullet-icon-bg: #ede7fb; + --pill-navbar-spacer: calc(80px + env(safe-area-inset-bottom, 0px)); + --pill-navbar-clearance: calc(64px + env(safe-area-inset-bottom, 0px)); + --sheet-bg: #fff; + --toast-bg: #1a1a1a; + --toast-color: #fafafa; + --text-color: #040404; +} + +html.palette-dark { + --black: #fbfbfb; + --cream: #d9d9d9; + --dark80: rgba(244, 245, 248, 0.8); + --dark70: rgba(244, 245, 248, 0.7); + --dark50: rgba(244, 245, 248, 0.5); + --dark30: rgba(244, 245, 248, 0.3); + --dark20: rgba(244, 245, 248, 0.2); + --dark15: rgba(244, 245, 248, 0.15); + --dark10: rgba(244, 245, 248, 0.1); + --dark05: rgba(244, 245, 248, 0.05); + --greenbg: #092d13; + --orange: #e69b39; + --orangebg: #2e1800; + --purple10: #39199820; + --purplebg: #39199833; + --red: #e04d4d; + --redbg: #380008; + --yellow: #ffdd46; + --yellowoutlier: #ffdd46; + --fade-rgb: 16, 16, 16; + --background-color: #101010; + --background-focused: var(--dark10); + --tab-bar-background: transparent; + --logo-color: #fbfbfb; + --toggle-background: #ddd; + --danger: #e04d4d; + --purpletext: #9678f7; + --bullet-icon-bg: #1f1050; + --sheet-bg: #1a1a1a; + --toast-bg: #2a2a2a; + --toast-color: #e0e0e0; + --rsbs-bg: #202020; + --text-color: #fbfbfb; +} + /* from tailwindcss/base */ blockquote, dl, @@ -48,8 +131,13 @@ html, body, #root { height: 100%; + color: var(--black); overscroll-behavior: none; - background-color: var(--ion-background-color, #fff); + background-color: var(--background-color, #fff); +} + +* { + box-sizing: border-box; } /* Fixed overlay for iOS status bar color sampling. @@ -63,10 +151,11 @@ body::before { left: 0; right: 0; height: env(safe-area-inset-top); - background-color: var(--ion-background-color, #fff); + background-color: var(--background-color, #fff); z-index: 99999; pointer-events: none; } + body { margin: 0; font-family: @@ -74,17 +163,27 @@ body { sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + overflow: hidden; + touch-action: manipulation; + -webkit-user-drag: none; + -ms-content-zooming: none; + word-wrap: break-word; + overscroll-behavior-y: none; + -webkit-text-size-adjust: none; + text-size-adjust: none; } -.first-letter::first-letter { - text-transform: uppercase; -} - -p a { - color: inherit; - cursor: pointer; - text-decoration: underline; - text-underline-offset: 2px; +p { + &.first-letter::first-letter { + text-transform: uppercase; + } + & a { + color: inherit; + cursor: pointer; + text-decoration: underline; + text-underline-offset: 2px; + } } iframe#webpack-dev-server-client-overlay { @@ -98,17 +197,94 @@ iframe { border: none; } -button.atcb-button { - box-shadow: none; +.button { + border: none; + border-radius: 8px; + box-shadow: 0 4px 0 0 var(--btn-shadow-color); + cursor: pointer; + font-family: 'Geist Mono', monospace; + font-size: 0.875rem; + font-weight: 400; + letter-spacing: 0%; + min-height: 40px; + padding: 0; + text-transform: uppercase; + transform: translateY(0); + transition: + transform 100ms cubic-bezier(0.165, 0.84, 0.44, 1), + box-shadow 100ms cubic-bezier(0.165, 0.84, 0.44, 1); + width: 100%; + &:disabled { + cursor: default; + opacity: 0.5; + pointer-events: none; + } + &.pressed { + transform: translateY(4px); + box-shadow: 0 0 0 0 var(--btn-shadow-color); + } + &.clear { + border: none; + background: none; + box-shadow: none; + color: var(--black); + &.pressed { + transform: scale(0.97); + } + } + &.copy { + aspect-ratio: 1; + align-items: center; + background: var(--dark05); + box-shadow: none; + color: var(--dark30); + cursor: pointer; + display: flex; + justify-content: center; + padding: 0; + touch-action: manipulation; + width: auto; + } + &.dark { + background: var(--purple); + box-shadow: 0 4px 0 0 var(--btn-shadow-color); + color: white; + } + &.outline { + background: none; + border-color: var(--dark50); + box-shadow: 0 4px 0 0 rgba(0, 0, 0, 0.1); + color: var(--dark50); + } + &.pill-nav-btn { + box-shadow: none; + } + &.red { + background: var(--red); + border-color: var(--red); + box-shadow: 0 4px 0 0 #6b0e0e; + color: white; + } + &.secondary { + background: var(--dark20); + border-color: var(--black); + box-shadow: 0 4px 0 0 rgba(0, 0, 0, 0.1); + color: var(--black); + } + &[size='small'] { + min-height: 16px; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } } -button.reminder-button.action-sheet-button { - width: calc(100% - 2rem); +button.reminder-button { + width: 100%; margin: 1rem; - padding: 0; + padding: 1rem; border: 1px solid var(--dark50); border-radius: 0.25rem; - background-color: #e5e5e5; + background-color: var(--background-color); color: var(--color); font-size: 1rem; font-weight: 500; @@ -116,16 +292,172 @@ button.reminder-button.action-sheet-button { display: flex; justify-content: center; align-items: center; - & span.action-sheet-button-inner { + & span { justify-content: center; } } -.ion-palette-dark button.reminder-button.action-sheet-button { +.buttons-on-bottom { + padding: 1rem; +} + +@media (prefers-reduced-motion: reduce) { + button { + transition: none; + &.pressed { + transform: none; + } + } +} + +.content { + width: 100%; + height: 100%; + display: block; + position: relative; + font-family: + 'Geist', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + 'Roboto', + 'Oxygen', + 'Ubuntu', + 'Cantarell', + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + & > .content-shell { + padding-top: 2rem; + height: 100%; + } +} + +/* Hide scrollbar for Chrome, Safari and Opera */ +.content::part(scroll)::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +.content::part(scroll) { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +.grid { + padding: 10px; + width: 100%; + & > div { + display: flex; + width: 100%; + & > div { + flex: 1; + &:last-child { + justify-content: flex-end; + } + } + } +} + +.has-pill-navbar { + & .content { + --padding-bottom: var(--pill-navbar-clearance); + } + & .content:has(.scroll-fade)::part(scroll) { + -webkit-mask-image: linear-gradient( + to bottom, + #000 0, + #000 calc(100% - var(--pill-navbar-clearance) - 56px), + rgba(0, 0, 0, 0.86) calc(100% - var(--pill-navbar-clearance) - 12px), + rgba(0, 0, 0, 0.54) calc(100% - var(--pill-navbar-clearance) + 18px), + rgba(0, 0, 0, 0.16) calc(100% - 18px), + transparent 100% + ); + mask-image: linear-gradient( + to bottom, + #000 0, + #000 calc(100% - var(--pill-navbar-clearance) - 56px), + rgba(0, 0, 0, 0.86) calc(100% - var(--pill-navbar-clearance) - 12px), + rgba(0, 0, 0, 0.54) calc(100% - var(--pill-navbar-clearance) + 18px), + rgba(0, 0, 0, 0.16) calc(100% - 18px), + transparent 100% + ); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; + } + & .content.no-content-fade::part(scroll) { + -webkit-mask-image: none; + mask-image: none; + } +} + +.header { + align-items: center; + border-bottom: 1px solid var(--dark10); + display: flex; + height: 4rem; +} + +.input { + flex: 1; + border: none; + font-size: 1rem; + min-height: 36px; + background-color: transparent; + caret-color: var(--black); + color: var(--black); +} + +input:focus::placeholder { + color: transparent; /* Hide placeholder on focus */ +} + +.palette-dark button.reminder-button.action-sheet-button { background-color: #222; color: #f7fafc; } +.label { + align-items: center; + display: flex; + gap: 0.25rem; + flex: 1; +} + +.page { + inset: 0; + display: flex; + position: absolute; + flex-direction: column; + justify-content: space-between; + contain: layout size style; + z-index: 0; + & > div { + max-width: 640px; + margin: auto; + } +} + +.pull-to-refresh { + height: 0; + opacity: 0; + width: 100%; + display: flex; + overflow: hidden; + align-items: center; + justify-content: center; + transition: + height 0.5s ease, + opacity 0.5s ease; + &.show { + opacity: 1; + height: 60px; + } +} + .spinner { width: 20px; height: 20px; @@ -135,6 +467,14 @@ button.reminder-button.action-sheet-button { animation: spin 1s linear infinite; } +.title { + text-align: center; + font-family: var(--heading-font); + letter-spacing: -0.5px; + font-size: 1.25rem; + font-weight: 500; +} + @keyframes spin { 0% { transform: rotate(0deg); @@ -186,16 +526,19 @@ button.reminder-button.action-sheet-button { .focusable { &:focus-visible { - background-color: var(--ion-background-focused); + background-color: var(--background-focused); } } -.dashed-hr { +hr { height: 0; width: 100%; border: none; background: none; - border-top: 1px dashed var(--dark10); + border-top: 1px solid var(--dark20); + &.dashed { + border-top-style: dashed; + } } /* Theme transition ripple */ @@ -258,7 +601,7 @@ button.reminder-button.action-sheet-button { gap: 4px; padding: 4px; border-radius: 999px; - background-color: var(--ion-background-color); + background-color: var(--background-color); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08), 0 1px 3px -1px rgba(0, 0, 0, 0.12), @@ -267,7 +610,7 @@ button.reminder-button.action-sheet-button { pointer-events: auto; } -.ion-palette-dark .pill-navbar { +.palette-dark .pill-navbar { background-color: #1c1c1e; box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1), @@ -301,7 +644,7 @@ button.reminder-button.action-sheet-button { background-color: var(--dark05); } -.ion-palette-dark .pill-nav-btn--active { +.palette-dark .pill-nav-btn--active { color: #fbfbfb; } @@ -342,7 +685,7 @@ button.reminder-button.action-sheet-button { pointer-events: none; } -.ion-palette-dark .pill-navbar-haze { +.palette-dark .pill-navbar-haze { background: radial-gradient( ellipse 78% 88% at 50% 100%, rgba(var(--fade-rgb), 0.84) 0%, @@ -439,3 +782,129 @@ button.reminder-button.action-sheet-button { position: absolute; inset: -4px; } + +/* ============================================================ + Switch aka Toggle aka Checkbox + ============================================================ */ + +.cl-toggle-switch { + position: relative; +} + +.cl-switch { + position: relative; + display: inline-block; +} +/* Input */ +.cl-switch > input { + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + z-index: -1; + position: absolute; + right: 6px; + top: -8px; + display: block; + margin: 0; + border-radius: 50%; + width: 40px; + height: 40px; + background-color: rgb(0, 0, 0, 0.38); + outline: none; + opacity: 0; + transform: scale(1); + pointer-events: none; + transition: + opacity 0.3s 0.1s, + transform 0.2s 0.1s; +} +/* Track */ +.cl-switch > span::before { + content: ''; + float: right; + margin: 5px 0 5px 10px; + border-radius: 7px; + width: 36px; + height: 14px; + background-color: rgba(124, 124, 124, 0.38); + vertical-align: top; + transition: + background-color 0.2s, + opacity 0.2s; +} +/* Thumb */ +.cl-switch > span::after { + content: ''; + position: absolute; + top: 2px; + right: 16px; + border-radius: 50%; + width: 20px; + height: 20px; + background-color: #fff; + box-shadow: + 0 3px 1px -2px rgba(0, 0, 0, 0.2), + 0 2px 2px 0 rgba(0, 0, 0, 0.14), + 0 1px 5px 0 rgba(0, 0, 0, 0.12); + transition: + background-color 0.2s, + transform 0.2s; +} +/* Checked */ +.cl-switch > input:checked { + right: -10px; + background-color: var(--magenta); +} + +.cl-switch > input:checked + span::before { + background-color: var(--magenta); +} + +.cl-switch > input:checked + span::after { + background-color: var(--purple); + transform: translateX(16px); +} +/* Hover, Focus */ +.cl-switch:hover > input { + opacity: 0.04; +} + +.cl-switch > input:focus { + opacity: 0.12; +} + +.cl-switch:hover > input:focus { + opacity: 0.16; +} +/* Active */ +.cl-switch > input:active { + opacity: 1; + transform: scale(0); + transition: + transform 0s, + opacity 0s; +} + +.cl-switch > input:active + span::before { + background-color: #8f8f8f; +} + +.cl-switch > input:checked:active + span::before { + background-color: #85b8b7; +} +/* Disabled */ +.cl-switch > input:disabled { + opacity: 0; +} + +.cl-switch > input:disabled + span::before { + background-color: #ddd; +} + +.cl-switch > input:checked:disabled + span::before { + background-color: #bfdbda; +} + +.cl-switch > input:checked:disabled + span::after { + background-color: #61b5b4; +} diff --git a/src/index.tsx b/src/index.tsx index e8669affd..70ee8ba42 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,8 +1,6 @@ import ReactDOM from 'react-dom/client' import './index.css' -import './ionic.css' import App from './App' -// import IconPreview from './screens/IconPreview' import { AspProvider } from './providers/asp' import { ConfigProvider } from './providers/config' import { FiatProvider } from './providers/fiat' diff --git a/src/ionic.css b/src/ionic.css deleted file mode 100644 index a5753b870..000000000 --- a/src/ionic.css +++ /dev/null @@ -1,429 +0,0 @@ -:root { - --black: #040404; - --cream: #391998; - --dark80: rgba(var(--ion-color-dark-rgb), 0.8); - --dark70: rgba(var(--ion-color-dark-rgb), 0.7); - --dark50: rgba(var(--ion-color-dark-rgb), 0.5); - --dark30: rgba(var(--ion-color-dark-rgb), 0.3); - --dark20: rgba(var(--ion-color-dark-rgb), 0.2); - --dark15: rgba(var(--ion-color-dark-rgb), 0.15); - --dark10: rgba(var(--ion-color-dark-rgb), 0.1); - --dark05: rgba(var(--ion-color-dark-rgb), 0.05); - --green: #60b18a; - --greenbg: #054a19; - --grey: #747474; - --liz: #d5c6ff; - --logo-color: #391998; - --magenta: #8563e4; - --orange: #ff8e24; - --orangebg: #ff8e2418; - --purple: #391998; - --purple10: #39199815; - --purple20: #39199833; - --purplebg: #24154f; - --purpletext: #7043f4; - --red: #a51515; - --redbg: #68222b; - --white: #fbfbfb; - --yellow: #e2c542; - --yellowoutlier: #debb20; - --fade-rgb: 255, 255, 255; - --ion-background-color: #fff; - --ion-background-focused: var(--purple20); - --ion-tab-bar-background: transparent; - --ion-backdrop-color: #fff; - --toggle-background: #fbfbfb; - --danger: #a51515; - --warning: #c3730a; - --success: #60b18a; - --ion-font-family: - 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', - 'Droid Sans', 'Helvetica Neue', sans-serif; - --heading-font: 'TT Firs Neue', 'Geist', sans-serif; - --animation-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; - --bullet-icon-bg: #ede7fb; - --pill-navbar-spacer: calc(80px + env(safe-area-inset-bottom, 0px)); - --pill-navbar-clearance: calc(64px + env(safe-area-inset-bottom, 0px)); - --sheet-bg: #fff; - --toast-bg: #1a1a1a; - --toast-color: #fafafa; -} - -.ion-palette-dark { - --black: #fbfbfb; - --cream: #d9d9d9; - --greenbg: #092d13; - --orange: #e69b39; - --orangebg: #2e1800; - --purple10: #39199820; - --purplebg: #39199833; - --red: #e04d4d; - --redbg: #380008; - --yellow: #ffdd46; - --yellowoutlier: #ffdd46; - --fade-rgb: 16, 16, 16; - --ion-background-color: #101010; - --ion-background-focused: var(--dark10); - --ion-tab-bar-background: transparent; - --ion-backdrop-color: #101010; - --logo-color: #fbfbfb; - --toggle-background: #ddd; - --danger: #e04d4d; - --purpletext: #9678f7; - --bullet-icon-bg: #1f1050; - --sheet-bg: #1a1a1a; - --toast-bg: #2a2a2a; - --toast-color: #e0e0e0; -} - -.ion-palette-dark ion-modal { - --box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.4), 0 -1px 0 rgba(255, 255, 255, 0.08); - --border-color: rgba(255, 255, 255, 0.1); -} - -.my-ion-action-sheet div.action-sheet-group { - background-color: var(--ion-background-color); - border-top: var(--dark50) 1px solid; - border-top-left-radius: 1rem; - border-top-right-radius: 1rem; - & .action-sheet-title { - font-size: 1rem; - font-weight: 700; - text-align: center; - & .action-sheet-sub-title { - font-size: 0.8rem; - font-weight: 400; - } - } - & button { - border-radius: 0.5rem; - padding-top: 1rem; - padding-bottom: 1rem; - } -} - -ion-button { - --border-radius: 0.5rem; - --ripple-color: transparent; - --ion-color-activated: transparent; - --box-shadow: none; - --btn-shadow-color: rgba(0, 0, 0, 0.25); - - border-radius: 0.5rem; - font-family: 'Geist Mono', monospace; - font-size: 0.875rem; - font-weight: 400; - letter-spacing: 0%; - min-height: 40px; - text-transform: uppercase; - width: 100%; - transform: translateY(0); - box-shadow: 0 4px 0 0 var(--btn-shadow-color); - transition: - transform 100ms cubic-bezier(0.165, 0.84, 0.44, 1), - box-shadow 100ms cubic-bezier(0.165, 0.84, 0.44, 1); - &::part(native) { - box-shadow: none !important; - } - &::part(native):focus-visible { - outline: 2px solid var(--ion-color-dark); - outline-offset: 2px; - } - &::part(native)::after { - display: none !important; - } - &.pressed { - transform: translateY(4px); - box-shadow: 0 0 0 0 var(--btn-shadow-color); - } - &.clear { - --background: none; - --border-color: none; - --color: var(--black); - - box-shadow: none; - - &.pressed { - transform: scale(0.97); - box-shadow: none; - } - } - &.dark { - --background: var(--purple); - --border-color: var(--purple); - --btn-shadow-color: #1a0b4a; - --color: white; - } - &.outline { - --background: none; - --border-color: var(--dark50); - --btn-shadow-color: rgba(0, 0, 0, 0.1); - --color: var(--dark50); - } - &.red { - --background: var(--red); - --border-color: var(--red); - --btn-shadow-color: #6b0e0e; - --color: white; - } - &.secondary { - --background: var(--dark20); - --border-color: var(--black); - --btn-shadow-color: rgba(0, 0, 0, 0.1); - --color: var(--black); - } - &[size='small'] { - min-height: 16px; - --padding-top: 0.5rem; - --padding-bottom: 0.5rem; - } -} - -@media (prefers-reduced-motion: reduce) { - ion-button { - transition: none; - &.pressed { - transform: none; - } - } -} - -ion-app ion-header { - align-items: center; - border-bottom: 1px solid var(--dark10); - color: var(--ion-color-dark); - display: flex; - height: 4rem; - & ion-grid { - margin: auto; - } -} - -ion-app > .ion-page { - max-width: 640px; - margin: auto; - top: env(safe-area-inset-top); -} - -ion-tab-bar { - --background: transparent; - --border: none; - --ion-tab-bar-background: transparent; - background: transparent; - box-shadow: none; - border: none; - pointer-events: none; - position: absolute; - left: 0; - right: 0; - bottom: 0; - z-index: 0; - height: var(--pill-navbar-spacer); - min-height: var(--pill-navbar-spacer); -} - -ion-tab-bar ion-tab-button { - opacity: 0; - --background: transparent; -} - -.content-shell { - height: 100%; - padding-top: 2rem; -} - -ion-footer.buttons-on-bottom { - padding-bottom: calc(var(--ion-padding, 16px) + env(safe-area-inset-bottom, 0px)); -} - -ion-app.has-pill-navbar ion-content { - --padding-bottom: var(--pill-navbar-clearance); -} - -ion-app.has-pill-navbar ion-content:has(.scroll-fade)::part(scroll) { - -webkit-mask-image: linear-gradient( - to bottom, - #000 0, - #000 calc(100% - var(--pill-navbar-clearance) - 56px), - rgba(0, 0, 0, 0.86) calc(100% - var(--pill-navbar-clearance) - 12px), - rgba(0, 0, 0, 0.54) calc(100% - var(--pill-navbar-clearance) + 18px), - rgba(0, 0, 0, 0.16) calc(100% - 18px), - transparent 100% - ); - mask-image: linear-gradient( - to bottom, - #000 0, - #000 calc(100% - var(--pill-navbar-clearance) - 56px), - rgba(0, 0, 0, 0.86) calc(100% - var(--pill-navbar-clearance) - 12px), - rgba(0, 0, 0, 0.54) calc(100% - var(--pill-navbar-clearance) + 18px), - rgba(0, 0, 0, 0.16) calc(100% - 18px), - transparent 100% - ); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-size: 100% 100%; - mask-size: 100% 100%; -} - -ion-app.has-pill-navbar ion-content.no-content-fade::part(scroll) { - -webkit-mask-image: none; - mask-image: none; -} - -ion-checkbox { - --border-color: var(--dark10); - --border-color-checked: var(--ion-color-danger); - --border-radius: 6px; - --checkbox-background: var(--dark10); - --checkbox-background-checked: var(--ion-color-danger); - --checkmark-color: #fbfbfb; - --checkmark-width: 2px; - --size: 24px; -} - -ion-checkbox::part(label) { - font-size: 13px; -} - -/* Hide scrollbar for Chrome, Safari and Opera */ -ion-content::part(scroll)::-webkit-scrollbar { - display: none; -} - -/* Hide scrollbar for IE, Edge and Firefox */ -ion-content::part(scroll) { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ -} - -ion-action-sheet.my-ion-action-sheet { - --backdrop-opacity: 0.6; - --color: var(--ion-color-dark-rgb); - --max-width: 100%; - --width: 100%; -} - -ion-modal { - z-index: 200 !important; - --backdrop-opacity: 0.4; - --background: var(--sheet-bg, #fff); - --border-radius: 16px 16px 0 0; - --height: auto; - --max-width: 100%; - --width: 100%; - --overflow: visible; - --box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.12), 0 -1px 6px rgba(0, 0, 0, 0.04); - --border-width: 1px 0 0 0; - --border-style: solid; - --border-color: var(--dark10); -} - -ion-modal::part(content) { - position: absolute; - bottom: 0; -} - -ion-select { - --background: var(--dark20); - --border-radius: 0.5rem; - width: 100%; - &::part(container) { - padding: 0 1rem; - } -} - -input.native-input:focus::placeholder { - color: transparent; /* Hide placeholder on focus */ -} - -ion-toast { - --background: var(--greenbg); - --border-radius: 0.5rem; - --color: var(--white); - --padding: 1rem; - --max-width: 260px; - text-align: center; -} - -ion-toast::part(button) { - border-left: 1px solid var(--white); - color: var(--white); - font-size: 0.7rem; - text-transform: uppercase; -} - -.toast-container { - text-align: center; -} - -ion-toggle { - margin: 12px; - border: 1px solid var(--black); - - --track-background: var(--toggle-background); - --track-background-checked: var(--toggle-background); - - --handle-background: var(--grey); - --handle-background-checked: var(--purple); - - --handle-width: 13px; - --handle-height: 13px; - --handle-max-height: auto; - --handle-spacing: 0; - - --handle-border-radius: 0; - --handle-box-shadow: none; -} - -ion-toggle::part(track) { - height: 15px; - width: 30px; - border-radius: 0; - - /* Required for iOS handle to overflow the height of the track */ - overflow: visible; -} - -ion-input[readonly] { - --color: var(--dark50); - --highlight-color-focused: none; -} - -@keyframes toast-in { - from { - opacity: 0; - transform: translateY(-12px) scale(0.95); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -@keyframes toast-out { - from { - opacity: 1; - transform: translateY(0) scale(1); - } - to { - opacity: 0; - transform: translateY(-12px) scale(0.95); - } -} - -@media (prefers-reduced-motion: reduce) { - @keyframes toast-in { - from, - to { - opacity: 1; - transform: none; - } - } - @keyframes toast-out { - from, - to { - opacity: 0; - transform: none; - } - } -} diff --git a/src/lib/animations.ts b/src/lib/animations.ts index 0221455b9..8ca8ce8a3 100644 --- a/src/lib/animations.ts +++ b/src/lib/animations.ts @@ -58,7 +58,7 @@ export const overlayStyle = { zIndex: 10, display: 'flex', flexDirection: 'column' as const, - background: 'var(--ion-background-color)', + background: 'var(--background-color)', } // Overlay slide-up animation — used for keyboard, scanner, etc. diff --git a/src/lib/toast.ts b/src/lib/toast.ts deleted file mode 100644 index c771780b3..000000000 --- a/src/lib/toast.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ToastOptions } from '@ionic/react' - -const defaultToastOptions: ToastOptions = { - duration: 1000, - position: 'top', -} - -const toastReloadButton = { - text: 'reload', - handler: () => window.location.reload(), -} - -export const copiedToClipboard: ToastOptions = { - ...defaultToastOptions, - message: 'Copied to clipboard', -} - -export const newVersionAvailable: ToastOptions = { - ...defaultToastOptions, - buttons: [toastReloadButton], - duration: 0, - message: 'New version available', -} - -export const backupToNostr: ToastOptions = { - ...defaultToastOptions, - message: 'Nostr backup updated', -} diff --git a/src/providers/config.tsx b/src/providers/config.tsx index 56e425749..b6cae5689 100644 --- a/src/providers/config.tsx +++ b/src/providers/config.tsx @@ -101,7 +101,7 @@ export const ConfigProvider = ({ children }: { children: ReactNode }) => { const applyTheme = (theme: Themes) => { const resolved = resolveTheme(theme) setEffectiveTheme(resolved) - const darkPalette = 'ion-palette-dark' + const darkPalette = 'palette-dark' const root = document.documentElement if (resolved === Themes.Dark) root.classList.add(darkPalette) else root.classList.remove(darkPalette) diff --git a/src/screens/Apps/Assets/Burn.tsx b/src/screens/Apps/Assets/Burn.tsx index 13262e321..1bee6c525 100644 --- a/src/screens/Apps/Assets/Burn.tsx +++ b/src/screens/Apps/Assets/Burn.tsx @@ -109,6 +109,7 @@ export default function AppAssetBurn() { - + - + {balance > 0 ? : null} diff --git a/src/screens/Apps/Assets/Index.tsx b/src/screens/Apps/Assets/Index.tsx index aed5abc8f..cebe7c776 100644 --- a/src/screens/Apps/Assets/Index.tsx +++ b/src/screens/Apps/Assets/Index.tsx @@ -112,6 +112,7 @@ export default function AppAssets() { icon={asset.icon} decimals={asset.decimals} onClick={() => handleAssetClick(asset.assetId)} + darkPurple /> )) )} diff --git a/src/screens/Apps/Assets/Mint.tsx b/src/screens/Apps/Assets/Mint.tsx index ca84b386a..b02c74ac2 100644 --- a/src/screens/Apps/Assets/Mint.tsx +++ b/src/screens/Apps/Assets/Mint.tsx @@ -213,6 +213,7 @@ export default function AppAssetMint() { ticker={ticker} icon={iconUrl && !iconError ? iconUrl : undefined} decimals={isNaN(parsedDecimals) ? 0 : parsedDecimals} + darkPurple />
diff --git a/src/screens/Apps/Assets/MintSuccess.tsx b/src/screens/Apps/Assets/MintSuccess.tsx index e441c8be7..c316f693d 100644 --- a/src/screens/Apps/Assets/MintSuccess.tsx +++ b/src/screens/Apps/Assets/MintSuccess.tsx @@ -45,6 +45,7 @@ export default function AppAssetMintSuccess() { icon={icon} name={name} ticker={ticker} + darkPurple /> diff --git a/src/screens/Settings/Backup.tsx b/src/screens/Settings/Backup.tsx index 51aad35c3..c82a3ef8f 100644 --- a/src/screens/Settings/Backup.tsx +++ b/src/screens/Settings/Backup.tsx @@ -88,6 +88,7 @@ export default function Backup() { const toggleNostrBackup = async () => { const newConfig = { ...config, nostrBackup: !config.nostrBackup } updateConfig(newConfig) + console.log('Updating backup with new config', newConfig) if (newConfig.nostrBackup) { const backupProvider = new BackupProvider({ pubkey: config.pubkey }, new IndexedDbSwapRepository()) await backupProvider.fullBackup(newConfig, arkadeSwaps ?? undefined).catch((error) => { diff --git a/src/screens/Settings/Delegates.tsx b/src/screens/Settings/Delegates.tsx index 36ffd73ff..de571836d 100644 --- a/src/screens/Settings/Delegates.tsx +++ b/src/screens/Settings/Delegates.tsx @@ -20,8 +20,7 @@ import Text, { TextSecondary } from '../../components/Text' import { decodeArkAddress, isArkAddress } from '../../lib/address' import { Network } from '@arkade-os/boltz-swap' import { copyToClipboard } from '../../lib/clipboard' -import { copiedToClipboard } from '../../lib/toast' -import { useIonToast } from '@ionic/react' +import { useToast } from '../../components/Toast' // format the URL to ensure it has the correct protocol and no trailing slashes const formatUrl = (host: string, path: string): string => { @@ -124,7 +123,7 @@ function DelegateCard() { const { wallet } = useContext(WalletContext) const { setOption } = useContext(OptionsContext) - const [present] = useIonToast() + const { toast } = useToast() const [active, setActive] = useState(false) const [delegate, setDelegate] = useState(getDelegateUrlForNetwork(aspInfo.network as Network)) @@ -144,7 +143,7 @@ function DelegateCard() { const handleCopy = async (value: string) => { await copyToClipboard(value) - present(copiedToClipboard) + toast('Copied to clipboard') } const nextRolloverText = wallet.nextRollover @@ -163,7 +162,7 @@ function DelegateCard() { -
+
{delegate.url} diff --git a/src/screens/Settings/General.tsx b/src/screens/Settings/General.tsx index b7a5d3b5e..4c8f3528a 100644 --- a/src/screens/Settings/General.tsx +++ b/src/screens/Settings/General.tsx @@ -55,16 +55,11 @@ export default function General() { option={SettingsOptions.Theme} value={config.theme === Themes.Auto ? `Auto (${systemTheme})` : config.theme} /> -
+
-
+
-
+
diff --git a/src/screens/Settings/Vtxos.tsx b/src/screens/Settings/Vtxos.tsx index b673ca006..bd338b0d8 100644 --- a/src/screens/Settings/Vtxos.tsx +++ b/src/screens/Settings/Vtxos.tsx @@ -23,8 +23,8 @@ import { EmptyCoinsList } from '../../components/Empty' import WarningBox from '../../components/Warning' import { ExtendedCoin, ExtendedVirtualCoin, isVtxoExpiringSoon } from '@arkade-os/sdk' import { consoleError } from '../../lib/logs' -import { IonCol, IonGrid, IonRow } from '@ionic/react' import * as Sentry from '@sentry/react' +import Grid from '../../components/Grid' export default function Vtxos() { const { aspInfo, calcBestMarketHour } = useContext(AspContext) @@ -224,9 +224,9 @@ export default function Vtxos() { } return (
- - - + +
+
{amount} {assets?.map((a) => ( @@ -235,13 +235,13 @@ export default function Vtxos() { ))} - - {tags} - +
+
{tags}
+
{expiry} - - - +
+
+
) } diff --git a/src/screens/Wallet/Receive/QrCode.tsx b/src/screens/Wallet/Receive/QrCode.tsx index 304015bae..d17fc463d 100644 --- a/src/screens/Wallet/Receive/QrCode.tsx +++ b/src/screens/Wallet/Receive/QrCode.tsx @@ -457,13 +457,11 @@ export default function ReceiveQRCode() { - - - + ) diff --git a/src/screens/Wallet/Receive/Success.tsx b/src/screens/Wallet/Receive/Success.tsx index 11fdd37fc..4c3428d9d 100644 --- a/src/screens/Wallet/Receive/Success.tsx +++ b/src/screens/Wallet/Receive/Success.tsx @@ -3,11 +3,8 @@ import Button from '../../../components/Button' import ButtonsOnBottom from '../../../components/ButtonsOnBottom' import Content from '../../../components/Content' import FlexCol from '../../../components/FlexCol' -import FlexRow from '../../../components/FlexRow' import Padded from '../../../components/Padded' -import Shadow from '../../../components/Shadow' import Text from '../../../components/Text' -import AssetAvatar from '../../../components/AssetAvatar' import SuccessIcon from '../../../icons/Success' import Success from '../../../components/Success' import { NotificationsContext } from '../../../providers/notifications' @@ -20,6 +17,7 @@ import { FiatContext } from '../../../providers/fiat' import { WalletContext } from '../../../providers/wallet' import { consoleError } from '../../../lib/logs' import type { AssetDetails } from '@arkade-os/sdk' +import AssetCard from '../../../components/AssetCard' export default function ReceiveSuccess() { const { config, useFiat } = useContext(ConfigContext) @@ -98,22 +96,14 @@ export default function ReceiveSuccess() { const icon = meta?.icon return ( - - - - - - {name} - {ticker ? ( - - {ticker} - - ) : null} - - - {formatAssetAmount(a.amount, meta?.decimals ?? 0)} - - + ) })} diff --git a/src/screens/Wallet/Send/Form.tsx b/src/screens/Wallet/Send/Form.tsx index c7e711a2b..3db3261cd 100644 --- a/src/screens/Wallet/Send/Form.tsx +++ b/src/screens/Wallet/Send/Form.tsx @@ -1,5 +1,5 @@ import { useContext, useEffect, useState } from 'react' -import { V2BrantaClient, BrantaServerBaseUrl, Payment } from '@branta-ops/branta' +import { V2BrantaClient, BrantaServerBaseUrl } from '@branta-ops/branta' import Button from '../../../components/Button' import ErrorMessage from '../../../components/Error' import ButtonsOnBottom from '../../../components/ButtonsOnBottom' @@ -50,6 +50,13 @@ import { AnimatePresence, motion } from 'framer-motion' import { overlaySlideUp, overlayStyle } from '../../../lib/animations' import { useReducedMotion } from '../../../hooks/useReducedMotion' +// TODO: Replace when SDK is accurate +type BrantaPayment = Partial< + Awaited>['payment'] & { + platform_logo_url: string + } +> + const brantaClient = new V2BrantaClient({ baseUrl: BrantaServerBaseUrl.Production, }) @@ -85,7 +92,7 @@ export default function SendForm() { const [receivingAddresses, setReceivingAddresses] = useState() const [scan, setScan] = useState(false) const [rawScanData, setRawScanData] = useState('') - const [brantaPayment, setBrantaPayment] = useState(null) + const [brantaPayment, setBrantaPayment] = useState(null) const [brantaLoading, setBrantaLoading] = useState(false) const [selectedAsset, setSelectedAsset] = useState(null) const [showAssetSelector, setShowAssetSelector] = useState(false) @@ -299,7 +306,7 @@ export default function SendForm() { setBrantaLoading(true) brantaClient .getPaymentsByQRCode(rawScanData) - .then((payments: Payment[]) => { + .then((payments: BrantaPayment[]) => { if (cancelled) return const payment = payments?.[0] ?? null if (payment) { diff --git a/src/screens/Wallet/Send/Success.tsx b/src/screens/Wallet/Send/Success.tsx index 86d853a58..1ebf4e2e1 100644 --- a/src/screens/Wallet/Send/Success.tsx +++ b/src/screens/Wallet/Send/Success.tsx @@ -8,16 +8,14 @@ import Button from '../../../components/Button' import ButtonsOnBottom from '../../../components/ButtonsOnBottom' import Success from '../../../components/Success' import FlexCol from '../../../components/FlexCol' -import FlexRow from '../../../components/FlexRow' import Padded from '../../../components/Padded' -import Shadow from '../../../components/Shadow' import Text from '../../../components/Text' -import AssetAvatar from '../../../components/AssetAvatar' import SuccessIcon from '../../../icons/Success' import { formatAssetAmount, prettyAmount, prettyFiatAmount } from '../../../lib/format' import { ConfigContext } from '../../../providers/config' import { FiatContext } from '../../../providers/fiat' import { WalletContext } from '../../../providers/wallet' +import AssetCard from '../../../components/AssetCard' export default function SendSuccess() { const { config, useFiat } = useContext(ConfigContext) @@ -48,7 +46,7 @@ export default function SendSuccess() { ? prettyFiatAmount(toFiat(totalSats), config.fiat) : prettyAmount(totalSats) - if (isAssetSend) { + if (isAssetSend && assetId) { return ( <>
@@ -59,31 +57,14 @@ export default function SendSuccess() { Payment sent! - - - - - - - {assetName} - {assetTicker ? ( - - {assetTicker} - - ) : null} - - - {formatAssetAmount(assetAmountValue, assetDecimals)} - - - + {displayAmount} sent successfully diff --git a/src/test/App.test.tsx b/src/test/App.test.tsx index e9ffef349..8f9c7437f 100644 --- a/src/test/App.test.tsx +++ b/src/test/App.test.tsx @@ -1,5 +1,4 @@ import { act, render, screen, waitFor } from '@testing-library/react' -import type { ReactNode } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import App, { appReloader } from '../App' import { AspContext } from '../providers/asp' @@ -26,43 +25,6 @@ vi.mock('../lib/jsCapabilities', () => ({ detectJSCapabilities: vi.fn().mockResolvedValue({ isSupported: true }), })) -vi.mock('@ionic/react', async (importOriginal) => { - const React = await import('react') - const actual = await importOriginal() - - const IonTab = React.forwardRef(function MockIonTab( - { children }: { children: ReactNode }, - ref: React.ForwardedRef<{ setActive: () => void; classList: { add: () => void; remove: () => void } }>, - ) { - React.useImperativeHandle(ref, () => ({ - setActive: () => {}, - classList: { - add: () => {}, - remove: () => {}, - }, - })) - - return
{children}
- }) - - return { - ...actual, - IonApp: ({ children, className }: { children: ReactNode; className?: string }) => ( -
- {children} -
- ), - IonPage: ({ children }: { children: ReactNode }) =>
{children}
, - IonTab, - IonTabBar: ({ children }: { children: ReactNode }) =>
{children}
, - IonTabButton: ({ children, onClick }: { children: ReactNode; onClick?: () => void }) => ( - - ), - IonTabs: ({ children }: { children: ReactNode }) =>
{children}
, - setupIonicReact: vi.fn(), - } -}) - function renderApp({ authState, initialized, @@ -163,7 +125,7 @@ describe('App startup routing', () => { it('keeps authenticated but uninitialized wallets on loading', async () => { const { navigate, unlockWallet } = renderApp({ authState: 'authenticated', initialized: false }) - await waitFor(() => expect(screen.getByTestId('ion-app')).toBeInTheDocument()) + await waitFor(() => expect(screen.getByTestId('app')).toBeInTheDocument()) expect(unlockWallet).not.toHaveBeenCalled() expect(navigate).not.toHaveBeenCalledWith(Pages.Unlock) }) @@ -212,28 +174,28 @@ describe('Navbar visibility', () => { renderApp({ authState: 'locked', initialized: false, screen: Pages.Wallet, tab: Tabs.Wallet }) await screen.findByText('Unlock') - const ionApp = screen.getByTestId('ion-app') + const ionApp = screen.getByTestId('app') expect(ionApp.className).not.toContain('has-pill-navbar') }) it('hides navbar during loading hold', async () => { renderApp({ authState: 'authenticated', initialized: false, screen: Pages.Wallet, tab: Tabs.Wallet }) - const ionApp = await screen.findByTestId('ion-app') + const ionApp = await screen.findByTestId('app') expect(ionApp.className).not.toContain('has-pill-navbar') }) it('shows navbar on wallet root when authenticated and initialized', async () => { renderApp({ authState: 'authenticated', initialized: true, screen: Pages.Wallet, tab: Tabs.Wallet }) - const ionApp = await screen.findByTestId('ion-app') + const ionApp = await screen.findByTestId('app') expect(ionApp.className).toContain('has-pill-navbar') }) it('shows navbar on apps root when authenticated and initialized', async () => { renderApp({ authState: 'authenticated', initialized: true, screen: Pages.Apps, tab: Tabs.Apps }) - const ionApp = await screen.findByTestId('ion-app') + const ionApp = await screen.findByTestId('app') expect(ionApp.className).toContain('has-pill-navbar') }) @@ -246,7 +208,7 @@ describe('Navbar visibility', () => { option: SettingsOptions.Menu, }) - const ionApp = await screen.findByTestId('ion-app') + const ionApp = await screen.findByTestId('app') expect(ionApp.className).toContain('has-pill-navbar') }) @@ -259,7 +221,7 @@ describe('Navbar visibility', () => { option: SettingsOptions.Password, }) - const ionApp = await screen.findByTestId('ion-app') + const ionApp = await screen.findByTestId('app') expect(ionApp.className).not.toContain('has-pill-navbar') }) }) diff --git a/src/test/e2e/asset.test.ts b/src/test/e2e/asset.test.ts index d29ffe86f..34ce0b390 100644 --- a/src/test/e2e/asset.test.ts +++ b/src/test/e2e/asset.test.ts @@ -77,11 +77,10 @@ test('should reissue an asset with control token', async ({ page }) => { // mint asset with control token await page.getByText('Mint', { exact: true }).click() await page.waitForSelector('text=Mint Asset', { state: 'visible' }) - await page.getByTestId('asset-amount').locator('input:not(.cloned-input)').fill('500') - await page.getByTestId('asset-name').locator('input:not(.cloned-input)').fill('ReissueCoin') - await page.getByTestId('asset-ticker').locator('input:not(.cloned-input)').fill('RSI') - const decimalsInput = page.getByTestId('asset-decimals').locator('input:not(.cloned-input)') - await decimalsInput.clear() + await page.getByTestId('asset-amount').fill('500') + await page.getByTestId('asset-name').fill('ReissueCoin') + await page.getByTestId('asset-ticker').fill('RSI') + const decimalsInput = page.getByTestId('asset-decimals') await decimalsInput.fill('0') // select control asset from dropdown @@ -103,7 +102,7 @@ test('should reissue an asset with control token', async ({ page }) => { await page.waitForSelector('text=Additional Amount', { state: 'visible' }) // fill amount and submit - await page.getByTestId('asset-amount').locator('input:not(.cloned-input)').fill('200') + await page.getByTestId('asset-amount').fill('200') await page.getByText('Reissue', { exact: true }).click() // confirm modal diff --git a/src/test/e2e/delegate.test.ts b/src/test/e2e/delegate.test.ts index eba159b2f..f135df590 100644 --- a/src/test/e2e/delegate.test.ts +++ b/src/test/e2e/delegate.test.ts @@ -14,17 +14,10 @@ test('should toggle delegates', async ({ page }) => { await expect(toggle).toBeVisible() // delegate may default to off in CI (no delegator service) - const initialChecked = await toggle.getAttribute('checked') + const initialChecked = await toggle.getAttribute('data-checked') await toggle.click() - const maybeLater = page.getByRole('button', { name: 'Maybe later' }) - await maybeLater.waitFor({ state: 'visible', timeout: 150 }).catch(() => {}) - if (await maybeLater.isVisible()) { - await maybeLater.click({ force: true }) - await maybeLater.waitFor({ state: 'hidden' }).catch(() => {}) - } - // toggle triggers window.location.reload(), wait for wallet to load await waitForWalletPage(page) await page.getByTestId('tab-settings').click() @@ -33,11 +26,12 @@ test('should toggle delegates', async ({ page }) => { toggle = page.getByTestId('toggle-delegates') const expectedAfterToggle = initialChecked === 'true' ? 'false' : 'true' - await expect(toggle).toHaveAttribute('checked', expectedAfterToggle) if (expectedAfterToggle === 'true') { + expect(await toggle.getAttribute('data-checked')).toBe('true') await expect(page.getByTestId('delegate-card')).toBeVisible() } else { + expect(await toggle.getAttribute('data-checked')).toBe('false') await expect(page.getByTestId('delegate-card')).not.toBeVisible() } }) diff --git a/src/test/e2e/keyboard.test.ts b/src/test/e2e/keyboard.test.ts index 80d5d1a2c..7e5f9f3f6 100644 --- a/src/test/e2e/keyboard.test.ts +++ b/src/test/e2e/keyboard.test.ts @@ -6,7 +6,6 @@ async function setupWalletAndOpenKeyboard(page: Page) { await createWallet(page) await page.getByText('Receive').click() await page.getByText('Add amount').click() - await page.locator('ion-input[name="receive-amount-sheet"] input').click() await page.waitForSelector('text=Save', { state: 'visible' }) } diff --git a/src/test/e2e/nostr.test.ts b/src/test/e2e/nostr.test.ts index a404971b0..e5406d2ed 100644 --- a/src/test/e2e/nostr.test.ts +++ b/src/test/e2e/nostr.test.ts @@ -33,7 +33,7 @@ test('should save config to nostr', async ({ page }) => { // enable nostr backups await page.getByTestId('tab-settings').click() await page.getByText('backup', { exact: true }).click() - await page.getByText('Enable Nostr backups').click() + await page.getByTestId('toggle-backup').click() // change fiat currency to euro await page.getByLabel('Go back').click() @@ -50,7 +50,7 @@ test('should save config to nostr', async ({ page }) => { // disable nostr backups await page.getByLabel('Go back').click() await page.getByText('backup', { exact: true }).click() - await page.getByText('Enable Nostr backups').click() + await page.getByTestId('toggle-backup').click() // change fiat currency to usd await page.getByLabel('Go back').click() @@ -153,7 +153,7 @@ test('should save swaps to nostr', async ({ page, isMobile }) => { // enable nostr backups await page.getByTestId('tab-settings').click() await page.getByText('backup', { exact: true }).click() - await page.getByText('Enable Nostr backups').click() + await page.getByTestId('toggle-backup').click() await sleep(3000) // wait for backup to complete // restore wallet diff --git a/src/test/e2e/receive.test.ts b/src/test/e2e/receive.test.ts index 6fde1a276..959f44f95 100644 --- a/src/test/e2e/receive.test.ts +++ b/src/test/e2e/receive.test.ts @@ -59,10 +59,9 @@ test('changing amount should update the invoice', async ({ page, isMobile }) => // fill amount to receive if provided await page.getByText('Add amount').click() if (isMobile) { - await page.locator('ion-input[name="receive-amount-sheet"] input').click() await handleKeyboardInput(page, sats) } else { - await page.locator('ion-input[name="receive-amount-sheet"] input').fill(sats.toString()) + await page.locator('input[name="receive-amount-sheet"]').fill(sats.toString()) await page.getByText('Set amount').click() } @@ -81,14 +80,13 @@ test('changing amount should update the invoice', async ({ page, isMobile }) => await page.getByText('Edit amount').click() if (isMobile) { - await page.locator('ion-input[name="receive-amount-sheet"] input').click() // delete previous amount for (let i = 0; i < sats.toString().length + 1; i++) { await page.getByTestId('keyboard-x').click() } await handleKeyboardInput(page, newSats) } else { - await page.locator('ion-input[name="receive-amount-sheet"] input').fill(newSats.toString()) + await page.locator('input[name="receive-amount-sheet"]').fill(newSats.toString()) await page.getByText('Set amount').click() } diff --git a/src/test/e2e/send.test.ts b/src/test/e2e/send.test.ts index 5ebc79dcf..cf66aa8b4 100644 --- a/src/test/e2e/send.test.ts +++ b/src/test/e2e/send.test.ts @@ -35,7 +35,7 @@ test('should send sats (some and max) to ark address', async ({ page, isMobile } await page.getByText('Send').click() // fill address - await page.locator('ion-input[name="send-address"] input').fill(someArkAddress) + await page.locator('input[name="send-address"]').fill(someArkAddress) // click max await page.getByTestId('input-amount-max').click() @@ -83,7 +83,7 @@ test('should send assets (some and max) to ark address', async ({ page, isMobile await page.getByText('Send').click() // fill address - await page.locator('ion-input[name="send-address"] input').fill(someArkAddress) + await page.locator('input[name="send-address"]').fill(someArkAddress) // select asset await page.getByTestId('asset-selector').click() @@ -91,10 +91,10 @@ test('should send assets (some and max) to ark address', async ({ page, isMobile // fill amount if (isMobile) { - await page.locator('ion-input[name="send-amount"] input').click() + await page.locator('input[name="send-amount"]').click() await handleKeyboardInput(page, 200) } else { - await page.locator('ion-input[name="send-amount"] input').fill('200') + await page.locator('input[name="send-amount"]').fill('200') } // continue to details page @@ -119,7 +119,7 @@ test('should send assets (some and max) to ark address', async ({ page, isMobile await page.getByText('Send').click() // fill address - await page.locator('ion-input[name="send-address"] input').fill(someArkAddress) + await page.locator('input[name="send-address"]').fill(someArkAddress) // select asset await page.getByTestId('asset-selector').click() @@ -179,7 +179,7 @@ test('should send sats (some and max) to onchain address with chain swap', async await page.getByText('Send').click() // fill address - await page.locator('ion-input[name="send-address"] input').fill(someOnchainAddress) + await page.locator('input[name="send-address"]').fill(someOnchainAddress) // click max await page.getByTestId('input-amount-max').click() @@ -240,7 +240,7 @@ test('should send sats (some and max) to onchain address with collaborative exit await page.getByText('Send').click() // fill address - await page.locator('ion-input[name="send-address"] input').fill(someOnchainAddress) + await page.locator('input[name="send-address"]').fill(someOnchainAddress) // click max await page.getByTestId('input-amount-max').click() diff --git a/src/test/e2e/swap.test.ts b/src/test/e2e/swap.test.ts index 870c2d96c..0b740596e 100644 --- a/src/test/e2e/swap.test.ts +++ b/src/test/e2e/swap.test.ts @@ -178,7 +178,7 @@ test('should refund failing swap', async ({ page }) => { // try to send funds to Lightning await page.getByText('Send').click() - await page.locator('ion-input[name="send-address"] input').fill(invoice) + await page.locator('input[name="send-address"]').fill(invoice) await page.getByText('Continue').click() await page.getByText('Tap to Sign').click() await page.getByTestId('loading-logo').waitFor({ timeout: 3000 }) diff --git a/src/test/e2e/utils.ts b/src/test/e2e/utils.ts index 04f721665..097e3cd60 100644 --- a/src/test/e2e/utils.ts +++ b/src/test/e2e/utils.ts @@ -70,15 +70,14 @@ export async function mintAsset(page: Page, opts: MintAssetOptions): Promise { async function resetWallet(page: Page): Promise { await navigateToSettings(page) await page.getByText('Reset wallet').click() - await page.getByText('I have backed up my wallet').click() + await page.getByTestId('checkbox').click() await page.getByRole('contentinfo').getByText('Reset wallet').click() } async function restoreWallet(page: Page, nsec: string): Promise { await page.getByText('Other login options').click() await page.getByText('Restore wallet').click() - await page.locator('ion-input[name="private-key"] input').fill(nsec) + await page.locator('input[name="private-key"]').fill(nsec) await page.getByText('Continue').click() await waitForWalletPage(page) } diff --git a/src/test/screens/settings/backup.test.tsx b/src/test/screens/settings/backup.test.tsx index 407bb3237..485db3dd3 100644 --- a/src/test/screens/settings/backup.test.tsx +++ b/src/test/screens/settings/backup.test.tsx @@ -44,6 +44,6 @@ describe('Backup screen with default password', () => { await screen.findByText('Private key') expect(screen.getByTestId('toggle-backup')).toBeInTheDocument() - expect(screen.getByTestId('toggle-backup').getAttribute('checked')).toBe('true') + expect(screen.getByTestId('toggle-backup').getAttribute('data-checked')).toBe('true') }) }) diff --git a/src/test/screens/settings/delegates.test.tsx b/src/test/screens/settings/delegates.test.tsx index 03eaae926..ac6d07ef9 100644 --- a/src/test/screens/settings/delegates.test.tsx +++ b/src/test/screens/settings/delegates.test.tsx @@ -40,6 +40,6 @@ describe('Delegates screen', () => { expect(screen.getByText('Use default Arkade delegate')).toBeInTheDocument() expect(screen.getByText(/Delegates can only renew your VTXOs/)).toBeInTheDocument() expect(screen.getByTestId('delegate-card')).toBeInTheDocument() - expect(screen.getByTestId('toggle-delegates').getAttribute('checked')).toBe('true') + expect(screen.getByTestId('toggle-delegates').getAttribute('data-checked')).toBe('true') }) }) diff --git a/src/test/screens/settings/notifications.test.tsx b/src/test/screens/settings/notifications.test.tsx index 576bc8ffb..a671f065a 100644 --- a/src/test/screens/settings/notifications.test.tsx +++ b/src/test/screens/settings/notifications.test.tsx @@ -13,7 +13,7 @@ describe('Notifications screen', () => { ) expect(screen.getByText('Allow notifications')).toBeInTheDocument() expect(screen.getByTestId('toggle-notifications')).toBeInTheDocument() - expect(screen.getByTestId('toggle-notifications').getAttribute('checked')).toBe('true') + expect(screen.getByTestId('toggle-notifications').getAttribute('data-checked')).toBe('true') }) it('renders the notifications screen with the correct toggle', () => { @@ -23,6 +23,6 @@ describe('Notifications screen', () => { , ) expect(screen.getByTestId('toggle-notifications')).toBeInTheDocument() - expect(screen.getByTestId('toggle-notifications').getAttribute('checked')).toBe('true') + expect(screen.getByTestId('toggle-notifications').getAttribute('data-checked')).toBe('true') }) }) diff --git a/src/test/setup.ts b/src/test/setup.ts index 85d7b43e9..8c3c0c341 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -1,5 +1,4 @@ import '@testing-library/jest-dom/vitest' -import { setupIonicReact } from '@ionic/react' import { afterEach, beforeEach, vi } from 'vitest' // jsdom adds ontouchstart which makes isMobileBrowser=true; remove it to simulate desktop @@ -17,8 +16,6 @@ const createMatchMedia = (query: string): MediaQueryList => dispatchEvent: () => false, }) as unknown as MediaQueryList -setupIonicReact() - // Provide a stable matchMedia implementation for tests (used by useReducedMotion, usePwaInstalled, etc.). // Keep this as a plain function so vi.restoreAllMocks() does not reset it. Object.defineProperty(window, 'matchMedia', {