diff --git a/apps/demo-wallet-native/.maestro/.env.example b/apps/demo-wallet-native/.maestro/.env.example new file mode 100644 index 000000000..88ad98ea4 --- /dev/null +++ b/apps/demo-wallet-native/.maestro/.env.example @@ -0,0 +1,11 @@ +# Wallet credentials +MNEMONIC="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon" +PASSWORD=1111 + +# dApp URL for e2e tests +DAPP_URL=https://allure-test-runner.vercel.app/e2e + +# Allure TestOps integration +ALLURE_API_TOKEN=your-allure-api-token +ALLURE_BASE_URL=https://tontech.testops.cloud +ALLURE_PROJECT_ID=100 diff --git a/apps/demo-wallet-native/.maestro/allure.ts b/apps/demo-wallet-native/.maestro/allure.ts new file mode 100644 index 000000000..fbaf9a7a7 --- /dev/null +++ b/apps/demo-wallet-native/.maestro/allure.ts @@ -0,0 +1,164 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +interface TokenResponse { + access_token: string; + token_type: string; + expires_in: number; + scope: string; +} + +export interface AllureConfig { + baseUrl: string; + apiToken: string; + projectId: number; +} + +export type TestCaseData = { + precondition: string; + expectedResult: string; + isPositiveCase: boolean; +}; + +/** + * Получает JWT токен для Allure TestOps API + */ +async function getAllureToken(config: AllureConfig): Promise { + const { baseUrl, apiToken } = config; + + const formData = new FormData(); + formData.append('grant_type', 'apitoken'); + formData.append('scope', 'openid'); + formData.append('token', apiToken); + + const response = await fetch(`${baseUrl}/api/uaa/oauth/token`, { + method: 'POST', + headers: { Accept: 'application/json' }, + body: formData, + }); + + if (!response.ok) { + throw new Error(`Failed to get token: ${response.status} ${response.statusText}`); + } + + const tokenData: TokenResponse = await response.json(); + return tokenData.access_token; +} + +/** + * Создает конфигурацию Allure TestOps из переменных окружения + */ +export function createAllureConfig(): AllureConfig { + const baseUrl = process.env.ALLURE_BASE_URL || 'https://tontech.testops.cloud'; + const apiToken = process.env.ALLURE_API_TOKEN; + const projectId = parseInt(process.env.ALLURE_PROJECT_ID || '100'); + + if (!apiToken) { + throw new Error('ALLURE_API_TOKEN environment variable is required'); + } + + return { baseUrl, apiToken, projectId }; +} + +/** + * Утилита для работы с Allure TestOps API + */ +export class AllureApiClient { + private config: AllureConfig; + private token?: string; + private tokenExpiry?: number; + + constructor(config: AllureConfig) { + this.config = config; + } + + /** + * Получает актуальный токен (с кэшированием) + */ + private async getValidToken(): Promise { + const now = Date.now(); + + if (!this.token || !this.tokenExpiry || now >= this.tokenExpiry) { + this.token = await getAllureToken(this.config); + // Токен действует 1 час, обновляем за 5 минут до истечения + this.tokenExpiry = now + 55 * 60 * 1000; + } + + return this.token; + } + + /** + * Выполняет авторизованный запрос к Allure API + */ + private async makeRequest(endpoint: string, options: { headers?: Record } = {}): Promise { + const token = await this.getValidToken(); + + const response = await fetch(`${this.config.baseUrl}${endpoint}`, { + ...options, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + ...options.headers, + }, + }); + + if (!response.ok) { + throw new Error(`API request failed: ${response.status} ${response.statusText}`); + } + + return response; + } + + /** + * Получает информацию о тест-кейсе по allureId + */ + async getTestCase(allureId: string): Promise { + const response = await this.makeRequest(`/api/rs/testcase/allureId/${allureId}`); + return await response.json(); + } + + /** + * Получает информацию о тест-кейсе по ID + */ + async getTestCaseById(id: string): Promise { + const response = await this.makeRequest(`/api/testcase/${id}`); + return await response.json(); + } +} + +/** + * Получает данные тест-кейса и извлекает precondition и expectedResult + */ +export async function getTestCaseData(allureClient: AllureApiClient, allureId: string): Promise { + const testCaseData = await allureClient.getTestCaseById(allureId); + + if (typeof testCaseData !== 'object' || testCaseData === null || !('name' in testCaseData)) { + throw new Error('Test case data is not an object'); + } + + const data = testCaseData as { name: string; precondition?: string; expectedResult?: string }; + const isPositiveCase = !String(data.name).toLowerCase().includes('error'); + + return { + precondition: parseAllureField(data.precondition || ''), + expectedResult: parseAllureField(data.expectedResult || ''), + isPositiveCase, + }; +} + +function parseAllureField(value: string): string { + // Extract JSON from Markdown code block if present + const jsonMatch = value.match(/```(?:json)?\s*([\s\S]*?)```/); + + if (jsonMatch) { + return jsonMatch[1].trim(); + } + + return value.trim(); +} diff --git a/apps/demo-wallet-native/.maestro/config.ts b/apps/demo-wallet-native/.maestro/config.ts new file mode 100644 index 000000000..86fdf0fd9 --- /dev/null +++ b/apps/demo-wallet-native/.maestro/config.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export interface TestConfig { + name: string; + file: string; + allureId?: string; +} + +export interface TestsConfig { + tests: TestConfig[]; +} + +export const config: TestsConfig = { + tests: [ + { + name: 'Import wallet', + file: 'tests/import-wallet.yaml', + }, + { + name: 'Connect/Disconnect', + file: 'tests/connect-disconnect.yaml', + }, + // { + // name: 'Sign text', + // file: 'tests/sign-data-test.yaml', + // allureId: '2258', + // }, + // { + // name: 'Sign cell', + // file: 'tests/sign-data-test.yaml', + // allureId: '2260', + // }, + // { + // name: 'Sign binary', + // file: 'tests/sign-data-test.yaml', + // allureId: '2259', + // }, + ], +}; diff --git a/apps/demo-wallet-native/.maestro/flows/connect-wallet.yaml b/apps/demo-wallet-native/.maestro/flows/connect-wallet.yaml new file mode 100644 index 000000000..83599ed84 --- /dev/null +++ b/apps/demo-wallet-native/.maestro/flows/connect-wallet.yaml @@ -0,0 +1,31 @@ +appId: org.ton.demowallet +env: + DAPP_URL: https://allure-test-runner.vercel.app/e2e +--- +# Open dApp and connect +- openLink: + link: ${DAPP_URL} + browser: true + +- assertVisible: + text: "Connect Wallet" + enabled: true + +- tapOn: "Connect Wallet" + +- assertVisible: + text: "Tonkeeper" + +- tapOn: "Tonkeeper" + +- assertVisible: + text: "Open" + +- tapOn: "Open" + +# Approve connection in wallet +- assertVisible: + id: connect-approve + +- tapOn: + id: connect-approve diff --git a/apps/demo-wallet-native/.maestro/flows/unlock-wallet.yaml b/apps/demo-wallet-native/.maestro/flows/unlock-wallet.yaml new file mode 100644 index 000000000..6b9f93838 --- /dev/null +++ b/apps/demo-wallet-native/.maestro/flows/unlock-wallet.yaml @@ -0,0 +1,16 @@ +appId: org.ton.demowallet +--- +- assertVisible: + id: unlock-password-input + +- tapOn: + id: unlock-password-input + +- inputText: ${PASSWORD} +- hideKeyboard + +- tapOn: + id: unlock-submit-button + +- assertVisible: + id: ton-balance-card diff --git a/apps/demo-wallet-native/.maestro/run-tests.ts b/apps/demo-wallet-native/.maestro/run-tests.ts new file mode 100644 index 000000000..ed2c9c979 --- /dev/null +++ b/apps/demo-wallet-native/.maestro/run-tests.ts @@ -0,0 +1,133 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/* eslint-disable no-console */ + +import { execSync } from 'child_process'; +import { resolve } from 'path'; + +import { config } from 'dotenv'; + +import { config as testsConfig, type TestConfig } from './config'; +import { AllureApiClient, createAllureConfig, getTestCaseData } from './allure'; +import { escapeShellArg } from './utils'; + +// Load .env from .maestro folder +config({ path: resolve(__dirname, '.env') }); + +async function runTest( + testConfig: TestConfig, + allureClient: AllureApiClient | null, + baseEnvVars: string, +): Promise { + console.log(`\n${'='.repeat(60)}`); + console.log(`Running: ${testConfig.name}${testConfig.allureId ? ` (allureId: ${testConfig.allureId})` : ''}`); + console.log('='.repeat(60)); + + let envVars = baseEnvVars; + + // Only fetch Allure data if allureId is provided + if (testConfig.allureId && allureClient) { + try { + const testData = await getTestCaseData(allureClient, testConfig.allureId); + + const precondition = escapeShellArg(testData.precondition); + const expectedResult = escapeShellArg(testData.expectedResult); + + envVars += ` -e ALLURE_ID=${testConfig.allureId} -e PRECONDITION=${precondition} -e EXPECTED_RESULT=${expectedResult}`; + } catch (error) { + console.error(`Failed to fetch Allure data for ${testConfig.name}:`, error); + return false; + } + } + + const command = `maestro test ${envVars} ${testConfig.file}`; + + try { + execSync(command, { stdio: 'inherit', cwd: __dirname }); + console.log(`✅ ${testConfig.name} - PASSED`); + return true; + } catch { + console.error(`❌ ${testConfig.name} - FAILED`); + return false; + } +} + +async function main() { + const filterName = process.argv[2]; + + // Environment variables from .env + const password = process.env.PASSWORD || '1111'; + const mnemonic = process.env.MNEMONIC || ''; + const dappUrl = process.env.DAPP_URL || 'https://allure-test-runner.vercel.app/e2e'; + + let baseEnvVars = `-e PASSWORD=${escapeShellArg(password)} -e DAPP_URL=${escapeShellArg(dappUrl)}`; + + if (mnemonic) { + baseEnvVars += ` -e MNEMONIC=${escapeShellArg(mnemonic)}`; + } + + // Load tests config first to check if we need Allure + let testsToRun = testsConfig.tests; + + // Filter tests if name provided + if (filterName) { + testsToRun = testsToRun.filter( + (t) => t.name.toLowerCase().includes(filterName.toLowerCase()) || t.allureId === filterName, + ); + + if (testsToRun.length === 0) { + console.error(`No tests found matching: ${filterName}`); + process.exit(1); + } + } + + // Check if any test requires Allure + const needsAllure = testsToRun.some((t) => t.allureId); + + // Create Allure client only if needed + let allureClient: AllureApiClient | null = null; + if (needsAllure) { + try { + const allureConfig = createAllureConfig(); + allureClient = new AllureApiClient(allureConfig); + } catch (error) { + console.error('Failed to create Allure client:', error); + process.exit(1); + } + } + + console.log(`Running ${testsToRun.length} test(s)...`); + + const results: { name: string; passed: boolean }[] = []; + + for (const testConfig of testsToRun) { + const passed = await runTest(testConfig, allureClient, baseEnvVars); + results.push({ name: testConfig.name, passed }); + } + + // Summary + console.log(`\n${'='.repeat(60)}`); + console.log('SUMMARY'); + console.log('='.repeat(60)); + + const passed = results.filter((r) => r.passed).length; + const failed = results.filter((r) => !r.passed).length; + + for (const result of results) { + console.log(`${result.passed ? '✅' : '❌'} ${result.name}`); + } + + console.log(`\nTotal: ${results.length} | Passed: ${passed} | Failed: ${failed}`); + + if (failed > 0) { + process.exit(1); + } +} + +void main(); diff --git a/apps/demo-wallet-native/.maestro/tests/connect-disconnect.yaml b/apps/demo-wallet-native/.maestro/tests/connect-disconnect.yaml new file mode 100644 index 000000000..4c0bfd58b --- /dev/null +++ b/apps/demo-wallet-native/.maestro/tests/connect-disconnect.yaml @@ -0,0 +1,25 @@ +appId: org.ton.demowallet +env: + PASSWORD: 1111 + DAPP_URL: https://allure-test-runner.vercel.app/e2e +--- +- launchApp + +- runFlow: + file: ../flows/unlock-wallet.yaml + env: + PASSWORD: ${PASSWORD} + +- runFlow: + file: ../flows/connect-wallet.yaml + env: + DAPP_URL: ${DAPP_URL} + +- openLink: + link: ${DAPP_URL} + browser: true + +- scrollUntilVisible: + element: "Disconnect Wallet" + +- tapOn: "Disconnect Wallet" diff --git a/apps/demo-wallet-native/.maestro/tests/import-wallet.yaml b/apps/demo-wallet-native/.maestro/tests/import-wallet.yaml new file mode 100644 index 000000000..0fcdddd4c --- /dev/null +++ b/apps/demo-wallet-native/.maestro/tests/import-wallet.yaml @@ -0,0 +1,53 @@ +appId: org.ton.demowallet +env: + MNEMONIC: ${MNEMONIC || "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"} + PASSWORD: 1111 + DAPP_URL: https://allure-test-runner.vercel.app/e2e +--- +- launchApp: + clearState: true + +# Start screen +- assertVisible: + id: start-import-button +- assertVisible: + id: start-create-button + +# Tap Import Wallet +- tapOn: + id: start-import-button + +# Password screen +- assertVisible: + id: password-input +- tapOn: + id: password-input +- inputText: ${PASSWORD} +- tapOn: + id: confirm-password-input +- inputText: ${PASSWORD} +- hideKeyboard +- tapOn: + id: submit-new-password + +# Import Mnemonic screen +- assertVisible: Import Wallet +- assertVisible: Enter Recovery Phrase + +# Enter mnemonic +- tapOn: + id: mnemonic-input +- inputText: ${MNEMONIC} +- hideKeyboard + +# Select network +- tapOn: + id: tab-control-mainnet + +# Import +- tapOn: + id: import-wallet-button + +# Should navigate to wallet screen +- assertVisible: + id: ton-balance-card diff --git a/apps/demo-wallet-native/.maestro/tests/sign-data-test.yaml b/apps/demo-wallet-native/.maestro/tests/sign-data-test.yaml new file mode 100644 index 000000000..f278f400b --- /dev/null +++ b/apps/demo-wallet-native/.maestro/tests/sign-data-test.yaml @@ -0,0 +1,79 @@ +appId: org.ton.demowallet +env: + PASSWORD: 1111 + DAPP_URL: https://allure-test-runner.vercel.app/e2e + PRECONDITION: ${PRECONDITION} + EXPECTED_RESULT: ${EXPECTED_RESULT} +--- +# Launch app and unlock wallet +- launchApp + +- runFlow: + file: ../flows/unlock-wallet.yaml + env: + PASSWORD: ${PASSWORD} + +- runFlow: + file: ../flows/connect-wallet.yaml + env: + DAPP_URL: ${DAPP_URL} + +# Go back to dApp, fill precondition/expectedResult, and trigger Sign Data +- openLink: + link: ${DAPP_URL} + browser: true + +# Fill precondition field if provided +- scrollUntilVisible: + element: "Sign Data" +- tapOn: + text: "Precondition (JSON)" + below: "Sign Data Test" +- waitForAnimationToEnd: + timeout: 1000 +- longPressOn: + text: "Precondition (JSON)" + below: "Sign Data Test" +- tapOn: 'Select All' +- eraseText +- inputText: ${PRECONDITION} +- hideKeyboard + +# Fill expected result field (the one after Sign Data Test) +- scrollUntilVisible: + element: "Sign Data" +- tapOn: + text: "Expected Result (JSON)" + below: "Sign Data Test" +- longPressOn: + text: "Expected Result (JSON)" + below: "Sign Data Test" +- tapOn: 'Select All' +- eraseText +- inputText: ${EXPECTED_RESULT} +- hideKeyboard + +- scrollUntilVisible: + element: "Sign Data" + +- tapOn: "Sign Data" + +- launchApp: + stopApp: false + +# Approve sign data in wallet +- assertVisible: + id: sign-data-approve + +- tapOn: + id: sign-data-approve + +- launchApp: + appId: com.apple.mobilesafari + stopApp: false + +- scrollUntilVisible: + element: "Validation Passed" + +- assertVisible: + text: "Validation Passed" diff --git a/apps/demo-wallet-native/.maestro/utils.ts b/apps/demo-wallet-native/.maestro/utils.ts new file mode 100644 index 000000000..d704fd8c4 --- /dev/null +++ b/apps/demo-wallet-native/.maestro/utils.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export const escapeShellArg = (arg: string): string => { + // Replace single quotes with escaped version and wrap in single quotes + return `'${arg.replace(/'/g, "'\\''")}'`; +}; diff --git a/apps/demo-wallet-native/app.json b/apps/demo-wallet-native/app.json index e3d83133a..b5bc309b7 100644 --- a/apps/demo-wallet-native/app.json +++ b/apps/demo-wallet-native/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Ton Wallet", "slug": "ton-wallet", - "scheme": "ton-wallet", + "scheme": "tonkeeper", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", @@ -15,7 +15,7 @@ }, "ios": { "supportsTablet": true, - "bundleIdentifier": "com.heyllog.tonwallet" + "bundleIdentifier": "org.ton.demowallet" }, "android": { "adaptiveIcon": { @@ -24,7 +24,7 @@ }, "edgeToEdgeEnabled": true, "predictiveBackGestureEnabled": false, - "package": "com.heyllog.tonwallet" + "package": "org.ton.demowallet" }, "web": { "favicon": "./assets/favicon.png" diff --git a/apps/demo-wallet-native/docs/e2e-testing.md b/apps/demo-wallet-native/docs/e2e-testing.md new file mode 100644 index 000000000..ab93c3ab4 --- /dev/null +++ b/apps/demo-wallet-native/docs/e2e-testing.md @@ -0,0 +1,90 @@ +# E2E Testing Guide + +This guide explains how to run end-to-end tests for the demo-wallet-native app using [Maestro](https://maestro.mobile.dev/). + +## Prerequisites + +1. **Install Maestro** + + Follow the installation guide: https://docs.maestro.dev/getting-started/installing-maestro + +2. **Build the app** + ```bash + pnpm ios + # or + pnpm android + ``` + +3. **Configure environment** + ```bash + cp .maestro/.env.example .maestro/.env + ``` + + Edit `.maestro/.env` and set your values: + - `MNEMONIC` - wallet seed phrase for import tests + - `PASSWORD` - wallet password (default: 1111) + - `DAPP_URL` - dApp URL for connect tests + - `ALLURE_API_TOKEN` - Allure TestOps API token (for Allure integration) + - `ALLURE_BASE_URL` - Allure TestOps URL (default: https://tontech.testops.cloud) + - `ALLURE_PROJECT_ID` - Allure project ID (default: 100) + +## Running Tests + +### Run all tests +```bash +pnpm e2e +``` + +### Run specific test by name +```bash +pnpm e2e "Sign text" +``` + +### Run test by Allure ID +```bash +pnpm e2e 2258 +``` + +### Run any Maestro test directly +```bash +maestro test .maestro/tests/.yaml +``` + +## Test Configuration + +Tests are configured in `.maestro/config.ts`. Each test has: +- `name` - test display name +- `file` - path to Maestro YAML file +- `allureId` - (optional) Allure TestOps test case ID for fetching precondition/expectedResult + +## Test Files + +| File | Description | +|------|-------------| +| `tests/import-wallet.yaml` | Import wallet with mnemonic | +| `tests/connect-disconnect.yaml` | Connect to dApp and disconnect | +| `tests/sign-data-test.yaml` | Sign data request (requires Allure) | + +## Flows (Reusable) + +| Flow | Description | +|------|-------------| +| `flows/unlock-wallet.yaml` | Unlock wallet with password | +| `flows/connect-wallet.yaml` | Connect to dApp via Tonkeeper | + +## Architecture + +``` +.maestro/ +├── config.ts # Test configuration +├── run-tests.ts # Test runner script +├── allure.ts # Allure TestOps API client +├── utils.ts # Utility functions +├── flows/ # Reusable Maestro flows +│ ├── unlock-wallet.yaml +│ └── connect-wallet.yaml +└── tests/ # Test files + ├── import-wallet.yaml + ├── connect-disconnect.yaml + └── sign-data-test.yaml +``` diff --git a/apps/demo-wallet-native/package.json b/apps/demo-wallet-native/package.json index dce86f15e..7cb1c19d3 100644 --- a/apps/demo-wallet-native/package.json +++ b/apps/demo-wallet-native/package.json @@ -7,7 +7,8 @@ "android": "expo run:android", "ios": "expo run:ios", "typecheck": "tsc --noEmit --emitDeclarationOnly false", - "clean": "git clean -xdf .cache .expo .turbo android ios node_modules" + "clean": "git clean -xdf .cache .expo .turbo android ios node_modules", + "e2e": "npx tsx .maestro/run-tests.ts" }, "dependencies": { "@craftzdog/react-native-buffer": "6.1.1", @@ -72,6 +73,7 @@ "devDependencies": { "@biomejs/biome": "2.3.2", "@types/react": "catalog:", + "dotenv": "^17.2.3", "typescript": "5.9.3", "ultracite": "6.1.0" }, diff --git a/apps/demo-wallet-native/src/app/(auth)/(tabs)/history.tsx b/apps/demo-wallet-native/src/app/(auth)/(tabs)/history.tsx index 4a5b3760d..de47ef2a7 100644 --- a/apps/demo-wallet-native/src/app/(auth)/(tabs)/history.tsx +++ b/apps/demo-wallet-native/src/app/(auth)/(tabs)/history.tsx @@ -34,12 +34,6 @@ const HistoryScreen: FC = () => { setSelectedEvent(null); }, []); - const renderHeader = () => ( - - Transactions - - ); - const renderContent = () => { if (error) { return ; @@ -59,12 +53,14 @@ const HistoryScreen: FC = () => { return ( - contentContainerStyle={styles.list} data={events as Event[]} keyExtractor={(item) => item.eventId} ListHeaderComponent={ <> - {renderHeader()} + + Transactions + + {renderContent()} } @@ -96,8 +92,6 @@ const styles = StyleSheet.create(({ sizes }, runtime) => ({ marginTop: runtime.insets.top, marginLeft: runtime.insets.left, marginRight: runtime.insets.right, - }, - list: { paddingTop: sizes.page.paddingTop, paddingBottom: sizes.page.paddingBottom, paddingHorizontal: sizes.page.paddingHorizontal, diff --git a/apps/demo-wallet-native/src/app/(auth)/(tabs)/wallet.tsx b/apps/demo-wallet-native/src/app/(auth)/(tabs)/wallet.tsx index 0b2e1a8e2..4c9f80ddd 100644 --- a/apps/demo-wallet-native/src/app/(auth)/(tabs)/wallet.tsx +++ b/apps/demo-wallet-native/src/app/(auth)/(tabs)/wallet.tsx @@ -97,7 +97,7 @@ const WalletHomeScreen: FC = () => { - router.push('/connect-dapp')}> + router.push('/connect-dapp')} testID="scan-button"> @@ -121,7 +121,7 @@ const WalletHomeScreen: FC = () => { savedWallets={savedWallets} /> - + diff --git a/apps/demo-wallet-native/src/app/(auth)/connect-dapp.tsx b/apps/demo-wallet-native/src/app/(auth)/connect-dapp.tsx index 1ab96da24..930f7472d 100644 --- a/apps/demo-wallet-native/src/app/(auth)/connect-dapp.tsx +++ b/apps/demo-wallet-native/src/app/(auth)/connect-dapp.tsx @@ -74,6 +74,7 @@ const ConnectDAppScreen: FC = () => { handleConnect(tonConnectUrl)} disabled={!tonConnectUrl.trim() || isConnecting} style={styles.connectButton} diff --git a/apps/demo-wallet-native/src/app/(non-auth)/add-new-wallet.tsx b/apps/demo-wallet-native/src/app/(non-auth)/add-new-wallet.tsx index d92f13a2a..a1de59e0c 100644 --- a/apps/demo-wallet-native/src/app/(non-auth)/add-new-wallet.tsx +++ b/apps/demo-wallet-native/src/app/(non-auth)/add-new-wallet.tsx @@ -58,11 +58,11 @@ const StartScreen: FC = () => { - + Create New Wallet - + Import Wallet diff --git a/apps/demo-wallet-native/src/app/(non-auth)/import-mnemonic.tsx b/apps/demo-wallet-native/src/app/(non-auth)/import-mnemonic.tsx index 614fcebf7..1c295ffe0 100644 --- a/apps/demo-wallet-native/src/app/(non-auth)/import-mnemonic.tsx +++ b/apps/demo-wallet-native/src/app/(non-auth)/import-mnemonic.tsx @@ -78,6 +78,7 @@ const ImportMnemonicScreen: FC = () => { { { { /> { - + Continue diff --git a/apps/demo-wallet-native/src/app/(non-auth)/unlock-wallet.tsx b/apps/demo-wallet-native/src/app/(non-auth)/unlock-wallet.tsx index dc806181e..64a59c7c0 100644 --- a/apps/demo-wallet-native/src/app/(non-auth)/unlock-wallet.tsx +++ b/apps/demo-wallet-native/src/app/(non-auth)/unlock-wallet.tsx @@ -92,6 +92,7 @@ const UnlockWalletScreen: FC = () => { Enter your password to unlock your wallet. { - + Continue - + Reset Wallet diff --git a/apps/demo-wallet-native/src/app/+native-intent.tsx b/apps/demo-wallet-native/src/app/+native-intent.tsx new file mode 100644 index 000000000..dd81ac991 --- /dev/null +++ b/apps/demo-wallet-native/src/app/+native-intent.tsx @@ -0,0 +1,17 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export function redirectSystemPath({ path }: { path: string; initial: boolean }): string | null { + // Handle TON Connect deeplinks - prevent Expo Router from treating them as routes + if (path.startsWith('tonkeeper://ton-connect') || path.includes('tc://') || path.includes('ton://')) { + // Return null to cancel navigation - useDeepLinkHandler will handle the URL + return null; + } + + return path; +} diff --git a/apps/demo-wallet-native/src/app/_layout.tsx b/apps/demo-wallet-native/src/app/_layout.tsx index 68d7fff8f..60682f0b9 100644 --- a/apps/demo-wallet-native/src/app/_layout.tsx +++ b/apps/demo-wallet-native/src/app/_layout.tsx @@ -46,7 +46,8 @@ const RootLayout: FC = () => { storage={walletProviderStorage} walletKitConfig={{ storage: walletKitStorage, - bridgeUrl: 'https://walletbot.me/tonconnect-bridge/bridge', + bridgeUrl: 'https://bridge.tonapi.io/bridge', + // bridgeUrl: 'https://walletbot.me/tonconnect-bridge/bridge', tonApiKeyMainnet: ENV_TON_API_KEY_MAINNET, tonApiKeyTestnet: ENV_TON_API_KEY_TESTNET, }} diff --git a/apps/demo-wallet-native/src/core/components/active-touch-action/active-touch-action.tsx b/apps/demo-wallet-native/src/core/components/active-touch-action/active-touch-action.tsx index b0e3d645f..c53258c64 100644 --- a/apps/demo-wallet-native/src/core/components/active-touch-action/active-touch-action.tsx +++ b/apps/demo-wallet-native/src/core/components/active-touch-action/active-touch-action.tsx @@ -17,6 +17,7 @@ export interface ActiveTouchActionProps extends PropsWithChildren { disabled?: boolean; scaling?: number; hitSlop?: number; + testID?: string; } export const ActiveTouchAction: FC = ({ @@ -27,6 +28,7 @@ export const ActiveTouchAction: FC = ({ scaling = 0.96, children, hitSlop, + testID, }) => { const pressed = useSharedValue(false); @@ -48,6 +50,7 @@ export const ActiveTouchAction: FC = ({ return ( Promise | void; disabled?: boolean; + testID?: string; } export const ButtonContainer: FC = ({ @@ -30,6 +31,7 @@ export const ButtonContainer: FC = colorScheme = 'primary', variant = 'standard', disabled = false, + testID, }) => { styles.useVariants({ variant, colorScheme }); @@ -49,6 +51,7 @@ export const ButtonContainer: FC = return ( = ({ style, onClose, ...props }) ); }; -const styles = StyleSheet.create(({ colors, sizes }) => ({ - container: { - width: '100%', - position: 'relative', - height: 40, - alignItems: 'center', - justifyContent: 'center', - marginBottom: sizes.space.vertical, - }, - backButton: { - position: 'absolute', - left: 1, - top: 8, - }, - title: { - color: colors.text.highlight, - textAlign: 'center', - }, +const styles = StyleSheet.create(() => ({ closeButton: { position: 'absolute', top: 6, - right: 12, - }, - cancelButton: { - position: 'absolute', - top: 3, - right: 0, - paddingHorizontal: 14, - paddingVertical: 8, - }, - cancelButtonText: { - color: colors.text.highlight, + right: 6, }, })); diff --git a/apps/demo-wallet-native/src/core/components/screen-header/container.tsx b/apps/demo-wallet-native/src/core/components/screen-header/container.tsx index c9804d2db..5aaa6ce87 100644 --- a/apps/demo-wallet-native/src/core/components/screen-header/container.tsx +++ b/apps/demo-wallet-native/src/core/components/screen-header/container.tsx @@ -12,17 +12,30 @@ import { StyleSheet } from 'react-native-unistyles'; import { Row } from '../grid'; -export const ScreenHeaderContainer: FC = ({ style, ...props }) => { - return ; +interface Props extends ViewProps { + type?: 'screen' | 'modal'; +} + +export const ScreenHeaderContainer: FC = ({ style, type = 'screen', ...props }) => { + return ( + + ); }; const styles = StyleSheet.create(({ sizes }) => ({ container: { - width: '100%', position: 'relative', - height: 40, + height: 45, alignItems: 'center', justifyContent: 'center', marginBottom: sizes.space.vertical, }, + screen: {}, + modal: { + marginTop: sizes.page.paddingTop * 2, + marginHorizontal: sizes.page.paddingHorizontal, + }, })); diff --git a/apps/demo-wallet-native/src/core/components/screen-header/left-side.tsx b/apps/demo-wallet-native/src/core/components/screen-header/left-side.tsx index 797737fe9..da9173e70 100644 --- a/apps/demo-wallet-native/src/core/components/screen-header/left-side.tsx +++ b/apps/demo-wallet-native/src/core/components/screen-header/left-side.tsx @@ -21,6 +21,6 @@ const styles = StyleSheet.create(() => ({ justifyContent: 'center', position: 'absolute', left: 0, - height: 40, + height: 45, }, })); diff --git a/apps/demo-wallet-native/src/core/components/screen-header/right-side.tsx b/apps/demo-wallet-native/src/core/components/screen-header/right-side.tsx index 5fee213f9..345cdaf5a 100644 --- a/apps/demo-wallet-native/src/core/components/screen-header/right-side.tsx +++ b/apps/demo-wallet-native/src/core/components/screen-header/right-side.tsx @@ -22,7 +22,7 @@ const styles = StyleSheet.create(({ sizes }) => ({ alignItems: 'center', position: 'absolute', right: 0, - height: 40, + height: 45, gap: sizes.space.horizontal / 2, }, })); diff --git a/apps/demo-wallet-native/src/core/components/screen-header/screen-header.tsx b/apps/demo-wallet-native/src/core/components/screen-header/screen-header.tsx index 81ec296ee..20e731b46 100644 --- a/apps/demo-wallet-native/src/core/components/screen-header/screen-header.tsx +++ b/apps/demo-wallet-native/src/core/components/screen-header/screen-header.tsx @@ -86,7 +86,7 @@ const styles = StyleSheet.create(({ colors, sizes }) => ({ container: { width: '100%', position: 'relative', - height: 40, + height: 45, alignItems: 'center', justifyContent: 'center', marginBottom: sizes.space.vertical, diff --git a/apps/demo-wallet-native/src/core/components/tab-control/tab-control.tsx b/apps/demo-wallet-native/src/core/components/tab-control/tab-control.tsx index df22dec4d..37d8ac6e7 100644 --- a/apps/demo-wallet-native/src/core/components/tab-control/tab-control.tsx +++ b/apps/demo-wallet-native/src/core/components/tab-control/tab-control.tsx @@ -35,6 +35,7 @@ export const TabControl = ({ {options.map((option) => ( onOptionPress?.(option.value)} diff --git a/apps/demo-wallet-native/src/features/nft/components/nft-details-modal/nft-details-modal.tsx b/apps/demo-wallet-native/src/features/nft/components/nft-details-modal/nft-details-modal.tsx index 40e61bca2..ac2f05269 100644 --- a/apps/demo-wallet-native/src/features/nft/components/nft-details-modal/nft-details-modal.tsx +++ b/apps/demo-wallet-native/src/features/nft/components/nft-details-modal/nft-details-modal.tsx @@ -63,15 +63,15 @@ export const NftDetailsModal: FC = ({ nft, visible, onClos return ( - - - {nftName} + + {nftName} - - - - + + + + + {nftImage ? ( @@ -136,7 +136,6 @@ export const NftDetailsModal: FC = ({ nft, visible, onClos const styles = StyleSheet.create(({ sizes, colors }, runtime) => ({ contentContainer: { paddingBottom: runtime.insets.bottom + sizes.page.paddingBottom, - paddingVertical: sizes.block.paddingVertical, paddingHorizontal: sizes.page.paddingHorizontal, marginLeft: runtime.insets.left, marginRight: runtime.insets.right, diff --git a/apps/demo-wallet-native/src/features/send/components/token-list-sheet/token-list-sheet.tsx b/apps/demo-wallet-native/src/features/send/components/token-list-sheet/token-list-sheet.tsx index 528d3bf7b..f99407dc4 100644 --- a/apps/demo-wallet-native/src/features/send/components/token-list-sheet/token-list-sheet.tsx +++ b/apps/demo-wallet-native/src/features/send/components/token-list-sheet/token-list-sheet.tsx @@ -6,10 +6,9 @@ * */ -import { Ionicons } from '@expo/vector-icons'; import type { FC } from 'react'; import { ScrollView, View } from 'react-native'; -import { StyleSheet, useUnistyles } from 'react-native-unistyles'; +import { StyleSheet } from 'react-native-unistyles'; import { useFormattedTonBalance, useJettons } from '@ton/demo-core'; import type { AddressJetton } from '@ton/walletkit'; @@ -19,6 +18,7 @@ import { AppModal } from '@/core/components/app-modal'; import { AppText } from '@/core/components/app-text'; import { CircleLogo } from '@/core/components/circle-logo'; import { TextAmount } from '@/core/components/text-amount'; +import { ScreenHeader } from '@/core/components/screen-header'; interface SelectedToken { type: 'TON' | 'JETTON'; @@ -37,8 +37,6 @@ export const TokenListSheet: FC = ({ isOpen, onClose, onSel const { userJettons, formatJettonAmount } = useJettons(); const tonBalance = useFormattedTonBalance(); - const { theme } = useUnistyles(); - const handleSelectTon = () => { onSelectTon(); onClose(); @@ -51,17 +49,15 @@ export const TokenListSheet: FC = ({ isOpen, onClose, onSel return ( - - - - Select - + + Select - - - - + + + + + @@ -124,22 +120,7 @@ export const TokenListSheet: FC = ({ isOpen, onClose, onSel const styles = StyleSheet.create(({ sizes, colors }, runtime) => ({ container: { paddingHorizontal: sizes.page.paddingHorizontal, - paddingVertical: sizes.block.paddingVertical, - marginBottom: runtime.insets.bottom * 2, - }, - header: { - position: 'relative', - marginTop: 10, - marginBottom: 20, - }, - title: { - color: colors.text.highlight, - textAlign: 'center', - }, - closeButton: { - position: 'absolute', - top: 0, - right: 12, + paddingBottom: runtime.insets.bottom + sizes.block.paddingVertical, }, tokenItem: { flexDirection: 'row', diff --git a/apps/demo-wallet-native/src/features/settings/components/app-wrapper/app-wrapper.tsx b/apps/demo-wallet-native/src/features/settings/components/app-wrapper/app-wrapper.tsx index a26d350f0..68a53432f 100644 --- a/apps/demo-wallet-native/src/features/settings/components/app-wrapper/app-wrapper.tsx +++ b/apps/demo-wallet-native/src/features/settings/components/app-wrapper/app-wrapper.tsx @@ -14,6 +14,7 @@ import { useWalletStore } from '@ton/demo-core'; import { useInitialRedirect } from 'src/features/settings/hooks/use-initial-redirect'; import { useAppFonts } from '../../hooks/use-app-fonts'; +import { useDeepLinkHandler } from '../../hooks/use-deep-link-handler'; import { useTheme } from '../../hooks/use-theme'; import { useWalletDataUpdater } from '../../hooks/use-wallet-data-updater'; import { setIsAppReady } from '../../store/actions/is-app-ready'; @@ -34,6 +35,7 @@ export const AppWrapper: FC = ({ children }) => { useInitialRedirect(isLoaderShown); useWalletDataUpdater(); + useDeepLinkHandler(); useEffect(() => { // eslint-disable-next-line no-undef diff --git a/apps/demo-wallet-native/src/features/settings/hooks/use-deep-link-handler.ts b/apps/demo-wallet-native/src/features/settings/hooks/use-deep-link-handler.ts new file mode 100644 index 000000000..b3c3059ab --- /dev/null +++ b/apps/demo-wallet-native/src/features/settings/hooks/use-deep-link-handler.ts @@ -0,0 +1,104 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import * as Linking from 'expo-linking'; +import { useEffect, useCallback, useRef } from 'react'; +import { Alert } from 'react-native'; +import { useTonConnect, useWalletStore } from '@ton/demo-core'; + +const extractTonConnectUrl = (deepLinkUrl: string): string | null => { + if (deepLinkUrl.startsWith('tc://') || deepLinkUrl.startsWith('ton://')) { + return deepLinkUrl; + } + + const tkMatch = deepLinkUrl.match(/^tonkeeper:\/\/ton-connect\?(.+)$/); + + if (tkMatch) { + return `tc://?${tkMatch[1]}`; + } + + return null; +}; + +export const useDeepLinkHandler = (): void => { + const { handleTonConnectUrl } = useTonConnect(); + + const isUnlocked = useWalletStore((state) => state.auth.isUnlocked); + const isProcessingRef = useRef(false); + const pendingUrlRef = useRef(null); + + const processDeepLink = useCallback( + async (url: string | null) => { + if (!url || isProcessingRef.current) return; + + const tonConnectUrl = extractTonConnectUrl(url); + + if (!tonConnectUrl) { + return; + } + + if (!isUnlocked) { + pendingUrlRef.current = tonConnectUrl; + return; + } + + isProcessingRef.current = true; + + try { + await handleTonConnectUrl(tonConnectUrl); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to handle deep link:', error); + Alert.alert('Connection Failed', 'Failed to connect to the dApp. Please try again.'); + } finally { + isProcessingRef.current = false; + } + }, + [handleTonConnectUrl, isUnlocked], + ); + + useEffect(() => { + if (isUnlocked && pendingUrlRef.current) { + const url = pendingUrlRef.current; + pendingUrlRef.current = null; + void (async () => { + isProcessingRef.current = true; + try { + await handleTonConnectUrl(url); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to handle pending deep link:', error); + Alert.alert('Connection Failed', 'Failed to connect to the dApp. Please try again.'); + } finally { + isProcessingRef.current = false; + } + })(); + } + }, [isUnlocked, handleTonConnectUrl]); + + useEffect(() => { + const handleInitialUrl = async (): Promise => { + const initialUrl = await Linking.getInitialURL(); + if (initialUrl) { + await processDeepLink(initialUrl); + } + }; + + void handleInitialUrl(); + }, [processDeepLink]); + + useEffect(() => { + const subscription = Linking.addEventListener('url', (event) => { + void processDeepLink(event.url); + }); + + return () => { + subscription.remove(); + }; + }, [processDeepLink]); +}; diff --git a/apps/demo-wallet-native/src/features/ton-connect/components/action-buttons/action-buttons.tsx b/apps/demo-wallet-native/src/features/ton-connect/components/action-buttons/action-buttons.tsx index f200d8dc8..ae6481d63 100644 --- a/apps/demo-wallet-native/src/features/ton-connect/components/action-buttons/action-buttons.tsx +++ b/apps/demo-wallet-native/src/features/ton-connect/components/action-buttons/action-buttons.tsx @@ -19,6 +19,8 @@ interface ActionButtonsProps { onSecondaryPress: () => void; isLoading?: boolean; isPrimaryDisabled?: boolean; + primaryTestID?: string; + secondaryTestID?: string; } export const ActionButtons: FC = ({ @@ -28,10 +30,13 @@ export const ActionButtons: FC = ({ onSecondaryPress, isLoading = false, isPrimaryDisabled = false, + primaryTestID, + secondaryTestID, }) => { return ( = ({ = ({ request, isO return ( <> - - + + + Connect Request + + + + = ({ request, isO {selectedWallet && ( - Wallet: - - setShowWalletSelector(true)}> - - Change wallet - - + Wallet + + {availableWallets.length > 1 && ( + setShowWalletSelector(true)}> + + Change wallet + + + )} {selectedWallet && ( @@ -138,26 +146,31 @@ export const ConnectRequestModal: FC = ({ request, isO onSecondaryPress={handleReject} isLoading={isLoading} isPrimaryDisabled={!selectedWallet} + primaryTestID="connect-approve" + secondaryTestID="connect-reject" /> - - - - setShowWalletSelector(false)} - wallets={availableWallets} - selectedWallet={selectedWallet} - onSelectWallet={setSelectedWallet} - title="Select Wallet" - /> + + + setShowWalletSelector(false)} + wallets={availableWallets} + selectedWallet={selectedWallet} + onSelectWallet={setSelectedWallet} + title="Select Wallet" + /> + ); }; -const styles = StyleSheet.create(({ colors, sizes }) => ({ - container: { +const styles = StyleSheet.create(({ colors, sizes }, runtime) => ({ + contentContainer: { + paddingBottom: runtime.insets.bottom + sizes.page.paddingBottom, + paddingHorizontal: sizes.page.paddingHorizontal, + marginLeft: runtime.insets.left, + marginRight: runtime.insets.right, gap: sizes.space.vertical, - paddingBottom: sizes.space.vertical, }, permissionsSection: { gap: sizes.space.vertical / 2, diff --git a/apps/demo-wallet-native/src/features/ton-connect/components/sign-data-request-modal/sign-data-request-modal.tsx b/apps/demo-wallet-native/src/features/ton-connect/components/sign-data-request-modal/sign-data-request-modal.tsx index 4fd4710a7..b24cfc454 100644 --- a/apps/demo-wallet-native/src/features/ton-connect/components/sign-data-request-modal/sign-data-request-modal.tsx +++ b/apps/demo-wallet-native/src/features/ton-connect/components/sign-data-request-modal/sign-data-request-modal.tsx @@ -17,11 +17,12 @@ import { SectionTitle } from '../section-title'; import { ActionButtons } from '../action-buttons'; import { SuccessView } from '../success-view'; -import { AppBottomSheet } from '@/core/components/app-bottom-sheet'; +import { AppModal } from '@/core/components/app-modal'; import { AppText } from '@/core/components/app-text'; +import { Block } from '@/core/components/block'; +import { ScreenHeader } from '@/core/components/screen-header'; import { WarningBox } from '@/core/components/warning-box'; import { WalletInfoBlock } from '@/features/wallets'; -import { Block } from '@/core/components/block'; interface SignDataRequestModalProps { request: EventSignDataRequest; @@ -144,15 +145,20 @@ export const SignDataRequestModal: FC = ({ request, i if (showSuccess) { return ( - {}} isDisabledClose isScrollable={false}> + {}}> - + ); } return ( - - + + + Sign Data Request + + + + = ({ request, i onPrimaryPress={handleApprove} onSecondaryPress={handleReject} isLoading={isLoading} + primaryTestID="sign-data-approve" + secondaryTestID="sign-data-reject" /> - - + + ); }; const styles = StyleSheet.create(({ colors, sizes }) => ({ + scrollView: { + flex: 1, + }, container: { gap: sizes.space.vertical, + paddingHorizontal: sizes.page.paddingHorizontal, paddingBottom: sizes.space.vertical, }, walletSection: { diff --git a/apps/demo-wallet-native/src/features/ton-connect/components/ton-connect-handler/ton-connect-handler.tsx b/apps/demo-wallet-native/src/features/ton-connect/components/ton-connect-handler/ton-connect-handler.tsx index 88533c999..6fa166023 100644 --- a/apps/demo-wallet-native/src/features/ton-connect/components/ton-connect-handler/ton-connect-handler.tsx +++ b/apps/demo-wallet-native/src/features/ton-connect/components/ton-connect-handler/ton-connect-handler.tsx @@ -7,19 +7,25 @@ */ import type { FC } from 'react'; -import { useTonConnect, useTransactionRequests, useSignDataRequests } from '@ton/demo-core'; +import { useTonConnect, useTransactionRequests, useSignDataRequests, useWalletStore } from '@ton/demo-core'; import { ConnectRequestModal } from '../connect-request-modal'; import { TransactionRequestModal } from '../transaction-request-modal'; import { SignDataRequestModal } from '../sign-data-request-modal'; export const TonConnectHandler: FC = () => { + const isUnlocked = useWalletStore((state) => state.auth.isUnlocked); + const { pendingConnectRequest, isConnectModalOpen, approveConnectRequest, rejectConnectRequest } = useTonConnect(); const { pendingTransactionRequest, isTransactionModalOpen, approveTransactionRequest, rejectTransactionRequest } = useTransactionRequests(); const { pendingSignDataRequest, isSignDataModalOpen, approveSignDataRequest, rejectSignDataRequest } = useSignDataRequests(); + if (!isUnlocked) { + return null; + } + return ( <> {pendingConnectRequest && ( diff --git a/apps/demo-wallet-native/src/features/ton-connect/components/ton-connect-link-input/ton-connect-link-input.tsx b/apps/demo-wallet-native/src/features/ton-connect/components/ton-connect-link-input/ton-connect-link-input.tsx index b1759ecd2..273949fc6 100644 --- a/apps/demo-wallet-native/src/features/ton-connect/components/ton-connect-link-input/ton-connect-link-input.tsx +++ b/apps/demo-wallet-native/src/features/ton-connect/components/ton-connect-link-input/ton-connect-link-input.tsx @@ -52,6 +52,7 @@ export const TonConnectLinkInput: FC = ({ = ({ autoCorrect={false} /> - + diff --git a/apps/demo-wallet-native/src/features/ton-connect/components/transaction-request-modal/transaction-request-modal.tsx b/apps/demo-wallet-native/src/features/ton-connect/components/transaction-request-modal/transaction-request-modal.tsx index c33752e04..c27566b30 100644 --- a/apps/demo-wallet-native/src/features/ton-connect/components/transaction-request-modal/transaction-request-modal.tsx +++ b/apps/demo-wallet-native/src/features/ton-connect/components/transaction-request-modal/transaction-request-modal.tsx @@ -8,7 +8,7 @@ import type { EventTransactionRequest } from '@ton/walletkit'; import { type FC, useState, useMemo, useEffect } from 'react'; -import { View } from 'react-native'; +import { View, ScrollView } from 'react-native'; import { StyleSheet } from 'react-native-unistyles'; import { useWallet } from '@ton/demo-core'; import { useWalletKit } from '@ton/demo-core'; @@ -19,8 +19,9 @@ import { SectionTitle } from '../section-title'; import { ActionButtons } from '../action-buttons'; import { SuccessView } from '../success-view'; -import { AppBottomSheet } from '@/core/components/app-bottom-sheet'; +import { AppModal } from '@/core/components/app-modal'; import { AppText } from '@/core/components/app-text'; +import { ScreenHeader } from '@/core/components/screen-header'; import { WarningBox } from '@/core/components/warning-box'; import { WalletInfoBlock } from '@/features/wallets'; @@ -70,15 +71,20 @@ export const TransactionRequestModal: FC = ({ requ if (showSuccess) { return ( - {}} isDisabledClose isScrollable={false}> + {}}> - + ); } return ( - - + + + Transaction Request + + + + = ({ requ onPrimaryPress={handleApprove} onSecondaryPress={handleReject} isLoading={isLoading} + primaryTestID="send-transaction-approve" + secondaryTestID="send-transaction-reject" /> - - + + ); }; const styles = StyleSheet.create(({ colors, sizes }) => ({ + scrollView: { + flex: 1, + }, container: { gap: sizes.space.vertical, + paddingHorizontal: sizes.page.paddingHorizontal, paddingBottom: sizes.space.vertical, }, walletSection: { diff --git a/apps/demo-wallet-native/src/features/transactions/components/transaction-details-modal/transaction-details-modal.tsx b/apps/demo-wallet-native/src/features/transactions/components/transaction-details-modal/transaction-details-modal.tsx index 05c7e9d9f..21bc2e428 100644 --- a/apps/demo-wallet-native/src/features/transactions/components/transaction-details-modal/transaction-details-modal.tsx +++ b/apps/demo-wallet-native/src/features/transactions/components/transaction-details-modal/transaction-details-modal.tsx @@ -99,15 +99,15 @@ export const TransactionDetailsModal: FC = ({ even return ( - - - Transaction Details + + Transaction Details - - - - + + + + + {valueImage ? ( @@ -246,7 +246,6 @@ export const TransactionDetailsModal: FC = ({ even const styles = StyleSheet.create(({ sizes, colors }, runtime) => ({ contentContainer: { paddingBottom: runtime.insets.bottom + sizes.page.paddingBottom, - paddingVertical: sizes.block.paddingVertical, paddingHorizontal: sizes.page.paddingHorizontal, marginLeft: runtime.insets.left, marginRight: runtime.insets.right, diff --git a/apps/demo-wallet-native/src/features/wallets/components/wallet-selector-modal/wallet-selector-modal.tsx b/apps/demo-wallet-native/src/features/wallets/components/wallet-selector-modal/wallet-selector-modal.tsx index 72d737853..fc1958ec8 100644 --- a/apps/demo-wallet-native/src/features/wallets/components/wallet-selector-modal/wallet-selector-modal.tsx +++ b/apps/demo-wallet-native/src/features/wallets/components/wallet-selector-modal/wallet-selector-modal.tsx @@ -9,13 +9,14 @@ import type { IWallet } from '@ton/walletkit'; import { useWallet } from '@ton/demo-core'; import type { FC } from 'react'; -import { View } from 'react-native'; +import { ScrollView } from 'react-native'; import { StyleSheet } from 'react-native-unistyles'; import { WalletInfoBlock } from '../wallet-info-block'; -import { AppBottomSheet } from '@/core/components/app-bottom-sheet'; import { ActiveTouchAction } from '@/core/components/active-touch-action'; +import { AppModal } from '@/core/components/app-modal'; +import { ScreenHeader } from '@/core/components/screen-header'; interface WalletSelectorModalProps { isOpen: boolean; @@ -43,15 +44,20 @@ export const WalletSelectorModal: FC = ({ }; return ( - - + + + {title} + + + + {wallets.map((wallet, index) => { const address = wallet.getAddress(); const savedWallet = savedWallets.find((w) => w.address === address); const isSelected = selectedWallet === wallet; return ( - handleSelect(wallet)}> + handleSelect(wallet)}> = ({ ); })} - - + + ); }; -const styles = StyleSheet.create(({ sizes, colors }) => ({ - list: { +const styles = StyleSheet.create(({ sizes, colors }, runtime) => ({ + contentContainer: { + paddingBottom: runtime.insets.bottom + sizes.page.paddingBottom, + paddingHorizontal: sizes.page.paddingHorizontal, + marginLeft: runtime.insets.left, + marginRight: runtime.insets.right, gap: sizes.space.vertical / 2, }, selected: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e738899c4..7cb475387 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -373,6 +373,9 @@ importers: '@types/react': specifier: 'catalog:' version: 19.1.17 + dotenv: + specifier: ^17.2.3 + version: 17.2.3 typescript: specifier: 5.9.3 version: 5.9.3