diff --git a/.github/workflows/ipad-build.yaml b/.github/workflows/ipad-build.yaml new file mode 100644 index 000000000..824ada9f1 --- /dev/null +++ b/.github/workflows/ipad-build.yaml @@ -0,0 +1,135 @@ +name: Tonkeeper iPad Build +on: + workflow_call: + secrets: + APP_STORE_CONNECT_TEAM_ID: + required: true + APPLE_API_ISSUER: + required: true + APPLE_API_KEY: + required: true + APPLE_API_KEY_ID: + required: true + BUILD_CERTIFICATE_BASE64: + required: true + BUILD_CERTIFICATE_PASSPHRASE: + required: true + BUILD_PROVISION_PROFILE_BASE64: + required: true + REACT_APP_MEASUREMENT_ID: + required: true + VITE_APP_APTABASE: + required: true + REACT_APP_TG_BOT_ID: + required: true + REACT_APP_STONFI_REFERRAL_ADDRESS: + required: true +env: + node-version: 20.18.1 + ruby-version: 3.3.6 + +jobs: + ipad-build: + name: ipad-build + runs-on: macos-15 + timeout-minutes: 20 + + steps: + - name: Checkout to git repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.node-version }} + + - name: Pods cache + uses: actions/cache@v4 + with: + path: ./apps/tablet/ios/App/Pods + key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-pods- + + - name: Set up Ruby and Gemfile dependencies + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ env.ruby-version }} + bundler-cache: true + working-directory: './apps/tablet' + + - name: Enable Corepack + run: | + corepack enable + + - name: Yarn cache + uses: actions/cache@v4 + with: + path: ./.yarn + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Run install + uses: borales/actions-yarn@v5 + with: + cmd: install + + - name: Install pods dependencies + working-directory: ./apps/tablet/ios/App + run: bundle exec pod install + + - name: Run build js and capacitor + uses: borales/actions-yarn@v5 + env: + VITE_APP_AMPLITUDE: '' + VITE_APP_MEASUREMENT_ID: ${{ secrets.REACT_APP_MEASUREMENT_ID }} + VITE_APP_APTABASE: ${{ secrets.VITE_APP_APTABASE }} + VITE_APP_APTABASE_HOST: https://anonymous-analytics.tonkeeper.com + VITE_APP_LOCALES: en,zh_TW,zh_CN,id,ru,it,es,uk,tr,bg,uz,bn + VITE_APP_TONCONSOLE_HOST: https://pro.tonconsole.com + VITE_APP_TG_BOT_ID: ${{ secrets.REACT_APP_TG_BOT_ID }} + VITE_APP_STONFI_REFERRAL_ADDRESS: ${{ secrets.REACT_APP_STONFI_REFERRAL_ADDRESS }} + with: + cmd: build:ipad + + - name: Build & upload iOS binary + working-directory: ./apps/tablet + run: bundle exec fastlane ios beta + env: + APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }} + BUNDLE_IDENTIFIER: com.tonapps.tonkeeperpro + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + P12_PASSWORD: ${{ secrets.BUILD_CERTIFICATE_PASSPHRASE }} + APPLE_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER }} + APPLE_KEY_CONTENT: ${{ secrets.APPLE_API_KEY }} + APPLE_PROFILE_NAME: GitHub CI/CD iPad + BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }} + + - name: Upload logs to artifacts + uses: actions/upload-artifact@v3 + if: failure() + with: + name: gum-logs + path: /Users/runner/Library/Logs/gym/*.log + + - name: Summary + run: | + echo '### Successful iPad build 🚀🚀🚀' >> $GITHUB_STEP_SUMMARY + echo 'Well done!' >> $GITHUB_STEP_SUMMARY + echo 'The app with build version: ${{ env.VERSION_CODE }}(${{ env.BUILD_CODE }})' >> $GITHUB_STEP_SUMMARY + echo 'Uploaded to TestFlight' >> $GITHUB_STEP_SUMMARY + + - name: Comment PR + uses: thollander/actions-comment-pull-request@v3 + if: github.event_name == 'pull_request' + with: + message: | + ### Successful iPad build 🚀🚀🚀 + Well done! + The app with build version: ${{ env.VERSION_CODE }}(${{ env.BUILD_CODE }}) + Uploaded to TestFlight + comment-tag: ipad_build diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 9bfff0516..965bbb454 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -82,6 +82,14 @@ jobs: p12-file-base64: ${{ secrets.IDENTITY_P12_B64 }} p12-password: ${{ secrets.IDENTITY_PASSPHRASE }} + - name: Yarn cache + uses: actions/cache@v4 + with: + path: ./.yarn + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Run install uses: borales/actions-yarn@v5 with: @@ -144,6 +152,14 @@ jobs: run: | corepack enable + - name: Yarn cache + uses: actions/cache@v4 + with: + path: ./.yarn + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Run install uses: borales/actions-yarn@v5 with: @@ -205,6 +221,14 @@ jobs: run: | corepack enable + - name: Yarn cache + uses: actions/cache@v4 + with: + path: ./.yarn + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Run install uses: borales/actions-yarn@v5 with: @@ -235,6 +259,10 @@ jobs: path: | ${{ github.workspace }}/apps/extension/dist/firefox + ipad-build: + uses: ./.github/workflows/ipad-build.yaml + secrets: inherit + web-tests: needs: web-build uses: ./.github/workflows/web-tests.yaml diff --git a/.github/workflows/web-build.yaml b/.github/workflows/web-build.yaml index f9943cbcf..7c212642a 100644 --- a/.github/workflows/web-build.yaml +++ b/.github/workflows/web-build.yaml @@ -48,6 +48,14 @@ jobs: run: | corepack enable + - name: Yarn cache + uses: actions/cache@v4 + with: + path: ./.yarn + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Run install uses: borales/actions-yarn@v5 with: diff --git a/.github/workflows/web-tests.yaml b/.github/workflows/web-tests.yaml index f92918390..1e7b37b5f 100644 --- a/.github/workflows/web-tests.yaml +++ b/.github/workflows/web-tests.yaml @@ -49,6 +49,14 @@ jobs: run: | corepack enable + - name: Yarn cache + uses: actions/cache@v4 + with: + path: ./.yarn + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Run install uses: borales/actions-yarn@v5 with: diff --git a/apps/desktop/src/app/components/TonConnectSubscription.tsx b/apps/desktop/src/app/components/TonConnectSubscription.tsx index 5bce02dc8..676a6f185 100644 --- a/apps/desktop/src/app/components/TonConnectSubscription.tsx +++ b/apps/desktop/src/app/components/TonConnectSubscription.tsx @@ -1,7 +1,4 @@ -import { - useResponseSendMutation, - SendTransactionAppRequest -} from '@tonkeeper/uikit/dist/components/connect/connectHook'; +import { useResponseSendMutation } from '@tonkeeper/uikit/dist/components/connect/connectHook'; import { TonTransactionNotification } from '@tonkeeper/uikit/dist/components/connect/TonTransactionNotification'; import { useSendNotificationAnalytics } from '@tonkeeper/uikit/dist/hooks/amplitude'; import { useCallback, useEffect, useState } from 'react'; @@ -10,6 +7,7 @@ import { useDisconnectTonConnectApp } from '@tonkeeper/uikit/dist/state/tonConnect'; import { sendBackground } from '../../libs/backgroudService'; +import { SendTransactionAppRequest } from '@tonkeeper/core/dist/entries/tonConnect'; export const TonConnectSubscription = () => { const [request, setRequest] = useState(undefined); diff --git a/apps/desktop/src/electron/background.ts b/apps/desktop/src/electron/background.ts index aa8da234e..4394c0d97 100644 --- a/apps/desktop/src/electron/background.ts +++ b/apps/desktop/src/electron/background.ts @@ -1,9 +1,9 @@ import { shell, systemPreferences, app } from 'electron'; import keytar from 'keytar'; import { Message } from '../libs/message'; -import { TonConnectSSE } from './sseEvetns'; import { mainStorage } from './storageService'; import { cookieJar } from './cookie'; +import { tonConnectSSE } from './sseEvetns'; const service = 'tonkeeper.com'; @@ -30,9 +30,9 @@ export const handleBackgroundMessage = async (message: Message): Promise; - private closeConnection: () => void | null = null; - - private static instance: TonConnectSSE = null; - - static getInstance() { - if (this.instance != null) return this.instance; - return (this.instance = new TonConnectSSE()); - } - - constructor() { - this.reconnect(); - } - - public reconnect() { - log.info('Reconnect.'); - return this.init().then(() => this.connect()); - } - - public async init() { - this.lastEventId = await getLastEventId(mainStorage); - - const walletsState = (await accountsStorage(mainStorage).getAccounts()).flatMap( - a => a.allTonWallets - ); - - this.connections = []; - this.dist = {}; - - for (const wallet of walletsState) { - const walletConnections = await getTonWalletConnections(mainStorage, wallet); - - this.connections = this.connections.concat(walletConnections); - walletConnections.forEach(item => { - this.dist[item.clientSessionId] = wallet.id; - }); - } - } - - public sendDisconnect = async (connection: AccountConnection | AccountConnection[]) => { - const connectionsToDisconnect = Array.isArray(connection) ? connection : [connection]; - await Promise.allSettled( - connectionsToDisconnect.map((item, index) => - replyDisconnectResponse({ - connection: item, - request: { id: (Date.now() + index).toString() } - }) - ) - ); - await this.reconnect(); - }; - - private onDisconnect = async ({ connection, request }: TonConnectAppRequest) => { - const accounts = await accountsStorage(mainStorage).getAccounts(); - const wallet = getWalletById(accounts, this.dist[connection.clientSessionId]); - - if (!wallet || !isStandardTonWallet(wallet)) { - return; - } - - await disconnectAppConnection({ - storage: mainStorage, - wallet, - clientSessionId: connection.clientSessionId - }); - await replyDisconnectResponse({ connection, request }); - await this.reconnect(); - MainWindow.mainWindow.webContents.send('disconnect', connection); - }; - - private handleMessage = async (params: TonConnectAppRequest) => { - switch (params.request.method) { - case 'disconnect': { - return this.onDisconnect(params); - } - case 'sendTransaction': { - const value = { - connection: params.connection, - id: params.request.id, - payload: JSON.parse(params.request.params[0]) - }; - - const walletId = this.dist[params.connection.clientSessionId]; - - const activeAccount = await accountsStorage(mainStorage).getActiveAccount(); - const activeWallet = activeAccount.activeTonWallet; - - const window = await MainWindow.bringToFront(); - - if (activeWallet.id !== walletId) { - const accountToActivate = ( - await accountsStorage(mainStorage).getAccounts() - ).find(a => a.getTonWallet(walletId) !== undefined); - - accountToActivate.setActiveTonWallet(walletId); - await accountsStorage(mainStorage).updateAccountInState(accountToActivate); - await accountsStorage(mainStorage).setActiveAccountId(accountToActivate.id); - window.webContents.send('refresh'); - await delay(500); - } - - window.webContents.send('sendTransaction', value); - return; - } - default: { - return replyBadRequestResponse(params); - } - } - }; - - public async connect() { - this.destroy(); - if (this.connections.length === 0) { - log.info('Missing connection.'); - } - this.closeConnection = subscribeTonConnect({ - storage: mainStorage, - handleMessage: this.handleMessage, - connections: this.connections, - lastEventId: this.lastEventId, - EventSourceClass: EventSourcePolyfill as any - }); - } - - public destroy() { - if (this.closeConnection) { - log.info('Close connection.'); - this.closeConnection(); +export const tonConnectSSE = new TonConnectSSE({ + storage: mainStorage, + listeners: { + onDisconnect: connection => + MainWindow.mainWindow.webContents.send('disconnect', connection), + onSendTransaction: params => + MainWindow.mainWindow.webContents.send('sendTransaction', params) + }, + system: { + log, + refresh: () => MainWindow.mainWindow.webContents.send('refresh'), + bringToFront: async () => { + await MainWindow.bringToFront(); } - } -} + }, + EventSourcePolyfill: EventSourcePolyfill +}); diff --git a/apps/desktop/src/index.ts b/apps/desktop/src/index.ts index 9668e20e7..8c4c021e0 100644 --- a/apps/desktop/src/index.ts +++ b/apps/desktop/src/index.ts @@ -9,7 +9,7 @@ import { setProtocolHandlerOSX, setProtocolHandlerWindowsLinux } from './electron/protocol'; -import { TonConnectSSE } from './electron/sseEvetns'; +import { tonConnectSSE } from './electron/sseEvetns'; app.setName('Tonkeeper Pro'); @@ -21,11 +21,9 @@ if (require('electron-squirrel-startup')) { app.quit(); } -const connection = TonConnectSSE.getInstance(); - const onUnLock = () => { log.info('unlock-screen'); - connection.reconnect(); + tonConnectSSE.reconnect(); }; if (process.platform != 'linux') { @@ -34,7 +32,7 @@ if (process.platform != 'linux') { app.on('before-quit', async e => { e.preventDefault(); - connection.destroy(); + tonConnectSSE.destroy(); if (process.platform != 'linux') { powerMonitor.off('unlock-screen', onUnLock); } diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index 66eff0c5b..759cdda14 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -1,10 +1,10 @@ // See the Electron documentation for details on how to use preload scripts: // https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts -import { SendTransactionAppRequest } from '@tonkeeper/uikit/dist/components/connect/connectHook'; import { contextBridge, ipcRenderer } from 'electron'; import { Message } from './libs/message'; import { AccountConnection } from '@tonkeeper/core/dist/service/tonConnect/connectionService'; +import { SendTransactionAppRequest } from '@tonkeeper/core/dist/entries/tonConnect'; contextBridge.exposeInMainWorld('backgroundApi', { platform: () => process.platform, diff --git a/apps/tablet/.env.example b/apps/tablet/.env.example new file mode 100644 index 000000000..86d0a079a --- /dev/null +++ b/apps/tablet/.env.example @@ -0,0 +1,7 @@ +VITE_APP_LOCALES=en,zh_TW,zh_CN,id,ru,it,es,uk,tr,bg,uz,bn +VITE_APP_APTABASE_HOST= +VITE_APP_APTABASE= +VITE_APP_TONCONSOLE_HOST=https://pro.tonconsole.com +VITE_APP_TG_BOT_ID=6345183204 +VITE_APP_TG_BOT_ORIGIN=https://tonkeeper.com +VITE_APP_STONFI_REFERRAL_ADDRESS=UQCthC8ICK7K8Hkfm9smblLFroKrYrEMwZuoD4Nbm5LswUnc diff --git a/apps/tablet/.gitignore b/apps/tablet/.gitignore new file mode 100644 index 000000000..b394013ab --- /dev/null +++ b/apps/tablet/.gitignore @@ -0,0 +1,8 @@ +.idea/ +node_modules/ +.vscode/ +*.map +.DS_Store +.sourcemaps +dist/ +/capacitor.live-reload-config.ts diff --git a/apps/tablet/Gemfile b/apps/tablet/Gemfile new file mode 100644 index 000000000..1f39b80e2 --- /dev/null +++ b/apps/tablet/Gemfile @@ -0,0 +1,7 @@ +source 'https://rubygems.org' +# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version +ruby ">= 3.3.0" +gem 'cocoapods', '~> 1.13' +gem "fastlane" +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/apps/tablet/Gemfile.lock b/apps/tablet/Gemfile.lock new file mode 100644 index 000000000..c73c45a14 --- /dev/null +++ b/apps/tablet/Gemfile.lock @@ -0,0 +1,323 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + activesupport (7.2.2) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + algoliasearch (1.27.5) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.3.0) + aws-partitions (1.1017.0) + aws-sdk-core (3.214.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.96.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.176.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.2.0) + benchmark (0.4.0) + bigdecimal (3.1.8) + claide (1.1.0) + cocoapods (1.16.2) + addressable (~> 2.8) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.16.2) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.8.0) + nap (~> 1.0) + ruby-macho (>= 2.3.0, < 3.0) + xcodeproj (>= 1.27.0, < 2.0) + cocoapods-core (1.16.2) + activesupport (>= 5.0, < 8) + addressable (~> 2.8) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + netrc (~> 0.11) + public_suffix (~> 4.0) + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.5) + cocoapods-downloader (2.1) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.1) + cocoapods-trunk (1.6.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.2.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + concurrent-ruby (1.3.4) + connection_pool (2.4.1) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + drb (2.2.1) + emoji_regex (3.2.3) + escape (0.0.4) + ethon (0.16.0) + ffi (>= 1.15.0) + excon (0.112.0) + faraday (1.10.4) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.3.1) + fastlane (2.225.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored (~> 1.2) + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (>= 0.1.1, < 1.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-plugin-json (1.1.7) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) + ffi (1.17.0) + ffi (1.17.0-aarch64-linux-gnu) + ffi (1.17.0-aarch64-linux-musl) + ffi (1.17.0-arm-linux-gnu) + ffi (1.17.0-arm-linux-musl) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86-linux-gnu) + ffi (1.17.0-x86-linux-musl) + ffi (1.17.0-x86_64-darwin) + ffi (1.17.0-x86_64-linux-gnu) + ffi (1.17.0-x86_64-linux-musl) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.7.1) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.7) + domain_name (~> 0.5) + httpclient (2.8.3) + i18n (1.14.6) + concurrent-ruby (~> 1.0) + jmespath (1.6.2) + json (2.9.0) + jwt (2.9.3) + base64 + logger (1.6.2) + mini_magick (4.13.2) + mini_mime (1.1.5) + minitest (5.25.3) + molinillo (0.8.0) + multi_json (1.15.0) + multipart-post (2.4.1) + nanaimo (0.4.0) + nap (1.1.0) + naturally (2.2.1) + netrc (0.11.0) + nkf (0.2.0) + optparse (0.6.0) + os (1.1.4) + plist (3.7.1) + public_suffix (4.0.7) + rake (13.2.1) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.3.9) + rouge (2.0.7) + ruby-macho (2.5.1) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + securerandom (0.4.0) + security (0.1.5) + signet (0.19.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + sysrandom (1.0.5) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + typhoeus (1.4.1) + ethon (>= 0.9.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + ruby + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + cocoapods (~> 1.13) + fastlane + fastlane-plugin-json + +RUBY VERSION + ruby 3.3.6p108 + +BUNDLED WITH + 2.5.22 diff --git a/apps/tablet/README.md b/apps/tablet/README.md new file mode 100644 index 000000000..4a4771453 --- /dev/null +++ b/apps/tablet/README.md @@ -0,0 +1,11 @@ +# Tonkeeper tablet + +### Start development server +1. `chmod +x live-reload.sh` +2. `yarn start` +3. Open xcode and run the app in ./ios directory + +### Build app +1. `chmod +x build.sh` +2. `yarn build` +3. Open xcode and run the app in ./ios directory diff --git a/apps/tablet/build.sh b/apps/tablet/build.sh new file mode 100755 index 000000000..204fe15a4 --- /dev/null +++ b/apps/tablet/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash +rm -f vite.log +CAPACITOR_CONFIG_FILE="./capacitor.live-reload-config.ts" + +cat > "$CAPACITOR_CONFIG_FILE" <> $GITHUB_ENV") + sh("echo BUILD_CODE=#{build_number} >> $GITHUB_ENV") + end + + private_lane :import_cert do |options| + cert_path = "#{Dir.tmpdir}/build_certificate.p12" + File.write(cert_path, Base64.decode64(ENV['BUILD_CERTIFICATE_BASE64'])) + import_certificate( + certificate_path: cert_path, + certificate_password: ENV['P12_PASSWORD'] || "", + keychain_name: options[:keychain_name], + keychain_password: options[:keychain_password], + log_output: true + ) + File.delete(cert_path) + end + + private_lane :cleanup_keychain do |options| + delete_keychain( + name: options[:keychain_name] + ) + end + + private_lane :install_profile do + profile_path = "#{Dir.tmpdir}/build_pp.mobileprovision" + File.write(profile_path, Base64.decode64(ENV['BUILD_PROVISION_PROFILE_BASE64'])) + UI.user_error!("Failed to create provisioning profile at #{profile_path}") unless File.exist?(profile_path) + ENV['PROVISIONING_PROFILE_PATH'] = profile_path + install_provisioning_profile(path: profile_path) + File.delete(profile_path) + end + + private_lane :update_project_settings do + app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier) + team_id = CredentialsManager::AppfileConfig.try_fetch_value(:team_id) + + update_code_signing_settings( + path: xcodeproj, + use_automatic_signing: false, + code_sign_identity: "iPhone Distribution", + team_id: team_id, + bundle_identifier: app_identifier, + profile_name: ENV['APPLE_PROFILE_NAME'], + ) + + update_project_team( + path: xcodeproj, + teamid: team_id + ) + end + + private_lane :build_app_with_signing do |options| + unlock_keychain( + path: options[:keychain_name], + password: options[:keychain_password], + set_default: false + ) + app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier) + build_app( + workspace: workspace, + scheme: "App", + configuration: "Release", + export_method: "app-store", + output_name: "TonkeeperPro.ipa", + export_options: { + provisioningProfiles: { + app_identifier => ENV['APPLE_PROFILE_NAME'] + } + }, + xcargs: "-verbose", + buildlog_path: "./build_logs", + export_xcargs: "-allowProvisioningUpdates", + ) + end + + private_lane :submit_to_testflight do + api_key = lane_context[SharedValues::APP_STORE_CONNECT_API_KEY] + ipa = lane_context[SharedValues::IPA_OUTPUT_PATH] + changelog = changelog_from_git_commits( + merge_commit_filtering: "exclude_merges", + between: ["origin/main", "HEAD"] + ) + + pilot( + api_key: api_key, + ipa: ipa, + changelog: changelog, + skip_waiting_for_build_processing: true, + skip_submission: true, + distribute_external: false, + notify_external_testers: false + ) + end + end \ No newline at end of file diff --git a/apps/tablet/fastlane/Pluginfile b/apps/tablet/fastlane/Pluginfile new file mode 100644 index 000000000..bffd8eb24 --- /dev/null +++ b/apps/tablet/fastlane/Pluginfile @@ -0,0 +1,5 @@ +# Autogenerated by fastlane +# +# Ensure this file is checked in to source control! + +gem 'fastlane-plugin-json' diff --git a/apps/tablet/fastlane/README.md b/apps/tablet/fastlane/README.md new file mode 100644 index 000000000..233c5febd --- /dev/null +++ b/apps/tablet/fastlane/README.md @@ -0,0 +1,40 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## iOS + +### ios beta + +```sh +[bundle exec] fastlane ios beta +``` + +Export ipa and submit to TestFlight + +### ios bump_build_number + +```sh +[bundle exec] fastlane ios bump_build_number +``` + + + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/apps/tablet/fastlane/metadata/copyright.txt b/apps/tablet/fastlane/metadata/copyright.txt new file mode 100644 index 000000000..772be4161 --- /dev/null +++ b/apps/tablet/fastlane/metadata/copyright.txt @@ -0,0 +1 @@ +2024 Ton APPS UK Limited \ No newline at end of file diff --git a/apps/tablet/fastlane/metadata/default/description.txt b/apps/tablet/fastlane/metadata/default/description.txt new file mode 100644 index 000000000..90de97c02 --- /dev/null +++ b/apps/tablet/fastlane/metadata/default/description.txt @@ -0,0 +1 @@ +Tonkeeper Pro is the easiest way to store, send, and receive Toncoin on The Open Network, which is a powerful new blockchain that offers unprecedented transaction speeds and throughput while offering a robust programming environment for smart contract applications. \ No newline at end of file diff --git a/apps/tablet/fastlane/metadata/default/keywords.txt b/apps/tablet/fastlane/metadata/default/keywords.txt new file mode 100644 index 000000000..f0831c68e --- /dev/null +++ b/apps/tablet/fastlane/metadata/default/keywords.txt @@ -0,0 +1 @@ +TON, Toncoin, TON Wallet, TON Coin, Ton keeper, crypto \ No newline at end of file diff --git a/apps/tablet/fastlane/metadata/default/marketing_url.txt b/apps/tablet/fastlane/metadata/default/marketing_url.txt new file mode 100644 index 000000000..211d157e5 --- /dev/null +++ b/apps/tablet/fastlane/metadata/default/marketing_url.txt @@ -0,0 +1 @@ +https://tonkeeper.com \ No newline at end of file diff --git a/apps/tablet/fastlane/metadata/default/name.txt b/apps/tablet/fastlane/metadata/default/name.txt new file mode 100644 index 000000000..de441efe7 --- /dev/null +++ b/apps/tablet/fastlane/metadata/default/name.txt @@ -0,0 +1 @@ +Tonkeeper Pro \ No newline at end of file diff --git a/apps/tablet/fastlane/metadata/default/support_url.txt b/apps/tablet/fastlane/metadata/default/support_url.txt new file mode 100644 index 000000000..211d157e5 --- /dev/null +++ b/apps/tablet/fastlane/metadata/default/support_url.txt @@ -0,0 +1 @@ +https://tonkeeper.com \ No newline at end of file diff --git a/apps/tablet/fastlane/screenshots/0x2ss.png b/apps/tablet/fastlane/screenshots/0x2ss.png new file mode 100644 index 000000000..29f472c9d Binary files /dev/null and b/apps/tablet/fastlane/screenshots/0x2ss.png differ diff --git a/apps/tablet/index.html b/apps/tablet/index.html new file mode 100644 index 000000000..3197e4b60 --- /dev/null +++ b/apps/tablet/index.html @@ -0,0 +1,18 @@ + + + + + + + Tonkeeper Pro + + + + + +
+ + diff --git a/apps/tablet/ios/.gitignore b/apps/tablet/ios/.gitignore new file mode 100644 index 000000000..f47029973 --- /dev/null +++ b/apps/tablet/ios/.gitignore @@ -0,0 +1,13 @@ +App/build +App/Pods +App/output +App/App/public +DerivedData +xcuserdata + +# Cordova plugins for Capacitor +capacitor-cordova-ios-plugins + +# Generated Config files +App/App/capacitor.config.json +App/App/config.xml diff --git a/apps/tablet/ios/App/App.xcodeproj/project.pbxproj b/apps/tablet/ios/App/App.xcodeproj/project.pbxproj new file mode 100644 index 000000000..f2cb1bf89 --- /dev/null +++ b/apps/tablet/ios/App/App.xcodeproj/project.pbxproj @@ -0,0 +1,440 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; + 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; + 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; + 504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; }; + 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; + 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; + 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; + A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; }; + B3313CCD2CEB43AB00E31C31 /* CustomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3313CCC2CEB43AB00E31C31 /* CustomViewController.swift */; }; + B3313CCF2CEB459400E31C31 /* BiometricPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3313CCE2CEB459400E31C31 /* BiometricPlugin.swift */; }; + B3313CD12CEB4C7E00E31C31 /* SecureStoragePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3313CD02CEB4C7E00E31C31 /* SecureStoragePlugin.swift */; }; + B3E0A8272D03497E0089241A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B3E0A8262D03497E0089241A /* GoogleService-Info.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; + 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; + 504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; + AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = ""; }; + B3313CCC2CEB43AB00E31C31 /* CustomViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomViewController.swift; sourceTree = ""; }; + B3313CCE2CEB459400E31C31 /* BiometricPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricPlugin.swift; sourceTree = ""; }; + B3313CD02CEB4C7E00E31C31 /* SecureStoragePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStoragePlugin.swift; sourceTree = ""; }; + B3CCE8EB2CFDE9B40083D875 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = ""; }; + B3E0A8262D03497E0089241A /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + B3E0A8282D034A140089241A /* AppRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AppRelease.entitlements; sourceTree = ""; }; + FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 504EC3011FED79650016851F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = { + isa = PBXGroup; + children = ( + AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 504EC2FB1FED79650016851F = { + isa = PBXGroup; + children = ( + 504EC3061FED79650016851F /* App */, + 504EC3051FED79650016851F /* Products */, + 7F8756D8B27F46E3366F6CEA /* Pods */, + 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */, + ); + sourceTree = ""; + }; + 504EC3051FED79650016851F /* Products */ = { + isa = PBXGroup; + children = ( + 504EC3041FED79650016851F /* App.app */, + ); + name = Products; + sourceTree = ""; + }; + 504EC3061FED79650016851F /* App */ = { + isa = PBXGroup; + children = ( + B3E0A8282D034A140089241A /* AppRelease.entitlements */, + B3E0A8262D03497E0089241A /* GoogleService-Info.plist */, + B3CCE8EB2CFDE9B40083D875 /* App.entitlements */, + 50379B222058CBB4000EE86E /* capacitor.config.json */, + 504EC3071FED79650016851F /* AppDelegate.swift */, + 504EC30B1FED79650016851F /* Main.storyboard */, + 504EC30E1FED79650016851F /* Assets.xcassets */, + 504EC3101FED79650016851F /* LaunchScreen.storyboard */, + 504EC3131FED79650016851F /* Info.plist */, + 2FAD9762203C412B000D30F8 /* config.xml */, + 50B271D01FEDC1A000F3C39B /* public */, + B3313CCC2CEB43AB00E31C31 /* CustomViewController.swift */, + B3313CCE2CEB459400E31C31 /* BiometricPlugin.swift */, + B3313CD02CEB4C7E00E31C31 /* SecureStoragePlugin.swift */, + ); + path = App; + sourceTree = ""; + }; + 7F8756D8B27F46E3366F6CEA /* Pods */ = { + isa = PBXGroup; + children = ( + FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */, + AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 504EC3031FED79650016851F /* App */ = { + isa = PBXNativeTarget; + buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */; + buildPhases = ( + 6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */, + 504EC3001FED79650016851F /* Sources */, + 504EC3011FED79650016851F /* Frameworks */, + 504EC3021FED79650016851F /* Resources */, + 9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = App; + productName = App; + productReference = 504EC3041FED79650016851F /* App.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 504EC2FC1FED79650016851F /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0920; + TargetAttributes = { + 504EC3031FED79650016851F = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 504EC2FB1FED79650016851F; + packageReferences = ( + ); + productRefGroup = 504EC3051FED79650016851F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 504EC3031FED79650016851F /* App */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 504EC3021FED79650016851F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */, + 50B271D11FEDC1A000F3C39B /* public in Resources */, + 504EC30F1FED79650016851F /* Assets.xcassets in Resources */, + 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */, + 504EC30D1FED79650016851F /* Main.storyboard in Resources */, + 2FAD9763203C412B000D30F8 /* config.xml in Resources */, + B3E0A8272D03497E0089241A /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 504EC3001FED79650016851F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B3313CCD2CEB43AB00E31C31 /* CustomViewController.swift in Sources */, + B3313CCF2CEB459400E31C31 /* BiometricPlugin.swift in Sources */, + 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, + B3313CD12CEB4C7E00E31C31 /* SecureStoragePlugin.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 504EC30B1FED79650016851F /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 504EC30C1FED79650016851F /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 504EC3101FED79650016851F /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 504EC3111FED79650016851F /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 504EC3141FED79650016851F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 504EC3151FED79650016851F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 504EC3171FED79650016851F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = App/App.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 11; + DEVELOPMENT_TEAM = CT523DK2KC; + INFOPLIST_FILE = App/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.0; + OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; + PRODUCT_BUNDLE_IDENTIFIER = com.tonapps.tonkeeperpro; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 2; + }; + name = Debug; + }; + 504EC3181FED79650016851F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = App/AppRelease.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 11; + DEVELOPMENT_TEAM = CT523DK2KC; + INFOPLIST_FILE = App/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tonapps.tonkeeperpro; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 2; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 504EC3141FED79650016851F /* Debug */, + 504EC3151FED79650016851F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 504EC3171FED79650016851F /* Debug */, + 504EC3181FED79650016851F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 504EC2FC1FED79650016851F /* Project object */; +} diff --git a/apps/tablet/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/apps/tablet/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/apps/tablet/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/apps/tablet/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/tablet/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/apps/tablet/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/tablet/ios/App/App.xcworkspace/contents.xcworkspacedata b/apps/tablet/ios/App/App.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..b301e824b --- /dev/null +++ b/apps/tablet/ios/App/App.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/apps/tablet/ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/tablet/ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/apps/tablet/ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/tablet/ios/App/App/App.entitlements b/apps/tablet/ios/App/App/App.entitlements new file mode 100644 index 000000000..bb30651f7 --- /dev/null +++ b/apps/tablet/ios/App/App/App.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + keychain-access-groups + + $(AppIdentifierPrefix)com.tonapps.tonkeeperpro + + + diff --git a/apps/tablet/ios/App/App/AppDelegate.swift b/apps/tablet/ios/App/App/AppDelegate.swift new file mode 100644 index 000000000..0f2fd9487 --- /dev/null +++ b/apps/tablet/ios/App/App/AppDelegate.swift @@ -0,0 +1,67 @@ +import UIKit +import Capacitor +import FirebaseCore +import FirebaseMessaging + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + FirebaseApp.configure() + + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + // Called when the app was launched with a url. Feel free to add additional processing here, + // but if you want the App API to support tracking app url opens, make sure to keep this call + return ApplicationDelegateProxy.shared.application(app, open: url, options: options) + } + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + // Called when the app was launched with an activity, including Universal Links. + // Feel free to add additional processing here, but if you want the App API to support + // tracking app url opens, make sure to keep this call + return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) + } + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + Messaging.messaging().apnsToken = deviceToken + Messaging.messaging().token(completion: { (token, error) in + if let error = error { + NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error) + } else if let token = token { + NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: token) + } + }) + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error) + } +} diff --git a/apps/tablet/ios/App/App/AppRelease.entitlements b/apps/tablet/ios/App/App/AppRelease.entitlements new file mode 100644 index 000000000..bb30651f7 --- /dev/null +++ b/apps/tablet/ios/App/App/AppRelease.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + keychain-access-groups + + $(AppIdentifierPrefix)com.tonapps.tonkeeperpro + + + diff --git a/apps/tablet/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/tablet/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..7d26086ae --- /dev/null +++ b/apps/tablet/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images": [ + { + "filename": "icon-appstore@1024.png", + "idiom": "universal", + "platform" : "ios", + "size": "1024x1024" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/apps/tablet/ios/App/App/Assets.xcassets/AppIcon.appiconset/icon-appstore@1024.png b/apps/tablet/ios/App/App/Assets.xcassets/AppIcon.appiconset/icon-appstore@1024.png new file mode 100644 index 000000000..51e6de1f5 Binary files /dev/null and b/apps/tablet/ios/App/App/Assets.xcassets/AppIcon.appiconset/icon-appstore@1024.png differ diff --git a/apps/tablet/ios/App/App/Assets.xcassets/Contents.json b/apps/tablet/ios/App/App/Assets.xcassets/Contents.json new file mode 100644 index 000000000..da4a164c9 --- /dev/null +++ b/apps/tablet/ios/App/App/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/tablet/ios/App/App/Base.lproj/LaunchScreen.storyboard b/apps/tablet/ios/App/App/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..e7ae5d780 --- /dev/null +++ b/apps/tablet/ios/App/App/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/tablet/ios/App/App/Base.lproj/Main.storyboard b/apps/tablet/ios/App/App/Base.lproj/Main.storyboard new file mode 100644 index 000000000..3e0e11cc9 --- /dev/null +++ b/apps/tablet/ios/App/App/Base.lproj/Main.storyboard @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/tablet/ios/App/App/BiometricPlugin.swift b/apps/tablet/ios/App/App/BiometricPlugin.swift new file mode 100644 index 000000000..68d601773 --- /dev/null +++ b/apps/tablet/ios/App/App/BiometricPlugin.swift @@ -0,0 +1,54 @@ +import Foundation +import LocalAuthentication +import Capacitor + + +@objc public class BiometricPlugin: CAPPlugin, CAPBridgedPlugin { + public let identifier = "BiometricPlugin" + public let jsName = "Biometric" + public let pluginMethods: [CAPPluginMethod] = [ + CAPPluginMethod(name: "canPrompt", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "prompt", returnType: CAPPluginReturnPromise) + ] + + + @objc func canPrompt(_ call: CAPPluginCall) { + var isAvailable = false; + if #available(iOS 11, *) { + let authContext = LAContext() + + let _ = authContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) + switch(authContext.biometryType) { + case .none: + isAvailable = false; + case .touchID: + isAvailable = true; + case .faceID: + isAvailable = true; + default: + isAvailable = false; + } + } + + call.resolve(["isAvailable": isAvailable]) + } + + @objc func prompt(_ call: CAPPluginCall) { + let authContext = LAContext() + + authContext.touchIDAuthenticationAllowableReuseDuration = 60; + + let reason = call.getString("reason") ?? "Access requires authentication" + + authContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason ) { success, error in + if success { + DispatchQueue.main.async { + call.resolve() + } + } else { + call.reject(error?.localizedDescription ?? "Failed to authenticate") + } + } + } + +} diff --git a/apps/tablet/ios/App/App/CustomViewController.swift b/apps/tablet/ios/App/App/CustomViewController.swift new file mode 100644 index 000000000..38923d756 --- /dev/null +++ b/apps/tablet/ios/App/App/CustomViewController.swift @@ -0,0 +1,34 @@ +// +// CustomViewController.swift +// App +// +// Created by Andreev Sergey on 18/11/2024. +// + +import UIKit +import Capacitor + +class CustomViewController: CAPBridgeViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + override open func capacitorDidLoad() { + bridge?.registerPluginInstance(BiometricPlugin()) + bridge?.registerPluginInstance(SecureStoragePlugin()) + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/apps/tablet/ios/App/App/GoogleService-Info.plist b/apps/tablet/ios/App/App/GoogleService-Info.plist new file mode 100644 index 000000000..d03fe032c --- /dev/null +++ b/apps/tablet/ios/App/App/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyBfuV_ymekwDZwlD6TkvKB5292oYd96dbM + GCM_SENDER_ID + 488327313434 + PLIST_VERSION + 1 + BUNDLE_ID + com.tonapps.tonkeeperpro + PROJECT_ID + tonkeeper-web + STORAGE_BUCKET + tonkeeper-web.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:488327313434:ios:85fdb7916fec750461f9a3 + + \ No newline at end of file diff --git a/apps/tablet/ios/App/App/Info.plist b/apps/tablet/ios/App/App/Info.plist new file mode 100644 index 000000000..d7fb5866a --- /dev/null +++ b/apps/tablet/ios/App/App/Info.plist @@ -0,0 +1,85 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Tonkeeper Pro + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleURLTypes + + + CFBundleURLSchemes + + tonkeeper + tonkeeper-tc + tc + ton + + + + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + ITSAppUsesNonExemptEncryption + + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSCameraUsageDescription + Tonkeeper need access to your camera to read QR codes. + NSFaceIDUsageDescription + Tonkeeper use Face ID to securely authenticate your access to mnemonic key. + NSPhotoLibraryUsageDescription + Tonkeeper use photo library to read QR codes from images. + UIBackgroundModes + + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullScreen + + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + WKAppBoundDomains + + dev-pro.tonconsole.com + pro.tonconsole.com + + + diff --git a/apps/tablet/ios/App/App/SecureStoragePlugin.swift b/apps/tablet/ios/App/App/SecureStoragePlugin.swift new file mode 100644 index 000000000..6a69e1c31 --- /dev/null +++ b/apps/tablet/ios/App/App/SecureStoragePlugin.swift @@ -0,0 +1,66 @@ +import Capacitor +import Security + +@objc public class SecureStoragePlugin: CAPPlugin, CAPBridgedPlugin { + public let identifier = "SecureStoragePlugin" + public let jsName = "SecureStorage" + + public let pluginMethods: [CAPPluginMethod] = [ + CAPPluginMethod(name: "storeData", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getData", returnType: CAPPluginReturnPromise) + ] + + + @objc func storeData(_ call: CAPPluginCall) { + guard let id = call.getString("id"), + let dataString = call.getString("data"), + let data = dataString.data(using: .utf8) else { + call.reject("Missing required parameters") + return + } + + let keychainQuery: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + kSecAttrAccount: id, + kSecValueData: data + ] + + SecItemDelete(keychainQuery as CFDictionary) + + + let status = SecItemAdd(keychainQuery as CFDictionary, nil) + + if status == errSecSuccess { + call.resolve() + } else { + call.reject("Error storing data: \(status)") + } + } + + + @objc func getData(_ call: CAPPluginCall) { + guard let id = call.getString("id") else { + call.reject("Missing required parameter: id") + return + } + + let keychainQuery: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: id, + kSecReturnData: true, + kSecMatchLimit: kSecMatchLimitOne + ] + + var result: AnyObject? + let status = SecItemCopyMatching(keychainQuery as CFDictionary, &result) + + if status == errSecSuccess, let data = result as? Data, let stringData = String(data: data, encoding: .utf8) { + call.resolve([ + "data": stringData + ]) + } else { + call.reject("Error retrieving data: \(status)") + } + } +} diff --git a/apps/tablet/ios/App/Podfile b/apps/tablet/ios/App/Podfile new file mode 100644 index 000000000..291234d5a --- /dev/null +++ b/apps/tablet/ios/App/Podfile @@ -0,0 +1,32 @@ +require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers' + +platform :ios, '13.0' +use_frameworks! + +# workaround to avoid Xcode caching of Pods that requires +# Product -> Clean Build Folder after new Cordova plugins installed +# Requires CocoaPods 1.6 or newer +install! 'cocoapods', :disable_input_output_paths => true + +def capacitor_pods + pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' + pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' + pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' + pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera' + pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard' + pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device' + pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences' + pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications' + pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen' +end + +target 'App' do + capacitor_pods + # Add your Pods here + + pod 'FirebaseMessaging' +end + +post_install do |installer| + assertDeploymentTarget(installer) +end diff --git a/apps/tablet/ios/App/Podfile.lock b/apps/tablet/ios/App/Podfile.lock new file mode 100644 index 000000000..9374e3041 --- /dev/null +++ b/apps/tablet/ios/App/Podfile.lock @@ -0,0 +1,137 @@ +PODS: + - Capacitor (6.2.0): + - CapacitorCordova + - CapacitorApp (6.0.2): + - Capacitor + - CapacitorCamera (6.1.1): + - Capacitor + - CapacitorClipboard (6.0.2): + - Capacitor + - CapacitorCordova (6.2.0) + - CapacitorDevice (6.0.2): + - Capacitor + - CapacitorPreferences (6.0.3): + - Capacitor + - CapacitorPushNotifications (6.0.3): + - Capacitor + - CapacitorSplashScreen (6.0.3): + - Capacitor + - FirebaseCore (11.6.0): + - FirebaseCoreInternal (~> 11.6.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreInternal (11.6.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseInstallations (11.6.0): + - FirebaseCore (~> 11.6.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - FirebaseMessaging (11.6.0): + - FirebaseCore (~> 11.6.0) + - FirebaseInstallations (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Reachability (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - nanopb (~> 3.30910.0) + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleUtilities/AppDelegateSwizzler (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.0.2): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.0.2): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.0.2)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/Reachability (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - PromisesObjC (2.4.0) + +DEPENDENCIES: + - "Capacitor (from `../../node_modules/@capacitor/ios`)" + - "CapacitorApp (from `../../node_modules/@capacitor/app`)" + - "CapacitorCamera (from `../../node_modules/@capacitor/camera`)" + - "CapacitorClipboard (from `../../node_modules/@capacitor/clipboard`)" + - "CapacitorCordova (from `../../node_modules/@capacitor/ios`)" + - "CapacitorDevice (from `../../node_modules/@capacitor/device`)" + - "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)" + - "CapacitorPushNotifications (from `../../node_modules/@capacitor/push-notifications`)" + - "CapacitorSplashScreen (from `../../node_modules/@capacitor/splash-screen`)" + - FirebaseMessaging + +SPEC REPOS: + trunk: + - FirebaseCore + - FirebaseCoreInternal + - FirebaseInstallations + - FirebaseMessaging + - GoogleDataTransport + - GoogleUtilities + - nanopb + - PromisesObjC + +EXTERNAL SOURCES: + Capacitor: + :path: "../../node_modules/@capacitor/ios" + CapacitorApp: + :path: "../../node_modules/@capacitor/app" + CapacitorCamera: + :path: "../../node_modules/@capacitor/camera" + CapacitorClipboard: + :path: "../../node_modules/@capacitor/clipboard" + CapacitorCordova: + :path: "../../node_modules/@capacitor/ios" + CapacitorDevice: + :path: "../../node_modules/@capacitor/device" + CapacitorPreferences: + :path: "../../node_modules/@capacitor/preferences" + CapacitorPushNotifications: + :path: "../../node_modules/@capacitor/push-notifications" + CapacitorSplashScreen: + :path: "../../node_modules/@capacitor/splash-screen" + +SPEC CHECKSUMS: + Capacitor: 05d35014f4425b0740fc8776481f6a369ad071bf + CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7 + CapacitorCamera: fc099c42b3cbb1e8e945bc945114ff830fa82dfd + CapacitorClipboard: 4443c3cdb7c77b1533dfe3ff0f9f7756aa8579df + CapacitorCordova: b33e7f4aa4ed105dd43283acdd940964374a87d9 + CapacitorDevice: 9efd479d71d1baad74b75df531184c3f730eaa48 + CapacitorPreferences: f3eadae2369ac3ab8e21743a2959145b0d1286a3 + CapacitorPushNotifications: 9b178e010634d2f7bfca97b81478503463f86b6c + CapacitorSplashScreen: fd8bf1bf9081d9aa8817b7cd37d740d1bdaf2fb2 + FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa + FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 + FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c + FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + +PODFILE CHECKSUM: f9d707744c97cd8f502d129ac50e6f2a2abd9aca + +COCOAPODS: 1.16.2 diff --git a/apps/tablet/live-reload.sh b/apps/tablet/live-reload.sh new file mode 100755 index 000000000..8c67941c0 --- /dev/null +++ b/apps/tablet/live-reload.sh @@ -0,0 +1,35 @@ +#!/bin/bash +rm -f vite.log +vite --host &> vite.log & +VITE_PID=$! + +echo "Starting Vite..." +sleep 3 + +LAN_URL=$(grep -Eo 'http://[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+' vite.log | head -n 1) + +if [ -z "$LAN_URL" ]; then + echo "Failed to detect Vite server LAN URL. Exiting." + exit 1 +fi + +echo "Vite is running at $LAN_URL" + +CAPACITOR_CONFIG_FILE="./capacitor.live-reload-config.ts" + +cat > "$CAPACITOR_CONFIG_FILE" < { + const { t: tSimple, i18n } = useTranslation(); + + const t = useTWithReplaces(tSimple); + + const translation = useMemo(() => { + const languages = langs.split(','); + const client: I18nContext = { + t, + i18n: { + enable: true, + reloadResources: i18n.reloadResources, + changeLanguage: i18n.changeLanguage as any, + language: i18n.language, + languages: languages + } + }; + return client; + }, [t, i18n]); + + useEffect(() => { + document.body.classList.add(TABLET_APPLICATION_ID); + }, []); + + return ( + + }> + + + + + + + + + + ); +}; + +const router = createMemoryRouter([ + { + path: '/*', + element: + } +]); + +export const App = () => { + return ; +}; + +const ThemeAndContent = () => { + const { data } = useProBackupState(); + return ( + + + + + + + + + + + + ); +}; + +const FullSizeWrapper = styled(Container)` + max-width: 800px; +`; + +const Wrapper = styled.div` + box-sizing: border-box; + + height: 100%; + display: flex; + flex-direction: column; + background-color: ${props => props.theme.backgroundPage}; + white-space: pre-wrap; +`; + +const WideLayout = styled.div` + width: 100%; + height: 100%; + display: flex; +`; + +const WideContent = styled.div` + flex: 1; + min-width: 0; + min-height: 0; +`; + +const WalletLayout = styled.div` + display: flex; + flex-direction: column; + height: 100%; +`; + +const WalletLayoutBody = styled.div` + flex: 1; + display: flex; + max-height: calc(100% - ${desktopHeaderContainerHeight}); +`; + +const WalletRoutingWrapper = styled.div` + flex: 1; + overflow: auto; + position: relative; +`; + +const PreferencesLayout = styled.div` + height: calc(100% - ${desktopHeaderContainerHeight}); + display: flex; + overflow: auto; +`; + +const PreferencesRoutingWrapper = styled.div` + flex: 1; + overflow: auto; + position: relative; +`; + +const FullSizeWrapperBounded = styled(FullSizeWrapper)` + max-height: 100%; + overflow: auto; + + justify-content: center; +`; + +export const Loader: FC = () => { + const network = useActiveTonNetwork(); + const { data: activeAccount, isLoading: activeWalletLoading } = useActiveAccountQuery(); + const { data: accounts, isLoading: isWalletsLoading } = useAccountsStateQuery(); + const { data: lang, isLoading: isLangLoading } = useUserLanguage(); + const { data: devSettings } = useDevSettings(); + const { isLoading: globalPreferencesLoading } = useGlobalPreferencesQuery(); + const { isLoading: globalSetupLoading } = useGlobalSetup(); + + const lock = useLock(sdk); + const { i18n } = useTranslation(); + const { data: fiat } = useUserFiatQuery(); + + const tonendpoint = useTonendpoint({ + targetEnv: TABLET_APPLICATION_ID, + build: sdk.version, + network, + lang, + platform: TABLET_APPLICATION_ID + }); + const { data: config } = useTonenpointConfig(tonendpoint); + + useAppHeight(); + + const { data: tracker } = useAnalytics(sdk.version, activeAccount!, accounts); + + useEffect(() => { + if (lang && i18n.language !== localizationText(lang)) { + i18n.reloadResources([localizationText(lang)]).then(() => + i18n.changeLanguage(localizationText(lang)) + ); + } + }, [lang, i18n]); + + useEffect(() => { + if (config && config.mainnetConfig.tonapiIOEndpoint) { + sdk.notifications = new TabletNotifications(config.mainnetConfig, sdk.storage); + } + }, [config]); + + if ( + activeWalletLoading || + isLangLoading || + isWalletsLoading || + config === undefined || + lock === undefined || + fiat === undefined || + !devSettings || + globalPreferencesLoading || + globalSetupLoading + ) { + return ; + } + + const context: IAppContext = { + mainnetApi: getApiConfig( + config.mainnetConfig, + Network.MAINNET, + import.meta.env.VITE_APP_TONCONSOLE_HOST + ), + testnetApi: getApiConfig(config.mainnetConfig, Network.TESTNET), + fiat, + mainnetConfig: config.mainnetConfig, + testnetConfig: config.testnetConfig, + tonendpoint, + standalone: true, + extension: false, + proFeatures: true, + experimental: true, + ios: false, + env: { + tgAuthBotId: import.meta.env.VITE_APP_TG_BOT_ID, + stonfiReferralAddress: import.meta.env.VITE_APP_STONFI_REFERRAL_ADDRESS + }, + defaultWalletVersion: WalletVersion.V5R1 + }; + + return ( + + + + + + + + + ); +}; + +const usePrefetch = () => { + useRecommendations(); + useCanPromptTouchId(); +}; + +export const Content: FC<{ + activeAccount?: Account | null; + lock: boolean; +}> = ({ activeAccount, lock }) => { + const location = useLocation(); + useWindowsScroll(); + useAppWidth(); + useTrackLocation(); + usePrefetch(); + useDebuggingTools(); + + if (lock) { + return ( + + + + ); + } + + if (!activeAccount || location.pathname.startsWith(AppRoute.import)) { + return ( + + + + + + ); + } + + return ( + + + + + } /> + } /> + } /> + } /> + } + /> + } /> + + + + + ); +}; + +const WalletContent = () => { + return ( + + + + + + + + }> + } /> + } + /> + } /> + + } /> + + } + /> + } + /> + } + /> + } /> + } /> + + + + + + ); +}; + +const PreferencesContent = () => { + return ( + <> + + + + + + + + + ); +}; + +const OldAppRouting = () => { + return ( + + + + + ); +}; + +const BackgroundElements = () => { + const onRefresh = useCallback(async () => { + const promise1 = queryClient.invalidateQueries(); + const promise2 = new Promise(r => setTimeout(r, 1000)); + + await Promise.all([promise1, promise2]); + }, []); + + return ( + <> + + + + + + + + + + + + + + ); +}; diff --git a/apps/tablet/src/app/components/DeepLink.tsx b/apps/tablet/src/app/components/DeepLink.tsx new file mode 100644 index 000000000..ce9779a5a --- /dev/null +++ b/apps/tablet/src/app/components/DeepLink.tsx @@ -0,0 +1,43 @@ +import { ConnectItemReply, DAppManifest } from '@tonkeeper/core/dist/entries/tonConnect'; +import { TonConnectParams } from '@tonkeeper/core/dist/service/tonConnect/connectionService'; +import { TonConnectNotification } from '@tonkeeper/uikit/dist/components/connect/TonConnectNotification'; +import { + useResponseConnectionMutation, + useGetConnectInfo +} from '@tonkeeper/uikit/dist/components/connect/connectHook'; +import { useEffect, useState } from 'react'; +import { subscribeToTonOrTonConnectUrlOpened, tonConnectSSE } from "../../libs/tonConnect"; + +export const DeepLinkSubscription = () => { + const [params, setParams] = useState(null); + + const { mutateAsync, reset } = useGetConnectInfo(); + const { mutateAsync: responseConnectionAsync, reset: responseReset } = + useResponseConnectionMutation(); + + const handlerClose = async (replyItems?: ConnectItemReply[], manifest?: DAppManifest) => { + if (!params) return; + responseReset(); + try { + await responseConnectionAsync({ params, replyItems, manifest }); + } finally { + setParams(null); + await tonConnectSSE.reconnect(); + } + }; + + useEffect(() => { + return subscribeToTonOrTonConnectUrlOpened(async (url: string) => { + reset(); + setParams(await mutateAsync(url)); + }); + }, []); + + return ( + + ); +}; diff --git a/apps/tablet/src/app/components/PullToRefresh.tsx b/apps/tablet/src/app/components/PullToRefresh.tsx new file mode 100644 index 000000000..217741a98 --- /dev/null +++ b/apps/tablet/src/app/components/PullToRefresh.tsx @@ -0,0 +1,121 @@ +import styled from "styled-components"; +import { useState, useCallback, useEffect, FC } from "react"; +import { RefreshIcon } from "@tonkeeper/uikit/dist/components/Icon"; + +const THRESHOLD = 300; +const INITIAL_SHIFT = 4; +const SHIFT_LIMIT = 16; +const PULL_RESISTANCE_KOEF = 0.2; + +export function usePullToRefresh(onRefresh:() => Promise ) { + const [startY, setStartY] = useState(0); + const [pullProgress, setPullProgress] = useState(0); + const [isRefreshing, setIsRefreshing] = useState(false); + + const handleTouchStart = useCallback((e: TouchEvent) => { + if (window.scrollY === 0) { + setStartY(e.touches[0].clientY); + } + }, []); + + const handleTouchMove = useCallback((e: TouchEvent) => { + if (startY === 0 || isRefreshing) return; + + const y = e.touches[0].clientY; + let pull = Math.max(0, y - startY); + + if (pull < THRESHOLD) { + pull = 0; + } else { + pull = Math.min(INITIAL_SHIFT + pull * PULL_RESISTANCE_KOEF, SHIFT_LIMIT); + } + + // Prevent default scrolling while pulling + if (pull > 0) { + e.preventDefault(); + } + + setPullProgress(pull); + }, [startY, isRefreshing]); + + const handleTouchEnd = useCallback(async () => { + if (pullProgress >= 1 && !isRefreshing) { + setIsRefreshing(true); + try { + await onRefresh(); + } finally { + setIsRefreshing(false); + } + } + setStartY(0); + setPullProgress(0); + }, [pullProgress, isRefreshing, onRefresh]); + + useEffect(() => { + document.addEventListener('touchstart', handleTouchStart, { passive: false }); + document.addEventListener('touchmove', handleTouchMove, { passive: false }); + document.addEventListener('touchend', handleTouchEnd); + + return () => { + document.removeEventListener('touchstart', handleTouchStart); + document.removeEventListener('touchmove', handleTouchMove); + document.removeEventListener('touchend', handleTouchEnd); + }; + }, [handleTouchStart, handleTouchMove, handleTouchEnd]); + + return { + pullProgress, + isRefreshing, + }; +} + + +export const RefreshContainer = styled.div<{ $pullProgress: number }>` + position: fixed; + top: 0; + left: 50%; + transform: translateX(-50%) translateY(${props => props.$pullProgress > 0 ? props.$pullProgress + 'px' : '-100%'}); + height: 36px; + width: 36px; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.2s; + z-index: 50; + background: ${p => p.theme.buttonTertiaryBackground}; + border-radius: 50%; + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.04); +`; + +export const IconWrapper = styled.div<{ $rotation: number; $isRefreshing: boolean }>` + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + transform: rotate(${props => props.$rotation}deg); + transition: transform 0.2s; + animation: ${props => props.$isRefreshing ? 'spin 1s linear infinite' : 'none'}; + + @keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } +`; + +export const PullToRefresh: FC<{ onRefresh: () => Promise}> = ({ onRefresh }) => { + const { pullProgress, isRefreshing } = usePullToRefresh(onRefresh); + const rotation = Math.min(pullProgress * 360, 360); + + return ( + + + + + + ); +} diff --git a/apps/tablet/src/app/components/TonConnectSubscription.tsx b/apps/tablet/src/app/components/TonConnectSubscription.tsx new file mode 100644 index 000000000..2b84c2adb --- /dev/null +++ b/apps/tablet/src/app/components/TonConnectSubscription.tsx @@ -0,0 +1,61 @@ +import { useResponseSendMutation } from '@tonkeeper/uikit/dist/components/connect/connectHook'; +import { TonTransactionNotification } from '@tonkeeper/uikit/dist/components/connect/TonTransactionNotification'; +import { useSendNotificationAnalytics } from '@tonkeeper/uikit/dist/hooks/amplitude'; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + tonConnectAppManuallyDisconnected$, useAppTonConnectConnections, + useDisconnectTonConnectApp +} from "@tonkeeper/uikit/dist/state/tonConnect"; +import { SendTransactionAppRequest } from '@tonkeeper/core/dist/entries/tonConnect'; +import { + subscribeToTonConnectDisconnect, + subscribeToTonConnectSendTransaction, + tonConnectSSE +} from "../../libs/tonConnect"; +import { useActiveWallet } from "@tonkeeper/uikit/dist/state/wallet"; + +export const TonConnectSubscription = () => { + const [request, setRequest] = useState(undefined); + + const { mutateAsync: responseSendAsync } = useResponseSendMutation(); + const { mutate: disconnect } = useDisconnectTonConnectApp({ skipEmit: true }); + const { data: appConnections } = useAppTonConnectConnections(); + const wallet = useActiveWallet(); + const activeWalletConnections = useMemo(() => appConnections?.find(c => c.wallet.id === wallet.id)?.connections, [appConnections, wallet.id]); + + useSendNotificationAnalytics(request?.connection?.manifest); + + useEffect(() => subscribeToTonConnectSendTransaction(setRequest), [setRequest]); + + useEffect(() => subscribeToTonConnectDisconnect(disconnect), [disconnect]); + + useEffect(() => { + return tonConnectAppManuallyDisconnected$.subscribe(value => { + if (value) { + tonConnectSSE.sendDisconnect(value); + } + }); + }, []); + + useEffect(() => { + if (activeWalletConnections && JSON.stringify(activeWalletConnections) !== JSON.stringify(tonConnectSSE.currentConnections)) { + tonConnectSSE.reconnect(); + } + }, [activeWalletConnections]); + + const handleClose = useCallback( + async (boc?: string) => { + if (!request) return; + try { + await responseSendAsync({ request, boc }); + } finally { + setRequest(undefined); + } + }, + [request, responseSendAsync, setRequest] + ); + + return ( + + ); +}; diff --git a/apps/tablet/src/app/i18n.ts b/apps/tablet/src/app/i18n.ts new file mode 100644 index 000000000..6ebeb26f9 --- /dev/null +++ b/apps/tablet/src/app/i18n.ts @@ -0,0 +1,18 @@ +import resources from '@tonkeeper/locales/dist/i18n/resources.json'; +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +i18n.use(initReactI18next) // passes i18n down to react-i18next + .init({ + resources, + debug: false, + lng: 'en', // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources + // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage + // if you're using a language detector, do not define the lng option + fallbackLng: 'en', + interpolation: { + escapeValue: false // react already safes from xss + } + }); + +export default i18n; diff --git a/apps/tablet/src/index.tsx b/apps/tablet/src/index.tsx new file mode 100644 index 000000000..ab3404c8e --- /dev/null +++ b/apps/tablet/src/index.tsx @@ -0,0 +1,6 @@ +import { createRoot } from 'react-dom/client'; +import { App } from './app/App'; +import './app/i18n'; + +const root = createRoot(document.getElementById('root')!); +root.render(); diff --git a/apps/tablet/src/libs/appSdk.ts b/apps/tablet/src/libs/appSdk.ts new file mode 100644 index 000000000..a25801da1 --- /dev/null +++ b/apps/tablet/src/libs/appSdk.ts @@ -0,0 +1,87 @@ +import { + BaseApp, + IAppSdk, + KeychainPassword, + TouchId, + CookieService, NotificationService +} from "@tonkeeper/core/dist/AppSdk"; +import packageJson from '../../package.json'; +import { TabletStorage } from './storage'; +import { Clipboard } from '@capacitor/clipboard'; +import { getWindow } from './utils'; +import { Biometric, SecureStorage } from "./plugins"; +import { CapacitorCookies } from "@capacitor/core"; +import { Device } from '@capacitor/device'; + +export class KeychainTablet implements KeychainPassword { + setPassword = async (publicKey: string, mnemonic: string) => { + await SecureStorage.storeData({ + id: `Wallet-${publicKey}`, + data: mnemonic + }); + }; + + getPassword = async (publicKey: string) => { + const { data } = await SecureStorage.getData({ + id: `Wallet-${publicKey}` + }); + return data!; + }; +} + +export class CookieTablet implements CookieService { + cleanUp = async () => { + await CapacitorCookies.clearAllCookies(); + }; +} + +export class TouchIdTablet implements TouchId { + canPrompt = async () => { + try { + const result = await Biometric.canPrompt(); + return result.isAvailable; + } catch (e) { + console.error('TOUCH ID rejected, cause', e); + return false; + } + }; + + prompt = async (reason: (lang: string) => string) => { + return Biometric.prompt(reason('en')); + }; +} + +export const TABLET_APPLICATION_ID = 'tablet' as const; + +export class TabletAppSdk extends BaseApp implements IAppSdk { + keychain = new KeychainTablet(); + cookie = new CookieTablet(); + /** + * initialises in the App component when config is fetched + */ + notifications: NotificationService | undefined = undefined; + + touchId = new TouchIdTablet(); + + constructor() { + super(new TabletStorage()); + } + + copyToClipboard = async (value: string, notification?: string) => { + await Clipboard.write({ string: value }); + this.topMessage(notification); + }; + + openPage = async (url: string) => { + getWindow()?.open(url, '_blank'); + }; + + version = packageJson.version ?? 'Unknown'; + + targetEnv = TABLET_APPLICATION_ID; +} + +export const getTabletOS = async ()=> { + const info = await Device.getInfo(); + return info.platform; +} diff --git a/apps/tablet/src/libs/hooks.ts b/apps/tablet/src/libs/hooks.ts new file mode 100644 index 000000000..0293ba652 --- /dev/null +++ b/apps/tablet/src/libs/hooks.ts @@ -0,0 +1,74 @@ +import { useQuery } from '@tanstack/react-query'; +import { Account } from '@tonkeeper/core/dist/entries/account'; +import { throttle } from '@tonkeeper/core/dist/utils/common'; +import { Analytics, AnalyticsGroup, toWalletType } from '@tonkeeper/uikit/dist/hooks/analytics'; +import { QueryKey } from '@tonkeeper/uikit/dist/libs/queryKey'; +import { useEffect } from 'react'; +import { useActiveTonNetwork } from '@tonkeeper/uikit/dist/state/wallet'; +import { getTabletOS, TABLET_APPLICATION_ID } from "./appSdk"; +import { AptabaseWeb } from "@tonkeeper/uikit/dist/hooks/analytics/aptabase-web"; + +export const useAppHeight = () => { + useEffect(() => { + const appHeight = throttle(() => { + const doc = document.documentElement; + doc.style.setProperty('--app-height', `${window.innerHeight}px`); + }, 50); + window.addEventListener('resize', appHeight); + appHeight(); + + return () => { + window.removeEventListener('resize', appHeight); + }; + }, []); +}; + +export const useAppWidth = () => { + useEffect(() => { + const appWidth = throttle(() => { + const doc = document.documentElement; + const app = (document.getElementById('root') as HTMLDivElement).childNodes.item( + 0 + ) as HTMLDivElement; + + doc.style.setProperty('--app-width', `${app.clientWidth}px`); + }, 50); + window.addEventListener('resize', appWidth); + + appWidth(); + + return () => { + window.removeEventListener('resize', appWidth); + }; + }, []); +}; + +export const useAnalytics = (version: string, activeAccount?: Account, accounts?: Account[]) => { + const network = useActiveTonNetwork(); + + return useQuery( + [QueryKey.analytics], + async () => { + const tracker = new AnalyticsGroup( + new AptabaseWeb( + import.meta.env.VITE_APP_APTABASE_HOST, + import.meta.env.VITE_APP_APTABASE, + version + ) + ); + + tracker.init({ + application: 'Tablet', + walletType: toWalletType(activeAccount?.activeTonWallet), + activeAccount: activeAccount!, + accounts: accounts!, + network, + version, + platform: `${TABLET_APPLICATION_ID}-${await getTabletOS()}` + }); + + return tracker; + }, + { enabled: accounts != null && activeAccount !== undefined } + ); +}; diff --git a/apps/tablet/src/libs/plugins.ts b/apps/tablet/src/libs/plugins.ts new file mode 100644 index 000000000..95c482eed --- /dev/null +++ b/apps/tablet/src/libs/plugins.ts @@ -0,0 +1,18 @@ +import { registerPlugin } from '@capacitor/core'; + +export interface BiometricPlugin { + canPrompt(): Promise<{isAvailable: boolean}> + + prompt(reason: string): Promise; +} + +export const Biometric = registerPlugin('Biometric'); + + +export interface SecureStoragePlugin { + storeData(params:{ id: string, data: string }): Promise + + getData(params:{ id: string }): Promise<{ data: string }> +} + +export const SecureStorage = registerPlugin('SecureStorage'); diff --git a/apps/tablet/src/libs/scroll.ts b/apps/tablet/src/libs/scroll.ts new file mode 100644 index 000000000..d8d76b46d --- /dev/null +++ b/apps/tablet/src/libs/scroll.ts @@ -0,0 +1,30 @@ +export const disableScroll = () => { + document.documentElement.classList.add('is-locked'); + window.document.body.style.paddingRight = `${getScrollbarWidth()}px`; +}; + +export const enableScroll = () => { + document.documentElement.classList.remove('is-locked'); + window.document.body.style.paddingRight = '0px'; +}; + +export const getScrollbarWidth = () => { + // Creating invisible container + const outer = document.createElement('div'); + outer.style.visibility = 'hidden'; + outer.style.overflow = 'scroll'; // forcing scrollbar to appear + (outer.style as any).msOverflowStyle = 'scrollbar'; // needed for WinJS apps + document.body.appendChild(outer); + + // Creating inner element and placing it in the container + const inner = document.createElement('div'); + outer.appendChild(inner); + + // Calculating difference between container's full width and the child width + const scrollbarWidth = outer.offsetWidth - inner.offsetWidth; + + // Removing temporary elements from the DOM + outer.parentNode!.removeChild(outer); + + return scrollbarWidth; +}; diff --git a/apps/tablet/src/libs/storage.ts b/apps/tablet/src/libs/storage.ts new file mode 100644 index 000000000..abbc65578 --- /dev/null +++ b/apps/tablet/src/libs/storage.ts @@ -0,0 +1,34 @@ +import { IStorage } from '@tonkeeper/core/dist/Storage'; +import { Preferences } from '@capacitor/preferences'; + +export class TabletStorage implements IStorage { + get = async (key: string): Promise => { + const { value } = await Preferences.get({ key }); + return value ? JSON.parse(value) as R : null; + }; + + set = async (key: string, value: R) => { + await Preferences.set({ key, value: JSON.stringify(value) }); + return value; + }; + + setBatch = async >(values: V) => { + const operations = Object.entries(values).map(([key, value]) => + Preferences.set({ key, value: JSON.stringify(value) }) + ); + await Promise.all(operations); + return values; + }; + + delete = async (key: string) => { + const payload = await this.get(key); + if (payload !== null) { + await Preferences.remove({ key }); + } + return payload; + }; + + clear = async (): Promise => { + await Preferences.clear(); + }; +} diff --git a/apps/tablet/src/libs/tabletNotifications.ts b/apps/tablet/src/libs/tabletNotifications.ts new file mode 100644 index 000000000..15c878731 --- /dev/null +++ b/apps/tablet/src/libs/tabletNotifications.ts @@ -0,0 +1,121 @@ +import { NotificationService } from "@tonkeeper/core/dist/AppSdk"; +import { TonContract } from "@tonkeeper/core/dist/entries/wallet"; +import { PushNotifications, Token } from "@capacitor/push-notifications"; +import { TonendpointConfig } from "@tonkeeper/core/dist/tonkeeperApi/tonendpoint"; +import { Device } from "@capacitor/device"; +import { APIConfig } from "@tonkeeper/core/dist/entries/apis"; +import { IStorage } from "@tonkeeper/core/dist/Storage"; +import { AppKey } from "@tonkeeper/core/dist/Keys"; + + +const requestPushPermission = async () => { + const permission = await PushNotifications.requestPermissions(); + if (permission.receive !== 'granted') { + throw new Error('Push notifications permission not granted'); + } + + const p = new Promise((res, rej) => { + PushNotifications.addListener('registration', (token: Token) => { + if (!token.value) { + rej(new Error('Push notifications permission not granted')); + return; + } + res(token.value); + PushNotifications.removeAllListeners(); + }); + + PushNotifications.addListener('registrationError', (error: any) => { + rej(error); + PushNotifications.removeAllListeners(); + }); + + setTimeout(() => { + rej(new Error('Push notifications permission timeout')); + PushNotifications.removeAllListeners(); + }, 10000); + }); + + await PushNotifications.register(); + + return p; +} + +const removeLastSlash = (url: string) => url.replace(/\/$/, ''); + +export class TabletNotifications implements NotificationService { + private readonly baseUrl: string; + constructor(config: TonendpointConfig, private readonly storage: IStorage) { + this.baseUrl = removeLastSlash(config.tonapiIOEndpoint!); + } + + async subscribe(_: APIConfig, wallet: TonContract, __: (bufferToSign: Buffer) => Promise) { + const token = await requestPushPermission(); + + const endpoint = `${this.baseUrl}/v1/internal/pushes/plain/subscribe`; + const deviceId = await Device.getId(); + + const result = await (await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + locale: (await Device.getLanguageCode()).value, + device: deviceId.identifier, + accounts: [{ address: wallet.rawAddress }], + token + }), + })).json(); + + if (!result.ok) { + throw new Error('Subscribe failed due to API error'); + } + + const records = await this.getRecords(); + records[wallet.rawAddress] = true; + await this.storage.set(AppKey.NOTIFICATIONS, records); + } + async unsubscribe(address?: string) { + const deviceId = await Device.getId(); + const endpoint = `${this.baseUrl}/v1/internal/pushes/plain/unsubscribe`; + + const result = await (await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + device: deviceId.identifier, + accounts: address ? [{ address }] : undefined + }), + })).json(); + + if (!result.ok) { + throw new Error('Unsubscribe failed due to API error'); + } + + let records = await this.getRecords(); + if (address) { + records[address] = false; + } else { + records = {} + } + + await PushNotifications.unregister(); + await this.storage.set(AppKey.NOTIFICATIONS, records); + } + async subscribeTonConnect() { + throw new Error('Method not supported.'); + } + async unsubscribeTonConnect() { + throw new Error('Method not supported.'); + } + async subscribed(address: string) { + const records = await this.getRecords(); + return records[address] ?? false; + } + + private async getRecords() { + return (await this.storage.get>(AppKey.NOTIFICATIONS)) ?? {}; + } +} diff --git a/apps/tablet/src/libs/tonConnect.ts b/apps/tablet/src/libs/tonConnect.ts new file mode 100644 index 000000000..d71d09219 --- /dev/null +++ b/apps/tablet/src/libs/tonConnect.ts @@ -0,0 +1,58 @@ +import { TonConnectSSE } from "@tonkeeper/core/dist/service/tonConnect/ton-connect-sse"; +import { TabletStorage } from "./storage"; +import { AccountConnection } from "@tonkeeper/core/dist/service/tonConnect/connectionService"; +import { SendTransactionAppRequest } from "@tonkeeper/core/dist/entries/tonConnect"; +import { App } from "@capacitor/app"; + +export const tonConnectSSE = new TonConnectSSE({ + storage: new TabletStorage(), + listeners: { + onDisconnect: connection => { + onDisconnectListeners.forEach(listener => listener(connection)); + }, + onSendTransaction: params => { + onSendTransactionListeners.forEach(listener => listener(params)); + } + } +}); + +let onTonOrTonConnectUrlOpened: ((url: string) => void)[] = []; +let onDisconnectListeners: ((connection: AccountConnection) => void)[] = []; +let onSendTransactionListeners: ((value: SendTransactionAppRequest) => void)[] = []; + +export const subscribeToTonConnectDisconnect = (listener: (connection: AccountConnection) => void) => { + onDisconnectListeners.push(listener); + return () => { + onDisconnectListeners = onDisconnectListeners.filter(l => l !== listener); + } +}; + +export const subscribeToTonConnectSendTransaction = (listener: (value: SendTransactionAppRequest) => void) => { + onSendTransactionListeners.push(listener); + return () => { + onSendTransactionListeners = onSendTransactionListeners.filter(l => l !== listener); + } +}; + +export const subscribeToTonOrTonConnectUrlOpened = (listener: (url: string) => void) => { + onTonOrTonConnectUrlOpened.push(listener); + return () => { + onTonOrTonConnectUrlOpened = onTonOrTonConnectUrlOpened.filter(l => l !== listener); + } +}; + +App.addListener('appUrlOpen', ({ url }) => { + if (url) { + console.info('Received URL:', url); + onTonOrTonConnectUrlOpened.forEach(listener => listener(url)); + } +}); + + +App.addListener('appStateChange', async ({ isActive }) => { + if (isActive) { + tonConnectSSE.reconnect(); + } else { + tonConnectSSE.destroy(); + } +}); diff --git a/apps/tablet/src/libs/utils.ts b/apps/tablet/src/libs/utils.ts new file mode 100644 index 000000000..cbd363f93 --- /dev/null +++ b/apps/tablet/src/libs/utils.ts @@ -0,0 +1,7 @@ +export function getWindow() { + if (typeof window !== 'undefined') { + return window; + } + + return undefined; +} diff --git a/apps/tablet/src/manifest.json b/apps/tablet/src/manifest.json new file mode 100644 index 000000000..2ed570131 --- /dev/null +++ b/apps/tablet/src/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "App", + "short_name": "App", + "start_url": "index.html", + "display": "standalone", + "icons": [{ + "src": "assets/imgs/logo.png", + "sizes": "512x512", + "type": "image/png" + }], + "background_color": "#31d53d", + "theme_color": "#31d53d" +} diff --git a/apps/tablet/src/vite-env.d.ts b/apps/tablet/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/apps/tablet/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/tablet/tsconfig.json b/apps/tablet/tsconfig.json new file mode 100644 index 000000000..8f9d0bf15 --- /dev/null +++ b/apps/tablet/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "allowSyntheticDefaultImports": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }], + "ts-node": { + "compilerOptions": { + "module": "CommonJS" + } + } +} diff --git a/apps/tablet/tsconfig.node.json b/apps/tablet/tsconfig.node.json new file mode 100644 index 000000000..af7c06e30 --- /dev/null +++ b/apps/tablet/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/tablet/vite.config.ts b/apps/tablet/vite.config.ts new file mode 100644 index 000000000..538e3bc1a --- /dev/null +++ b/apps/tablet/vite.config.ts @@ -0,0 +1,37 @@ +import { defineConfig } from 'vite'; +import { nodePolyfills } from "vite-plugin-node-polyfills"; +import react from "@vitejs/plugin-react"; +import path from "path"; + + +export default defineConfig({ + build: { + outDir: './dist', + minify: false, + emptyOutDir: true + }, + plugins: [ + nodePolyfills({ + globals: { + Buffer: true, + global: true, + process: true + }, + include: ['stream', 'buffer', 'crypto'] + }), + react() + ], + resolve: { + alias: { + react: path.resolve(__dirname, './node_modules/react'), + 'react-dom': path.resolve(__dirname, './node_modules/react-dom'), + '@ton/core': path.resolve(__dirname, '../../packages/core/node_modules/@ton/core'), + '@ton/crypto': path.resolve(__dirname, '../../packages/core/node_modules/@ton/crypto'), + '@ton/ton': path.resolve(__dirname, '../../packages/core/node_modules/@ton/ton'), + 'react-router-dom': path.resolve(__dirname, './node_modules/react-router-dom'), + 'styled-components': path.resolve(__dirname, './node_modules/styled-components'), + 'react-i18next': path.resolve(__dirname, './node_modules/react-i18next'), + '@tanstack/react-query': path.resolve(__dirname, './node_modules/@tanstack/react-query') + } + } +}); diff --git a/package.json b/package.json index 474742e1a..42c3afbd1 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "deploy:twa": "npx turbo build:twa && npx wrangler pages deploy apps/twa/build", "build:extension": "npx turbo build:extension", "build:desktop": "npx turbo build:desktop", + "build:ipad": "npx turbo build:ipad", "build:pkg": "npx turbo build:pkg" }, "private": true, diff --git a/packages/core/src/AppSdk.ts b/packages/core/src/AppSdk.ts index c26526c78..48f6712a1 100644 --- a/packages/core/src/AppSdk.ts +++ b/packages/core/src/AppSdk.ts @@ -182,4 +182,4 @@ export class MockAppSdk extends BaseApp { } } -export type TargetEnv = 'web' | 'extension' | 'desktop' | 'twa'; +export type TargetEnv = 'web' | 'extension' | 'desktop' | 'twa' | 'tablet'; diff --git a/packages/core/src/Keys.ts b/packages/core/src/Keys.ts index 85647c1ee..8333cbaf8 100644 --- a/packages/core/src/Keys.ts +++ b/packages/core/src/Keys.ts @@ -45,5 +45,6 @@ export enum AppKey { SWAP_CUSTOM_ASSETS = 'swap_custom_assets', SWAP_OPTIONS = 'swap_options', - BATTERY_AUTH_TOKEN = 'battery_auth_token' + BATTERY_AUTH_TOKEN = 'battery_auth_token', + NOTIFICATIONS = 'notifications' } diff --git a/packages/core/src/entries/tonConnect.ts b/packages/core/src/entries/tonConnect.ts index a999e61cc..e024290dc 100644 --- a/packages/core/src/entries/tonConnect.ts +++ b/packages/core/src/entries/tonConnect.ts @@ -306,3 +306,9 @@ export interface TonConnectMessageRequest { from: string; connection: AccountConnection; } + +export interface SendTransactionAppRequest { + id: string; + connection: AccountConnection; + payload: TonConnectTransactionPayload; +} diff --git a/packages/core/src/service/tonConnect/httpBridge.ts b/packages/core/src/service/tonConnect/httpBridge.ts index 2287f6779..cda1b5642 100644 --- a/packages/core/src/service/tonConnect/httpBridge.ts +++ b/packages/core/src/service/tonConnect/httpBridge.ts @@ -91,6 +91,7 @@ export const subscribeTonConnect = ({ const eventSource = new EventSourceClass(url); const onMessage = (params: MessageEvent) => { + console.log('sse message received', params.data); setLastEventId(storage, params.lastEventId); const { from, message }: TonConnectRequest = JSON.parse(params.data); @@ -98,6 +99,7 @@ export const subscribeTonConnect = ({ const connection = connections.find(item => item.clientSessionId === from); if (!connection) return; + console.log('sse message processed', params.data); handleMessage(decryptTonConnectMessage({ message, from, connection })); }; diff --git a/packages/core/src/service/tonConnect/ton-connect-sse.ts b/packages/core/src/service/tonConnect/ton-connect-sse.ts new file mode 100644 index 000000000..d82265a72 --- /dev/null +++ b/packages/core/src/service/tonConnect/ton-connect-sse.ts @@ -0,0 +1,192 @@ +import { + AccountConnection, + disconnectAppConnection, + getTonWalletConnections +} from './connectionService'; +import { isStandardTonWallet, WalletId } from '../../entries/wallet'; +import { getLastEventId, subscribeTonConnect } from './httpBridge'; +import { accountsStorage } from '../accountsStorage'; +import { IStorage } from '../../Storage'; +import { SendTransactionAppRequest, TonConnectAppRequest } from '../../entries/tonConnect'; +import { getWalletById } from '../../entries/account'; +import { replyBadRequestResponse, replyDisconnectResponse } from './actionService'; +import { delay } from '../../utils/common'; + +type Logger = { + info: (message: string) => void; +}; + +type System = { + refresh?: () => void; + bringToFront?: () => Promise; + log?: Logger; +}; + +type Listeners = { + onDisconnect: (connection: AccountConnection) => void; + onSendTransaction: (value: SendTransactionAppRequest) => void; +}; + +export class TonConnectSSE { + private lastEventId: string | undefined; + + private connections: AccountConnection[] = []; + + private dist: Record = {}; + + private closeConnection: (() => void) | null = null; + + private readonly storage: IStorage; + + private readonly EventSourcePolyfill: typeof EventSource; + + private readonly system: Omit & { log: Logger }; + + private readonly listeners: Listeners; + + public get currentConnections() { + return this.connections; + } + + constructor({ + storage, + listeners, + EventSourcePolyfill = EventSource, + system = {} + }: { + storage: IStorage; + listeners: Listeners; + EventSourcePolyfill?: typeof EventSource; + system?: System; + }) { + this.storage = storage; + this.EventSourcePolyfill = EventSourcePolyfill; + this.system = { log: console, ...system }; + this.listeners = listeners; + this.reconnect(); + } + + public async reconnect() { + this.system.log.info('Reconnect.'); + await this.init(); + return this.connect(); + } + + public async init() { + this.lastEventId = await getLastEventId(this.storage); + + const walletsState = (await accountsStorage(this.storage).getAccounts()).flatMap( + a => a.allTonWallets + ); + + this.connections = []; + this.dist = {}; + + for (const wallet of walletsState) { + const walletConnections = await getTonWalletConnections(this.storage, wallet); + + this.connections = this.connections.concat(walletConnections); + walletConnections.forEach(item => { + this.dist[item.clientSessionId] = wallet.id; + }); + } + } + + public sendDisconnect = async (connection: AccountConnection | AccountConnection[]) => { + const connectionsToDisconnect = Array.isArray(connection) ? connection : [connection]; + await Promise.allSettled( + connectionsToDisconnect.map((item, index) => + replyDisconnectResponse({ + connection: item, + request: { id: (Date.now() + index).toString() } + }) + ) + ); + await this.reconnect(); + }; + + private onDisconnect = async ({ connection, request }: TonConnectAppRequest) => { + const accounts = await accountsStorage(this.storage).getAccounts(); + const wallet = getWalletById(accounts, this.dist[connection.clientSessionId]); + + if (!wallet || !isStandardTonWallet(wallet)) { + return; + } + + await disconnectAppConnection({ + storage: this.storage, + wallet, + clientSessionId: connection.clientSessionId + }); + await replyDisconnectResponse({ connection, request }); + await this.reconnect(); + this.listeners.onDisconnect(connection); + }; + + private handleMessage = async (params: TonConnectAppRequest) => { + switch (params.request.method) { + case 'disconnect': { + return this.onDisconnect(params); + } + case 'sendTransaction': { + const value = { + connection: params.connection, + id: params.request.id, + payload: JSON.parse(params.request.params[0]) + }; + + const walletId = this.dist[params.connection.clientSessionId]; + + const activeAccount = await accountsStorage(this.storage).getActiveAccount(); + if (!activeAccount) { + throw new Error('Account not found'); + } + const activeWallet = activeAccount.activeTonWallet; + + await this.system.bringToFront?.(); + + if (activeWallet.id !== walletId) { + const accountToActivate = ( + await accountsStorage(this.storage).getAccounts() + ).find(a => a.getTonWallet(walletId) !== undefined); + + if (!accountToActivate) { + throw new Error('Account not found'); + } + + accountToActivate.setActiveTonWallet(walletId); + await accountsStorage(this.storage).updateAccountInState(accountToActivate); + await accountsStorage(this.storage).setActiveAccountId(accountToActivate.id); + this.system.refresh?.(); + await delay(500); + } + + return this.listeners.onSendTransaction(value); + } + default: { + return replyBadRequestResponse(params); + } + } + }; + + public async connect() { + this.destroy(); + if (this.connections.length === 0) { + this.system.log.info('Missing connection.'); + } + this.closeConnection = subscribeTonConnect({ + storage: this.storage, + handleMessage: this.handleMessage, + connections: this.connections, + lastEventId: this.lastEventId, + EventSourceClass: this.EventSourcePolyfill + }); + } + + public destroy() { + if (this.closeConnection) { + this.system.log.info('Close connection.'); + this.closeConnection(); + } + } +} diff --git a/packages/core/src/tonkeeperApi/tonendpoint.ts b/packages/core/src/tonkeeperApi/tonendpoint.ts index d980af37d..ada40a702 100644 --- a/packages/core/src/tonkeeperApi/tonendpoint.ts +++ b/packages/core/src/tonkeeperApi/tonendpoint.ts @@ -5,7 +5,7 @@ import { DAppTrack } from '../service/urlService'; import { FetchAPI } from '../tonApiV2'; export interface BootParams { - platform: 'ios' | 'android' | 'web' | 'desktop'; + platform: 'ios' | 'android' | 'web' | 'desktop' | 'tablet'; lang: 'en' | 'ru' | string; build: string; // "2.8.0" network: Network; @@ -80,6 +80,7 @@ export interface TonendpointConfig { battery_beta?: boolean; disable_battery?: boolean; disable_battery_send?: boolean; + isOnReview?: boolean; } const defaultTonendpoint = 'https://api.tonkeeper.com'; // 'http://localhost:1339'; diff --git a/packages/uikit/src/components/Footer.tsx b/packages/uikit/src/components/Footer.tsx index 04c075361..a086ac60e 100644 --- a/packages/uikit/src/components/Footer.tsx +++ b/packages/uikit/src/components/Footer.tsx @@ -7,6 +7,7 @@ import { scrollToTop } from '../libs/common'; import { AppRoute } from '../libs/routes'; import { ActivityIcon, BrowserIcon, SettingsIcon, WalletIcon } from './NavigationIcons'; import { Label3 } from './Text'; +import { HideOnReview } from './ios/HideOnReview'; const Button = styled.div<{ active: boolean }>` user-select: none; @@ -113,15 +114,17 @@ export const Footer: FC<{ standalone?: boolean; sticky?: boolean }> = ({ standal {t('activity_screen_title')} - {hideBrowser === true ? null : ( - - )} + + {hideBrowser === true ? null : ( + + )} + ) : ( - + + + )} void onClose }) => { return ( - <> + {() => } - + ); }; diff --git a/packages/uikit/src/components/home/BuyAction.tsx b/packages/uikit/src/components/home/BuyAction.tsx index dec4abf9a..e6c98ad25 100644 --- a/packages/uikit/src/components/home/BuyAction.tsx +++ b/packages/uikit/src/components/home/BuyAction.tsx @@ -5,7 +5,6 @@ import { import React, { FC, useCallback, useMemo, useState } from 'react'; import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import styled, { css } from 'styled-components'; -import { useAppContext } from '../../hooks/appContext'; import { useAppSdk } from '../../hooks/appSdk'; import { useTranslation } from '../../hooks/translation'; import { AppRoute, SettingsRoute } from '../../libs/routes'; @@ -25,6 +24,7 @@ import { Action } from './Actions'; import { BuyItemNotification } from './BuyItemNotification'; import { BuyIcon, SellIcon } from './HomeIcons'; import { useActiveConfig } from '../../state/wallet'; +import { HideOnReview } from '../ios/HideOnReview'; const BuyList: FC<{ items: TonendpoinFiatItem[]; kind: 'buy' | 'sell' }> = ({ items, kind }) => { return ( @@ -117,9 +117,11 @@ export const BuyNotification: FC<{ }, [open, buy]); return ( - - {Content} - + + + {Content} + + ); }; @@ -143,10 +145,10 @@ export const BuyAction: FC = () => { }, [searchParams, setSearchParams]); return ( - <> + } title={'wallet_buy'} action={toggle} /> - + ); }; diff --git a/packages/uikit/src/components/home/TonActions.tsx b/packages/uikit/src/components/home/TonActions.tsx index 0d385be1d..ac101a7ea 100644 --- a/packages/uikit/src/components/home/TonActions.tsx +++ b/packages/uikit/src/components/home/TonActions.tsx @@ -8,6 +8,7 @@ import { BuyAction } from './BuyAction'; import { ReceiveAction } from './ReceiveAction'; import { SwapAction } from './SwapAction'; import { Network } from '@tonkeeper/core/dist/entries/network'; +import { HideOnReview } from '../ios/HideOnReview'; export const HomeActions: FC<{ chain?: BLOCKCHAIN_NAME }> = () => { const isReadOnly = useIsActiveWalletWatchOnly(); @@ -18,7 +19,9 @@ export const HomeActions: FC<{ chain?: BLOCKCHAIN_NAME }> = () => { {!isTestnet && } {!isReadOnly && } - {!isTestnet && !isReadOnly && } + + {!isTestnet && !isReadOnly && } + {/* */} ); diff --git a/packages/uikit/src/components/ios/HideOnReview.tsx b/packages/uikit/src/components/ios/HideOnReview.tsx new file mode 100644 index 000000000..cc392bab2 --- /dev/null +++ b/packages/uikit/src/components/ios/HideOnReview.tsx @@ -0,0 +1,12 @@ +import { useIsOnIosReview } from '../../hooks/ios'; +import { FC, PropsWithChildren } from 'react'; + +export const HideOnReview: FC = ({ children }) => { + const isOnReview = useIsOnIosReview(); + + if (isOnReview) { + return null; + } + + return <>{children}; +}; diff --git a/packages/uikit/src/components/pro/ProNotification.tsx b/packages/uikit/src/components/pro/ProNotification.tsx index 8ec9a37e9..19ea6daa9 100644 --- a/packages/uikit/src/components/pro/ProNotification.tsx +++ b/packages/uikit/src/components/pro/ProNotification.tsx @@ -1,14 +1,17 @@ import { FC } from 'react'; import { Notification } from '../Notification'; import { ProSettingsContent } from '../settings/ProSettings'; +import { HideOnReview } from '../ios/HideOnReview'; export const ProNotification: FC<{ isOpen: boolean; onClose: (success?: boolean) => void }> = ({ isOpen, onClose }) => { return ( - onClose()}> - {() => onClose(true)} />} - + + onClose()}> + {() => onClose(true)} />} + + ); }; diff --git a/packages/uikit/src/components/shared/ScanQR.tsx b/packages/uikit/src/components/shared/ScanQR.tsx index 770871409..a5bc60553 100644 --- a/packages/uikit/src/components/shared/ScanQR.tsx +++ b/packages/uikit/src/components/shared/ScanQR.tsx @@ -50,7 +50,15 @@ function Scan({ const startScanning = async () => { try { const videoInputDevices = await BrowserQRCodeReader.listVideoInputDevices(); - const selectedDeviceId = videoInputDevices[0].deviceId; + + // Prefer the back camera + const backCamera = videoInputDevices.find( + device => + device?.label?.toLowerCase().includes('back') || + device?.deviceId?.includes('camera') + ); + + const selectedDeviceId = backCamera?.deviceId ?? videoInputDevices[0].deviceId; controlsRef.current = await codeReader.decodeFromVideoDevice( selectedDeviceId, diff --git a/packages/uikit/src/desktop-pages/browser/DesktopBrowserRecommendationsPage.tsx b/packages/uikit/src/desktop-pages/browser/DesktopBrowserRecommendationsPage.tsx index b08e24a0b..52b45b504 100644 --- a/packages/uikit/src/desktop-pages/browser/DesktopBrowserRecommendationsPage.tsx +++ b/packages/uikit/src/desktop-pages/browser/DesktopBrowserRecommendationsPage.tsx @@ -4,6 +4,7 @@ import { useOpenBrowser } from '../../hooks/amplitude'; import { useRecommendations } from '../../hooks/browser/useRecommendations'; import { PromotionsCarousel } from '../../components/browser/PromotionsCarousel'; import { DesktopCategoryBlock } from './DesktopCategoryBlock'; +import { HideOnReview } from '../../components/ios/HideOnReview'; const PromotionsCarouselStyled = styled(PromotionsCarousel)` margin-top: 1rem; @@ -38,15 +39,17 @@ export const DesktopBrowserRecommendationsPage: FC = () => { } return ( - - - {data.categories.map((category, index) => ( - - ))} - + + + + {data.categories.map((category, index) => ( + + ))} + + ); }; diff --git a/packages/uikit/src/desktop-pages/coin/DesktopCoinPage.tsx b/packages/uikit/src/desktop-pages/coin/DesktopCoinPage.tsx index 6a37113fd..aab32fefe 100644 --- a/packages/uikit/src/desktop-pages/coin/DesktopCoinPage.tsx +++ b/packages/uikit/src/desktop-pages/coin/DesktopCoinPage.tsx @@ -3,7 +3,7 @@ import { tonAssetAddressFromString } from '@tonkeeper/core/dist/entries/crypto/a import { eqAddresses } from '@tonkeeper/core/dist/utils/address'; import { shiftedDecimals } from '@tonkeeper/core/dist/utils/balance'; import BigNumber from 'bignumber.js'; -import { FC, useCallback, useEffect, useMemo, useRef } from 'react'; +import { FC, useEffect, useMemo, useRef } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import styled from 'styled-components'; import { ArrowDownIcon, ArrowUpIcon, PlusIcon, SwapIcon } from '../../components/Icon'; @@ -29,15 +29,11 @@ import { toTokenRate, useRate } from '../../state/rates'; import { useAllSwapAssets } from '../../state/swap/useSwapAssets'; import { useSwapFromAsset } from '../../state/swap/useSwapForm'; import { useTonendpointBuyMethods } from '../../state/tonendpoint'; -import { - useActiveTonNetwork, - useActiveWallet, - useIsActiveWalletWatchOnly -} from '../../state/wallet'; +import { useActiveTonNetwork, useIsActiveWalletWatchOnly } from '../../state/wallet'; import { OtherHistoryFilters } from '../../components/desktop/history/DesktopHistoryFilters'; -import { useQueryClient } from '@tanstack/react-query'; -import { QueryKey } from '../../libs/queryKey'; import { Network } from '@tonkeeper/core/dist/entries/network'; +import { useIsOnIosReview } from '../../hooks/ios'; +import { HideOnReview } from '../../components/ios/HideOnReview'; export const DesktopCoinPage = () => { const navigate = useNavigate(); @@ -138,18 +134,20 @@ const CoinHeader: FC<{ token: string }> = ({ token }) => { {t('wallet_receive')} - {swapAsset && ( - - - {t('wallet_swap')} - - )} - {canBuy && ( - - - {t('wallet_buy')} - - )} + + {swapAsset && ( + + + {t('wallet_swap')} + + )} + {canBuy && ( + + + {t('wallet_buy')} + + )} + diff --git a/packages/uikit/src/desktop-pages/dashboard/index.tsx b/packages/uikit/src/desktop-pages/dashboard/index.tsx index 7ab0c0607..584a33dfc 100644 --- a/packages/uikit/src/desktop-pages/dashboard/index.tsx +++ b/packages/uikit/src/desktop-pages/dashboard/index.tsx @@ -6,6 +6,7 @@ import { DesktopDashboardHeader } from '../../components/desktop/header/DesktopD import { desktopHeaderContainerHeight } from '../../components/desktop/header/DesktopHeaderElements'; import { ProBanner } from '../../components/pro/ProBanner'; import { useProState } from '../../state/pro'; +import { HideOnReview } from '../../components/ios/HideOnReview'; const DashboardTableStyled = styled(DashboardTable)``; @@ -36,11 +37,13 @@ const DashboardPage: FC = () => { - {shouldShowProBanner && ( - - - - )} + + {shouldShowProBanner && ( + + + + )} + ); diff --git a/packages/uikit/src/desktop-pages/settings/DesktopWalletSettingsPage.tsx b/packages/uikit/src/desktop-pages/settings/DesktopWalletSettingsPage.tsx index 4f00d9972..d3d692b86 100644 --- a/packages/uikit/src/desktop-pages/settings/DesktopWalletSettingsPage.tsx +++ b/packages/uikit/src/desktop-pages/settings/DesktopWalletSettingsPage.tsx @@ -6,6 +6,7 @@ import { CoinsIcon, ExitIcon, KeyIcon, + NotificationOutlineIcon, SaleBadgeIcon, SwitchIcon, UnpinIconOutline @@ -28,8 +29,7 @@ import { import { AccountMAM, isAccountVersionEditable, - AccountTonMultisig, - getNetworkByAccount + AccountTonMultisig } from '@tonkeeper/core/dist/entries/account'; import { useRenameNotification } from '../../components/modals/RenameNotificationControlled'; import { useRecoveryNotification } from '../../components/modals/RecoveryNotificationControlled'; @@ -40,8 +40,8 @@ import { useMultisigTogglePinForWallet } from '../../state/multisig'; import { useDeleteAccountNotification } from '../../components/modals/DeleteAccountNotificationControlled'; -import { useBatteryBalance, useBatteryEnabledConfig } from '../../state/battery'; -import { Network } from '@tonkeeper/core/dist/entries/network'; +import React from 'react'; +import { useAppSdk } from '../../hooks/appSdk'; const SettingsListBlock = styled.div` padding: 0.5rem 0; @@ -111,6 +111,8 @@ export const DesktopWalletSettingsPage = () => { }).then(() => navigate(AppRoute.home)); }; + const notificationsAvailable = useAppSdk().notifications !== undefined; + return ( @@ -189,6 +191,14 @@ export const DesktopWalletSettingsPage = () => { {t('settings_collectibles_list')} + {notificationsAvailable && ( + + + + {t('settings_notifications')} + + + )} {!isReadOnly && ( diff --git a/packages/uikit/src/desktop-pages/settings/DesktopWalletSettingsRouting.tsx b/packages/uikit/src/desktop-pages/settings/DesktopWalletSettingsRouting.tsx index af2182899..783c34992 100644 --- a/packages/uikit/src/desktop-pages/settings/DesktopWalletSettingsRouting.tsx +++ b/packages/uikit/src/desktop-pages/settings/DesktopWalletSettingsRouting.tsx @@ -10,6 +10,7 @@ import { DesktopNftSettings } from './DesktopNftSettings'; import { MAMIndexesPage } from '../../pages/settings/MamIndexes'; import { LedgerIndexesPage } from '../../pages/settings/LedgerIndexes'; import { BatteryPage } from '../../pages/settings/Battery'; +import { Notifications } from "../../pages/settings/Notification"; const OldSettingsLayoutWrapper = styled.div` padding-top: 64px; @@ -43,6 +44,7 @@ export const DesktopWalletSettingsRouting = () => { } /> } /> } /> + } /> } /> ); diff --git a/packages/uikit/src/desktop-pages/swap/index.tsx b/packages/uikit/src/desktop-pages/swap/index.tsx index b26bf8389..8a38f42d6 100644 --- a/packages/uikit/src/desktop-pages/swap/index.tsx +++ b/packages/uikit/src/desktop-pages/swap/index.tsx @@ -13,10 +13,11 @@ import { fallbackRenderOver } from '../../components/Error'; import { SwapRefreshButton } from '../../components/swap/icon-buttons/SwapRefreshButton'; import { SwapSettingsButton } from '../../components/swap/icon-buttons/SwapSettingsButton'; import { useTranslation } from '../../hooks/translation'; +import { HideOnReview } from '../../components/ios/HideOnReview'; const SwapPageWrapper = styled.div` overflow-y: auto; - min-width: 640px; + min-width: 580px; `; const HeaderButtons = styled.div` @@ -73,8 +74,10 @@ const DesktopSwapPageContent = () => { export const DesktopSwapPage = () => { return ( - - - + + + + + ); }; diff --git a/packages/uikit/src/hooks/ios.ts b/packages/uikit/src/hooks/ios.ts index 15c856440..94ad456a8 100644 --- a/packages/uikit/src/hooks/ios.ts +++ b/packages/uikit/src/hooks/ios.ts @@ -1,3 +1,5 @@ +import { useAppContext } from './appContext'; + export function openIosKeyboard(keyboard: string, type = 'text', timerSeconds = 30) { const input = document.createElement('input'); input.setAttribute('type', type); @@ -18,3 +20,8 @@ export function hideIosKeyboard() { activeElement.blur(); } } + +export const useIsOnIosReview = () => { + const { mainnetConfig } = useAppContext(); + return Boolean(mainnetConfig.isOnReview); +}; diff --git a/packages/uikit/src/libs/queryKey.ts b/packages/uikit/src/libs/queryKey.ts index d9b8314c0..f30ebdfb4 100644 --- a/packages/uikit/src/libs/queryKey.ts +++ b/packages/uikit/src/libs/queryKey.ts @@ -32,6 +32,7 @@ export enum QueryKey { tonConnectConnection = 'tonConnectConnection', tonConnectLastEventId = 'tonConnectLastEventId', subscribed = 'subscribed', + globalSubscribed = 'globalSubscribed', featuredRecommendations = 'recommendations', experimental = 'experimental', diff --git a/packages/uikit/src/libs/routes.ts b/packages/uikit/src/libs/routes.ts index 40f3fad51..ed7c543f3 100644 --- a/packages/uikit/src/libs/routes.ts +++ b/packages/uikit/src/libs/routes.ts @@ -56,7 +56,8 @@ export enum WalletSettingsRoute { nft = '/nft', connectedApps = '/connected-apps', derivations = '/derivations', - battery = '/battery' + battery = '/battery', + notification = '/notification' } export enum BrowserRoute { diff --git a/packages/uikit/src/pages/browser/BrowserRecommendationsPage.tsx b/packages/uikit/src/pages/browser/BrowserRecommendationsPage.tsx index 506ef40db..7d9a41fa3 100644 --- a/packages/uikit/src/pages/browser/BrowserRecommendationsPage.tsx +++ b/packages/uikit/src/pages/browser/BrowserRecommendationsPage.tsx @@ -7,6 +7,7 @@ import { RecommendationsPageBodySkeleton } from '../../components/skeletons/Brow import { useOpenBrowser } from '../../hooks/amplitude'; import { useRecommendations } from '../../hooks/browser/useRecommendations'; import { CategoryBlock } from './CategoryBlock'; +import { HideOnReview } from '../../components/ios/HideOnReview'; const InnerBodyStyled = styled(InnerBody)` padding: 0; @@ -33,7 +34,7 @@ export const BrowserRecommendationsPage: FC = () => { }, [track, data]); return ( - <> + {data ? ( @@ -51,6 +52,6 @@ export const BrowserRecommendationsPage: FC = () => { )} - + ); }; diff --git a/packages/uikit/src/pages/browser/CategoryPage.tsx b/packages/uikit/src/pages/browser/CategoryPage.tsx index 5a633dc39..3a7375d07 100644 --- a/packages/uikit/src/pages/browser/CategoryPage.tsx +++ b/packages/uikit/src/pages/browser/CategoryPage.tsx @@ -6,6 +6,7 @@ import { CategoryGroupItem } from './CategoryBlock'; import { ListBlock } from '../../components/List'; import { useRecommendations } from '../../hooks/browser/useRecommendations'; import { RecommendationPageListItemSkeleton } from '../../components/skeletons/BrowserSkeletons'; +import { HideOnReview } from '../../components/ios/HideOnReview'; export const CategoryPage = () => { const { id } = useParams(); @@ -14,7 +15,7 @@ export const CategoryPage = () => { const group = data?.categories.find(item => item.id === id); return ( - <> + {group ? ( @@ -33,6 +34,6 @@ export const CategoryPage = () => { )} - + ); }; diff --git a/packages/uikit/src/pages/settings/MamIndexes.tsx b/packages/uikit/src/pages/settings/MamIndexes.tsx index 576bd18f5..6f7889daf 100644 --- a/packages/uikit/src/pages/settings/MamIndexes.tsx +++ b/packages/uikit/src/pages/settings/MamIndexes.tsx @@ -31,13 +31,12 @@ import { } from '../../components/desktop/DesktopViewLayout'; import { IconButtonTransparentBackground } from '../../components/fields/IconButton'; import { useProFeaturesNotification } from '../../components/modals/ProFeaturesNotificationControlled'; -import { useRecoveryNotification } from '../../components/modals/RecoveryNotificationControlled'; import { useRenameNotification } from '../../components/modals/RenameNotificationControlled'; -import { useAppContext } from '../../hooks/appContext'; import { useIsFullWidthMode } from '../../hooks/useIsFullWidthMode'; import { usePrevious } from '../../hooks/usePrevious'; import { scrollToContainersBottom } from '../../libs/web'; import { useProState } from '../../state/pro'; +import { HideOnReview } from '../../components/ios/HideOnReview'; const FirstLineContainer = styled.div` display: flex; @@ -141,7 +140,6 @@ export const MAMIndexesPageContent: FC<{ const { t } = useTranslation(); const config = useActiveConfig(); const { data: proState } = useProState(); - const { onOpen: recovery } = useRecoveryNotification(); const { onOpen: buyPro } = useProFeaturesNotification(); const ref = useRef(null); @@ -306,9 +304,11 @@ export const MAMIndexesPageContent: FC<{ {showByProButton ? ( - + + + ) : (