├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── config ├── env.js ├── getHttpsConfig.js ├── jest │ ├── babelTransform.js │ ├── cssTransform.js │ └── fileTransform.js ├── modules.js ├── paths.js ├── webpack.config.js ├── webpack │ └── persistentCache │ │ └── createEnvironmentHash.js └── webpackDevServer.config.js ├── docs ├── apple-touch-icon.png ├── asset-manifest.json ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── privacy-policy.txt ├── robots.txt ├── safari-pinned-tab.svg ├── static │ ├── css │ │ ├── main.219599c6.css │ │ └── main.219599c6.css.map │ └── js │ │ ├── main.9eade7c3.js │ │ ├── main.9eade7c3.js.LICENSE.txt │ │ └── main.9eade7c3.js.map ├── terms-of-use.txt └── tonconnect-manifest.json ├── package.json ├── public ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── privacy-policy.txt ├── robots.txt ├── safari-pinned-tab.svg ├── terms-of-use.txt └── tonconnect-manifest.json ├── scripts ├── build.js ├── start.js └── test.js ├── src ├── App.tsx ├── TonProofDemoApi.ts ├── app.scss ├── components │ ├── AppTitle │ │ ├── AppTitle.tsx │ │ ├── Debugger │ │ │ └── Debugger.tsx │ │ └── style.scss │ ├── AuthButton │ │ ├── AuthButton.tsx │ │ └── style.scss │ ├── TonProofDemo │ │ ├── TonProofDemo.tsx │ │ └── style.scss │ └── TxForm │ │ ├── TxForm.tsx │ │ └── style.scss ├── connector.ts ├── hooks │ ├── useForceUpdate.ts │ ├── useSlicedAddress.ts │ ├── useTonWallet.ts │ └── useTonWalletConnectionError.ts ├── index.scss ├── index.tsx ├── patch-local-storage-for-github-pages.ts ├── state │ ├── auth-payload.ts │ └── wallets-list.ts └── utils.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | docs 2 | config 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | }, 7 | extends: ['plugin:react/recommended', 'plugin:prettier/recommended'], 8 | parser: '@typescript-eslint/parser', 9 | plugins: ['react', 'react-hooks', '@typescript-eslint'], 10 | ignorePatterns: ['./docs/*'], 11 | overrides: [], 12 | parserOptions: { 13 | ecmaVersion: 'latest', 14 | sourceType: 'module', 15 | }, 16 | settings: { 17 | react: { 18 | version: 'detect', 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .idea 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | tabWidth: 2, 4 | useTabs: true, 5 | printWidth: 120, 6 | singleQuote: true, 7 | proseWrap: 'never', 8 | trailingComma: 'all', 9 | arrowParens: 'always', 10 | importOrder: ['', '^@/(.*)$', '^../(.*)', '^./(.*)'], 11 | importOrderSortSpecifiers: true, 12 | importOrderCaseInsensitive: true, 13 | importOrderGroupNamespaceSpecifiers: true, 14 | }; 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 tonkeeper 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TON Connect demo dApp with backend 2 | 3 | It is the example of an app that connects to TON wallet via TonConnect using [@tonconnect/sdk](https://www.npmjs.com/package/@tonconnect/sdk). 4 | 5 | There is an example of backend authorization with `ton_proof` in the app. See [demo-dapp-backend](https://github.com/ton-connect/demo-dapp-backend) for more details about the authorization. 6 | 7 | [Try the demo here](https://ton-connect.github.io/demo-dapp-with-backend/) 8 | 9 | ## Getting started 10 | 1. Install dependencies `yarn` 11 | 2. Start development server `yarn start` 12 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | const dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | // Don't include `.env.local` for `test` environment 21 | // since normally you expect tests to produce the same 22 | // results for everyone 23 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 24 | `${paths.dotenv}.${NODE_ENV}`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebook/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of webpack shims. 50 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example, . 77 | // This should only be used as an escape hatch. Normally you would put 78 | // images into the `src` and `import` them in code to get their paths. 79 | PUBLIC_URL: publicUrl, 80 | // We support configuring the sockjs pathname during development. 81 | // These settings let a developer run multiple simultaneous projects. 82 | // They are used as the connection `hostname`, `pathname` and `port` 83 | // in webpackHotDevClient. They are used as the `sockHost`, `sockPath` 84 | // and `sockPort` options in webpack-dev-server. 85 | WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST, 86 | WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH, 87 | WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT, 88 | // Whether or not react-refresh is enabled. 89 | // It is defined here so it is available in the webpackHotDevClient. 90 | FAST_REFRESH: process.env.FAST_REFRESH !== 'false', 91 | } 92 | ); 93 | // Stringify all values so we can feed into webpack DefinePlugin 94 | const stringified = { 95 | 'process.env': Object.keys(raw).reduce((env, key) => { 96 | env[key] = JSON.stringify(raw[key]); 97 | return env; 98 | }, {}), 99 | }; 100 | 101 | return { raw, stringified }; 102 | } 103 | 104 | module.exports = getClientEnvironment; 105 | -------------------------------------------------------------------------------- /config/getHttpsConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const crypto = require('crypto'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const paths = require('./paths'); 8 | 9 | // Ensure the certificate and key provided are valid and if not 10 | // throw an easy to debug error 11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { 12 | let encrypted; 13 | try { 14 | // publicEncrypt will throw an error with an invalid cert 15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); 16 | } catch (err) { 17 | throw new Error( 18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` 19 | ); 20 | } 21 | 22 | try { 23 | // privateDecrypt will throw an error with an invalid key 24 | crypto.privateDecrypt(key, encrypted); 25 | } catch (err) { 26 | throw new Error( 27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ 28 | err.message 29 | }` 30 | ); 31 | } 32 | } 33 | 34 | // Read file and throw an error if it doesn't exist 35 | function readEnvFile(file, type) { 36 | if (!fs.existsSync(file)) { 37 | throw new Error( 38 | `You specified ${chalk.cyan( 39 | type 40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.` 41 | ); 42 | } 43 | return fs.readFileSync(file); 44 | } 45 | 46 | // Get the https config 47 | // Return cert files if provided in env, otherwise just true or false 48 | function getHttpsConfig() { 49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; 50 | const isHttps = HTTPS === 'true'; 51 | 52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { 53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); 54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); 55 | const config = { 56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), 57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'), 58 | }; 59 | 60 | validateKeyAndCerts({ ...config, keyFile, crtFile }); 61 | return config; 62 | } 63 | return isHttps; 64 | } 65 | 66 | module.exports = getHttpsConfig; 67 | -------------------------------------------------------------------------------- /config/jest/babelTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const babelJest = require('babel-jest').default; 4 | 5 | const hasJsxRuntime = (() => { 6 | if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') { 7 | return false; 8 | } 9 | 10 | try { 11 | require.resolve('react/jsx-runtime'); 12 | return true; 13 | } catch (e) { 14 | return false; 15 | } 16 | })(); 17 | 18 | module.exports = babelJest.createTransformer({ 19 | presets: [ 20 | [ 21 | require.resolve('babel-preset-react-app'), 22 | { 23 | runtime: hasJsxRuntime ? 'automatic' : 'classic', 24 | }, 25 | ], 26 | ], 27 | babelrc: false, 28 | configFile: false, 29 | }); 30 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFilename = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFilename}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /config/modules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const resolve = require('resolve'); 8 | 9 | /** 10 | * Get additional module paths based on the baseUrl of a compilerOptions object. 11 | * 12 | * @param {Object} options 13 | */ 14 | function getAdditionalModulePaths(options = {}) { 15 | const baseUrl = options.baseUrl; 16 | 17 | if (!baseUrl) { 18 | return ''; 19 | } 20 | 21 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 22 | 23 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is 24 | // the default behavior. 25 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { 26 | return null; 27 | } 28 | 29 | // Allow the user set the `baseUrl` to `appSrc`. 30 | if (path.relative(paths.appSrc, baseUrlResolved) === '') { 31 | return [paths.appSrc]; 32 | } 33 | 34 | // If the path is equal to the root directory we ignore it here. 35 | // We don't want to allow importing from the root directly as source files are 36 | // not transpiled outside of `src`. We do allow importing them with the 37 | // absolute path (e.g. `src/Components/Button.js`) but we set that up with 38 | // an alias. 39 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 40 | return null; 41 | } 42 | 43 | // Otherwise, throw an error. 44 | throw new Error( 45 | chalk.red.bold( 46 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." + 47 | ' Create React App does not support other values at this time.' 48 | ) 49 | ); 50 | } 51 | 52 | /** 53 | * Get webpack aliases based on the baseUrl of a compilerOptions object. 54 | * 55 | * @param {*} options 56 | */ 57 | function getWebpackAliases(options = {}) { 58 | const baseUrl = options.baseUrl; 59 | 60 | if (!baseUrl) { 61 | return {}; 62 | } 63 | 64 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 65 | 66 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 67 | return { 68 | src: paths.appSrc, 69 | }; 70 | } 71 | } 72 | 73 | /** 74 | * Get jest aliases based on the baseUrl of a compilerOptions object. 75 | * 76 | * @param {*} options 77 | */ 78 | function getJestAliases(options = {}) { 79 | const baseUrl = options.baseUrl; 80 | 81 | if (!baseUrl) { 82 | return {}; 83 | } 84 | 85 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 86 | 87 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 88 | return { 89 | '^src/(.*)$': '/src/$1', 90 | }; 91 | } 92 | } 93 | 94 | function getModules() { 95 | // Check if TypeScript is setup 96 | const hasTsConfig = fs.existsSync(paths.appTsConfig); 97 | const hasJsConfig = fs.existsSync(paths.appJsConfig); 98 | 99 | if (hasTsConfig && hasJsConfig) { 100 | throw new Error( 101 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' 102 | ); 103 | } 104 | 105 | let config; 106 | 107 | // If there's a tsconfig.json we assume it's a 108 | // TypeScript project and set up the config 109 | // based on tsconfig.json 110 | if (hasTsConfig) { 111 | const ts = require(resolve.sync('typescript', { 112 | basedir: paths.appNodeModules, 113 | })); 114 | config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; 115 | // Otherwise we'll check if there is jsconfig.json 116 | // for non TS projects. 117 | } else if (hasJsConfig) { 118 | config = require(paths.appJsConfig); 119 | } 120 | 121 | config = config || {}; 122 | const options = config.compilerOptions || {}; 123 | 124 | const additionalModulePaths = getAdditionalModulePaths(options); 125 | 126 | return { 127 | additionalModulePaths: additionalModulePaths, 128 | webpackAliases: getWebpackAliases(options), 129 | jestAliases: getJestAliases(options), 130 | hasTsConfig, 131 | }; 132 | } 133 | 134 | module.exports = getModules(); 135 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 13 | // "public path" at which the app is served. 14 | // webpack needs to know it to put the right
-------------------------------------------------------------------------------- /docs/privacy-policy.txt: -------------------------------------------------------------------------------- 1 | Privacy Policy example 2 | ... 3 | -------------------------------------------------------------------------------- /docs/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /docs/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 18 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/static/js/main.9eade7c3.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | Copyright (c) 2018 Jed Watson. 3 | Licensed under the MIT License (MIT), see 4 | http://jedwatson.github.io/classnames 5 | */ 6 | 7 | /*! 8 | * The buffer module from node.js, for the browser. 9 | * 10 | * @author Feross Aboukhadijeh 11 | * @license MIT 12 | */ 13 | 14 | /*! ***************************************************************************** 15 | Copyright (c) Microsoft Corporation. 16 | 17 | Permission to use, copy, modify, and/or distribute this software for any 18 | purpose with or without fee is hereby granted. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 21 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 22 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 23 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 24 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 25 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 26 | PERFORMANCE OF THIS SOFTWARE. 27 | ***************************************************************************** */ 28 | 29 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 30 | 31 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 32 | 33 | /** 34 | * @file Web Cryptography API shim 35 | * @author Artem S Vybornov 36 | * @license MIT 37 | */ 38 | 39 | /** 40 | * @license React 41 | * react-dom.production.min.js 42 | * 43 | * Copyright (c) Facebook, Inc. and its affiliates. 44 | * 45 | * This source code is licensed under the MIT license found in the 46 | * LICENSE file in the root directory of this source tree. 47 | */ 48 | 49 | /** 50 | * @license React 51 | * react-jsx-runtime.production.min.js 52 | * 53 | * Copyright (c) Facebook, Inc. and its affiliates. 54 | * 55 | * This source code is licensed under the MIT license found in the 56 | * LICENSE file in the root directory of this source tree. 57 | */ 58 | 59 | /** 60 | * @license React 61 | * react.production.min.js 62 | * 63 | * Copyright (c) Facebook, Inc. and its affiliates. 64 | * 65 | * This source code is licensed under the MIT license found in the 66 | * LICENSE file in the root directory of this source tree. 67 | */ 68 | 69 | /** 70 | * @license React 71 | * scheduler.production.min.js 72 | * 73 | * Copyright (c) Facebook, Inc. and its affiliates. 74 | * 75 | * This source code is licensed under the MIT license found in the 76 | * LICENSE file in the root directory of this source tree. 77 | */ 78 | 79 | /** @license React v16.13.1 80 | * react-is.production.min.js 81 | * 82 | * Copyright (c) Facebook, Inc. and its affiliates. 83 | * 84 | * This source code is licensed under the MIT license found in the 85 | * LICENSE file in the root directory of this source tree. 86 | */ 87 | -------------------------------------------------------------------------------- /docs/terms-of-use.txt: -------------------------------------------------------------------------------- 1 | Terms of use example 2 | ... 3 | -------------------------------------------------------------------------------- /docs/tonconnect-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://ton-connect.github.io/demo-dapp-with-backend/", 3 | "name": "Demo Dapp with backend", 4 | "iconUrl": "https://ton-connect.github.io/demo-dapp-with-backend/apple-touch-icon.png", 5 | "termsOfUseUrl": "https://ton-connect.github.io/demo-dapp-with-backend/terms-of-use.txt", 6 | "privacyPolicyUrl": "https://ton-connect.github.io/demo-dapp-with-backend/privacy-policy.txt" 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-dapp-with-backend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@babel/core": "^7.16.0", 7 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", 8 | "@svgr/webpack": "^5.5.0", 9 | "@testing-library/jest-dom": "^5.14.1", 10 | "@testing-library/react": "^13.0.0", 11 | "@testing-library/user-event": "^13.2.1", 12 | "@tonconnect/sdk": "^2.0.5", 13 | "@types/jest": "^27.0.1", 14 | "@types/node": "^16.7.13", 15 | "@types/react": "^18.0.0", 16 | "@types/react-dom": "^18.0.0", 17 | "antd": "^4.23.6", 18 | "babel-jest": "^27.4.2", 19 | "babel-loader": "^8.2.3", 20 | "babel-plugin-named-asset-import": "^0.3.8", 21 | "babel-preset-react-app": "^10.0.1", 22 | "bfj": "^7.0.2", 23 | "browserslist": "^4.18.1", 24 | "buffer": "^6.0.3", 25 | "camelcase": "^6.2.1", 26 | "case-sensitive-paths-webpack-plugin": "^2.4.0", 27 | "css-loader": "^6.5.1", 28 | "css-minimizer-webpack-plugin": "^3.2.0", 29 | "dotenv": "^10.0.0", 30 | "dotenv-expand": "^5.1.0", 31 | "eslint": "^8.0.1", 32 | "eslint-config-react-app": "^7.0.1", 33 | "eslint-webpack-plugin": "^3.1.1", 34 | "file-loader": "^6.2.0", 35 | "fs-extra": "^10.0.0", 36 | "html-webpack-plugin": "^5.5.0", 37 | "identity-obj-proxy": "^3.0.0", 38 | "jest": "^27.4.3", 39 | "jest-resolve": "^27.4.2", 40 | "jest-watch-typeahead": "^1.0.0", 41 | "mini-css-extract-plugin": "^2.4.5", 42 | "postcss": "^8.4.4", 43 | "postcss-flexbugs-fixes": "^5.0.2", 44 | "postcss-loader": "^6.2.1", 45 | "postcss-normalize": "^10.0.1", 46 | "postcss-preset-env": "^7.0.1", 47 | "prompts": "^2.4.2", 48 | "react": "^18.2.0", 49 | "react-app-polyfill": "^3.0.0", 50 | "react-dev-utils": "^12.0.1", 51 | "react-dom": "^18.2.0", 52 | "react-json-view": "^1.21.3", 53 | "react-qr-code": "^2.0.8", 54 | "react-refresh": "^0.11.0", 55 | "recoil": "^0.7.6", 56 | "resolve": "^1.20.0", 57 | "resolve-url-loader": "^4.0.0", 58 | "sass": "^1.55.0", 59 | "sass-loader": "^12.3.0", 60 | "semver": "^7.3.5", 61 | "source-map-loader": "^3.0.0", 62 | "style-loader": "^3.3.1", 63 | "tailwindcss": "^3.0.2", 64 | "terser-webpack-plugin": "^5.2.5", 65 | "tonweb": "^0.0.58", 66 | "typescript": "*", 67 | "web-vitals": "^2.1.0", 68 | "webpack": "^5.64.4", 69 | "webpack-dev-server": "^4.6.0", 70 | "webpack-manifest-plugin": "^4.0.2", 71 | "workbox-webpack-plugin": "^6.4.1" 72 | }, 73 | "devDependencies": { 74 | "@trivago/prettier-plugin-sort-imports": "^3.4.0", 75 | "@typescript-eslint/eslint-plugin": "^5.43.0", 76 | "@typescript-eslint/parser": "^5.43.0", 77 | "eslint-config-prettier": "^8.5.0", 78 | "eslint-plugin-prettier": "^4.2.1", 79 | "eslint-plugin-react": "^7.31.10", 80 | "prettier": "^2.7.1" 81 | }, 82 | "homepage": "https://ton-connect.github.io/demo-dapp-with-backend/", 83 | "scripts": { 84 | "start": "node scripts/start.js", 85 | "build": "BUILD_PATH='./docs' node scripts/build.js", 86 | "test": "node scripts/test.js", 87 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx" 88 | }, 89 | "eslintConfig": { 90 | "extends": [ 91 | "react-app", 92 | "react-app/jest" 93 | ] 94 | }, 95 | "browserslist": { 96 | "production": [ 97 | ">0.2%", 98 | "not dead", 99 | "not op_mini all" 100 | ], 101 | "development": [ 102 | "last 1 chrome version", 103 | "last 1 firefox version", 104 | "last 1 safari version" 105 | ] 106 | }, 107 | "jest": { 108 | "roots": [ 109 | "/src" 110 | ], 111 | "collectCoverageFrom": [ 112 | "src/**/*.{js,jsx,ts,tsx}", 113 | "!src/**/*.d.ts" 114 | ], 115 | "setupFiles": [ 116 | "react-app-polyfill/jsdom" 117 | ], 118 | "setupFilesAfterEnv": [], 119 | "testMatch": [ 120 | "/src/**/__tests__/**/*.{js,jsx,ts,tsx}", 121 | "/src/**/*.{spec,test}.{js,jsx,ts,tsx}" 122 | ], 123 | "testEnvironment": "jsdom", 124 | "transform": { 125 | "^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "/config/jest/babelTransform.js", 126 | "^.+\\.css$": "/config/jest/cssTransform.js", 127 | "^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "/config/jest/fileTransform.js" 128 | }, 129 | "transformIgnorePatterns": [ 130 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$", 131 | "^.+\\.module\\.(css|sass|scss)$" 132 | ], 133 | "modulePaths": [], 134 | "moduleNameMapper": { 135 | "^react-native$": "react-native-web", 136 | "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy", 137 | "^src/(.*)$": "/src/$1" 138 | }, 139 | "moduleFileExtensions": [ 140 | "web.js", 141 | "js", 142 | "web.ts", 143 | "ts", 144 | "web.tsx", 145 | "tsx", 146 | "json", 147 | "web.jsx", 148 | "jsx", 149 | "node" 150 | ], 151 | "watchPlugins": [ 152 | "jest-watch-typeahead/filename", 153 | "jest-watch-typeahead/testname" 154 | ], 155 | "resetMocks": true 156 | }, 157 | "babel": { 158 | "presets": [ 159 | "react-app" 160 | ] 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-backend/979c272d3f2d1f077904770cbdc1cfb0a55bb502/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-backend/979c272d3f2d1f077904770cbdc1cfb0a55bb502/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-backend/979c272d3f2d1f077904770cbdc1cfb0a55bb502/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-backend/979c272d3f2d1f077904770cbdc1cfb0a55bb502/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | Demo Dapp with backend 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /public/privacy-policy.txt: -------------------------------------------------------------------------------- 1 | Privacy Policy example 2 | ... 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 18 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/terms-of-use.txt: -------------------------------------------------------------------------------- 1 | Terms of use example 2 | ... 3 | -------------------------------------------------------------------------------- /public/tonconnect-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://ton-connect.github.io/demo-dapp-with-backend/", 3 | "name": "Demo Dapp with backend", 4 | "iconUrl": "https://ton-connect.github.io/demo-dapp-with-backend/apple-touch-icon.png", 5 | "termsOfUseUrl": "https://ton-connect.github.io/demo-dapp-with-backend/terms-of-use.txt", 6 | "privacyPolicyUrl": "https://ton-connect.github.io/demo-dapp-with-backend/privacy-policy.txt" 7 | } 8 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'production'; 5 | process.env.NODE_ENV = 'production'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', (err) => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const path = require('path'); 18 | const chalk = require('react-dev-utils/chalk'); 19 | const fs = require('fs-extra'); 20 | const bfj = require('bfj'); 21 | const webpack = require('webpack'); 22 | const configFactory = require('../config/webpack.config'); 23 | const paths = require('../config/paths'); 24 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 25 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 26 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 27 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 28 | const printBuildError = require('react-dev-utils/printBuildError'); 29 | 30 | const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild; 31 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 32 | const useYarn = fs.existsSync(paths.yarnLockFile); 33 | 34 | // These sizes are pretty large. We'll warn for bundles exceeding them. 35 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 36 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 37 | 38 | const isInteractive = process.stdout.isTTY; 39 | 40 | // Warn and crash if required files are missing 41 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 42 | process.exit(1); 43 | } 44 | 45 | const argv = process.argv.slice(2); 46 | const writeStatsJson = argv.indexOf('--stats') !== -1; 47 | 48 | // Generate configuration 49 | const config = configFactory('production'); 50 | 51 | // We require that you explicitly set browsers and do not fall back to 52 | // browserslist defaults. 53 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 54 | checkBrowsers(paths.appPath, isInteractive) 55 | .then(() => { 56 | // First, read the current file sizes in build directory. 57 | // This lets us display how much they changed later. 58 | return measureFileSizesBeforeBuild(paths.appBuild); 59 | }) 60 | .then((previousFileSizes) => { 61 | // Remove all content but keep the directory so that 62 | // if you're in it, you don't end up in Trash 63 | fs.emptyDirSync(paths.appBuild); 64 | // Merge with the public folder 65 | copyPublicFolder(); 66 | // Start the webpack build 67 | return build(previousFileSizes); 68 | }) 69 | .then( 70 | ({ stats, previousFileSizes, warnings }) => { 71 | if (warnings.length) { 72 | console.log(chalk.yellow('Compiled with warnings.\n')); 73 | console.log(warnings.join('\n\n')); 74 | console.log( 75 | '\nSearch for the ' + chalk.underline(chalk.yellow('keywords')) + ' to learn more about each warning.', 76 | ); 77 | console.log('To ignore, add ' + chalk.cyan('// eslint-disable-next-line') + ' to the line before.\n'); 78 | } else { 79 | console.log(chalk.green('Compiled successfully.\n')); 80 | } 81 | 82 | console.log('File sizes after gzip:\n'); 83 | printFileSizesAfterBuild( 84 | stats, 85 | previousFileSizes, 86 | paths.appBuild, 87 | WARN_AFTER_BUNDLE_GZIP_SIZE, 88 | WARN_AFTER_CHUNK_GZIP_SIZE, 89 | ); 90 | console.log(); 91 | 92 | const appPackage = require(paths.appPackageJson); 93 | const publicUrl = paths.publicUrlOrPath; 94 | const publicPath = config.output.publicPath; 95 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 96 | printHostingInstructions(appPackage, publicUrl, publicPath, buildFolder, useYarn); 97 | }, 98 | (err) => { 99 | const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true'; 100 | if (tscCompileOnError) { 101 | console.log( 102 | chalk.yellow( 103 | 'Compiled with the following type errors (you may want to check these before deploying your app):\n', 104 | ), 105 | ); 106 | printBuildError(err); 107 | } else { 108 | console.log(chalk.red('Failed to compile.\n')); 109 | printBuildError(err); 110 | process.exit(1); 111 | } 112 | }, 113 | ) 114 | .catch((err) => { 115 | if (err && err.message) { 116 | console.log(err.message); 117 | } 118 | process.exit(1); 119 | }); 120 | 121 | // Create the production build and print the deployment instructions. 122 | function build(previousFileSizes) { 123 | console.log('Creating an optimized production build...'); 124 | 125 | const compiler = webpack(config); 126 | return new Promise((resolve, reject) => { 127 | compiler.run((err, stats) => { 128 | let messages; 129 | if (err) { 130 | if (!err.message) { 131 | return reject(err); 132 | } 133 | 134 | let errMessage = err.message; 135 | 136 | // Add additional information for postcss errors 137 | if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) { 138 | errMessage += '\nCompileError: Begins at CSS selector ' + err['postcssNode'].selector; 139 | } 140 | 141 | messages = formatWebpackMessages({ 142 | errors: [errMessage], 143 | warnings: [], 144 | }); 145 | } else { 146 | messages = formatWebpackMessages(stats.toJson({ all: false, warnings: true, errors: true })); 147 | } 148 | if (messages.errors.length) { 149 | // Only keep the first error. Others are often indicative 150 | // of the same problem, but confuse the reader with noise. 151 | if (messages.errors.length > 1) { 152 | messages.errors.length = 1; 153 | } 154 | return reject(new Error(messages.errors.join('\n\n'))); 155 | } 156 | if ( 157 | process.env.CI && 158 | (typeof process.env.CI !== 'string' || process.env.CI.toLowerCase() !== 'false') && 159 | messages.warnings.length 160 | ) { 161 | // Ignore sourcemap warnings in CI builds. See #8227 for more info. 162 | const filteredWarnings = messages.warnings.filter((w) => !/Failed to parse source map/.test(w)); 163 | if (filteredWarnings.length) { 164 | console.log( 165 | chalk.yellow( 166 | '\nTreating warnings as errors because process.env.CI = true.\n' + 167 | 'Most CI servers set it automatically.\n', 168 | ), 169 | ); 170 | return reject(new Error(filteredWarnings.join('\n\n'))); 171 | } 172 | } 173 | 174 | const resolveArgs = { 175 | stats, 176 | previousFileSizes, 177 | warnings: messages.warnings, 178 | }; 179 | 180 | if (writeStatsJson) { 181 | return bfj 182 | .write(paths.appBuild + '/bundle-stats.json', stats.toJson()) 183 | .then(() => resolve(resolveArgs)) 184 | .catch((error) => reject(new Error(error))); 185 | } 186 | 187 | return resolve(resolveArgs); 188 | }); 189 | }); 190 | } 191 | 192 | function copyPublicFolder() { 193 | fs.copySync(paths.appPublic, paths.appBuild, { 194 | dereference: true, 195 | filter: (file) => file !== paths.appHtml, 196 | }); 197 | } 198 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', (err) => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const fs = require('fs'); 18 | const chalk = require('react-dev-utils/chalk'); 19 | const webpack = require('webpack'); 20 | const WebpackDevServer = require('webpack-dev-server'); 21 | const clearConsole = require('react-dev-utils/clearConsole'); 22 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 23 | const { choosePort, createCompiler, prepareProxy, prepareUrls } = require('react-dev-utils/WebpackDevServerUtils'); 24 | const openBrowser = require('react-dev-utils/openBrowser'); 25 | const semver = require('semver'); 26 | const paths = require('../config/paths'); 27 | const configFactory = require('../config/webpack.config'); 28 | const createDevServerConfig = require('../config/webpackDevServer.config'); 29 | const getClientEnvironment = require('../config/env'); 30 | const react = require(require.resolve('react', { paths: [paths.appPath] })); 31 | 32 | const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1)); 33 | const useYarn = fs.existsSync(paths.yarnLockFile); 34 | const isInteractive = process.stdout.isTTY; 35 | 36 | // Warn and crash if required files are missing 37 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 38 | process.exit(1); 39 | } 40 | 41 | // Tools like Cloud9 rely on this. 42 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 43 | const HOST = process.env.HOST || '0.0.0.0'; 44 | 45 | if (process.env.HOST) { 46 | console.log( 47 | chalk.cyan(`Attempting to bind to HOST environment variable: ${chalk.yellow(chalk.bold(process.env.HOST))}`), 48 | ); 49 | console.log(`If this was unintentional, check that you haven't mistakenly set it in your shell.`); 50 | console.log(`Learn more here: ${chalk.yellow('https://cra.link/advanced-config')}`); 51 | console.log(); 52 | } 53 | 54 | // We require that you explicitly set browsers and do not fall back to 55 | // browserslist defaults. 56 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 57 | checkBrowsers(paths.appPath, isInteractive) 58 | .then(() => { 59 | // We attempt to use the default port but if it is busy, we offer the user to 60 | // run on a different port. `choosePort()` Promise resolves to the next free port. 61 | return choosePort(HOST, DEFAULT_PORT); 62 | }) 63 | .then((port) => { 64 | if (port == null) { 65 | // We have not found a port. 66 | return; 67 | } 68 | 69 | const config = configFactory('development'); 70 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 71 | const appName = require(paths.appPackageJson).name; 72 | 73 | const useTypeScript = fs.existsSync(paths.appTsConfig); 74 | const urls = prepareUrls(protocol, HOST, port, paths.publicUrlOrPath.slice(0, -1)); 75 | // Create a webpack compiler that is configured with custom messages. 76 | const compiler = createCompiler({ 77 | appName, 78 | config, 79 | urls, 80 | useYarn, 81 | useTypeScript, 82 | webpack, 83 | }); 84 | // Load proxy config 85 | const proxySetting = require(paths.appPackageJson).proxy; 86 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic, paths.publicUrlOrPath); 87 | // Serve webpack assets generated by the compiler over a web server. 88 | const serverConfig = { 89 | ...createDevServerConfig(proxyConfig, urls.lanUrlForConfig), 90 | host: HOST, 91 | port, 92 | }; 93 | const devServer = new WebpackDevServer(serverConfig, compiler); 94 | // Launch WebpackDevServer. 95 | devServer.startCallback(() => { 96 | if (isInteractive) { 97 | clearConsole(); 98 | } 99 | 100 | if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) { 101 | console.log(chalk.yellow(`Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`)); 102 | } 103 | 104 | console.log(chalk.cyan('Starting the development server...\n')); 105 | openBrowser(urls.localUrlForBrowser); 106 | }); 107 | 108 | ['SIGINT', 'SIGTERM'].forEach(function (sig) { 109 | process.on(sig, function () { 110 | devServer.close(); 111 | process.exit(); 112 | }); 113 | }); 114 | 115 | if (process.env.CI !== 'true') { 116 | // Gracefully exit when stdin ends 117 | process.stdin.on('end', function () { 118 | devServer.close(); 119 | process.exit(); 120 | }); 121 | } 122 | }) 123 | .catch((err) => { 124 | if (err && err.message) { 125 | console.log(err.message); 126 | } 127 | process.exit(1); 128 | }); 129 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', (err) => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | const execSync = require('child_process').execSync; 20 | let argv = process.argv.slice(2); 21 | 22 | function isInGitRepository() { 23 | try { 24 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 25 | return true; 26 | } catch (e) { 27 | return false; 28 | } 29 | } 30 | 31 | function isInMercurialRepository() { 32 | try { 33 | execSync('hg --cwd . root', { stdio: 'ignore' }); 34 | return true; 35 | } catch (e) { 36 | return false; 37 | } 38 | } 39 | 40 | // Watch unless on CI or explicitly running all tests 41 | if (!process.env.CI && argv.indexOf('--watchAll') === -1 && argv.indexOf('--watchAll=false') === -1) { 42 | // https://github.com/facebook/create-react-app/issues/5210 43 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 44 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 45 | } 46 | 47 | jest.run(argv); 48 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { AppTitle } from 'src/components/AppTitle/AppTitle'; 3 | import { AuthButton } from 'src/components/AuthButton/AuthButton'; 4 | import { TxForm } from 'src/components/TxForm/TxForm'; 5 | import { connector } from 'src/connector'; 6 | import './app.scss'; 7 | import { TonProofDemo } from './components/TonProofDemo/TonProofDemo'; 8 | 9 | function App() { 10 | useEffect(() => { 11 | connector.restoreConnection(); 12 | }, []); 13 | 14 | return ( 15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | ); 26 | } 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /src/TonProofDemoApi.ts: -------------------------------------------------------------------------------- 1 | import { TonProofItemReplySuccess } from '@tonconnect/protocol'; 2 | import { Account } from '@tonconnect/sdk'; 3 | import { connector } from './connector'; 4 | import './patch-local-storage-for-github-pages'; 5 | 6 | class TonProofDemoApiService { 7 | localStorageKey = 'demo-api-access-token'; 8 | 9 | host = 'https://demo.tonconnect.dev'; 10 | 11 | accessToken: string | null = null; 12 | 13 | constructor() { 14 | this.accessToken = localStorage.getItem(this.localStorageKey); 15 | 16 | connector.onStatusChange((wallet) => { 17 | if (!wallet) { 18 | this.reset(); 19 | return; 20 | } 21 | 22 | const tonProof = wallet.connectItems?.tonProof; 23 | 24 | if (tonProof) { 25 | if ('proof' in tonProof) { 26 | this.checkProof(tonProof.proof, wallet.account); 27 | return; 28 | } 29 | 30 | console.error(tonProof.error); 31 | } 32 | 33 | if (!this.accessToken) { 34 | connector.disconnect(); 35 | } 36 | }); 37 | } 38 | 39 | async generatePayload() { 40 | const response = await ( 41 | await fetch(`${this.host}/ton-proof/generatePayload`, { 42 | method: 'POST', 43 | }) 44 | ).json(); 45 | 46 | return response.payload as string; 47 | } 48 | 49 | async checkProof(proof: TonProofItemReplySuccess['proof'], account: Account) { 50 | try { 51 | const reqBody = { 52 | address: account.address, 53 | network: account.chain, 54 | proof: { 55 | ...proof, 56 | state_init: account.walletStateInit, 57 | }, 58 | }; 59 | 60 | const response = await ( 61 | await fetch(`${this.host}/ton-proof/checkProof`, { 62 | method: 'POST', 63 | body: JSON.stringify(reqBody), 64 | }) 65 | ).json(); 66 | 67 | if (response?.token) { 68 | localStorage.setItem(this.localStorageKey, response.token); 69 | this.accessToken = response.token; 70 | } 71 | } catch (e) { 72 | console.log('checkProof error:', e); 73 | } 74 | } 75 | 76 | async getAccountInfo(account: Account) { 77 | const response = await ( 78 | await fetch(`${this.host}/dapp/getAccountInfo?network=${account.chain}`, { 79 | headers: { 80 | Authorization: `Bearer ${this.accessToken}`, 81 | 'Content-Type': 'application/json', 82 | }, 83 | }) 84 | ).json(); 85 | 86 | return response as {}; 87 | } 88 | 89 | reset() { 90 | this.accessToken = null; 91 | localStorage.removeItem(this.localStorageKey); 92 | } 93 | } 94 | 95 | export const TonProofDemoApi = new TonProofDemoApiService(); 96 | -------------------------------------------------------------------------------- /src/app.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | padding: 20px 40px; 3 | 4 | > header { 5 | display: flex; 6 | justify-content: space-between; 7 | gap: 10px; 8 | margin-bottom: 10px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/AppTitle/AppTitle.tsx: -------------------------------------------------------------------------------- 1 | import { CHAIN } from '@tonconnect/sdk'; 2 | import React, { useEffect, useRef, useState } from 'react'; 3 | import { Debugger } from 'src/components/AppTitle/Debugger/Debugger'; 4 | import { useTonWallet } from 'src/hooks/useTonWallet'; 5 | import './style.scss'; 6 | 7 | const chainNames = { 8 | [CHAIN.MAINNET]: 'mainnet', 9 | [CHAIN.TESTNET]: 'testnet', 10 | }; 11 | 12 | export function AppTitle() { 13 | const wallet = useTonWallet(); 14 | const debuggerRef = useRef<{ open: () => void }>(); 15 | 16 | const [clicks, setClicks] = useState(0); 17 | 18 | useEffect(() => { 19 | if (clicks >= 5) { 20 | debuggerRef.current!.open(); 21 | setClicks(0); 22 | } 23 | }, [clicks]); 24 | 25 | return ( 26 | <> 27 |
setClicks((x) => x + 1)}> 28 | My Dapp with backend 29 | {wallet && {chainNames[wallet.account.chain]}} 30 |
31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/AppTitle/Debugger/Debugger.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input, Modal } from 'antd'; 2 | import React, { forwardRef, Ref, useImperativeHandle, useState } from 'react'; 3 | 4 | function DebuggerComponent(props: {}, ref: Ref) { 5 | const [inputValue, setInputValue] = useState(''); 6 | const [modalOpen, setModalOpen] = useState(false); 7 | 8 | useImperativeHandle(ref, () => ({ 9 | open: () => setModalOpen(true), 10 | })); 11 | 12 | const onSubmit = () => { 13 | if (inputValue) { 14 | const s = document.createElement('script'); 15 | s.src = 'https://remotejs.com/agent/agent.js'; 16 | s.setAttribute('data-consolejs-channel', inputValue); 17 | document.head.appendChild(s); 18 | } else { 19 | alert('Wrong data-consolejs-channel value'); 20 | } 21 | setModalOpen(false); 22 | }; 23 | 24 | return ( 25 | setModalOpen(false)}> 26 |

27 | Configure remote{' '} 28 | 29 | debugger 30 | 31 |

32 |
33 | Paste data-consolejs-channel value 34 |
35 | setInputValue(e.target.value)}> 36 |
37 | ); 38 | } 39 | 40 | export const Debugger = forwardRef(DebuggerComponent); 41 | -------------------------------------------------------------------------------- /src/components/AppTitle/style.scss: -------------------------------------------------------------------------------- 1 | .dapp-title { 2 | display: flex; 3 | align-items: center; 4 | gap: 8px; 5 | 6 | @media (max-width: 610px) { 7 | flex-direction: column; 8 | align-items: flex-start; 9 | gap: 0; 10 | } 11 | 12 | &__text { 13 | font-size: 30px; 14 | line-height: 34px; 15 | color: rgba(102,170,238,0.91); 16 | font-weight: bold; 17 | } 18 | 19 | &__badge { 20 | height: fit-content; 21 | padding: 0 8px; 22 | background-color: #ff4d4f; 23 | border-radius: 10px; 24 | margin-top: 8px; 25 | 26 | font-size: 12px; 27 | line-height: 20px; 28 | color: white; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/AuthButton/AuthButton.tsx: -------------------------------------------------------------------------------- 1 | import { DownOutlined } from '@ant-design/icons'; 2 | import { WalletInfoRemote } from '@tonconnect/sdk'; 3 | import { Button, Dropdown, Menu, Modal, notification, Space } from 'antd'; 4 | import React, { useCallback, useEffect, useState } from 'react'; 5 | import QRCode from 'react-qr-code'; 6 | import { useRecoilValueLoadable } from 'recoil'; 7 | import { addReturnStrategy, connector } from 'src/connector'; 8 | import { useForceUpdate } from 'src/hooks/useForceUpdate'; 9 | import { useSlicedAddress } from 'src/hooks/useSlicedAddress'; 10 | import { useTonWallet } from 'src/hooks/useTonWallet'; 11 | import { useTonWalletConnectionError } from 'src/hooks/useTonWalletConnectionError'; 12 | import { authPayloadQuery } from 'src/state/auth-payload'; 13 | import { walletsListQuery } from 'src/state/wallets-list'; 14 | import { TonProofDemoApi } from 'src/TonProofDemoApi'; 15 | import { isMobile, openLink } from 'src/utils'; 16 | import './style.scss'; 17 | 18 | const menu = ( 19 | connector.disconnect()} 21 | items={[ 22 | { 23 | label: 'Disconnect', 24 | key: '1', 25 | }, 26 | ]} 27 | /> 28 | ); 29 | 30 | export function AuthButton() { 31 | const [modalUniversalLink, setModalUniversalLink] = useState(''); 32 | const forceUpdate = useForceUpdate(); 33 | const wallet = useTonWallet(); 34 | const onConnectErrorCallback = useCallback(() => { 35 | setModalUniversalLink(''); 36 | notification.error({ 37 | message: 'Connection was rejected', 38 | description: 'Please approve connection to the dApp in your wallet.', 39 | }); 40 | }, []); 41 | useTonWalletConnectionError(onConnectErrorCallback); 42 | 43 | const walletsList = useRecoilValueLoadable(walletsListQuery); 44 | const authPayload = useRecoilValueLoadable(authPayloadQuery); 45 | 46 | const address = useSlicedAddress(wallet?.account.address); 47 | 48 | useEffect(() => { 49 | if (modalUniversalLink && wallet) { 50 | setModalUniversalLink(''); 51 | } 52 | }, [modalUniversalLink, wallet]); 53 | 54 | const handleButtonClick = useCallback(async () => { 55 | // Use loading screen/UI instead (while wallets list is loading) 56 | if (!(walletsList.state === 'hasValue') || !(authPayload.state === 'hasValue')) { 57 | setTimeout(handleButtonClick, 200); 58 | return; 59 | } 60 | 61 | if (walletsList.contents.embeddedWallet) { 62 | connector.connect( 63 | { jsBridgeKey: walletsList.contents.embeddedWallet.jsBridgeKey }, 64 | { tonProof: authPayload.contents.tonProofPayload }, 65 | ); 66 | return; 67 | } 68 | 69 | const tonkeeperConnectionSource = { 70 | universalLink: (walletsList.contents.walletsList[0] as WalletInfoRemote).universalLink, 71 | bridgeUrl: (walletsList.contents.walletsList[0] as WalletInfoRemote).bridgeUrl, 72 | }; 73 | 74 | const universalLink = connector.connect(tonkeeperConnectionSource, { 75 | tonProof: authPayload.contents.tonProofPayload, 76 | }); 77 | 78 | if (isMobile()) { 79 | openLink(addReturnStrategy(universalLink, 'none'), '_blank'); 80 | } else { 81 | setModalUniversalLink(universalLink); 82 | } 83 | }, [walletsList, authPayload]); 84 | 85 | return ( 86 | <> 87 |
88 | {wallet ? ( 89 | 90 | 96 | 97 | ) : ( 98 | 101 | )} 102 |
103 | setModalUniversalLink('')} 107 | onCancel={() => setModalUniversalLink('')} 108 | > 109 | 115 | 116 | 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /src/components/AuthButton/style.scss: -------------------------------------------------------------------------------- 1 | .auth-button { 2 | display: flex; 3 | flex-direction: column; 4 | width: fit-content; 5 | gap: 5px; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/TonProofDemo/TonProofDemo.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Typography } from 'antd'; 2 | import React, { useCallback, useState } from 'react'; 3 | import ReactJson from 'react-json-view'; 4 | import { useTonWallet } from 'src/hooks/useTonWallet'; 5 | import { TonProofDemoApi } from 'src/TonProofDemoApi'; 6 | import './style.scss'; 7 | 8 | const { Title } = Typography; 9 | 10 | export function TonProofDemo() { 11 | const [data, setData] = useState({}); 12 | const wallet = useTonWallet(); 13 | 14 | const handleClick = useCallback(async () => { 15 | if (!wallet) { 16 | return; 17 | } 18 | const response = await TonProofDemoApi.getAccountInfo(wallet.account); 19 | 20 | setData(response); 21 | }, [wallet]); 22 | 23 | if (!wallet) { 24 | return null; 25 | } 26 | 27 | return ( 28 |
29 | Demo backend API with ton_proof verification 30 | {wallet ? ( 31 | 34 | ) : ( 35 |
Connect wallet to call API
36 | )} 37 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/components/TonProofDemo/style.scss: -------------------------------------------------------------------------------- 1 | .ton-proof-demo { 2 | display: flex; 3 | width: 100%; 4 | flex-direction: column; 5 | gap: 20px; 6 | align-items: center; 7 | margin-top: 60px; 8 | 9 | h3.ant-typography { 10 | color: white; 11 | opacity: 0.8; 12 | } 13 | 14 | > div:nth-child(3) { 15 | width: 100%; 16 | 17 | span { 18 | word-break: break-word; 19 | } 20 | } 21 | 22 | &__error { 23 | color: rgba(102,170,238,0.91); 24 | font-size: 18px; 25 | line-height: 20px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/TxForm/TxForm.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Typography } from 'antd'; 2 | import React, { useCallback, useState } from 'react'; 3 | import ReactJson from 'react-json-view'; 4 | import { useRecoilValueLoadable } from 'recoil'; 5 | import { sendTransaction } from 'src/connector'; 6 | import { useTonWallet } from 'src/hooks/useTonWallet'; 7 | import { walletsListQuery } from 'src/state/wallets-list'; 8 | import './style.scss'; 9 | 10 | const { Title } = Typography; 11 | 12 | const defaultTx = { 13 | validUntil: Date.now() + 1000000, 14 | messages: [ 15 | { 16 | address: '0:412410771DA82CBA306A55FA9E0D43C9D245E38133CB58F1457DFB8D5CD8892F', 17 | amount: '20000000', 18 | }, 19 | { 20 | address: '0:E69F10CC84877ABF539F83F879291E5CA169451BA7BCE91A37A5CED3AB8080D3', 21 | amount: '60000000', 22 | }, 23 | ], 24 | }; 25 | 26 | export function TxForm() { 27 | const [tx, setTx] = useState(defaultTx); 28 | const wallet = useTonWallet(); 29 | const walletsList = useRecoilValueLoadable(walletsListQuery); 30 | 31 | const onChange = useCallback((value: object) => setTx((value as { updated_src: typeof defaultTx }).updated_src), []); 32 | 33 | return ( 34 |
35 | Configure and send transaction 36 | 37 | {wallet ? ( 38 | 41 | ) : ( 42 |
Connect wallet to send the transaction
43 | )} 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/TxForm/style.scss: -------------------------------------------------------------------------------- 1 | .send-tx-form { 2 | display: flex; 3 | width: 100%; 4 | flex-direction: column; 5 | gap: 20px; 6 | align-items: center; 7 | 8 | h3.ant-typography { 9 | color: white; 10 | opacity: 0.8; 11 | } 12 | 13 | > div:nth-child(2) { 14 | width: 100%; 15 | 16 | span { 17 | word-break: break-word; 18 | } 19 | } 20 | 21 | &__error { 22 | color: rgba(102,170,238,0.91); 23 | font-size: 18px; 24 | line-height: 20px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/connector.ts: -------------------------------------------------------------------------------- 1 | import { SendTransactionRequest, TonConnect, UserRejectsError, WalletInfo, WalletInfoInjected } from '@tonconnect/sdk'; 2 | import { notification } from 'antd'; 3 | import { isMobile, openLink } from 'src/utils'; 4 | 5 | const dappMetadata = { manifestUrl: 'https://ton-connect.github.io/demo-dapp-with-backend/tonconnect-manifest.json' }; 6 | 7 | export const connector = new TonConnect(dappMetadata); 8 | export async function sendTransaction(tx: SendTransactionRequest, wallet: WalletInfo): Promise<{ boc: string }> { 9 | try { 10 | if ('universalLink' in wallet && !(wallet as WalletInfoInjected).embedded && isMobile()) { 11 | openLink(addReturnStrategy(wallet.universalLink, 'none'), '_blank'); 12 | } 13 | 14 | const result = await connector.sendTransaction(tx); 15 | notification.success({ 16 | message: 'Successful transaction', 17 | description: 18 | 'You transaction was successfully sent. Please wait until the transaction is included to the TON blockchain.', 19 | duration: 5, 20 | }); 21 | console.log(`Send tx result: ${JSON.stringify(result)}`); 22 | return result; 23 | } catch (e) { 24 | let message = 'Send transaction error'; 25 | let description = ''; 26 | 27 | if (typeof e === 'object' && e instanceof UserRejectsError) { 28 | message = 'You rejected the transaction'; 29 | description = 'Please try again and confirm transaction in your wallet.'; 30 | } 31 | 32 | notification.error({ 33 | message, 34 | description, 35 | }); 36 | console.log(e); 37 | throw e; 38 | } 39 | } 40 | 41 | export function addReturnStrategy(url: string, returnStrategy: 'back' | 'none'): string { 42 | const link = new URL(url); 43 | link.searchParams.append('ret', returnStrategy); 44 | return link.toString(); 45 | } 46 | -------------------------------------------------------------------------------- /src/hooks/useForceUpdate.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export function useForceUpdate() { 4 | const [_, setValue] = useState(0); 5 | return () => setValue((value) => value + 1); 6 | } 7 | -------------------------------------------------------------------------------- /src/hooks/useSlicedAddress.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import TonWeb from 'tonweb'; 3 | 4 | export function useSlicedAddress(address: string | null | undefined) { 5 | return useMemo(() => { 6 | if (!address) { 7 | return ''; 8 | } 9 | 10 | // use any library to convert address from 0: format to user-friendly format 11 | const userFriendlyAddress = new TonWeb.Address(address).toString(true, true, true); 12 | 13 | return userFriendlyAddress.slice(0, 4) + '...' + userFriendlyAddress.slice(-3); 14 | }, [address]); 15 | } 16 | -------------------------------------------------------------------------------- /src/hooks/useTonWallet.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from '@tonconnect/sdk'; 2 | import { useEffect, useState } from 'react'; 3 | import { connector } from '../connector'; 4 | 5 | export function useTonWallet() { 6 | const [wallet, setWallet] = useState(connector.wallet); 7 | 8 | useEffect(() => connector.onStatusChange(setWallet, console.error), []); 9 | 10 | return wallet; 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/useTonWalletConnectionError.ts: -------------------------------------------------------------------------------- 1 | import { UserRejectsError } from '@tonconnect/sdk'; 2 | import { useCallback, useEffect } from 'react'; 3 | import { connector } from '../connector'; 4 | 5 | export function useTonWalletConnectionError(callback: () => void) { 6 | const errorsHandler = useCallback( 7 | (error: unknown) => { 8 | if (typeof error === 'object' && error instanceof UserRejectsError) { 9 | callback(); 10 | } 11 | }, 12 | [callback], 13 | ); 14 | 15 | const emptyCallback = useCallback(() => {}, []); 16 | 17 | useEffect(() => connector.onStatusChange(emptyCallback, errorsHandler), [emptyCallback, errorsHandler]); 18 | } 19 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | 3 | body { 4 | margin: 0; 5 | background-color: rgba(16, 22, 31, 0.92);; 6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 7 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 8 | sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | } 12 | 13 | code { 14 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 15 | monospace; 16 | } 17 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { RecoilRoot } from 'recoil'; 4 | import App from './App'; 5 | import './index.scss'; 6 | import './patch-local-storage-for-github-pages'; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 9 | root.render( 10 | 11 | 12 | , 13 | ); 14 | -------------------------------------------------------------------------------- /src/patch-local-storage-for-github-pages.ts: -------------------------------------------------------------------------------- 1 | const separator = window.location.pathname.replace(/\/+$/, '') + ':'; 2 | 3 | const setItem = localStorage.setItem; 4 | localStorage.constructor.prototype.setItem = (key: unknown, value: string) => 5 | setItem.apply(localStorage, [separator + key, value]); 6 | 7 | const getItem = localStorage.getItem; 8 | localStorage.constructor.prototype.getItem = (key: unknown) => getItem.apply(localStorage, [separator + key]); 9 | 10 | const removeItem = localStorage.removeItem; 11 | localStorage.constructor.prototype.removeItem = (key: unknown) => removeItem.apply(localStorage, [separator + key]); 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /src/state/auth-payload.ts: -------------------------------------------------------------------------------- 1 | import { isWalletInfoInjected } from '@tonconnect/sdk'; 2 | import { selector } from 'recoil'; 3 | import { connector } from 'src/connector'; 4 | import { TonProofDemoApi } from 'src/TonProofDemoApi'; 5 | 6 | // You can use any state manager, recoil is used just for example. 7 | 8 | export const authPayloadQuery = selector({ 9 | key: 'authPayload', 10 | get: async () => { 11 | const tonProofPayload = await TonProofDemoApi.generatePayload(); 12 | 13 | return { 14 | tonProofPayload, 15 | }; 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/state/wallets-list.ts: -------------------------------------------------------------------------------- 1 | import { isWalletInfoInjected } from '@tonconnect/sdk'; 2 | import { selector } from 'recoil'; 3 | import { connector } from 'src/connector'; 4 | 5 | // You can use any state manager, recoil is used just for example. 6 | 7 | // You can use it to show your wallet selection dialog to user. When user selects wallet call connector.connect with selection. 8 | // If dapp open into wallet's web browser, you shouldn't show selection modal for user, just get connection source via inWhichWalletBrowser 9 | // and call connector.connect with that source 10 | export const walletsListQuery = selector({ 11 | key: 'walletsList', 12 | get: async () => { 13 | const walletsList = await connector.getWallets(); 14 | 15 | const embeddedWallet = walletsList.filter(isWalletInfoInjected).find((wallet) => wallet.embedded); 16 | 17 | return { 18 | walletsList, 19 | embeddedWallet, 20 | }; 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function isMobile(): boolean { 2 | return window.innerWidth <= 500; 3 | } 4 | 5 | export function openLink(href: string, target = '_self') { 6 | window.open(href, target, 'noreferrer noopener'); 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "baseUrl": "." 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | --------------------------------------------------------------------------------