├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── documentation.yml │ └── feature_request.yml ├── actions │ └── setup │ │ └── action.yml ├── docs │ └── development.md └── workflows │ └── ci.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .watchmanconfig ├── .yarnrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── declarations.d.ts ├── eas.json ├── example ├── .env.example ├── App.js ├── app.config.js ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png ├── babel.config.js ├── declarations.d.ts ├── eas-update-config.ts ├── eas.json ├── expo-crypto-shim.js ├── metro.config.js ├── package.json ├── src │ ├── assets │ │ ├── Close.png │ │ ├── Close@2x.png │ │ └── Close@3x.png │ ├── components │ │ ├── BlockchainActions.tsx │ │ └── RequestModal.tsx │ ├── constants │ │ ├── Config.ts │ │ ├── Contract.ts │ │ └── eip712.ts │ ├── screens │ │ └── App.tsx │ ├── types │ │ └── methods.ts │ └── utils │ │ ├── ContractUtil.ts │ │ └── MethodUtil.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock ├── lefthook.yml ├── package.json ├── scripts ├── bootstrap.js └── bump-version.sh ├── src ├── __tests__ │ └── index.test.tsx ├── assets │ ├── Backward.tsx │ ├── Checkmark.tsx │ ├── Chevron.tsx │ ├── Close.tsx │ ├── CopyLarge.tsx │ ├── LogoLockup.tsx │ ├── QRCode.tsx │ ├── Retry.tsx │ ├── Search.tsx │ ├── WCLogo.tsx │ ├── WalletIcon.tsx │ └── Warning.tsx ├── components │ ├── Image.tsx │ ├── ModalBackcard.tsx │ ├── QRCode.tsx │ ├── SearchBar.tsx │ ├── Shimmer.tsx │ ├── Text.tsx │ ├── Toast.tsx │ ├── Touchable.tsx │ ├── ViewAllBox.tsx │ ├── WalletImage.tsx │ ├── WalletItem.tsx │ ├── WalletItemLoader.tsx │ └── WalletLoadingThumbnail.tsx ├── config │ └── animations.ts ├── constants │ ├── Colors.ts │ └── Config.ts ├── controllers │ ├── AccountCtrl.ts │ ├── ApiCtrl.ts │ ├── AssetCtrl.ts │ ├── ClientCtrl.ts │ ├── ConfigCtrl.ts │ ├── ModalCtrl.ts │ ├── OptionsCtrl.ts │ ├── RouterCtrl.ts │ ├── ThemeCtrl.ts │ ├── ToastCtrl.ts │ └── WcConnectionCtrl.ts ├── hooks │ ├── useConfigure.ts │ ├── useConnectionHandler.ts │ ├── useDebounceCallback.ts │ ├── useOrientation.ts │ ├── useTheme.ts │ └── useWalletConnectModal.ts ├── index.tsx ├── modal │ ├── wcm-modal-router │ │ └── index.tsx │ └── wcm-modal │ │ └── index.tsx ├── partials │ ├── wcm-all-wallets-list │ │ ├── index.tsx │ │ └── styles.ts │ ├── wcm-all-wallets-search │ │ ├── index.tsx │ │ └── styles.ts │ └── wcm-modal-header │ │ ├── index.tsx │ │ └── styles.ts ├── types │ ├── controllerTypes.ts │ ├── coreTypes.ts │ └── routerTypes.ts ├── utils │ ├── AssetUtil.ts │ ├── ConstantsUtil.ts │ ├── CoreHelperUtil.ts │ ├── FetchUtil.ts │ ├── ProviderUtil.ts │ ├── QRCodeUtil.tsx │ ├── StorageUtil.ts │ └── UiUtil.ts └── views │ ├── wcm-all-wallets-view │ ├── index.tsx │ └── styles.ts │ ├── wcm-connect-view │ ├── index.tsx │ └── styles.ts │ ├── wcm-connecting-view │ ├── index.tsx │ └── styles.ts │ └── wcm-qr-code-view │ └── index.tsx ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "@react-native-community", 5 | "prettier" 6 | ], 7 | "plugins": ["promise"], 8 | "ignorePatterns":[ 9 | "node_modules/", 10 | "build/", 11 | "lib/" 12 | ], 13 | "rules": { 14 | "react/react-in-jsx-scope": 0, 15 | "no-duplicate-imports":"error", 16 | "promise/prefer-await-to-then": "error", 17 | "prettier/prettier": [ 18 | "error", 19 | { 20 | "quoteProps": "consistent", 21 | "singleQuote": true, 22 | "tabWidth": 2, 23 | "trailingComma": "es5", 24 | "useTabs": false 25 | } 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | 2 | name: 🐛 Bug Report 3 | description: Report a reproducible bug or regression in @walletconnect/modal-react-native. 4 | title: '[bug]: ' 5 | labels: ['bug', 'needs review'] 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Please provide all the information requested. Issues that do not follow this format are likely to stall. 11 | - type: textarea 12 | id: description 13 | attributes: 14 | label: Description 15 | description: Please provide a clear and concise description of what the bug is. Include screenshots if needed. Test using the [latest SDK release](https://github.com/WalletConnect/modal-react-native/releases) to make sure your issue has not already been fixed. 16 | validations: 17 | required: true 18 | - type: input 19 | id: version 20 | attributes: 21 | label: WalletConnect Modal SDK version 22 | description: What is the latest version of @walletconnect/modal-react-native that this issue reproduces on? 23 | placeholder: ex. 1.0.0-alpha.5 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: react-native-info 28 | attributes: 29 | label: Output of `npx react-native info` 30 | description: Run `npx react-native info` in your terminal, copy and paste the results here. 31 | validations: 32 | required: true 33 | - type: input 34 | id: expo 35 | attributes: 36 | label: Expo Version (if applies) 37 | description: Which SDK version of Expo are you using? 38 | placeholder: Expo 48.0.9 39 | - type: textarea 40 | id: reproduction 41 | attributes: 42 | label: Steps to reproduce 43 | description: Provide a detailed list of steps that reproduce the issue. 44 | validations: 45 | required: true 46 | - type: textarea 47 | id: extra 48 | attributes: 49 | label: Snack, code example, screenshot, or link to a repository 50 | description: | 51 | Please provide a Snack (https://snack.expo.dev/), a link to a repository on GitHub, or provide a minimal code example that reproduces the problem. 52 | You may provide a screenshot of the application if you think it is relevant to your bug report. 53 | Here are some tips for providing a minimal example: https://stackoverflow.com/help/mcve 54 | Please note that a reproducer is **mandatory**. Issues without reproducer are more likely to stall and will be closed. 55 | validations: 56 | required: true 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🤔 Questions and Help 4 | url: https://github.com/orgs/WalletConnect/discussions/ 5 | about: Looking for help with your app? Please refer to the Wallet Connect community's support resources. 6 | - name: 🚀 Discussions and Proposals 7 | url: https://github.com/orgs/WalletConnect/discussions 8 | about: Discuss the future of WalletConnect Modal SDK in the Wallet Connect community's discussions and proposals repository. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yml: -------------------------------------------------------------------------------- 1 | name: 📃 Documentation 2 | description: 'Report issue or suggest an improvement to docs.' 3 | title: '[docs] ' 4 | labels: ['docs', 'needs review'] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Summary 9 | description: | 10 | Clearly describe what is wrong or missing from the docs. 11 | validations: 12 | required: true 13 | - type: input 14 | attributes: 15 | label: Link to the related docs page 16 | validations: 17 | required: true 18 | - type: markdown 19 | attributes: 20 | value: If this is a typo, or something you feel up to fixing, please do so! You can edit any page in our docs by scrolling to the bottom and selecting `Edit this page`. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🙋 Feature Request 2 | description: 'Want us to add something to @walletconnect/modal-react-native?' 3 | title: '[feature] ' 4 | labels: ['feature-request', 'needs review'] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: What problem does this new feature solve? 9 | description: | 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | validations: 12 | required: true 13 | - type: textarea 14 | attributes: 15 | label: Describe the solution you'd like 16 | description: A clear and concise description of what you want to happen. 17 | validations: 18 | required: true 19 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup Node.js and install dependencies 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Setup Node.js 8 | uses: actions/setup-node@v3 9 | with: 10 | node-version-file: .nvmrc 11 | 12 | - name: Cache dependencies 13 | id: yarn-cache 14 | uses: actions/cache@v3 15 | with: 16 | path: | 17 | **/node_modules 18 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 19 | restore-keys: | 20 | ${{ runner.os }}-yarn- 21 | 22 | - name: Install dependencies 23 | if: steps.yarn-cache.outputs.cache-hit != 'true' 24 | run: | 25 | yarn install --cwd example --frozen-lockfile 26 | yarn install --frozen-lockfile 27 | shell: bash 28 | -------------------------------------------------------------------------------- /.github/docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Workspace setup 4 | 5 | Install dependencies from the repository's root directory (this will also set up the example project workspace): 6 | 7 | ```bash 8 | yarn 9 | ``` 10 | 11 | ## ProjectID setup 12 | 13 | Create an `.env` file in `/example` with the following content: 14 | 15 | ``` 16 | EXPO_PUBLIC_PROJECT_ID="YOUR_CLOUD_ID" 17 | ``` 18 | 19 | To create your ProjectID, head to [cloud.walletconnect.com](https://cloud.walletconnect.com/) 20 | 21 | ## Commands 22 | 23 | Execute all commands from the root. 24 | 25 | - `yarn example ios` - Run the example project in an iOS simulator. 26 | - `yarn example android` - Run the example project in an Android simulator. 27 | - `yarn lint` - Run the linter. 28 | - `yarn typecheck` - Run typescript checks. 29 | - `yarn test` - Run jest tests. 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup 18 | uses: ./.github/actions/setup 19 | 20 | - name: Lint files 21 | run: yarn lint 22 | 23 | - name: Typecheck files 24 | run: yarn typecheck 25 | 26 | test: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | 32 | - name: Setup 33 | uses: ./.github/actions/setup 34 | 35 | - name: Run unit tests 36 | run: yarn test --maxWorkers=2 --coverage 37 | 38 | build: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v3 43 | 44 | - name: Setup 45 | uses: ./.github/actions/setup 46 | 47 | - name: Build package 48 | run: yarn prepack 49 | 50 | update: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: 🏗 Setup repo 54 | uses: actions/checkout@v3 55 | 56 | - name: 📦 Setup node & Install dependencies 57 | uses: ./.github/actions/setup 58 | 59 | - name: 🏗 Setup EAS 60 | uses: expo/expo-github-action@v8 61 | with: 62 | eas-version: latest 63 | token: ${{ secrets.EXPO_TOKEN }} 64 | 65 | - name: 🚀 Create preview 66 | uses: expo/expo-github-action/preview@v8 67 | id: preview 68 | with: 69 | command: eas update --auto 70 | comment: true 71 | working-directory: example/ 72 | env: 73 | PROJECT_ID: ${{ secrets.CLOUD_PROJECT_ID }} 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .classpath 35 | .cxx 36 | .gradle 37 | .idea 38 | .project 39 | .settings 40 | local.properties 41 | android.iml 42 | 43 | # Cocoapods 44 | # 45 | example/ios/Pods 46 | 47 | # Ruby 48 | example/vendor/ 49 | 50 | # node.js 51 | # 52 | node_modules/ 53 | npm-debug.log 54 | yarn-debug.log 55 | yarn-error.log 56 | 57 | # BUCK 58 | buck-out/ 59 | \.buckd/ 60 | android/app/libs 61 | android/keystores/debug.keystore 62 | 63 | # Expo 64 | .expo/ 65 | example/dist/ 66 | **/.env 67 | 68 | # Turborepo 69 | .turbo/ 70 | 71 | # generated by bob 72 | lib/ 73 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.18.1 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "quoteProps": "consistent", 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "es5", 6 | "useTabs": false 7 | } -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # Override Yarn command so we can automatically setup the repo on running `yarn` 2 | 3 | yarn-path "scripts/bootstrap.js" 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [Support team.](mailto:support@walletconnect.com?subject=[modal-react-native]%20Bad%20Behavior) 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome, no matter how large or small! 4 | 5 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). 6 | 7 | ## Development workflow 8 | 9 | To get started with the project, run `yarn` in the root directory to install the required dependencies for each package: 10 | 11 | ```sh 12 | yarn 13 | ``` 14 | 15 | > While it's possible to use [`npm`](https://github.com/npm/cli), the tooling is built around [`yarn`](https://classic.yarnpkg.com/), so you'll have an easier time if you use `yarn` for development. 16 | 17 | While developing, you can run the [example app](/example/) to test your changes. Any changes you make in your library's JavaScript code will be reflected in the example app without a rebuild. If you change any native code, then you'll need to rebuild the example app. 18 | 19 | To start the packager: 20 | 21 | ```sh 22 | yarn example start 23 | ``` 24 | 25 | To run the example app on Android: 26 | 27 | ```sh 28 | yarn example android 29 | ``` 30 | 31 | To run the example app on iOS: 32 | 33 | ```sh 34 | yarn example ios 35 | ``` 36 | 37 | 38 | Make sure your code passes TypeScript and ESLint. Run the following to verify: 39 | 40 | ```sh 41 | yarn typecheck 42 | yarn lint 43 | ``` 44 | 45 | To fix formatting errors, run the following: 46 | 47 | ```sh 48 | yarn lint --fix 49 | ``` 50 | 51 | Remember to add tests for your change if possible. Run the unit tests by: 52 | 53 | ```sh 54 | yarn test 55 | ``` 56 | 57 | 58 | ### Commit message convention 59 | 60 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 61 | 62 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 63 | - `feat`: new features, e.g. add new method to the module. 64 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 65 | - `docs`: changes into documentation, e.g. add usage example for the module.. 66 | - `test`: adding or updating tests, e.g. add integration tests using detox. 67 | - `chore`: tooling changes, e.g. change CI config. 68 | 69 | Our pre-commit hooks verify that your commit message matches this format when committing. 70 | 71 | ### Linting and tests 72 | 73 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 74 | 75 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 76 | 77 | Our pre-commit hooks verify that the linter and tests pass when committing. 78 | 79 | ### Publishing to npm 80 | 81 | We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. 82 | 83 | To publish new versions, run the following: 84 | 85 | ```sh 86 | yarn release 87 | ``` 88 | 89 | ### Scripts 90 | 91 | The `package.json` file contains various scripts for common tasks: 92 | 93 | - `yarn bootstrap`: setup project by installing all dependencies and pods. 94 | - `yarn typecheck`: type-check files with TypeScript. 95 | - `yarn lint`: lint files with ESLint. 96 | - `yarn test`: run unit tests with Jest. 97 | - `yarn example start`: start the Metro server for the example app. 98 | - `yarn example android`: run the example app on Android. 99 | - `yarn example ios`: run the example app on iOS. 100 | 101 | ### Sending a pull request 102 | 103 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). 104 | 105 | When you're sending a pull request: 106 | 107 | - Prefer small pull requests focused on one change. 108 | - Verify that linters and tests are passing. 109 | - Review the documentation to make sure it looks good. 110 | - Follow the pull request template when opening a pull request. 111 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 112 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 | # WalletConnectModal SDK for React Native 2 | 3 | Simplest and most minimal way to connect your users with WalletConnect. 4 | 5 | ### 📚 [Documentation](https://docs.walletconnect.com/2.0/advanced/walletconnectmodal/about?platform=react-native) 6 | 7 | ### 🔎 [Examples](https://github.com/WalletConnect/react-native-examples/tree/main/dapps/ModalUProvider) 8 | 9 | ## Development 10 | 11 | Please follow [developer docs](./.github/docs/development.md) to set up WalletConnect Modal locally. 12 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 3.16.0" 4 | }, 5 | "build": { 6 | "development": { 7 | "developmentClient": true, 8 | "distribution": "internal" 9 | }, 10 | "preview": { 11 | "distribution": "internal" 12 | }, 13 | "production": {} 14 | }, 15 | "submit": { 16 | "production": {} 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/.env.example: -------------------------------------------------------------------------------- 1 | EXPO_PUBLIC_PROJECT_ID="YOUR_WALLET_CONNECT_CLOUD_ID" -------------------------------------------------------------------------------- /example/App.js: -------------------------------------------------------------------------------- 1 | import './eas-update-config'; 2 | 3 | export { default } from './src/screens/App'; 4 | -------------------------------------------------------------------------------- /example/app.config.js: -------------------------------------------------------------------------------- 1 | export default () => ({ 2 | expo: { 3 | name: 'walletconnect-modal-react-native-example', 4 | slug: 'walletconnect-modal-react-native-example', 5 | version: '1.0.0', 6 | orientation: 'default', 7 | icon: './assets/icon.png', 8 | scheme: 'rnwalletconnectmodalexpo', 9 | userInterfaceStyle: 'automatic', 10 | splash: { 11 | image: './assets/splash.png', 12 | resizeMode: 'contain', 13 | backgroundColor: '#ffffff', 14 | }, 15 | assetBundlePatterns: ['**/*'], 16 | ios: { 17 | bundleIdentifier: 'com.walletconnect.modal.rnexample', 18 | supportsTablet: true, 19 | infoPlist: { 20 | LSApplicationQueriesSchemes: [ 21 | 'metamask', 22 | 'trust', 23 | 'safe', 24 | 'rainbow', 25 | 'uniswap', 26 | 'zerion', 27 | 'imtokenv2', 28 | 'argent', 29 | 'spot', 30 | 'omni', 31 | 'dfw', 32 | 'tpoutside', 33 | 'robinhood-wallet', 34 | 'frontier', 35 | 'blockchain-wallet', 36 | 'safepalwallet', 37 | 'bitkeep', 38 | 'zengo', 39 | 'oneinch', 40 | 'bnc', 41 | 'exodus', 42 | 'ledgerlive', 43 | 'mewwallet', 44 | 'awallet', 45 | 'keyring', 46 | 'lobstr', 47 | 'ontoprovider', 48 | 'mathwallet', 49 | 'unstoppabledomains', 50 | 'obvious', 51 | 'fireblocks-wc', 52 | 'ambire', 53 | 'internetmoney', 54 | 'walletnow', 55 | 'bitcoincom', 56 | ], 57 | }, 58 | }, 59 | android: { 60 | package: 'com.walletconnect.modal.rnexample', 61 | adaptiveIcon: { 62 | foregroundImage: './assets/adaptive-icon.png', 63 | backgroundColor: '#ffffff', 64 | }, 65 | }, 66 | web: { 67 | favicon: './assets/favicon.png', 68 | }, 69 | extra: { 70 | eas: { 71 | projectId: 'f477ced2-c06e-470c-adcc-2c997a90cc4e', 72 | }, 73 | }, 74 | updates: { 75 | url: 'https://u.expo.dev/f477ced2-c06e-470c-adcc-2c997a90cc4e', 76 | }, 77 | runtimeVersion: { 78 | policy: 'sdkVersion', 79 | }, 80 | owner: 'nacho.walletconnect', 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WalletConnect/modal-react-native/d5d84412cfecaab132b6aaef542857b11a139dea/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WalletConnect/modal-react-native/d5d84412cfecaab132b6aaef542857b11a139dea/example/assets/favicon.png -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WalletConnect/modal-react-native/d5d84412cfecaab132b6aaef542857b11a139dea/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WalletConnect/modal-react-native/d5d84412cfecaab132b6aaef542857b11a139dea/example/assets/splash.png -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = function (api) { 5 | api.cache(true); 6 | 7 | return { 8 | presets: ['babel-preset-expo'], 9 | plugins: [ 10 | [ 11 | 'module-resolver', 12 | { 13 | extensions: ['.tsx', '.ts', '.js', '.json'], 14 | alias: { 15 | // For development, we want to alias the library to the source 16 | [pak.name]: path.join(__dirname, '..', pak.source), 17 | }, 18 | }, 19 | ], 20 | ], 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /example/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' {} 2 | -------------------------------------------------------------------------------- /example/eas-update-config.ts: -------------------------------------------------------------------------------- 1 | // Disabling console.error to avoid iOS crash: https://github.com/expo/expo/issues/21803 2 | if (!__DEV__) { 3 | console.error = () => {}; 4 | } 5 | -------------------------------------------------------------------------------- /example/eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 3.8.1", 4 | "promptToConfigurePushNotifications": false 5 | }, 6 | "build": { 7 | "development": { 8 | "developmentClient": true, 9 | "distribution": "internal", 10 | "ios": { 11 | "resourceClass": "m-medium" 12 | } 13 | }, 14 | "preview": { 15 | "distribution": "internal", 16 | "ios": { 17 | "resourceClass": "m-medium" 18 | } 19 | }, 20 | "production": { 21 | "ios": { 22 | "resourceClass": "m-medium" 23 | } 24 | } 25 | }, 26 | "submit": { 27 | "production": {} 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/expo-crypto-shim.js: -------------------------------------------------------------------------------- 1 | // https://github.com/expo/expo/issues/17270#issuecomment-1445149952 2 | // Polyfill for expo-crypto until issue with react-native-get-random-values is solved 3 | // Apply only with Expo SDK 48 4 | // For Expo SDK >= 49, install react-native-get-random-values@1.9.0 and copy expo config "exclude" in package.json 5 | 6 | import { getRandomValues as expoCryptoGetRandomValues } from 'expo-crypto'; 7 | 8 | class Crypto { 9 | getRandomValues = expoCryptoGetRandomValues; 10 | } 11 | 12 | // eslint-disable-next-line no-undef 13 | const webCrypto = typeof crypto !== 'undefined' ? crypto : new Crypto(); 14 | 15 | (() => { 16 | if (typeof crypto === 'undefined') { 17 | Object.defineProperty(window, 'crypto', { 18 | configurable: true, 19 | enumerable: true, 20 | get: () => webCrypto, 21 | }); 22 | } 23 | })(); 24 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const escape = require('escape-string-regexp'); 3 | const { getDefaultConfig } = require('@expo/metro-config'); 4 | const exclusionList = require('metro-config/src/defaults/exclusionList'); 5 | const pak = require('../package.json'); 6 | 7 | const root = path.resolve(__dirname, '..'); 8 | 9 | const modules = Object.keys({ 10 | ...pak.peerDependencies, 11 | }); 12 | 13 | const defaultConfig = getDefaultConfig(__dirname); 14 | 15 | module.exports = { 16 | ...defaultConfig, 17 | 18 | projectRoot: __dirname, 19 | watchFolders: [root], 20 | 21 | // We need to make sure that only one version is loaded for peerDependencies 22 | // So we block them at the root, and alias them to the versions in example's node_modules 23 | resolver: { 24 | ...defaultConfig.resolver, 25 | 26 | blacklistRE: exclusionList( 27 | modules.map( 28 | (m) => 29 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) 30 | ) 31 | ), 32 | 33 | extraNodeModules: modules.reduce((acc, name) => { 34 | acc[name] = path.join(__dirname, 'node_modules', name); 35 | return acc; 36 | }, {}), 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "walletconnect-modal-react-native-example", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "android:binary": "expo run:android", 9 | "ios": "expo start --ios", 10 | "ios:binary": "expo run:ios", 11 | "web": "expo start --web" 12 | }, 13 | "dependencies": { 14 | "@ethersproject/shims": "5.7.0", 15 | "@react-native-async-storage/async-storage": "1.18.2", 16 | "@react-native-community/netinfo": "9.3.10", 17 | "@walletconnect/encoding": "1.0.2", 18 | "@walletconnect/react-native-compat": "2.10.5", 19 | "ethers": "5.7.2", 20 | "expo": "^49.0.0", 21 | "expo-clipboard": "~4.3.0", 22 | "expo-constants": "~14.4.2", 23 | "expo-crypto": "~12.4.1", 24 | "expo-splash-screen": "~0.20.4", 25 | "expo-status-bar": "~1.6.0", 26 | "expo-system-ui": "~2.4.0", 27 | "expo-updates": "~0.18.10", 28 | "qrcode": "1.5.1", 29 | "react": "18.2.0", 30 | "react-dom": "18.2.0", 31 | "react-native": "0.72.3", 32 | "react-native-get-random-values": "1.9.0", 33 | "react-native-modal": "13.0.1", 34 | "react-native-svg": "13.9.0", 35 | "react-native-web": "~0.19.6" 36 | }, 37 | "expo": { 38 | "install": { 39 | "exclude": [ 40 | "react-native-get-random-values" 41 | ] 42 | } 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.20.0", 46 | "@expo/webpack-config": "^18.1.1", 47 | "babel-loader": "8.3.0", 48 | "babel-plugin-module-resolver": "4.1.0" 49 | }, 50 | "private": true 51 | } 52 | -------------------------------------------------------------------------------- /example/src/assets/Close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WalletConnect/modal-react-native/d5d84412cfecaab132b6aaef542857b11a139dea/example/src/assets/Close.png -------------------------------------------------------------------------------- /example/src/assets/Close@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WalletConnect/modal-react-native/d5d84412cfecaab132b6aaef542857b11a139dea/example/src/assets/Close@2x.png -------------------------------------------------------------------------------- /example/src/assets/Close@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WalletConnect/modal-react-native/d5d84412cfecaab132b6aaef542857b11a139dea/example/src/assets/Close@3x.png -------------------------------------------------------------------------------- /example/src/components/BlockchainActions.tsx: -------------------------------------------------------------------------------- 1 | import { useWalletConnectModal } from '@walletconnect/modal-react-native'; 2 | import { ethers } from 'ethers'; 3 | import { useMemo, useState } from 'react'; 4 | import { FlatList, StyleSheet, Text, TouchableOpacity } from 'react-native'; 5 | 6 | import type { 7 | AccountAction, 8 | FormattedRpcError, 9 | FormattedRpcResponse, 10 | RpcRequestParams, 11 | } from '../types/methods'; 12 | import { getFilterChanges, readContract } from '../utils/ContractUtil'; 13 | import { 14 | ethSign, 15 | sendTransaction, 16 | signMessage, 17 | signTransaction, 18 | signTypedData, 19 | } from '../utils/MethodUtil'; 20 | import { RequestModal } from './RequestModal'; 21 | 22 | interface Props { 23 | onButtonPress: () => void; 24 | } 25 | 26 | export function BlockchainActions({ onButtonPress }: Props) { 27 | const [rpcResponse, setRpcResponse] = useState(); 28 | const [rpcError, setRpcError] = useState(); 29 | const { provider, isConnected } = useWalletConnectModal(); 30 | 31 | const web3Provider = useMemo( 32 | () => (provider ? new ethers.providers.Web3Provider(provider) : undefined), 33 | [provider] 34 | ); 35 | 36 | const [loading, setLoading] = useState(false); 37 | const [modalVisible, setModalVisible] = useState(false); 38 | 39 | const onModalClose = () => { 40 | setModalVisible(false); 41 | setLoading(false); 42 | setRpcResponse(undefined); 43 | setRpcError(undefined); 44 | }; 45 | 46 | const getEthereumActions = () => { 47 | const wrapRpcRequest = 48 | ( 49 | method: string, 50 | rpcRequest: ({ 51 | web3Provider, 52 | method, 53 | }: RpcRequestParams) => Promise 54 | ) => 55 | async () => { 56 | if (!web3Provider) return; 57 | 58 | setRpcResponse(undefined); 59 | setRpcError(undefined); 60 | setModalVisible(true); 61 | try { 62 | setLoading(true); 63 | const result = await rpcRequest({ web3Provider, method }); 64 | setRpcResponse(result); 65 | setRpcError(undefined); 66 | } catch (error: any) { 67 | console.error('RPC request failed:', error); 68 | setRpcResponse(undefined); 69 | setRpcError({ method, error: error?.message }); 70 | } finally { 71 | setLoading(false); 72 | } 73 | }; 74 | 75 | const actions: AccountAction[] = [ 76 | { 77 | method: 'eth_sendTransaction', 78 | callback: wrapRpcRequest('eth_sendTransaction', sendTransaction), 79 | }, 80 | { 81 | method: 'eth_signTransaction', 82 | callback: wrapRpcRequest('eth_signTransaction', signTransaction), 83 | }, 84 | { 85 | method: 'personal_sign', 86 | callback: wrapRpcRequest('personal_sign', signMessage), 87 | }, 88 | { 89 | method: 'eth_sign (standard)', 90 | callback: wrapRpcRequest('eth_sign (standard)', ethSign), 91 | }, 92 | { 93 | method: 'eth_signTypedData', 94 | callback: wrapRpcRequest('eth_signTypedData', signTypedData), 95 | }, 96 | { 97 | method: 'read contract (mainnet)', 98 | callback: wrapRpcRequest('read contract', readContract), 99 | }, 100 | { 101 | method: 'filter contract (mainnet)', 102 | callback: wrapRpcRequest('filter contract', getFilterChanges), 103 | }, 104 | ]; 105 | return actions; 106 | }; 107 | 108 | return ( 109 | <> 110 | 114 | 115 | {isConnected ? 'Disconnect' : 'Connect Wallet'} 116 | 117 | 118 | } 119 | style={styles.list} 120 | contentContainerStyle={styles.listContent} 121 | renderItem={({ item }) => ( 122 | item.callback(web3Provider)} 126 | > 127 | {item.method} 128 | 129 | )} 130 | /> 131 | 138 | 139 | ); 140 | } 141 | 142 | const styles = StyleSheet.create({ 143 | button: { 144 | display: 'flex', 145 | justifyContent: 'center', 146 | alignItems: 'center', 147 | backgroundColor: '#3396FF', 148 | borderRadius: 20, 149 | width: 200, 150 | height: 50, 151 | borderWidth: 1, 152 | borderColor: 'rgba(0, 0, 0, 0.1)', 153 | marginTop: 4, 154 | }, 155 | buttonText: { 156 | color: 'white', 157 | fontWeight: '700', 158 | }, 159 | modalContainer: { 160 | padding: 16, 161 | backgroundColor: 'white', 162 | borderRadius: 8, 163 | }, 164 | title: { 165 | fontWeight: '600', 166 | fontSize: 16, 167 | textAlign: 'center', 168 | marginBottom: 8, 169 | }, 170 | subtitle: { 171 | fontWeight: 'bold', 172 | marginVertical: 4, 173 | }, 174 | responseText: { 175 | fontWeight: '300', 176 | }, 177 | list: { 178 | paddingTop: 60, 179 | }, 180 | listContent: { 181 | alignItems: 'center', 182 | }, 183 | }); 184 | -------------------------------------------------------------------------------- /example/src/components/RequestModal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityIndicator, 3 | Image, 4 | StyleSheet, 5 | Text, 6 | TouchableOpacity, 7 | View, 8 | } from 'react-native'; 9 | import Modal from 'react-native-modal'; 10 | import type { FormattedRpcError, FormattedRpcResponse } from '../types/methods'; 11 | import Close from '../assets/Close.png'; 12 | 13 | interface Props { 14 | isVisible: boolean; 15 | onClose: () => void; 16 | isLoading?: boolean; 17 | rpcResponse?: FormattedRpcResponse; 18 | rpcError?: FormattedRpcError; 19 | } 20 | 21 | export function RequestModal({ 22 | isVisible, 23 | onClose, 24 | isLoading, 25 | rpcResponse, 26 | rpcError, 27 | }: Props) { 28 | return ( 29 | 30 | 31 | 32 | 33 | 34 | {isLoading && ( 35 | <> 36 | Pending Request 37 | 38 | 39 | Approve or reject request using your wallet if needed 40 | 41 | 42 | )} 43 | {rpcResponse && ( 44 | <> 45 | 46 | Request Response 47 | 48 | {Object.keys(rpcResponse).map((key) => ( 49 | 50 | {key}:{' '} 51 | 52 | {rpcResponse[key as keyof FormattedRpcResponse]?.toString()} 53 | 54 | 55 | ))} 56 | 57 | )} 58 | {rpcError && ( 59 | <> 60 | 61 | Request Failure 62 | 63 | 64 | Method: {rpcError.method} 65 | 66 | 67 | Error: {rpcError.error} 68 | 69 | 70 | )} 71 | 72 | 73 | ); 74 | } 75 | 76 | const styles = StyleSheet.create({ 77 | closeButton: { 78 | alignSelf: 'flex-end', 79 | backgroundColor: 'white', 80 | height: 30, 81 | width: 30, 82 | alignItems: 'center', 83 | justifyContent: 'center', 84 | borderRadius: 100, 85 | margin: 8, 86 | }, 87 | innerContainer: { 88 | padding: 16, 89 | backgroundColor: 'white', 90 | borderRadius: 8, 91 | borderTopLeftRadius: 30, 92 | borderTopRightRadius: 30, 93 | borderBottomLeftRadius: 30, 94 | borderBottomRightRadius: 30, 95 | }, 96 | loader: { 97 | marginVertical: 24, 98 | }, 99 | title: { 100 | fontWeight: '600', 101 | fontSize: 16, 102 | textAlign: 'center', 103 | marginBottom: 8, 104 | }, 105 | successText: { 106 | color: '#3396FF', 107 | }, 108 | failureText: { 109 | color: '#F05142', 110 | }, 111 | subtitle: { 112 | fontWeight: 'bold', 113 | marginVertical: 4, 114 | }, 115 | center: { 116 | textAlign: 'center', 117 | }, 118 | responseText: { 119 | fontWeight: '300', 120 | }, 121 | }); 122 | -------------------------------------------------------------------------------- /example/src/constants/Config.ts: -------------------------------------------------------------------------------- 1 | import type { IProviderMetadata } from '@walletconnect/modal-react-native'; 2 | 3 | export const providerMetadata: IProviderMetadata = { 4 | name: 'React Native V2 dApp', 5 | description: 'RN dApp by WalletConnect', 6 | url: 'https://walletconnect.com/', 7 | icons: ['https://avatars.githubusercontent.com/u/37784886'], 8 | redirect: { 9 | native: 'rnwalletconnectmodalexpo://', 10 | }, 11 | }; 12 | 13 | export const sessionParams = { 14 | namespaces: { 15 | eip155: { 16 | methods: [ 17 | 'eth_sendTransaction', 18 | 'eth_signTransaction', 19 | 'eth_sign', 20 | 'personal_sign', 21 | 'eth_signTypedData', 22 | ], 23 | chains: ['eip155:1'], 24 | events: ['chainChanged', 'accountsChanged'], 25 | rpcMap: {}, 26 | }, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /example/src/constants/Contract.ts: -------------------------------------------------------------------------------- 1 | // Tether (USDT) 2 | const contractAddress = '0xdac17f958d2ee523a2206206994597c13d831ec7'; 3 | 4 | // vitalik.eth 5 | const balanceAddress = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; 6 | 7 | const readContractAbi = [ 8 | { 9 | constant: true, 10 | inputs: [], 11 | name: 'name', 12 | outputs: [ 13 | { 14 | name: '', 15 | type: 'string', 16 | }, 17 | ], 18 | payable: false, 19 | stateMutability: 'view', 20 | type: 'function', 21 | }, 22 | { 23 | constant: true, 24 | inputs: [], 25 | name: 'symbol', 26 | outputs: [ 27 | { 28 | name: '', 29 | type: 'string', 30 | }, 31 | ], 32 | payable: false, 33 | stateMutability: 'view', 34 | type: 'function', 35 | }, 36 | { 37 | constant: true, 38 | inputs: [], 39 | name: 'totalSupply', 40 | outputs: [ 41 | { 42 | name: '', 43 | type: 'uint256', 44 | }, 45 | ], 46 | payable: false, 47 | stateMutability: 'view', 48 | type: 'function', 49 | }, 50 | { 51 | constant: true, 52 | inputs: [ 53 | { 54 | name: '_owner', 55 | type: 'address', 56 | }, 57 | ], 58 | name: 'balanceOf', 59 | outputs: [ 60 | { 61 | name: 'balance', 62 | type: 'uint256', 63 | }, 64 | ], 65 | payable: false, 66 | stateMutability: 'view', 67 | type: 'function', 68 | }, 69 | ]; 70 | 71 | const getFilterChangesAbi = [ 72 | 'event Transfer(address indexed from, address indexed to, uint amount)', 73 | ]; 74 | 75 | export default { 76 | contractAddress, 77 | balanceAddress, 78 | readContractAbi, 79 | getFilterChangesAbi, 80 | }; 81 | -------------------------------------------------------------------------------- /example/src/constants/eip712.ts: -------------------------------------------------------------------------------- 1 | // From spec: https://eips.ethereum.org/EIPS/eip-712 2 | export const getTypedDataExample = (chainId?: number) => ({ 3 | types: { 4 | EIP712Domain: [ 5 | { name: 'name', type: 'string' }, 6 | { name: 'version', type: 'string' }, 7 | { name: 'chainId', type: 'uint256' }, 8 | { name: 'verifyingContract', type: 'address' }, 9 | ], 10 | Person: [ 11 | { name: 'name', type: 'string' }, 12 | { name: 'wallet', type: 'address' }, 13 | ], 14 | Mail: [ 15 | { name: 'from', type: 'Person' }, 16 | { name: 'to', type: 'Person' }, 17 | { name: 'contents', type: 'string' }, 18 | ], 19 | }, 20 | primaryType: 'Mail', 21 | domain: { 22 | name: 'Ether Mail', 23 | version: '1', 24 | chainId: chainId || 1, 25 | verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', 26 | }, 27 | message: { 28 | from: { 29 | name: 'Alice', 30 | wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', 31 | }, 32 | to: { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' }, 33 | contents: 'Hello, Bob!', 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /example/src/screens/App.tsx: -------------------------------------------------------------------------------- 1 | // import '../../expo-crypto-shim.js'; --> Only for Expo 48 2 | import { 3 | SafeAreaView, 4 | StyleSheet, 5 | Text, 6 | TouchableOpacity, 7 | View, 8 | } from 'react-native'; 9 | import '@walletconnect/react-native-compat'; 10 | import { 11 | WalletConnectModal, 12 | useWalletConnectModal, 13 | } from '@walletconnect/modal-react-native'; 14 | import '@ethersproject/shims'; 15 | import { setStringAsync } from 'expo-clipboard'; 16 | import { sessionParams, providerMetadata } from '../constants/Config'; 17 | import { BlockchainActions } from '../components/BlockchainActions'; 18 | 19 | export default function App() { 20 | const { isConnected, open, provider } = useWalletConnectModal(); 21 | 22 | const handleButtonPress = async () => { 23 | if (isConnected) { 24 | return provider?.disconnect(); 25 | } 26 | return open(); 27 | }; 28 | 29 | const onCopyClipboard = async (value: string) => { 30 | setStringAsync(value); 31 | }; 32 | 33 | return ( 34 | 35 | {isConnected ? ( 36 | 37 | ) : ( 38 | 39 | 40 | 41 | {isConnected ? 'Disconnect' : 'Connect Wallet'} 42 | 43 | 44 | 45 | )} 46 | 52 | 53 | ); 54 | } 55 | 56 | const styles = StyleSheet.create({ 57 | container: { 58 | flex: 1, 59 | }, 60 | connectContainer: { 61 | alignItems: 'center', 62 | justifyContent: 'center', 63 | flex: 1, 64 | }, 65 | button: { 66 | justifyContent: 'center', 67 | alignItems: 'center', 68 | backgroundColor: '#3396FF', 69 | borderRadius: 20, 70 | width: 200, 71 | height: 50, 72 | borderWidth: 1, 73 | borderColor: 'rgba(0, 0, 0, 0.1)', 74 | marginTop: 4, 75 | }, 76 | text: { 77 | color: 'white', 78 | fontWeight: '700', 79 | }, 80 | }); 81 | -------------------------------------------------------------------------------- /example/src/types/methods.ts: -------------------------------------------------------------------------------- 1 | import type { ethers } from 'ethers'; 2 | 3 | export interface FormattedRpcResponse { 4 | method: string; 5 | address: string; 6 | valid: boolean; 7 | result: string; 8 | error?: string; 9 | } 10 | 11 | export interface FormattedRpcError { 12 | method: string; 13 | error?: string; 14 | } 15 | 16 | export interface AccountAction { 17 | method: string; 18 | callback: (web3Provider?: ethers.providers.Web3Provider) => Promise; 19 | } 20 | 21 | export interface RpcRequestParams { 22 | method: string; 23 | web3Provider: ethers.providers.Web3Provider; 24 | } 25 | -------------------------------------------------------------------------------- /example/src/utils/ContractUtil.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | 3 | import CONTRACT_VALUES from '../constants/Contract'; 4 | import type { FormattedRpcResponse, RpcRequestParams } from '../types/methods'; 5 | 6 | export const readContract = async ({ 7 | web3Provider, 8 | method, 9 | }: RpcRequestParams): Promise => { 10 | if (!web3Provider) { 11 | throw new Error('web3Provider not connected'); 12 | } 13 | 14 | const contract = new ethers.Contract( 15 | CONTRACT_VALUES.contractAddress, 16 | CONTRACT_VALUES.readContractAbi, 17 | web3Provider 18 | ); 19 | 20 | // Read contract information 21 | const name = await contract.name(); 22 | const symbol = await contract.symbol(); 23 | const balance = await contract.balanceOf(CONTRACT_VALUES.balanceAddress); 24 | 25 | // Format the USDT for displaying to the user 26 | const formattedBalance = ethers.utils.formatUnits(balance, 6); 27 | 28 | return { 29 | method, 30 | address: CONTRACT_VALUES.contractAddress, 31 | valid: true, 32 | result: `name: ${name}, symbol: ${symbol}, balance: ${formattedBalance}`, 33 | }; 34 | }; 35 | 36 | export const getFilterChanges = async ({ 37 | web3Provider, 38 | method, 39 | }: RpcRequestParams): Promise => { 40 | if (!web3Provider) { 41 | throw new Error('web3Provider not connected'); 42 | } 43 | 44 | const contract = new ethers.Contract( 45 | CONTRACT_VALUES.contractAddress, 46 | CONTRACT_VALUES.getFilterChangesAbi, 47 | web3Provider 48 | ); 49 | 50 | // Filter for all token transfers 51 | const filterFrom = contract.filters.Transfer?.(null, null); 52 | 53 | // List all transfers sent in the last 100 blocks 54 | const transfers = await contract.queryFilter(filterFrom!, -100); 55 | 56 | return { 57 | method, 58 | address: CONTRACT_VALUES.contractAddress, 59 | valid: true, 60 | result: `transfers in last 100 blocks: ${transfers.length}`, 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /example/src/utils/MethodUtil.ts: -------------------------------------------------------------------------------- 1 | import { numberToHex, sanitizeHex, utf8ToHex } from '@walletconnect/encoding'; 2 | import type { TypedDataDomain, TypedDataField } from 'ethers'; 3 | import { recoverAddress } from '@ethersproject/transactions'; 4 | import { hashMessage } from '@ethersproject/hash'; 5 | import type { Bytes, SignatureLike } from '@ethersproject/bytes'; 6 | import { getTypedDataExample } from '../constants/eip712'; 7 | import { _TypedDataEncoder } from 'ethers/lib/utils'; 8 | import type { FormattedRpcResponse, RpcRequestParams } from '../types/methods'; 9 | 10 | export function verifyMessage( 11 | message: Bytes | string, 12 | signature: SignatureLike 13 | ): string { 14 | return recoverAddress(hashMessage(message), signature); 15 | } 16 | 17 | function verifyTypedData( 18 | domain: TypedDataDomain, 19 | types: Record>, 20 | value: Record, 21 | signature: SignatureLike 22 | ): string { 23 | return recoverAddress( 24 | _TypedDataEncoder.hash(domain, types, value), 25 | signature 26 | ); 27 | } 28 | 29 | const verifyEip155MessageSignature = ( 30 | message: string, 31 | signature: string, 32 | address: string 33 | ) => verifyMessage(message, signature).toLowerCase() === address.toLowerCase(); 34 | 35 | export const signMessage = async ({ 36 | web3Provider, 37 | method, 38 | }: RpcRequestParams): Promise => { 39 | if (!web3Provider) { 40 | throw new Error('web3Provider not connected'); 41 | } 42 | const msg = 'Hello World'; 43 | const hexMsg = utf8ToHex(msg, true); 44 | const [address] = await web3Provider.listAccounts(); 45 | if (!address) { 46 | throw new Error('No address found'); 47 | } 48 | 49 | const signature = await web3Provider.send('personal_sign', [hexMsg, address]); 50 | const valid = verifyEip155MessageSignature(msg, signature, address); 51 | return { 52 | method, 53 | address, 54 | valid, 55 | result: signature, 56 | }; 57 | }; 58 | 59 | export const ethSign = async ({ 60 | web3Provider, 61 | method, 62 | }: RpcRequestParams): Promise => { 63 | if (!web3Provider) { 64 | throw new Error('web3Provider not connected'); 65 | } 66 | const msg = 'hello world'; 67 | const [address] = await web3Provider.listAccounts(); 68 | 69 | if (!address) { 70 | throw new Error('No address found'); 71 | } 72 | 73 | const signature = await web3Provider.getSigner()._legacySignMessage(msg); 74 | 75 | return { 76 | method, 77 | address, 78 | valid: true, 79 | result: signature, 80 | }; 81 | }; 82 | 83 | export const signTypedData = async ({ 84 | web3Provider, 85 | method, 86 | }: RpcRequestParams): Promise => { 87 | if (!web3Provider) { 88 | throw new Error('web3Provider not connected'); 89 | } 90 | 91 | const { chainId } = await web3Provider.getNetwork(); 92 | const message = getTypedDataExample(chainId); 93 | 94 | const [address] = await web3Provider.listAccounts(); 95 | 96 | if (!address) { 97 | throw new Error('No address found'); 98 | } 99 | 100 | // eth_signTypedData params 101 | const params = [address, JSON.stringify(message)]; 102 | 103 | // send message 104 | const signature = await web3Provider.send('eth_signTypedData', params); 105 | 106 | // Separate `EIP712Domain` type from remaining types to verify, otherwise `ethers.utils.verifyTypedData` 107 | // will throw due to "unused" `EIP712Domain` type. 108 | // See: https://github.com/ethers-io/ethers.js/issues/687#issuecomment-714069471 109 | 110 | const { 111 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 112 | EIP712Domain, 113 | ...nonDomainTypes 114 | }: Record = message.types; 115 | 116 | const valid = 117 | verifyTypedData( 118 | message.domain, 119 | nonDomainTypes, 120 | message.message, 121 | signature 122 | ).toLowerCase() === address?.toLowerCase(); 123 | return { 124 | method, 125 | address, 126 | valid, 127 | result: signature, 128 | }; 129 | }; 130 | 131 | export const sendTransaction = async ({ 132 | web3Provider, 133 | method, 134 | }: RpcRequestParams): Promise => { 135 | if (!web3Provider) { 136 | throw new Error('web3Provider not connected'); 137 | } 138 | 139 | // Get the signer from the UniversalProvider 140 | const signer = web3Provider.getSigner(); 141 | const [address] = await web3Provider.listAccounts(); 142 | 143 | if (!address) { 144 | throw new Error('No address found'); 145 | } 146 | 147 | const amount = sanitizeHex(numberToHex(0)); 148 | 149 | const transaction = { 150 | from: address, 151 | to: address, 152 | value: amount, 153 | data: '0x', 154 | }; 155 | 156 | // Send the transaction using the signer 157 | const txResponse = await signer.sendTransaction(transaction); 158 | 159 | const transactionHash = txResponse.hash; 160 | console.log('transactionHash is ' + transactionHash); 161 | 162 | // Wait for the transaction to be mined (optional) 163 | const receipt = await txResponse.wait(); 164 | console.log('Transaction was mined in block:', receipt.blockNumber); 165 | 166 | return { 167 | method, 168 | address, 169 | valid: true, 170 | result: transactionHash, 171 | }; 172 | }; 173 | 174 | export const signTransaction = async ({ 175 | web3Provider, 176 | method, 177 | }: RpcRequestParams): Promise => { 178 | if (!web3Provider) { 179 | throw new Error('web3Provider not connected'); 180 | } 181 | const [address] = await web3Provider.listAccounts(); 182 | if (!address) { 183 | throw new Error('No address found'); 184 | } 185 | 186 | const tx = { 187 | from: address, 188 | to: address, 189 | data: '0x', 190 | value: sanitizeHex(numberToHex(0)), 191 | }; 192 | 193 | const signedTx = await web3Provider.send('eth_signTransaction', [tx]); 194 | 195 | return { 196 | method, 197 | address, 198 | valid: true, 199 | result: signedTx, 200 | }; 201 | }; 202 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | // Avoid expo-cli auto-generating a tsconfig 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const createExpoWebpackConfigAsync = require('@expo/webpack-config'); 3 | const { resolver } = require('./metro.config'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const node_modules = path.join(__dirname, 'node_modules'); 7 | 8 | module.exports = async function (env, argv) { 9 | const config = await createExpoWebpackConfigAsync(env, argv); 10 | 11 | config.module.rules.push({ 12 | test: /\.(js|jsx|ts|tsx)$/, 13 | include: path.resolve(root, 'src'), 14 | use: 'babel-loader', 15 | }); 16 | 17 | // We need to make sure that only one version is loaded for peerDependencies 18 | // So we alias them to the versions in example's node_modules 19 | Object.assign(config.resolve.alias, { 20 | ...resolver.extraNodeModules, 21 | 'react-native-web': path.join(node_modules, 'react-native-web'), 22 | }); 23 | 24 | return config; 25 | }; 26 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | lint: 5 | files: git diff --name-only @{push} 6 | glob: "*.{js,ts,jsx,tsx}" 7 | run: npx eslint {files} 8 | types: 9 | files: git diff --name-only @{push} 10 | glob: "*.{js,ts, jsx, tsx}" 11 | run: npx tsc --noEmit 12 | commit-msg: 13 | parallel: true 14 | commands: 15 | commitlint: 16 | run: npx commitlint --edit 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@walletconnect/modal-react-native", 3 | "version": "1.1.0", 4 | "main": "lib/commonjs/index", 5 | "module": "lib/module/index", 6 | "types": "lib/typescript/index.d.ts", 7 | "react-native": "src/index", 8 | "source": "src/index", 9 | "files": [ 10 | "src", 11 | "lib", 12 | "!**/__tests__", 13 | "!**/__fixtures__", 14 | "!**/__mocks__", 15 | "!**/.*" 16 | ], 17 | "scripts": { 18 | "test": "yarn jest --clearCache && yarn jest", 19 | "typecheck": "tsc --noEmit", 20 | "lint": "eslint \"**/*.{js,ts,tsx}\"", 21 | "prepack": "./scripts/bump-version.sh && bob build", 22 | "release": "release-it", 23 | "example": "yarn --cwd example", 24 | "bootstrap": "yarn example && yarn install" 25 | }, 26 | "keywords": [ 27 | "web3", 28 | "crypto", 29 | "ethereum", 30 | "modal", 31 | "walletconnect", 32 | "web3auth", 33 | "react-native" 34 | ], 35 | "repository": "https://github.com/WalletConnect/modal-react-native", 36 | "author": "WalletConnect (https://walletconnect.com)", 37 | "license": "Apache-2.0", 38 | "bugs": { 39 | "url": "https://github.com/WalletConnect/modal-react-native/issues" 40 | }, 41 | "homepage": "https://github.com/WalletConnect/modal-react-native", 42 | "publishConfig": { 43 | "registry": "https://registry.npmjs.org/", 44 | "access": "public" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "7.21.4", 48 | "@commitlint/config-conventional": "17.4.4", 49 | "@evilmartians/lefthook": "1.3.8", 50 | "@react-native-async-storage/async-storage": "1.18.1", 51 | "@react-native-community/eslint-config": "3.2.0", 52 | "@react-native-community/netinfo": "9.4.1", 53 | "@release-it/conventional-changelog": "5.1.1", 54 | "@types/jest": "28.1.2", 55 | "@types/node": "18.7.3", 56 | "@types/qrcode": "1.5.0", 57 | "@types/react": "17.0.21", 58 | "@types/react-native": "0.70.0", 59 | "@walletconnect/react-native-compat": "2.10.5", 60 | "@walletconnect/types": "2.5.2", 61 | "commitlint": "17.5.1", 62 | "del-cli": "5.0.0", 63 | "eslint": "8.36.0", 64 | "eslint-config-prettier": "8.8.0", 65 | "eslint-plugin-prettier": "4.2.1", 66 | "eslint-plugin-promise": "6.1.1", 67 | "jest": "28.1.3", 68 | "pod-install": "0.1.38", 69 | "prettier": "2.8.7", 70 | "react": "18.2.0", 71 | "react-native": "0.71.4", 72 | "react-native-builder-bob": "0.20.4", 73 | "react-native-get-random-values": "1.8.0", 74 | "react-native-modal": "13.0.1", 75 | "react-native-svg": "13.8.0", 76 | "release-it": "15.9.3", 77 | "typescript": "4.9.5" 78 | }, 79 | "resolutions": { 80 | "@types/react": "17.0.21" 81 | }, 82 | "peerDependencies": { 83 | "@react-native-async-storage/async-storage": ">=1.17.0", 84 | "@react-native-community/netinfo": ">=9.0.0", 85 | "@walletconnect/react-native-compat": ">=2.10.5", 86 | "react": "*", 87 | "react-native": "*", 88 | "react-native-get-random-values": ">=1.8.0", 89 | "react-native-modal": ">=13", 90 | "react-native-svg": ">=13" 91 | }, 92 | "engines": { 93 | "node": ">= 16.0.0" 94 | }, 95 | "packageManager": "yarn@1.22.15", 96 | "jest": { 97 | "preset": "react-native", 98 | "modulePathIgnorePatterns": [ 99 | "/example/node_modules", 100 | "/lib/" 101 | ] 102 | }, 103 | "commitlint": { 104 | "extends": [ 105 | "@commitlint/config-conventional" 106 | ] 107 | }, 108 | "release-it": { 109 | "git": { 110 | "commitMessage": "chore: release ${version}", 111 | "tagName": "v${version}" 112 | }, 113 | "npm": { 114 | "publish": true 115 | }, 116 | "github": { 117 | "release": true 118 | }, 119 | "plugins": { 120 | "@release-it/conventional-changelog": { 121 | "preset": "angular" 122 | } 123 | } 124 | }, 125 | "react-native-builder-bob": { 126 | "source": "src", 127 | "output": "lib", 128 | "targets": [ 129 | "commonjs", 130 | "module", 131 | [ 132 | "typescript", 133 | { 134 | "project": "tsconfig.build.json" 135 | } 136 | ] 137 | ] 138 | }, 139 | "dependencies": { 140 | "@walletconnect/core": "2.11.0", 141 | "@walletconnect/universal-provider": "2.11.0", 142 | "qrcode": "1.5.3", 143 | "valtio": "1.10.5" 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /scripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const child_process = require('child_process'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const args = process.argv.slice(2); 7 | const options = { 8 | cwd: process.cwd(), 9 | env: process.env, 10 | stdio: 'inherit', 11 | encoding: 'utf-8', 12 | }; 13 | 14 | if (os.type() === 'Windows_NT') { 15 | options.shell = true; 16 | } 17 | 18 | let result; 19 | 20 | if (process.cwd() !== root || args.length) { 21 | // We're not in the root of the project, or additional arguments were passed 22 | // In this case, forward the command to `yarn` 23 | result = child_process.spawnSync('yarn', args, options); 24 | } else { 25 | // If `yarn` is run without arguments, perform bootstrap 26 | result = child_process.spawnSync('yarn', ['bootstrap'], options); 27 | } 28 | 29 | process.exitCode = result.status; 30 | -------------------------------------------------------------------------------- /scripts/bump-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get version from package.json 4 | version=$(awk -F: '/"version":/ {print $2}' package.json | tr -d ' ",') 5 | # File where VERSION is defined 6 | file="src/constants/Config.ts" 7 | 8 | # Use sed to replace VERSION 9 | if [[ "$OSTYPE" == "darwin"* ]]; then 10 | # macOS 11 | sed -i '' "s/export const SDK_VERSION = '.*';/export const SDK_VERSION = '$version';/" $file 12 | else 13 | # Linux 14 | sed -i "s/export const SDK_VERSION = '.*';/export const SDK_VERSION = '$version';/" $file 15 | fi 16 | -------------------------------------------------------------------------------- /src/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | it.todo('write a test'); 2 | -------------------------------------------------------------------------------- /src/assets/Backward.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path, SvgProps } from 'react-native-svg'; 2 | 3 | const SvgBackward = (props: SvgProps) => ( 4 | 5 | 11 | 12 | ); 13 | export default SvgBackward; 14 | -------------------------------------------------------------------------------- /src/assets/Checkmark.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path, SvgProps } from 'react-native-svg'; 2 | const SvgCheckmark = (props: SvgProps) => ( 3 | 4 | 10 | 11 | ); 12 | export default SvgCheckmark; 13 | -------------------------------------------------------------------------------- /src/assets/Chevron.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path, SvgProps } from 'react-native-svg'; 2 | const SvgChevron = (props: SvgProps) => ( 3 | 4 | 10 | 11 | ); 12 | export default SvgChevron; 13 | -------------------------------------------------------------------------------- /src/assets/Close.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path, SvgProps } from 'react-native-svg'; 2 | 3 | const SvgClose = (props: SvgProps) => ( 4 | 5 | 9 | 10 | ); 11 | 12 | export default SvgClose; 13 | -------------------------------------------------------------------------------- /src/assets/CopyLarge.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path, SvgProps } from 'react-native-svg'; 2 | 3 | const SvgCopyLarge = (props: SvgProps) => ( 4 | 5 | 11 | 12 | ); 13 | 14 | export default SvgCopyLarge; 15 | -------------------------------------------------------------------------------- /src/assets/LogoLockup.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { G, Path, Defs, ClipPath, SvgProps } from 'react-native-svg'; 2 | 3 | const SvgLogoLockup = (props: SvgProps) => ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | 16 | export default SvgLogoLockup; 17 | -------------------------------------------------------------------------------- /src/assets/QRCode.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path, SvgProps } from 'react-native-svg'; 2 | 3 | const SvgQrCode = (props: SvgProps) => ( 4 | 5 | 9 | 15 | 19 | 20 | ); 21 | 22 | export default SvgQrCode; 23 | -------------------------------------------------------------------------------- /src/assets/Retry.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path, SvgProps } from 'react-native-svg'; 2 | 3 | const SvgRetry = (props: SvgProps) => ( 4 | 5 | 9 | 10 | ); 11 | export default SvgRetry; 12 | -------------------------------------------------------------------------------- /src/assets/Search.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path, SvgProps } from 'react-native-svg'; 2 | const SvgSearch = (props: SvgProps) => ( 3 | 4 | 10 | 11 | ); 12 | export default SvgSearch; 13 | -------------------------------------------------------------------------------- /src/assets/WCLogo.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path, SvgProps } from 'react-native-svg'; 2 | const SvgLogo = (props: SvgProps) => ( 3 | 4 | 8 | 9 | ); 10 | export default SvgLogo; 11 | -------------------------------------------------------------------------------- /src/assets/WalletIcon.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path, SvgProps } from 'react-native-svg'; 2 | const SvgWalletIcon = (props: SvgProps) => ( 3 | 4 | 10 | 16 | 17 | ); 18 | export default SvgWalletIcon; 19 | -------------------------------------------------------------------------------- /src/assets/Warning.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path, SvgProps } from 'react-native-svg'; 2 | const SvgWarning = (props: SvgProps) => ( 3 | 4 | 8 | 14 | 15 | ); 16 | export default SvgWarning; 17 | -------------------------------------------------------------------------------- /src/components/Image.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { 3 | Animated, 4 | Image as NativeImage, 5 | ImageProps as NativeProps, 6 | Platform, 7 | } from 'react-native'; 8 | 9 | export type ImageProps = Omit & { 10 | source: string; 11 | headers?: Record; 12 | }; 13 | 14 | function Image({ source, headers, style, ...rest }: ImageProps) { 15 | const opacity = useRef(new Animated.Value(0)); 16 | const isIOS = Platform.OS === 'ios'; 17 | 18 | // Fade in image on load for iOS. Android does this by default. 19 | const onLoadEnd = () => { 20 | Animated.timing(opacity.current, { 21 | toValue: 1, 22 | duration: 200, 23 | useNativeDriver: true, 24 | }).start(); 25 | }; 26 | 27 | return isIOS ? ( 28 | 34 | ) : ( 35 | 36 | ); 37 | } 38 | 39 | export default Image; 40 | -------------------------------------------------------------------------------- /src/components/ModalBackcard.tsx: -------------------------------------------------------------------------------- 1 | import { Platform, SafeAreaView, StyleSheet, View } from 'react-native'; 2 | 3 | import WCLogo from '../assets/LogoLockup'; 4 | import CloseIcon from '../assets/Close'; 5 | import useTheme from '../hooks/useTheme'; 6 | import Touchable from './Touchable'; 7 | 8 | interface Props { 9 | onClose: () => void; 10 | } 11 | 12 | export function ModalBackcard({ onClose }: Props) { 13 | const Theme = useTheme(); 14 | 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | const styles = StyleSheet.create({ 37 | placeholder: { 38 | borderTopLeftRadius: 8, 39 | borderTopRightRadius: 8, 40 | height: 80, 41 | width: '100%', 42 | position: 'absolute', 43 | }, 44 | container: { 45 | borderTopLeftRadius: 8, 46 | borderTopRightRadius: 8, 47 | height: 46, 48 | flexDirection: 'row', 49 | alignItems: 'center', 50 | justifyContent: 'space-between', 51 | marginHorizontal: 10, 52 | }, 53 | row: { 54 | flexDirection: 'row', 55 | }, 56 | buttonContainer: { 57 | height: 28, 58 | width: 28, 59 | borderRadius: 14, 60 | display: 'flex', 61 | justifyContent: 'center', 62 | alignItems: 'center', 63 | ...Platform.select({ 64 | ios: { 65 | shadowColor: 'rgba(0, 0, 0, 0.12)', 66 | shadowOpacity: 1, 67 | shadowOffset: { width: 0, height: 4 }, 68 | }, 69 | android: { 70 | borderColor: 'rgba(0, 0, 0, 0.12)', 71 | borderWidth: 1, 72 | elevation: 4, 73 | }, 74 | }), 75 | }, 76 | disconnectButton: { 77 | marginRight: 16, 78 | }, 79 | }); 80 | 81 | export default ModalBackcard; 82 | -------------------------------------------------------------------------------- /src/components/QRCode.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useMemo } from 'react'; 2 | import { Svg } from 'react-native-svg'; 3 | import { View, StyleSheet } from 'react-native'; 4 | 5 | import { QRCodeUtil } from '../utils/QRCodeUtil'; 6 | import WCLogo from '../assets/WCLogo'; 7 | import { DarkTheme, LightTheme } from '../constants/Colors'; 8 | 9 | interface Props { 10 | uri: string; 11 | size: number; 12 | theme?: 'light' | 'dark'; 13 | } 14 | 15 | function QRCode({ uri, size, theme = 'light' }: Props) { 16 | const Theme = theme === 'light' ? LightTheme : DarkTheme; 17 | 18 | const dots = useMemo( 19 | () => QRCodeUtil.generate(uri, size, size / 4, 'light'), 20 | [uri, size] 21 | ); 22 | 23 | return ( 24 | 27 | 28 | {dots} 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | const styles = StyleSheet.create({ 36 | container: { 37 | alignItems: 'center', 38 | justifyContent: 'center', 39 | borderRadius: 32, 40 | padding: 16, 41 | alignSelf: 'center', 42 | }, 43 | logo: { 44 | position: 'absolute', 45 | }, 46 | }); 47 | 48 | export default memo(QRCode); 49 | -------------------------------------------------------------------------------- /src/components/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import { 3 | Pressable, 4 | StyleProp, 5 | StyleSheet, 6 | TextInput, 7 | ViewStyle, 8 | } from 'react-native'; 9 | 10 | import useTheme from '../hooks/useTheme'; 11 | import SearchIcon from '../assets/Search'; 12 | 13 | interface Props { 14 | onChangeText: (text: string) => void; 15 | style?: StyleProp; 16 | } 17 | 18 | function SearchBar({ onChangeText, style }: Props) { 19 | const Theme = useTheme(); 20 | const inputRef = useRef(null); 21 | const [focused, setFocused] = useState(false); 22 | 23 | return ( 24 | inputRef.current?.focus()} 26 | style={[ 27 | styles.container, 28 | { 29 | backgroundColor: Theme.background3, 30 | borderColor: focused ? Theme.accent : Theme.overlayThin, 31 | }, 32 | style, 33 | ]} 34 | > 35 | 36 | setFocused(true)} 51 | onBlur={() => setFocused(false)} 52 | /> 53 | 54 | ); 55 | } 56 | 57 | const styles = StyleSheet.create({ 58 | container: { 59 | flex: 1, 60 | borderRadius: 100, 61 | height: 28, 62 | padding: 4, 63 | borderWidth: 1, 64 | alignItems: 'center', 65 | flexDirection: 'row', 66 | }, 67 | icon: { 68 | marginHorizontal: 8, 69 | }, 70 | input: { 71 | flex: 1, 72 | padding: 0, 73 | fontSize: 14, 74 | }, 75 | }); 76 | 77 | export default SearchBar; 78 | -------------------------------------------------------------------------------- /src/components/Shimmer.tsx: -------------------------------------------------------------------------------- 1 | import { Svg, Rect } from 'react-native-svg'; 2 | import { Animated, type StyleProp, type ViewStyle } from 'react-native'; 3 | import useTheme from '../hooks/useTheme'; 4 | 5 | const AnimatedRect = Animated.createAnimatedComponent(Rect); 6 | 7 | export interface ShimmerProps { 8 | width?: number; 9 | height?: number; 10 | duration?: number; 11 | borderRadius?: number; 12 | backgroundColor?: string; 13 | foregroundColor?: string; 14 | style?: StyleProp; 15 | } 16 | 17 | export const Shimmer = ({ 18 | width = 200, 19 | height = 200, 20 | duration = 1000, 21 | borderRadius = 0, 22 | backgroundColor, 23 | foregroundColor, 24 | style, 25 | }: ShimmerProps) => { 26 | const animatedValue = new Animated.Value(0); 27 | const Theme = useTheme(); 28 | 29 | const animatedProps = { 30 | fill: animatedValue.interpolate({ 31 | inputRange: [0, 0.5, 1], 32 | outputRange: [ 33 | backgroundColor ?? Theme.background2, 34 | foregroundColor ?? Theme.background3, 35 | backgroundColor ?? Theme.background3, 36 | ], 37 | }), 38 | width, 39 | height, 40 | x: 0, 41 | y: 0, 42 | rx: borderRadius, 43 | ry: borderRadius, 44 | }; 45 | 46 | Animated.loop( 47 | Animated.timing(animatedValue, { 48 | toValue: 1, 49 | duration, 50 | useNativeDriver: false, 51 | }) 52 | ).start(); 53 | 54 | return ( 55 | 56 | 57 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/components/Text.tsx: -------------------------------------------------------------------------------- 1 | import { StyleProp, Text as RNText, TextStyle, TextProps } from 'react-native'; 2 | import useTheme from '../hooks/useTheme'; 3 | 4 | interface Props extends TextProps { 5 | children: React.ReactNode; 6 | style?: StyleProp; 7 | } 8 | 9 | function Text({ children, style, ...props }: Props) { 10 | const Theme = useTheme(); 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | } 17 | 18 | export default Text; 19 | -------------------------------------------------------------------------------- /src/components/Toast.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo } from 'react'; 2 | import { Animated, Platform, StyleSheet } from 'react-native'; 3 | import { useSnapshot } from 'valtio'; 4 | 5 | import useTheme from '../hooks/useTheme'; 6 | import { ToastCtrl } from '../controllers/ToastCtrl'; 7 | import Checkmark from '../assets/Checkmark'; 8 | import Warning from '../assets/Warning'; 9 | import Text from './Text'; 10 | 11 | function Toast() { 12 | const Theme = useTheme(); 13 | const { open, message, variant } = useSnapshot(ToastCtrl.state); 14 | const toastOpacity = useMemo(() => new Animated.Value(0), []); 15 | const Icon = variant === 'success' ? Checkmark : Warning; 16 | const iconColor = variant === 'success' ? Theme.accent : Theme.negative; 17 | 18 | useEffect(() => { 19 | if (open) { 20 | Animated.timing(toastOpacity, { 21 | toValue: 1, 22 | duration: 150, 23 | useNativeDriver: true, 24 | }).start(); 25 | setTimeout(() => { 26 | Animated.timing(toastOpacity, { 27 | toValue: 0, 28 | duration: 300, 29 | useNativeDriver: true, 30 | }).start(() => { 31 | ToastCtrl.closeToast(); 32 | }); 33 | }, 2200); 34 | } 35 | }, [open, toastOpacity]); 36 | 37 | return open ? ( 38 | 48 | 49 | 50 | {message} 51 | 52 | 53 | ) : null; 54 | } 55 | 56 | const styles = StyleSheet.create({ 57 | container: { 58 | position: 'absolute', 59 | flexDirection: 'row', 60 | borderWidth: 1, 61 | borderRadius: 20, 62 | padding: 9, 63 | paddingHorizontal: 16, 64 | marginHorizontal: 16, 65 | bottom: 25, 66 | alignSelf: 'center', 67 | alignItems: 'center', 68 | justifyContent: 'center', 69 | ...Platform.select({ 70 | ios: { 71 | shadowColor: 'rgba(0, 0, 0, 0.12)', 72 | shadowOpacity: 1, 73 | shadowOffset: { width: 0, height: 4 }, 74 | }, 75 | android: { 76 | borderColor: 'rgba(0, 0, 0, 0.12)', 77 | borderWidth: 1, 78 | elevation: 4, 79 | }, 80 | }), 81 | }, 82 | icon: { 83 | marginRight: 6, 84 | }, 85 | text: { 86 | fontWeight: '600', 87 | }, 88 | }); 89 | 90 | export default Toast; 91 | -------------------------------------------------------------------------------- /src/components/Touchable.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { 3 | Animated, 4 | StyleSheet, 5 | TouchableOpacity, 6 | TouchableOpacityProps, 7 | } from 'react-native'; 8 | 9 | const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity); 10 | 11 | function Touchable({ 12 | children, 13 | onPress, 14 | style, 15 | ...props 16 | }: TouchableOpacityProps) { 17 | const scale = useRef(new Animated.Value(1)).current; 18 | const styles = StyleSheet.flatten([style]); 19 | 20 | const onPressIn = () => { 21 | Animated.spring(scale, { 22 | toValue: 0.95, 23 | useNativeDriver: true, 24 | }).start(); 25 | }; 26 | 27 | const onPressOut = () => { 28 | Animated.spring(scale, { 29 | toValue: 1, 30 | useNativeDriver: true, 31 | }).start(); 32 | }; 33 | 34 | return ( 35 | 43 | {children} 44 | 45 | ); 46 | } 47 | 48 | export default Touchable; 49 | -------------------------------------------------------------------------------- /src/components/ViewAllBox.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, View, Text, StyleProp, ViewStyle } from 'react-native'; 2 | 3 | import useTheme from '../hooks/useTheme'; 4 | import type { WcWallet } from '../types/controllerTypes'; 5 | import Touchable from './Touchable'; 6 | import { WALLET_HEIGHT, WALLET_WIDTH, WALLET_MARGIN } from './WalletItem'; 7 | import WalletImage from './WalletImage'; 8 | import { AssetUtil } from '../utils/AssetUtil'; 9 | import { ApiCtrl } from '../controllers/ApiCtrl'; 10 | 11 | interface Props { 12 | onPress: any; 13 | wallets: WcWallet[]; 14 | style?: StyleProp; 15 | } 16 | 17 | function ViewAllBox({ onPress, wallets, style }: Props) { 18 | const Theme = useTheme(); 19 | const _wallets = wallets.slice(0, 4); 20 | const _emptyBoxes = Array.from(Array(4 - _wallets.length).keys()); 21 | 22 | return ( 23 | 24 | 25 | 26 | {_wallets.map((wallet) => ( 27 | 34 | ))} 35 | {_emptyBoxes.map((_, i) => ( 36 | 37 | ))} 38 | 39 | 40 | 41 | 45 | View All 46 | 47 | 48 | 49 | ); 50 | } 51 | 52 | const styles = StyleSheet.create({ 53 | container: { 54 | width: WALLET_WIDTH, 55 | height: WALLET_HEIGHT, 56 | alignItems: 'center', 57 | marginVertical: WALLET_MARGIN, 58 | }, 59 | icons: { 60 | height: 60, 61 | width: 60, 62 | borderRadius: 16, 63 | borderWidth: 1, 64 | justifyContent: 'center', 65 | alignItems: 'center', 66 | }, 67 | icon: { 68 | margin: 1, 69 | }, 70 | row: { 71 | flexDirection: 'row', 72 | flexWrap: 'wrap', 73 | justifyContent: 'center', 74 | alignItems: 'center', 75 | }, 76 | text: { 77 | marginTop: 5, 78 | maxWidth: 100, 79 | fontWeight: '600', 80 | fontSize: 12, 81 | textAlign: 'center', 82 | }, 83 | }); 84 | 85 | export default ViewAllBox; 86 | -------------------------------------------------------------------------------- /src/components/WalletImage.tsx: -------------------------------------------------------------------------------- 1 | import { ImageStyle, StyleProp, StyleSheet, View } from 'react-native'; 2 | import useTheme from '../hooks/useTheme'; 3 | import WalletIcon from '../assets/WalletIcon'; 4 | import Image from './Image'; 5 | 6 | interface Props { 7 | size: 'xs' | 'sm' | 'md' | 'lg'; 8 | url?: string; 9 | imageHeaders?: Record; 10 | style?: StyleProp; 11 | } 12 | 13 | const sizeMap = { 14 | xs: 23, 15 | sm: 30, 16 | md: 60, 17 | lg: 90, 18 | }; 19 | 20 | function WalletImage({ url, imageHeaders, size, style }: Props) { 21 | const Theme = useTheme(); 22 | const sizeNum = sizeMap[size]; 23 | 24 | return url ? ( 25 | 39 | ) : ( 40 | 54 | 55 | 56 | ); 57 | } 58 | 59 | const styles = StyleSheet.create({ 60 | icon: { 61 | borderWidth: 1, 62 | }, 63 | placeholderIcon: { 64 | alignItems: 'center', 65 | justifyContent: 'center', 66 | borderStyle: 'dashed', 67 | borderWidth: 1, 68 | }, 69 | }); 70 | 71 | export default WalletImage; 72 | -------------------------------------------------------------------------------- /src/components/WalletItem.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { Text, StyleSheet, StyleProp, ViewStyle } from 'react-native'; 3 | 4 | import useTheme from '../hooks/useTheme'; 5 | import { UiUtil } from '../utils/UiUtil'; 6 | import Touchable from './Touchable'; 7 | import WalletImage from './WalletImage'; 8 | import { useSnapshot } from 'valtio'; 9 | import { ApiCtrl } from '../controllers/ApiCtrl'; 10 | 11 | interface Props { 12 | id: string; 13 | name: string; 14 | imageUrl?: string; 15 | onPress?: () => void; 16 | style?: StyleProp; 17 | isRecent?: boolean; 18 | } 19 | 20 | export const WALLET_MARGIN = 8; 21 | export const WALLET_WIDTH = 80; 22 | export const WALLET_HEIGHT = 98; 23 | export const WALLET_FULL_HEIGHT = WALLET_HEIGHT + WALLET_MARGIN * 2; 24 | 25 | function _WalletItem({ id, name, imageUrl, style, isRecent, onPress }: Props) { 26 | const Theme = useTheme(); 27 | const imageHeaders = ApiCtrl._getApiHeaders(); 28 | 29 | const { installed } = useSnapshot(ApiCtrl.state); 30 | const isInstalled = !!installed?.find((wallet) => wallet.id === id); 31 | 32 | return ( 33 | 34 | 35 | 39 | {UiUtil.getWalletName(name, true)} 40 | 41 | {(isRecent || isInstalled) && ( 42 | 43 | {isRecent ? 'RECENT' : 'INSTALLED'} 44 | 45 | )} 46 | 47 | ); 48 | } 49 | 50 | const styles = StyleSheet.create({ 51 | container: { 52 | width: WALLET_WIDTH, 53 | height: WALLET_HEIGHT, 54 | alignItems: 'center', 55 | marginVertical: WALLET_MARGIN, 56 | }, 57 | name: { 58 | marginTop: 5, 59 | maxWidth: 100, 60 | fontSize: 12, 61 | fontWeight: '600', 62 | textAlign: 'center', 63 | marginBottom: 2, 64 | }, 65 | status: { 66 | fontSize: 10, 67 | fontWeight: '700', 68 | textAlign: 'center', 69 | }, 70 | }); 71 | 72 | export const WalletItem = memo(_WalletItem, (prevProps, nextProps) => { 73 | return prevProps.name === nextProps.name; 74 | }); 75 | 76 | export default WalletItem; 77 | -------------------------------------------------------------------------------- /src/components/WalletItemLoader.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, type StyleProp, type ViewStyle, View } from 'react-native'; 2 | 3 | import { Shimmer } from './Shimmer'; 4 | import { WALLET_HEIGHT, WALLET_WIDTH } from './WalletItem'; 5 | 6 | export const WalletItemLoaderHeight = 98; 7 | 8 | export interface WalletItemLoaderProps { 9 | style?: StyleProp; 10 | } 11 | 12 | export function WalletItemLoader({ style }: WalletItemLoaderProps) { 13 | return ( 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | const styles = StyleSheet.create({ 22 | container: { 23 | height: WALLET_HEIGHT, 24 | alignItems: 'center', 25 | justifyContent: 'center', 26 | width: WALLET_WIDTH, 27 | borderRadius: 16, 28 | }, 29 | text: { 30 | marginTop: 8, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /src/components/WalletLoadingThumbnail.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { Animated, Easing, StyleSheet, View } from 'react-native'; 3 | import Svg, { Rect } from 'react-native-svg'; 4 | 5 | import useTheme from '../hooks/useTheme'; 6 | 7 | const AnimatedRect = Animated.createAnimatedComponent(Rect); 8 | 9 | interface Props { 10 | children?: React.ReactNode; 11 | imageSize?: number; 12 | showError?: boolean; 13 | } 14 | 15 | function WalletLoadingThumbnail({ children, showError }: Props) { 16 | const Theme = useTheme(); 17 | const spinValue = useRef(new Animated.Value(0)); 18 | 19 | useEffect(() => { 20 | const animation = Animated.timing(spinValue.current, { 21 | toValue: 1, 22 | duration: 1150, 23 | useNativeDriver: true, 24 | easing: Easing.linear, 25 | }); 26 | 27 | const loop = Animated.loop(animation); 28 | loop.start(); 29 | 30 | return () => { 31 | loop.stop(); 32 | }; 33 | }, [spinValue]); 34 | 35 | const spin = spinValue.current.interpolate({ 36 | inputRange: [0, 1], 37 | outputRange: [0, -371], 38 | }); 39 | 40 | return ( 41 | 42 | 43 | 55 | 56 | {showError && ( 57 | 65 | )} 66 | {children} 67 | 68 | ); 69 | } 70 | 71 | const styles = StyleSheet.create({ 72 | container: { 73 | alignItems: 'center', 74 | justifyContent: 'center', 75 | }, 76 | loader: { 77 | position: 'absolute', 78 | }, 79 | error: { 80 | position: 'absolute', 81 | borderWidth: 2, 82 | height: 106, 83 | width: 106, 84 | borderRadius: 31, 85 | }, 86 | }); 87 | 88 | export default WalletLoadingThumbnail; 89 | -------------------------------------------------------------------------------- /src/config/animations.ts: -------------------------------------------------------------------------------- 1 | import { Platform, UIManager } from 'react-native'; 2 | 3 | if (Platform.OS === 'android') { 4 | if (UIManager.setLayoutAnimationEnabledExperimental) { 5 | UIManager.setLayoutAnimationEnabledExperimental(true); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/constants/Colors.ts: -------------------------------------------------------------------------------- 1 | export const DarkTheme = { 2 | accent: '#47A1FF', 3 | foreground1: '#E4E7E7', 4 | foreground2: '#949E9E', 5 | foreground3: '#6E7777', 6 | foregroundInverse: '#FFFFFF', 7 | negative: '#F25A67', 8 | background1: '#141414', 9 | background2: '#272A2A', 10 | background3: '#3B4040', 11 | overlayThin: 'rgba(255, 255, 255, 0.1)', 12 | overlayThick: 'rgba(255, 255, 255, 0.3)', 13 | glass: 'rgb(39, 42, 42)', 14 | }; 15 | 16 | export const LightTheme = { 17 | accent: '#3396FF', 18 | foreground1: '#141414', 19 | foreground2: '#798686', 20 | foreground3: '#9EA9A9', 21 | foregroundInverse: '#000000', 22 | negative: '#F05142', 23 | background1: '#FFFFFF', 24 | background2: '#F1F3F3', 25 | background3: '#E4E7E7', 26 | overlayThin: 'rgba(0, 0, 0, 0.1)', 27 | overlayThick: 'rgba(0, 0, 0, 0.3)', 28 | glass: 'rgb(244, 245, 245)', 29 | }; 30 | -------------------------------------------------------------------------------- /src/constants/Config.ts: -------------------------------------------------------------------------------- 1 | import type { ISessionParams } from '../types/coreTypes'; 2 | 3 | const DEFAULT_CHAINS = ['eip155:1']; 4 | const REQUIRED_METHODS = ['eth_sendTransaction', 'personal_sign']; 5 | const REQUIRED_EVENTS = ['chainChanged', 'accountsChanged']; 6 | 7 | // DO NOT REMOVE, SHOULD MATCH CORE PACKAGE VERSION 8 | export const CORE_VERSION = '2.11.0'; 9 | 10 | // DO NOT REMOVE, SHOULD MATCH SDK PACKAGE VERSION 11 | export const SDK_VERSION = '1.1.0'; 12 | 13 | export const defaultSessionParams: ISessionParams = { 14 | namespaces: { 15 | eip155: { 16 | methods: REQUIRED_METHODS, 17 | chains: DEFAULT_CHAINS, 18 | events: REQUIRED_EVENTS, 19 | rpcMap: {}, 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/controllers/AccountCtrl.ts: -------------------------------------------------------------------------------- 1 | import { proxy } from 'valtio'; 2 | 3 | import { ClientCtrl } from './ClientCtrl'; 4 | 5 | // -- Types --------------------------------------------- // 6 | export interface AccountCtrlState { 7 | address?: string; 8 | isConnected: boolean; 9 | } 10 | 11 | // -- State --------------------------------------------- // 12 | const state = proxy({ 13 | address: undefined, 14 | isConnected: false, 15 | }); 16 | 17 | // -- Controller ---------------------------------------- // 18 | export const AccountCtrl = { 19 | state, 20 | 21 | async getAccount() { 22 | const provider = ClientCtrl.state.provider; 23 | const accounts: string[] | undefined = await provider?.request({ 24 | method: 'eth_accounts', 25 | }); 26 | 27 | if (accounts) { 28 | state.address = accounts[0]; 29 | state.isConnected = true; 30 | } 31 | }, 32 | 33 | setAddress(address: AccountCtrlState['address']) { 34 | state.address = address; 35 | }, 36 | 37 | setIsConnected(isConnected: AccountCtrlState['isConnected']) { 38 | state.isConnected = isConnected; 39 | }, 40 | 41 | resetAccount() { 42 | state.address = undefined; 43 | state.isConnected = false; 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/controllers/ApiCtrl.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | import { subscribeKey as subKey } from 'valtio/utils'; 3 | import { proxy } from 'valtio/vanilla'; 4 | import { CoreHelperUtil } from '../utils/CoreHelperUtil'; 5 | import { FetchUtil } from '../utils/FetchUtil'; 6 | import { StorageUtil } from '../utils/StorageUtil'; 7 | import type { 8 | ApiGetDataWalletsResponse, 9 | ApiGetWalletsRequest, 10 | ApiGetWalletsResponse, 11 | WcWallet, 12 | } from '../types/controllerTypes'; 13 | import { ConfigCtrl } from './ConfigCtrl'; 14 | import { AssetCtrl } from './AssetCtrl'; 15 | import { SDK_VERSION } from '../constants/Config'; 16 | 17 | // -- Helpers ------------------------------------------- // 18 | const baseUrl = CoreHelperUtil.getApiUrl(); 19 | const api = new FetchUtil({ baseUrl }); 20 | const defaultEntries = '48'; 21 | const recommendedEntries = '8'; 22 | const sdkType = 'wcm'; 23 | 24 | // -- Types --------------------------------------------- // 25 | export interface ApiCtrlState { 26 | prefetchPromise?: Promise; 27 | sdkVersion: `modal-react-native-${string}`; 28 | page: number; 29 | count: number; 30 | recommended: WcWallet[]; 31 | installed: WcWallet[]; 32 | wallets: WcWallet[]; 33 | search: WcWallet[]; 34 | } 35 | 36 | type StateKey = keyof ApiCtrlState; 37 | 38 | // -- State --------------------------------------------- // 39 | const state = proxy({ 40 | sdkVersion: `modal-react-native-${SDK_VERSION}`, 41 | page: 1, 42 | count: 0, 43 | recommended: [], 44 | wallets: [], 45 | search: [], 46 | installed: [], 47 | }); 48 | 49 | // -- Controller ---------------------------------------- // 50 | export const ApiCtrl = { 51 | state, 52 | 53 | platform() { 54 | return Platform.select({ default: 'ios', android: 'android' }); 55 | }, 56 | 57 | subscribeKey( 58 | key: K, 59 | callback: (value: ApiCtrlState[K]) => void 60 | ) { 61 | return subKey(state, key, callback); 62 | }, 63 | 64 | setSdkVersion(sdkVersion: ApiCtrlState['sdkVersion']) { 65 | state.sdkVersion = sdkVersion; 66 | }, 67 | 68 | _getApiHeaders() { 69 | return { 70 | 'x-project-id': ConfigCtrl.state.projectId, 71 | 'x-sdk-type': sdkType, 72 | 'x-sdk-version': state.sdkVersion, 73 | }; 74 | }, 75 | 76 | async _fetchWalletImage(imageId: string) { 77 | const imageUrl = `${api.baseUrl}/getWalletImage/${imageId}`; 78 | AssetCtrl.setWalletImage(imageId, imageUrl); 79 | }, 80 | 81 | async fetchInstalledWallets() { 82 | const path = Platform.select({ 83 | default: 'getIosData', 84 | android: 'getAndroidData', 85 | }); 86 | const { data: walletData } = await api.get({ 87 | path, 88 | headers: ApiCtrl._getApiHeaders(), 89 | }); 90 | 91 | const promises = walletData.map(async (item) => { 92 | return { 93 | id: item.id, 94 | isInstalled: await CoreHelperUtil.checkInstalled(item), 95 | }; 96 | }); 97 | 98 | const results = await Promise.all(promises); 99 | const installed = results 100 | .filter(({ isInstalled }) => isInstalled) 101 | .map(({ id }) => id); 102 | 103 | const { explorerExcludedWalletIds } = ConfigCtrl.state; 104 | const excludeWalletIds = CoreHelperUtil.isArray(explorerExcludedWalletIds) 105 | ? explorerExcludedWalletIds 106 | : []; 107 | 108 | if (installed.length > 0) { 109 | const { data } = await api.get({ 110 | path: '/getWallets', 111 | headers: ApiCtrl._getApiHeaders(), 112 | params: { 113 | page: '1', 114 | platform: this.platform(), 115 | entries: installed?.length.toString(), 116 | include: installed?.join(','), 117 | exclude: excludeWalletIds?.join(','), 118 | }, 119 | }); 120 | 121 | const walletImages = data.map((d) => d.image_id).filter(Boolean); 122 | await Promise.allSettled( 123 | (walletImages as string[]).map((id) => ApiCtrl._fetchWalletImage(id)) 124 | ); 125 | 126 | state.installed = data; 127 | } 128 | }, 129 | 130 | async fetchRecommendedWallets() { 131 | const { installed } = ApiCtrl.state; 132 | const { explorerRecommendedWalletIds, explorerExcludedWalletIds } = 133 | ConfigCtrl.state; 134 | const excludeWalletIds = CoreHelperUtil.isArray(explorerExcludedWalletIds) 135 | ? explorerExcludedWalletIds 136 | : []; 137 | 138 | const includeWalletIds = CoreHelperUtil.isArray( 139 | explorerRecommendedWalletIds 140 | ) 141 | ? explorerRecommendedWalletIds 142 | : []; 143 | 144 | const exclude = [ 145 | ...installed.map(({ id }) => id), 146 | ...(excludeWalletIds ?? []), 147 | ].filter(Boolean); 148 | 149 | const { data, count } = await api.get({ 150 | path: '/getWallets', 151 | headers: ApiCtrl._getApiHeaders(), 152 | params: { 153 | page: '1', 154 | platform: this.platform(), 155 | entries: recommendedEntries, 156 | include: includeWalletIds?.join(','), 157 | exclude: exclude?.join(','), 158 | }, 159 | }); 160 | 161 | const recent = await StorageUtil.getRecentWallet(); 162 | const recommendedImages = data.map((d) => d.image_id).filter(Boolean); 163 | await Promise.allSettled( 164 | ( 165 | [...recommendedImages, recent ? recent.image_id : undefined] as string[] 166 | ).map((id) => ApiCtrl._fetchWalletImage(id)) 167 | ); 168 | state.recommended = data; 169 | state.count = count ?? 0; 170 | }, 171 | 172 | async fetchWallets({ page }: Pick) { 173 | const { explorerExcludedWalletIds } = ConfigCtrl.state; 174 | const excludedIds = CoreHelperUtil.isArray(explorerExcludedWalletIds) 175 | ? explorerExcludedWalletIds 176 | : []; 177 | 178 | const exclude = [ 179 | ...state.installed.map(({ id }) => id), 180 | ...state.recommended.map(({ id }) => id), 181 | ...(excludedIds ?? []), 182 | ].filter(Boolean); 183 | const { data, count } = await api.get({ 184 | path: '/getWallets', 185 | headers: ApiCtrl._getApiHeaders(), 186 | params: { 187 | page: String(page), 188 | platform: this.platform(), 189 | entries: String(defaultEntries), 190 | 191 | exclude: exclude.join(','), 192 | }, 193 | }); 194 | 195 | const images = data.map((w) => w.image_id).filter(Boolean); 196 | await Promise.allSettled([ 197 | ...(images as string[]).map((id) => ApiCtrl._fetchWalletImage(id)), 198 | CoreHelperUtil.wait(300), 199 | ]); 200 | state.wallets = [...state.wallets, ...data]; 201 | state.count = count > state.count ? count : state.count; 202 | state.page = page; 203 | }, 204 | 205 | async searchWallet({ search }: Pick) { 206 | const { explorerExcludedWalletIds } = ConfigCtrl.state; 207 | const excludeWalletIds = CoreHelperUtil.isArray(explorerExcludedWalletIds) 208 | ? explorerExcludedWalletIds 209 | : []; 210 | state.search = []; 211 | const { data } = await api.get({ 212 | path: '/getWallets', 213 | headers: ApiCtrl._getApiHeaders(), 214 | params: { 215 | page: '1', 216 | platform: this.platform(), 217 | entries: String(defaultEntries), 218 | search, 219 | exclude: excludeWalletIds?.join(','), 220 | }, 221 | }); 222 | const images = data.map((w) => w.image_id).filter(Boolean); 223 | await Promise.allSettled([ 224 | ...(images as string[]).map((id) => ApiCtrl._fetchWalletImage(id)), 225 | CoreHelperUtil.wait(300), 226 | ]); 227 | state.search = data; 228 | }, 229 | 230 | async prefetch() { 231 | await ApiCtrl.fetchInstalledWallets(); 232 | 233 | state.prefetchPromise = Promise.race([ 234 | Promise.allSettled([ApiCtrl.fetchRecommendedWallets()]), 235 | CoreHelperUtil.wait(3000), 236 | ]); 237 | }, 238 | }; 239 | -------------------------------------------------------------------------------- /src/controllers/AssetCtrl.ts: -------------------------------------------------------------------------------- 1 | import { proxy } from 'valtio'; 2 | 3 | // -- Types --------------------------------------------- // 4 | export interface AssetCtrlState { 5 | walletImages: Record; 6 | } 7 | 8 | // -- State --------------------------------------------- // 9 | const state = proxy({ 10 | walletImages: {}, 11 | }); 12 | 13 | // -- Controller ---------------------------------------- // 14 | export const AssetCtrl = { 15 | state, 16 | 17 | setWalletImage(key: string, value: string) { 18 | state.walletImages[key] = value; 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/controllers/ClientCtrl.ts: -------------------------------------------------------------------------------- 1 | import { proxy, ref } from 'valtio'; 2 | import type { IProvider } from '../types/coreTypes'; 3 | 4 | // -- Types ---------------------------------------- // 5 | export interface ClientCtrlState { 6 | initialized: boolean; 7 | provider?: IProvider; 8 | sessionTopic?: string; 9 | } 10 | 11 | // -- State ---------------------------------------- // 12 | const state = proxy({ 13 | initialized: false, 14 | provider: undefined, 15 | sessionTopic: undefined, 16 | }); 17 | 18 | // -- Controller ---------------------------------------- // 19 | export const ClientCtrl = { 20 | state, 21 | 22 | setProvider(provider: ClientCtrlState['provider']) { 23 | if (!state.initialized && provider) { 24 | state.provider = ref(provider); 25 | } 26 | }, 27 | 28 | setInitialized(initialized: ClientCtrlState['initialized']) { 29 | state.initialized = initialized; 30 | }, 31 | 32 | setSessionTopic(topic: ClientCtrlState['sessionTopic']) { 33 | if (topic && state.provider) { 34 | state.sessionTopic = topic; 35 | } 36 | }, 37 | 38 | provider() { 39 | return state.provider; 40 | }, 41 | 42 | sessionTopic() { 43 | return state.sessionTopic; 44 | }, 45 | 46 | resetSession() { 47 | state.sessionTopic = undefined; 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/controllers/ConfigCtrl.ts: -------------------------------------------------------------------------------- 1 | import { proxy } from 'valtio'; 2 | 3 | import type { WcWallet } from '../types/controllerTypes'; 4 | import { StorageUtil } from '../utils/StorageUtil'; 5 | import type { IProviderMetadata, ISessionParams } from '../types/coreTypes'; 6 | 7 | // -- Types ---------------------------------------- // 8 | export interface ConfigCtrlState { 9 | projectId: string; 10 | sessionParams?: ISessionParams; 11 | recentWallet?: WcWallet; 12 | providerMetadata?: IProviderMetadata; 13 | explorerRecommendedWalletIds?: string[] | 'NONE'; 14 | explorerExcludedWalletIds?: string[] | 'ALL'; 15 | } 16 | 17 | // -- State ---------------------------------------- // 18 | const state = proxy({ 19 | projectId: '', 20 | sessionParams: undefined, 21 | recentWallet: undefined, 22 | providerMetadata: undefined, 23 | explorerRecommendedWalletIds: undefined, 24 | explorerExcludedWalletIds: undefined, 25 | }); 26 | 27 | // -- Controller ---------------------------------------- // 28 | export const ConfigCtrl = { 29 | state, 30 | 31 | setRecentWallet(wallet?: WcWallet) { 32 | state.recentWallet = wallet; 33 | }, 34 | 35 | getRecentWallet() { 36 | return state.recentWallet; 37 | }, 38 | 39 | getMetadata() { 40 | if (!state.providerMetadata) { 41 | throw new Error('Metadata not set'); 42 | } 43 | return state.providerMetadata; 44 | }, 45 | 46 | setConfig(config: Partial) { 47 | const { projectId, providerMetadata, sessionParams } = config; 48 | if (projectId && projectId !== state.projectId) { 49 | state.projectId = projectId; 50 | } 51 | 52 | if (providerMetadata && state.providerMetadata !== providerMetadata) { 53 | state.providerMetadata = providerMetadata; 54 | } 55 | 56 | if (sessionParams && sessionParams !== state.sessionParams) { 57 | state.sessionParams = sessionParams; 58 | } 59 | 60 | state.explorerRecommendedWalletIds = config.explorerRecommendedWalletIds; 61 | state.explorerExcludedWalletIds = config.explorerExcludedWalletIds; 62 | }, 63 | 64 | async loadRecentWallet() { 65 | state.recentWallet = await StorageUtil.getRecentWallet(); 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /src/controllers/ModalCtrl.ts: -------------------------------------------------------------------------------- 1 | import { proxy } from 'valtio'; 2 | 3 | import { ClientCtrl } from './ClientCtrl'; 4 | import { OptionsCtrl } from './OptionsCtrl'; 5 | import { AccountCtrl } from './AccountCtrl'; 6 | import { RouterCtrl } from './RouterCtrl'; 7 | import { WcConnectionCtrl } from './WcConnectionCtrl'; 8 | import { ConfigCtrl } from './ConfigCtrl'; 9 | import { CoreHelperUtil } from '../utils/CoreHelperUtil'; 10 | 11 | // -- Types ---------------------------------------- // 12 | export interface ModalCtrlState { 13 | open: boolean; 14 | } 15 | 16 | export interface OpenOptions { 17 | route?: 'ConnectWallet' | 'Qrcode' | 'WalletExplorer'; 18 | } 19 | 20 | // -- State ---------------------------------------- // 21 | const state = proxy({ 22 | open: false, 23 | }); 24 | 25 | // -- Controller ---------------------------------------- // 26 | export const ModalCtrl = { 27 | state, 28 | 29 | async open(options?: OpenOptions) { 30 | return new Promise((resolve) => { 31 | const { isDataLoaded } = OptionsCtrl.state; 32 | const { isConnected } = AccountCtrl.state; 33 | const { initialized } = ClientCtrl.state; 34 | const { explorerRecommendedWalletIds, explorerExcludedWalletIds } = 35 | ConfigCtrl.state; 36 | 37 | const explorerDisabled = 38 | explorerRecommendedWalletIds === 'NONE' || 39 | (explorerExcludedWalletIds === 'ALL' && 40 | !CoreHelperUtil.isArray(explorerRecommendedWalletIds)); 41 | 42 | WcConnectionCtrl.setPairingEnabled(true); 43 | 44 | if (isConnected) { 45 | // If already connected, do nothing 46 | return; 47 | } else if (options?.route) { 48 | RouterCtrl.replace(options.route); 49 | } else if (explorerDisabled) { 50 | RouterCtrl.replace('Qrcode'); 51 | } else { 52 | RouterCtrl.replace('ConnectWallet'); 53 | } 54 | 55 | // Open modal if async data is ready 56 | if (initialized && isDataLoaded) { 57 | state.open = true; 58 | resolve(); 59 | } 60 | // Otherwise (slow network) re-attempt open checks 61 | else { 62 | const interval = setInterval(() => { 63 | if (ClientCtrl.state.initialized && OptionsCtrl.state.isDataLoaded) { 64 | clearInterval(interval); 65 | state.open = true; 66 | resolve(); 67 | } 68 | }, 200); 69 | } 70 | }); 71 | }, 72 | 73 | close() { 74 | state.open = false; 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /src/controllers/OptionsCtrl.ts: -------------------------------------------------------------------------------- 1 | import { proxy } from 'valtio'; 2 | 3 | // -- Types ---------------------------------------- // 4 | export interface OptionsCtrlState { 5 | isDataLoaded: boolean; 6 | } 7 | 8 | // -- State ---------------------------------------- // 9 | const state = proxy({ 10 | isDataLoaded: false, 11 | }); 12 | 13 | // -- Controller ---------------------------------------- // 14 | export const OptionsCtrl = { 15 | state, 16 | 17 | setIsDataLoaded(isDataLoaded: OptionsCtrlState['isDataLoaded']) { 18 | state.isDataLoaded = isDataLoaded; 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/controllers/RouterCtrl.ts: -------------------------------------------------------------------------------- 1 | import { proxy } from 'valtio'; 2 | 3 | import { UiUtil } from '../utils/UiUtil'; 4 | import type { WcWallet } from '../types/controllerTypes'; 5 | 6 | // -- Types ---------------------------------------- // 7 | export type RouterView = 8 | | 'ConnectWallet' 9 | | 'Qrcode' 10 | | 'WalletExplorer' 11 | | 'Connecting'; 12 | 13 | export interface RouterCtrlState { 14 | history: RouterView[]; 15 | view: RouterView; 16 | data?: { 17 | wallet?: WcWallet; 18 | }; 19 | } 20 | 21 | // -- State ---------------------------------------- // 22 | const state = proxy({ 23 | history: ['ConnectWallet'], 24 | view: 'ConnectWallet', 25 | data: undefined, 26 | }); 27 | 28 | // -- Controller ---------------------------------------- // 29 | export const RouterCtrl = { 30 | state, 31 | 32 | push(view: RouterCtrlState['view'], data?: RouterCtrlState['data']) { 33 | if (view !== state.view) { 34 | UiUtil.layoutAnimation(); 35 | state.view = view; 36 | if (data) { 37 | state.data = data; 38 | } 39 | state.history.push(view); 40 | } 41 | }, 42 | 43 | replace(view: RouterCtrlState['view']) { 44 | state.view = view; 45 | state.history = [view]; 46 | }, 47 | 48 | goBack() { 49 | if (state.history.length > 1) { 50 | UiUtil.layoutAnimation(); 51 | state.history.pop(); 52 | const [last] = state.history.slice(-1); 53 | state.view = last || 'ConnectWallet'; 54 | } 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /src/controllers/ThemeCtrl.ts: -------------------------------------------------------------------------------- 1 | import { Appearance } from 'react-native'; 2 | import { proxy } from 'valtio'; 3 | 4 | // -- Types ---------------------------------------- // 5 | export interface ThemeCtrlState { 6 | themeMode?: 'dark' | 'light'; 7 | accentColor?: string; 8 | } 9 | 10 | // -- State ---------------------------------------- // 11 | const state = proxy({ 12 | themeMode: Appearance.getColorScheme() ?? 'light', 13 | accentColor: undefined, 14 | }); 15 | 16 | // -- Controller ---------------------------------------- // 17 | export const ThemeCtrl = { 18 | state, 19 | 20 | setThemeMode(themeMode?: ThemeCtrlState['themeMode'] | null) { 21 | state.themeMode = themeMode ?? Appearance.getColorScheme() ?? 'light'; 22 | }, 23 | 24 | setAccentColor(accentColor?: ThemeCtrlState['accentColor']) { 25 | state.accentColor = accentColor; 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/controllers/ToastCtrl.ts: -------------------------------------------------------------------------------- 1 | import { proxy } from 'valtio'; 2 | 3 | // -- Types ---------------------------------------- // 4 | export interface ToastCtrlState { 5 | open: boolean; 6 | message: string; 7 | variant: 'error' | 'success'; 8 | } 9 | 10 | // -- State ---------------------------------------- // 11 | const state = proxy({ 12 | open: false, 13 | message: '', 14 | variant: 'success', 15 | }); 16 | 17 | // -- Controller ---------------------------------------- // 18 | export const ToastCtrl = { 19 | state, 20 | 21 | openToast( 22 | message: ToastCtrlState['message'], 23 | variant: ToastCtrlState['variant'] 24 | ) { 25 | state.open = true; 26 | state.message = message; 27 | state.variant = variant; 28 | }, 29 | 30 | closeToast() { 31 | state.open = false; 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/controllers/WcConnectionCtrl.ts: -------------------------------------------------------------------------------- 1 | import { proxy } from 'valtio/vanilla'; 2 | 3 | // -- Types ---------------------------------------- // 4 | export interface WcConnectionCtrlState { 5 | pairingUri: string; 6 | pairingEnabled: boolean; 7 | pairingError: boolean; 8 | } 9 | 10 | // -- State ---------------------------------------- // 11 | const state = proxy({ 12 | pairingUri: '', 13 | pairingEnabled: false, 14 | pairingError: false, 15 | }); 16 | 17 | // -- Controller ---------------------------------------- // 18 | export const WcConnectionCtrl = { 19 | state, 20 | 21 | setPairingUri(pairingUri: WcConnectionCtrlState['pairingUri']) { 22 | state.pairingUri = pairingUri; 23 | }, 24 | 25 | setPairingError(pairingError: WcConnectionCtrlState['pairingError']) { 26 | state.pairingError = pairingError; 27 | }, 28 | 29 | setPairingEnabled(enabled: WcConnectionCtrlState['pairingEnabled']) { 30 | state.pairingEnabled = enabled; 31 | }, 32 | 33 | resetConnection() { 34 | state.pairingUri = ''; 35 | state.pairingError = false; 36 | state.pairingEnabled = false; 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/hooks/useConfigure.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react'; 2 | import { useColorScheme } from 'react-native'; 3 | import { SUBSCRIBER_EVENTS } from '@walletconnect/core'; 4 | import { OptionsCtrl } from '../controllers/OptionsCtrl'; 5 | import { ConfigCtrl } from '../controllers/ConfigCtrl'; 6 | import { ClientCtrl } from '../controllers/ClientCtrl'; 7 | import { AccountCtrl } from '../controllers/AccountCtrl'; 8 | import { WcConnectionCtrl } from '../controllers/WcConnectionCtrl'; 9 | import type { IProviderMetadata } from '../types/coreTypes'; 10 | import { createUniversalProvider } from '../utils/ProviderUtil'; 11 | import { StorageUtil } from '../utils/StorageUtil'; 12 | import { ThemeCtrl, ThemeCtrlState } from '../controllers/ThemeCtrl'; 13 | import { ToastCtrl } from '../controllers/ToastCtrl'; 14 | import { ApiCtrl } from '../controllers/ApiCtrl'; 15 | 16 | interface Props { 17 | projectId: string; 18 | providerMetadata: IProviderMetadata; 19 | relayUrl?: string; 20 | themeMode?: ThemeCtrlState['themeMode']; 21 | accentColor?: ThemeCtrlState['accentColor']; 22 | } 23 | 24 | export function useConfigure(config: Props) { 25 | const colorScheme = useColorScheme(); 26 | const { projectId, providerMetadata, relayUrl } = config; 27 | 28 | const resetApp = useCallback(() => { 29 | ClientCtrl.resetSession(); 30 | AccountCtrl.resetAccount(); 31 | WcConnectionCtrl.resetConnection(); 32 | StorageUtil.removeDeepLinkWallet(); 33 | }, []); 34 | 35 | const onSessionDelete = useCallback( 36 | ({ topic }: { topic: string }) => { 37 | const sessionTopic = ClientCtrl.sessionTopic(); 38 | if (topic === sessionTopic) { 39 | resetApp(); 40 | } 41 | }, 42 | [resetApp] 43 | ); 44 | 45 | const onDisplayUri = useCallback(async (uri: string) => { 46 | WcConnectionCtrl.setPairingUri(uri); 47 | }, []); 48 | 49 | useEffect(() => { 50 | if (!projectId) { 51 | console.error('projectId not found'); 52 | } 53 | }, [projectId]); 54 | 55 | /** 56 | * Set theme mode 57 | */ 58 | useEffect(() => { 59 | ThemeCtrl.setThemeMode(config.themeMode || colorScheme); 60 | ThemeCtrl.setAccentColor(config.accentColor); 61 | }, [config.themeMode, config.accentColor, colorScheme]); 62 | 63 | /** 64 | * Set config 65 | */ 66 | useEffect(() => { 67 | ConfigCtrl.setConfig(config); 68 | ConfigCtrl.loadRecentWallet(); 69 | // eslint-disable-next-line react-hooks/exhaustive-deps 70 | }, []); 71 | 72 | /** 73 | * Fetch initial wallet list 74 | */ 75 | useEffect(() => { 76 | async function fetchWallets() { 77 | try { 78 | if (!ApiCtrl.state.recommended.length) { 79 | await ApiCtrl.prefetch(); 80 | OptionsCtrl.setIsDataLoaded(true); 81 | } 82 | } catch (error) { 83 | ToastCtrl.openToast('Network error', 'error'); 84 | } 85 | } 86 | fetchWallets(); 87 | }, []); 88 | 89 | /** 90 | * Initialize provider 91 | */ 92 | useEffect(() => { 93 | async function initProvider() { 94 | try { 95 | const provider = await createUniversalProvider({ 96 | projectId, 97 | relayUrl, 98 | metadata: providerMetadata, 99 | }); 100 | if (provider) { 101 | ClientCtrl.setProvider(provider); 102 | provider.on('display_uri', onDisplayUri); 103 | provider.client.core.relayer.subscriber.on( 104 | SUBSCRIBER_EVENTS.deleted, 105 | onSessionDelete 106 | ); 107 | 108 | // Check if there is an active session 109 | if (provider.session) { 110 | ClientCtrl.setSessionTopic(provider.session.topic); 111 | await AccountCtrl.getAccount(); 112 | } 113 | ClientCtrl.setInitialized(true); 114 | } 115 | } catch (error) { 116 | console.error('Error initializing WalletConnect SDK', error); 117 | } 118 | } 119 | if (!ClientCtrl.provider() && projectId && providerMetadata) { 120 | initProvider(); 121 | } 122 | }, [projectId, providerMetadata, relayUrl, onDisplayUri, onSessionDelete]); 123 | } 124 | -------------------------------------------------------------------------------- /src/hooks/useConnectionHandler.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | import { useSnapshot } from 'valtio'; 3 | 4 | import { AccountCtrl } from '../controllers/AccountCtrl'; 5 | import { WcConnectionCtrl } from '../controllers/WcConnectionCtrl'; 6 | import { ClientCtrl } from '../controllers/ClientCtrl'; 7 | import { defaultSessionParams } from '../constants/Config'; 8 | import { ConfigCtrl } from '../controllers/ConfigCtrl'; 9 | import type { ISessionParams } from '../types/coreTypes'; 10 | import type { SessionTypes } from '@walletconnect/types'; 11 | import { StorageUtil } from '../utils/StorageUtil'; 12 | import { ModalCtrl } from '../controllers/ModalCtrl'; 13 | import { RouterCtrl } from '../controllers/RouterCtrl'; 14 | 15 | const FOUR_MIN_MS = 240000; 16 | 17 | export function useConnectionHandler() { 18 | const timeoutRef = useRef(null); 19 | const { isConnected } = useSnapshot(AccountCtrl.state); 20 | const { pairingEnabled, pairingUri } = useSnapshot(WcConnectionCtrl.state); 21 | const { provider } = useSnapshot(ClientCtrl.state); 22 | const { sessionParams } = useSnapshot(ConfigCtrl.state); 23 | 24 | const onSessionCreated = async (session: SessionTypes.Struct) => { 25 | WcConnectionCtrl.setPairingError(false); 26 | WcConnectionCtrl.setPairingEnabled(false); 27 | ClientCtrl.setSessionTopic(session.topic); 28 | const clearDeepLink = RouterCtrl.state.view === 'Qrcode'; 29 | 30 | try { 31 | if (clearDeepLink) { 32 | await StorageUtil.removeDeepLinkWallet(); 33 | } 34 | AccountCtrl.getAccount(); 35 | ModalCtrl.close(); 36 | } catch (error) {} 37 | }; 38 | 39 | const connectAndWait = useCallback(async () => { 40 | try { 41 | if (timeoutRef.current) clearTimeout(timeoutRef.current); 42 | 43 | if (!isConnected && pairingEnabled) { 44 | timeoutRef.current = setTimeout(connectAndWait, FOUR_MIN_MS); 45 | const session = await provider!.connect( 46 | (sessionParams as ISessionParams) ?? defaultSessionParams 47 | ); 48 | 49 | if (session) { 50 | onSessionCreated(session); 51 | } 52 | } 53 | } catch (error) { 54 | WcConnectionCtrl.setPairingUri(''); 55 | WcConnectionCtrl.setPairingError(true); 56 | } 57 | }, [isConnected, provider, sessionParams, pairingEnabled]); 58 | 59 | useEffect(() => { 60 | if (provider && !isConnected && pairingEnabled && !pairingUri) { 61 | connectAndWait(); 62 | } 63 | }, [provider, connectAndWait, isConnected, pairingEnabled, pairingUri]); 64 | 65 | return null; 66 | } 67 | -------------------------------------------------------------------------------- /src/hooks/useDebounceCallback.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | 3 | interface Props { 4 | callback: (args: any) => void; 5 | delay?: number; 6 | } 7 | 8 | export function useDebounceCallback({ callback, delay = 250 }: Props) { 9 | const timeoutRef = useRef(null); 10 | const callbackRef = useRef(callback); 11 | 12 | useEffect(() => { 13 | callbackRef.current = callback; 14 | }, [callback]); 15 | 16 | const debouncedCallback = useCallback( 17 | (args) => { 18 | if (timeoutRef.current) { 19 | clearTimeout(timeoutRef.current); 20 | } 21 | 22 | timeoutRef.current = setTimeout(() => { 23 | callbackRef.current(args); 24 | }, delay); 25 | }, 26 | [delay] 27 | ); 28 | 29 | useEffect(() => { 30 | return () => { 31 | if (timeoutRef.current) { 32 | clearTimeout(timeoutRef.current); 33 | } 34 | }; 35 | }, []); 36 | 37 | return debouncedCallback; 38 | } 39 | -------------------------------------------------------------------------------- /src/hooks/useOrientation.ts: -------------------------------------------------------------------------------- 1 | import { useWindowDimensions } from 'react-native'; 2 | 3 | export function useOrientation() { 4 | const window = useWindowDimensions(); 5 | 6 | return { 7 | width: window.width, 8 | height: window.height, 9 | isPortrait: window.height >= window.width, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { useSnapshot } from 'valtio'; 2 | import { DarkTheme, LightTheme } from '../constants/Colors'; 3 | import { ThemeCtrl } from '../controllers/ThemeCtrl'; 4 | 5 | function useTheme() { 6 | const { themeMode, accentColor } = useSnapshot(ThemeCtrl.state); 7 | const Theme = themeMode === 'dark' ? DarkTheme : LightTheme; 8 | if (accentColor) return Object.assign(Theme, { accent: accentColor }); 9 | 10 | return themeMode === 'dark' ? DarkTheme : LightTheme; 11 | } 12 | 13 | export default useTheme; 14 | -------------------------------------------------------------------------------- /src/hooks/useWalletConnectModal.ts: -------------------------------------------------------------------------------- 1 | import { useSnapshot } from 'valtio'; 2 | 3 | import { ModalCtrl } from '../controllers/ModalCtrl'; 4 | import { ClientCtrl } from '../controllers/ClientCtrl'; 5 | import { AccountCtrl } from '../controllers/AccountCtrl'; 6 | 7 | export function useWalletConnectModal() { 8 | const modalState = useSnapshot(ModalCtrl.state); 9 | const accountState = useSnapshot(AccountCtrl.state); 10 | const clientState = useSnapshot(ClientCtrl.state); 11 | 12 | return { 13 | isOpen: modalState.open, 14 | open: ModalCtrl.open, 15 | close: ModalCtrl.close, 16 | provider: clientState.initialized ? ClientCtrl.provider() : undefined, 17 | isConnected: accountState.isConnected, 18 | address: accountState.address, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import '@walletconnect/react-native-compat'; 2 | import './config/animations'; 3 | 4 | export { WalletConnectModal } from './modal/wcm-modal'; 5 | export { useWalletConnectModal } from './hooks/useWalletConnectModal'; 6 | export { IProvider, IProviderMetadata } from './types/coreTypes'; 7 | -------------------------------------------------------------------------------- /src/modal/wcm-modal-router/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useSnapshot } from 'valtio'; 3 | 4 | import { useOrientation } from '../../hooks/useOrientation'; 5 | import QRCodeView from '../../views/wcm-qr-code-view'; 6 | import AllWalletsView from '../../views/wcm-all-wallets-view'; 7 | import { RouterCtrl } from '../../controllers/RouterCtrl'; 8 | import ConnectView from '../../views/wcm-connect-view'; 9 | import ConnectingView from '../../views/wcm-connecting-view'; 10 | import useTheme from '../../hooks/useTheme'; 11 | import { StyleSheet, View } from 'react-native'; 12 | 13 | interface Props { 14 | onCopyClipboard?: (value: string) => void; 15 | } 16 | 17 | export function ModalRouter(props: Props) { 18 | const routerState = useSnapshot(RouterCtrl.state); 19 | const Theme = useTheme(); 20 | const { height, width, isPortrait } = useOrientation(); 21 | 22 | const ViewComponent = useMemo(() => { 23 | switch (routerState.view) { 24 | case 'ConnectWallet': 25 | return ConnectView; 26 | case 'WalletExplorer': 27 | return AllWalletsView; 28 | case 'Qrcode': 29 | return QRCodeView; 30 | case 'Connecting': 31 | return ConnectingView; 32 | default: 33 | return ConnectView; 34 | } 35 | }, [routerState.view]); 36 | 37 | return ( 38 | 39 | 45 | 46 | ); 47 | } 48 | 49 | const styles = StyleSheet.create({ 50 | wrapper: { 51 | borderTopLeftRadius: 30, 52 | borderTopRightRadius: 30, 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /src/modal/wcm-modal/index.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import Modal from 'react-native-modal'; 3 | import { useSnapshot } from 'valtio'; 4 | 5 | import ModalBackcard from '../../components/ModalBackcard'; 6 | import { ModalRouter } from '../wcm-modal-router'; 7 | import { ModalCtrl } from '../../controllers/ModalCtrl'; 8 | import { RouterCtrl } from '../../controllers/RouterCtrl'; 9 | import { useConnectionHandler } from '../../hooks/useConnectionHandler'; 10 | import type { ConfigCtrlState } from '../../controllers/ConfigCtrl'; 11 | import type { IProviderMetadata, ISessionParams } from '../../types/coreTypes'; 12 | import { useConfigure } from '../../hooks/useConfigure'; 13 | import Toast from '../../components/Toast'; 14 | import type { ThemeCtrlState } from '../../controllers/ThemeCtrl'; 15 | 16 | export type Props = Omit & 17 | ThemeCtrlState & { 18 | providerMetadata: IProviderMetadata; 19 | sessionParams?: ISessionParams; 20 | relayUrl?: string; 21 | onCopyClipboard?: (value: string) => void; 22 | }; 23 | 24 | export function WalletConnectModal(config: Props) { 25 | useConfigure(config); 26 | useConnectionHandler(); 27 | const { open } = useSnapshot(ModalCtrl.state); 28 | const { history } = useSnapshot(RouterCtrl.state); 29 | 30 | const onBackButtonPress = () => { 31 | if (history.length > 1) { 32 | return RouterCtrl.goBack(); 33 | } 34 | return ModalCtrl.close(); 35 | }; 36 | 37 | return ( 38 | 48 | 49 | 50 | 51 | 52 | ); 53 | } 54 | 55 | const styles = StyleSheet.create({ 56 | modal: { 57 | margin: 0, 58 | justifyContent: 'flex-end', 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /src/partials/wcm-all-wallets-list/index.tsx: -------------------------------------------------------------------------------- 1 | import { View, FlatList } from 'react-native'; 2 | import { useSnapshot } from 'valtio'; 3 | import useTheme from '../../hooks/useTheme'; 4 | import WalletItem, { 5 | WALLET_FULL_HEIGHT, 6 | WALLET_MARGIN, 7 | } from '../../components/WalletItem'; 8 | import type { WcWallet } from '../../types/controllerTypes'; 9 | import { AssetUtil } from '../../utils/AssetUtil'; 10 | import { ConfigCtrl } from '../../controllers/ConfigCtrl'; 11 | import styles from './styles'; 12 | import { WalletItemLoader } from '../../components/WalletItemLoader'; 13 | 14 | export interface AllWalletsListProps { 15 | columns: number; 16 | itemWidth: number; 17 | windowHeight: number; 18 | themeMode?: 'dark' | 'light'; 19 | isPortrait: boolean; 20 | onWalletPress: (wallet: WcWallet) => void; 21 | loading: boolean; 22 | onFetchNextPage: () => void; 23 | list: WcWallet[]; 24 | } 25 | 26 | export function AllWalletsList({ 27 | columns, 28 | itemWidth, 29 | windowHeight, 30 | themeMode, 31 | isPortrait, 32 | onWalletPress, 33 | loading, 34 | onFetchNextPage, 35 | list, 36 | }: AllWalletsListProps) { 37 | const { recentWallet } = useSnapshot(ConfigCtrl.state); 38 | const Theme = useTheme(); 39 | 40 | const loadingTemplate = (items: number) => { 41 | return ( 42 | 48 | {Array.from({ length: items }).map((_, index) => ( 49 | 56 | ))} 57 | 58 | ); 59 | }; 60 | 61 | const renderWallet = ({ item }: { item: WcWallet }) => { 62 | return ( 63 | onWalletPress(item)} 68 | imageUrl={AssetUtil.getWalletImage(item)} 69 | style={{ width: itemWidth }} 70 | /> 71 | ); 72 | }; 73 | 74 | if (loading) { 75 | return loadingTemplate(20); 76 | } 77 | 78 | return ( 79 | ({ 94 | length: WALLET_FULL_HEIGHT, 95 | offset: WALLET_FULL_HEIGHT * index, 96 | index, 97 | })} 98 | renderItem={renderWallet} 99 | /> 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /src/partials/wcm-all-wallets-list/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | listContentContainer: { 5 | paddingBottom: 12, 6 | }, 7 | loaderContainer: { 8 | flexDirection: 'row', 9 | flexWrap: 'wrap', 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/partials/wcm-all-wallets-search/index.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, FlatList } from 'react-native'; 2 | import { useSnapshot } from 'valtio'; 3 | import { ApiCtrl } from '../../controllers/ApiCtrl'; 4 | import useTheme from '../../hooks/useTheme'; 5 | import WalletItem, { 6 | WALLET_FULL_HEIGHT, 7 | WALLET_MARGIN, 8 | } from '../../components/WalletItem'; 9 | import type { WcWallet } from '../../types/controllerTypes'; 10 | import { AssetUtil } from '../../utils/AssetUtil'; 11 | import { ConfigCtrl } from '../../controllers/ConfigCtrl'; 12 | import styles from './styles'; 13 | import { WalletItemLoader } from '../../components/WalletItemLoader'; 14 | 15 | export interface AllWalletsSearchProps { 16 | columns: number; 17 | itemWidth: number; 18 | windowHeight: number; 19 | themeMode?: 'dark' | 'light'; 20 | isPortrait: boolean; 21 | onWalletPress: (wallet: WcWallet) => void; 22 | loading: boolean; 23 | } 24 | 25 | export function AllWalletsSearch({ 26 | columns, 27 | itemWidth, 28 | windowHeight, 29 | themeMode, 30 | isPortrait, 31 | onWalletPress, 32 | loading, 33 | }: AllWalletsSearchProps) { 34 | const { search } = useSnapshot(ApiCtrl.state); 35 | const { recentWallet } = useSnapshot(ConfigCtrl.state); 36 | const Theme = useTheme(); 37 | 38 | const loadingTemplate = (items: number) => { 39 | return ( 40 | 46 | {Array.from({ length: items }).map((_, index) => ( 47 | 54 | ))} 55 | 56 | ); 57 | }; 58 | 59 | const emptyTemplate = () => { 60 | return ( 61 | 67 | 68 | No results found 69 | 70 | 71 | ); 72 | }; 73 | 74 | const renderWallet = ({ item }: { item: WcWallet }) => { 75 | return ( 76 | onWalletPress(item)} 81 | imageUrl={AssetUtil.getWalletImage(item)} 82 | style={{ width: itemWidth }} 83 | /> 84 | ); 85 | }; 86 | 87 | if (loading) { 88 | return loadingTemplate(20); 89 | } 90 | 91 | return ( 92 | ({ 106 | length: WALLET_FULL_HEIGHT, 107 | offset: WALLET_FULL_HEIGHT * index, 108 | index, 109 | })} 110 | renderItem={renderWallet} 111 | /> 112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /src/partials/wcm-all-wallets-search/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | listContentContainer: { 5 | paddingBottom: 12, 6 | }, 7 | emptyContainer: { 8 | flex: 1, 9 | alignItems: 'center', 10 | justifyContent: 'center', 11 | }, 12 | emptyText: { 13 | fontSize: 16, 14 | fontWeight: '600', 15 | }, 16 | loaderContainer: { 17 | flexDirection: 'row', 18 | flexWrap: 'wrap', 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/partials/wcm-modal-header/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, type ReactNode, useEffect } from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | import { useSnapshot } from 'valtio'; 4 | 5 | import useTheme from '../../hooks/useTheme'; 6 | import Backward from '../../assets/Backward'; 7 | import { RouterCtrl } from '../../controllers/RouterCtrl'; 8 | import Touchable from '../../components/Touchable'; 9 | import styles from './styles'; 10 | 11 | interface Props { 12 | title?: string; 13 | onActionPress?: () => void; 14 | actionIcon?: ReactNode; 15 | actionDisabled?: boolean; 16 | shadow?: boolean; 17 | children?: ReactNode; 18 | } 19 | 20 | function ModalHeader({ 21 | title, 22 | onActionPress, 23 | actionIcon, 24 | actionDisabled, 25 | shadow, 26 | children, 27 | }: Props) { 28 | const Theme = useTheme(); 29 | const { history } = useSnapshot(RouterCtrl.state); 30 | const [showBack, setShowBack] = useState(false); 31 | 32 | useEffect(() => { 33 | setShowBack(history.length > 1); 34 | }, [history]); 35 | 36 | return ( 37 | 50 | {showBack ? ( 51 | 57 | 58 | 59 | ) : ( 60 | 61 | )} 62 | {children} 63 | {title && ( 64 | 65 | {title} 66 | 67 | )} 68 | {actionIcon && onActionPress ? ( 69 | 75 | {actionIcon} 76 | 77 | ) : ( 78 | 79 | )} 80 | 81 | ); 82 | } 83 | 84 | export default ModalHeader; 85 | -------------------------------------------------------------------------------- /src/partials/wcm-modal-header/styles.ts: -------------------------------------------------------------------------------- 1 | import { Platform, StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | flexDirection: 'row', 6 | justifyContent: 'space-between', 7 | alignItems: 'center', 8 | paddingHorizontal: 24, 9 | borderTopLeftRadius: 30, 10 | borderTopRightRadius: 30, 11 | height: 56, 12 | }, 13 | shadow: { 14 | zIndex: 1, 15 | ...Platform.select({ 16 | ios: { 17 | shadowOpacity: 1, 18 | shadowOffset: { width: 0, height: 6 }, 19 | }, 20 | }), 21 | }, 22 | button: { 23 | width: 24, 24 | height: 24, 25 | justifyContent: 'center', 26 | alignItems: 'center', 27 | }, 28 | title: { 29 | fontWeight: '600', 30 | fontSize: 20, 31 | lineHeight: 24, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/types/controllerTypes.ts: -------------------------------------------------------------------------------- 1 | export type CaipAddress = `${string}:${string}:${string}`; 2 | 3 | // -- ApiCtrl Types ------------------------------------------------------- 4 | export interface WcWallet { 5 | id: string; 6 | name: string; 7 | homepage?: string; 8 | image_id?: string; 9 | image_url?: string; 10 | order?: number; 11 | mobile_link?: string | null; 12 | desktop_link?: string | null; 13 | webapp_link?: string | null; 14 | app_store?: string | null; 15 | play_store?: string | null; 16 | } 17 | 18 | export interface DataWallet { 19 | id: string; 20 | ios_schema?: string; 21 | android_app_id?: string; 22 | } 23 | 24 | export interface ApiGetWalletsRequest { 25 | page: number; 26 | entries: number; 27 | search?: string; 28 | include?: string[]; 29 | exclude?: string[]; 30 | } 31 | 32 | export interface ApiGetWalletsResponse { 33 | data: WcWallet[]; 34 | count: number; 35 | } 36 | 37 | export interface ApiGetDataWalletsResponse { 38 | data: DataWallet[]; 39 | count: number; 40 | } 41 | -------------------------------------------------------------------------------- /src/types/coreTypes.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ConnectParams, 3 | Metadata, 4 | IUniversalProvider, 5 | } from '@walletconnect/universal-provider'; 6 | 7 | export type IProvider = IUniversalProvider; 8 | 9 | export interface IProviderMetadata extends Metadata { 10 | redirect: { 11 | native: string; 12 | universal?: string; 13 | }; 14 | } 15 | 16 | export type ISessionParams = ConnectParams; 17 | -------------------------------------------------------------------------------- /src/types/routerTypes.ts: -------------------------------------------------------------------------------- 1 | export type RouterProps = { 2 | isPortrait: boolean; 3 | windowHeight: number; 4 | windowWidth: number; 5 | onCopyClipboard?: (value: string) => void; 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/AssetUtil.ts: -------------------------------------------------------------------------------- 1 | import { AssetCtrl } from '../controllers/AssetCtrl'; 2 | import type { WcWallet } from '../types/controllerTypes'; 3 | 4 | export const AssetUtil = { 5 | getWalletImage(wallet?: WcWallet) { 6 | if (wallet?.image_url) { 7 | return wallet?.image_url; 8 | } 9 | 10 | if (wallet?.image_id) { 11 | return AssetCtrl.state.walletImages[wallet.image_id]; 12 | } 13 | 14 | return undefined; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/utils/ConstantsUtil.ts: -------------------------------------------------------------------------------- 1 | export const ConstantsUtil = { 2 | FOUR_MINUTES_MS: 240000, 3 | 4 | TEN_SEC_MS: 10000, 5 | 6 | ONE_SEC_MS: 1000, 7 | 8 | RESTRICTED_TIMEZONES: [ 9 | 'ASIA/SHANGHAI', 10 | 'ASIA/URUMQI', 11 | 'ASIA/CHONGQING', 12 | 'ASIA/HARBIN', 13 | 'ASIA/KASHGAR', 14 | 'ASIA/MACAU', 15 | 'ASIA/HONG_KONG', 16 | 'ASIA/MACAO', 17 | 'ASIA/BEIJING', 18 | 'ASIA/HARBIN', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /src/utils/CoreHelperUtil.ts: -------------------------------------------------------------------------------- 1 | import { Linking, Platform } from 'react-native'; 2 | import { ConstantsUtil } from './ConstantsUtil'; 3 | import type { CaipAddress, DataWallet } from '../types/controllerTypes'; 4 | 5 | // -- Helpers ----------------------------------------------------------------- 6 | async function isAppInstalledIos(deepLink?: string): Promise { 7 | try { 8 | return deepLink ? Linking.canOpenURL(deepLink) : Promise.resolve(false); 9 | } catch (error) { 10 | return Promise.resolve(false); 11 | } 12 | } 13 | 14 | async function isAppInstalledAndroid(packageName?: string): Promise { 15 | try { 16 | if ( 17 | !packageName || 18 | //@ts-ignore 19 | typeof global?.Application?.isAppInstalled !== 'function' 20 | ) { 21 | return Promise.resolve(false); 22 | } 23 | 24 | //@ts-ignore 25 | return global?.Application?.isAppInstalled(packageName); 26 | } catch (error) { 27 | return Promise.resolve(false); 28 | } 29 | } 30 | 31 | export const CoreHelperUtil = { 32 | isPairingExpired(expiry?: number) { 33 | return expiry ? expiry - Date.now() <= ConstantsUtil.TEN_SEC_MS : true; 34 | }, 35 | 36 | isAllowedRetry(lastRetry: number) { 37 | return Date.now() - lastRetry >= ConstantsUtil.ONE_SEC_MS; 38 | }, 39 | 40 | getPairingExpiry() { 41 | return Date.now() + ConstantsUtil.FOUR_MINUTES_MS; 42 | }, 43 | 44 | getPlainAddress(caipAddress: CaipAddress) { 45 | return caipAddress.split(':')[2]; 46 | }, 47 | 48 | async wait(milliseconds: number) { 49 | return new Promise((resolve) => { 50 | setTimeout(resolve, milliseconds); 51 | }); 52 | }, 53 | 54 | debounce(func: (...args: any[]) => unknown, timeout = 500) { 55 | let timer: ReturnType | undefined; 56 | 57 | return (...args: unknown[]) => { 58 | function next() { 59 | func(...args); 60 | } 61 | if (timer) { 62 | clearTimeout(timer); 63 | } 64 | timer = setTimeout(next, timeout); 65 | }; 66 | }, 67 | 68 | isHttpUrl(url: string) { 69 | return url.startsWith('http://') || url.startsWith('https://'); 70 | }, 71 | 72 | formatNativeUrl(appUrl: string, wcUri: string): string { 73 | if (CoreHelperUtil.isHttpUrl(appUrl)) { 74 | return this.formatUniversalUrl(appUrl, wcUri); 75 | } 76 | let safeAppUrl = appUrl; 77 | if (!safeAppUrl.includes('://')) { 78 | safeAppUrl = appUrl.replaceAll('/', '').replaceAll(':', ''); 79 | safeAppUrl = `${safeAppUrl}://`; 80 | } 81 | if (!safeAppUrl.endsWith('/')) { 82 | safeAppUrl = `${safeAppUrl}/`; 83 | } 84 | const encodedWcUrl = encodeURIComponent(wcUri); 85 | 86 | return `${safeAppUrl}wc?uri=${encodedWcUrl}`; 87 | }, 88 | 89 | formatUniversalUrl(appUrl: string, wcUri: string): string { 90 | if (!CoreHelperUtil.isHttpUrl(appUrl)) { 91 | return this.formatNativeUrl(appUrl, wcUri); 92 | } 93 | let safeAppUrl = appUrl; 94 | if (!safeAppUrl.endsWith('/')) { 95 | safeAppUrl = `${safeAppUrl}/`; 96 | } 97 | const encodedWcUrl = encodeURIComponent(wcUri); 98 | 99 | return `${safeAppUrl}wc?uri=${encodedWcUrl}`; 100 | }, 101 | 102 | openLink(url: string) { 103 | return Linking.openURL(url); 104 | }, 105 | 106 | formatBalance( 107 | balance: string | undefined, 108 | symbol: string | undefined, 109 | decimals = 3 110 | ) { 111 | let formattedBalance; 112 | 113 | if (balance === '0') { 114 | formattedBalance = '0.000'; 115 | } else if (typeof balance === 'string') { 116 | const number = Number(balance); 117 | if (number) { 118 | const regex = new RegExp(`^-?\\d+(?:\\.\\d{0,${decimals}})?`, 'u'); 119 | formattedBalance = number.toString().match(regex)?.[0]; 120 | } 121 | } 122 | 123 | return formattedBalance ? `${formattedBalance} ${symbol}` : '0.000'; 124 | }, 125 | 126 | isRestrictedRegion() { 127 | try { 128 | const { timeZone } = new Intl.DateTimeFormat().resolvedOptions(); 129 | const capTimeZone = timeZone.toUpperCase(); 130 | 131 | return ConstantsUtil.RESTRICTED_TIMEZONES.includes(capTimeZone); 132 | } catch { 133 | return false; 134 | } 135 | }, 136 | 137 | getApiUrl() { 138 | return CoreHelperUtil.isRestrictedRegion() 139 | ? 'https://api.web3modal.org' 140 | : 'https://api.web3modal.com'; 141 | }, 142 | 143 | getBlockchainApiUrl() { 144 | return CoreHelperUtil.isRestrictedRegion() 145 | ? 'https://rpc.walletconnect.org' 146 | : 'https://rpc.walletconnect.com'; 147 | }, 148 | 149 | async checkInstalled(wallet: DataWallet): Promise { 150 | let isInstalled = false; 151 | const scheme = wallet.ios_schema; 152 | const appId = wallet.android_app_id; 153 | try { 154 | isInstalled = await Platform.select({ 155 | ios: isAppInstalledIos(scheme), 156 | android: isAppInstalledAndroid(appId), 157 | default: Promise.resolve(false), 158 | }); 159 | } catch { 160 | isInstalled = false; 161 | } 162 | 163 | return isInstalled; 164 | }, 165 | 166 | isArray(data?: T | T[]): data is T[] { 167 | return Array.isArray(data) && data.length > 0; 168 | }, 169 | }; 170 | -------------------------------------------------------------------------------- /src/utils/FetchUtil.ts: -------------------------------------------------------------------------------- 1 | // -- Types ---------------------------------------------------------------------- 2 | interface Options { 3 | baseUrl: string; 4 | } 5 | 6 | interface RequestArguments { 7 | path: string; 8 | headers?: HeadersInit_; 9 | params?: Record; 10 | } 11 | 12 | interface PostArguments extends RequestArguments { 13 | body?: Record; 14 | } 15 | 16 | // -- Utility -------------------------------------------------------------------- 17 | export class FetchUtil { 18 | public baseUrl: Options['baseUrl']; 19 | 20 | public constructor({ baseUrl }: Options) { 21 | this.baseUrl = baseUrl; 22 | } 23 | 24 | public async get({ headers, ...args }: RequestArguments) { 25 | const url = this.createUrl(args).toString(); 26 | const response = await fetch(url, { method: 'GET', headers }); 27 | 28 | return response.json() as T; 29 | } 30 | 31 | public async post({ body, headers, ...args }: PostArguments) { 32 | const url = this.createUrl(args).toString(); 33 | const response = await fetch(url, { 34 | method: 'POST', 35 | headers, 36 | body: body ? JSON.stringify(body) : undefined, 37 | }); 38 | 39 | return response.json() as T; 40 | } 41 | 42 | public async put({ body, headers, ...args }: PostArguments) { 43 | const url = this.createUrl(args).toString(); 44 | const response = await fetch(url, { 45 | method: 'PUT', 46 | headers, 47 | body: body ? JSON.stringify(body) : undefined, 48 | }); 49 | 50 | return response.json() as T; 51 | } 52 | 53 | public async delete({ body, headers, ...args }: PostArguments) { 54 | const url = this.createUrl(args).toString(); 55 | const response = await fetch(url, { 56 | method: 'DELETE', 57 | headers, 58 | body: body ? JSON.stringify(body) : undefined, 59 | }); 60 | 61 | return response.json() as T; 62 | } 63 | 64 | private createUrl({ path, params }: RequestArguments) { 65 | const url = new URL(path, this.baseUrl); 66 | if (params) { 67 | Object.entries(params).forEach(([key, value]) => { 68 | if (value) { 69 | url.searchParams.append(key, value); 70 | } 71 | }); 72 | } 73 | 74 | return url; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/utils/ProviderUtil.ts: -------------------------------------------------------------------------------- 1 | import UniversalProvider from '@walletconnect/universal-provider'; 2 | import type { IProviderMetadata } from '../types/coreTypes'; 3 | 4 | export async function createUniversalProvider({ 5 | projectId, 6 | relayUrl, 7 | metadata, 8 | }: { 9 | projectId: string; 10 | metadata: IProviderMetadata; 11 | relayUrl?: string; 12 | }) { 13 | return UniversalProvider.init({ 14 | logger: __DEV__ ? 'info' : undefined, 15 | relayUrl, 16 | projectId, 17 | metadata, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/QRCodeUtil.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import { Line, Rect, Circle } from 'react-native-svg'; 3 | import QRCode from 'qrcode'; 4 | import { DarkTheme, LightTheme } from '../constants/Colors'; 5 | 6 | type CoordinateMapping = [number, number[]]; 7 | 8 | const CONNECTING_ERROR_MARGIN = 0.1; 9 | const CIRCLE_SIZE_MODIFIER = 2.5; 10 | const QRCODE_MATRIX_MARGIN = 7; 11 | 12 | function isAdjecentDots(cy: number, otherCy: number, cellSize: number) { 13 | if (cy === otherCy) { 14 | return false; 15 | } 16 | const diff = cy - otherCy < 0 ? otherCy - cy : cy - otherCy; 17 | 18 | return diff <= cellSize + CONNECTING_ERROR_MARGIN; 19 | } 20 | 21 | function getMatrix( 22 | value: string, 23 | errorCorrectionLevel: QRCode.QRCodeErrorCorrectionLevel 24 | ) { 25 | const arr = Array.prototype.slice.call( 26 | QRCode.create(value, { errorCorrectionLevel }).modules.data, 27 | 0 28 | ); 29 | const sqrt = Math.sqrt(arr.length); 30 | 31 | return arr.reduce( 32 | (rows, key, index) => 33 | (index % sqrt === 0 34 | ? rows.push([key]) 35 | : rows[rows.length - 1].push(key)) && rows, 36 | [] 37 | ); 38 | } 39 | 40 | export const QRCodeUtil = { 41 | generate( 42 | uri: string, 43 | size: number, 44 | logoSize: number, 45 | theme: 'dark' | 'light' 46 | ) { 47 | const dotColor = 48 | theme === 'light' ? DarkTheme.background1 : LightTheme.background1; 49 | const edgeColor = 50 | theme === 'light' ? LightTheme.background1 : DarkTheme.background1; 51 | const dots: ReactNode[] = []; 52 | const matrix = getMatrix(uri, 'Q'); 53 | const cellSize = size / matrix.length; 54 | const qrList = [ 55 | { x: 0, y: 0 }, 56 | { x: 1, y: 0 }, 57 | { x: 0, y: 1 }, 58 | ]; 59 | 60 | qrList.forEach(({ x, y }) => { 61 | const x1 = (matrix.length - QRCODE_MATRIX_MARGIN) * cellSize * x; 62 | const y1 = (matrix.length - QRCODE_MATRIX_MARGIN) * cellSize * y; 63 | const borderRadius = 0.32; 64 | for (let i = 0; i < qrList.length; i += 1) { 65 | const dotSize = cellSize * (QRCODE_MATRIX_MARGIN - i * 2); 66 | dots.push( 67 | 77 | ); 78 | } 79 | }); 80 | 81 | const clearArenaSize = Math.floor((logoSize + 25) / cellSize); 82 | const matrixMiddleStart = matrix.length / 2 - clearArenaSize / 2; 83 | const matrixMiddleEnd = matrix.length / 2 + clearArenaSize / 2 - 1; 84 | const circles: [number, number][] = []; 85 | 86 | // Getting coordinates for each of the QR code dots 87 | matrix.forEach((row: QRCode.QRCode[], i: number) => { 88 | row.forEach((_, j: number) => { 89 | if (matrix[i][j]) { 90 | if ( 91 | !( 92 | (i < QRCODE_MATRIX_MARGIN && j < QRCODE_MATRIX_MARGIN) || 93 | (i > matrix.length - (QRCODE_MATRIX_MARGIN + 1) && 94 | j < QRCODE_MATRIX_MARGIN) || 95 | (i < QRCODE_MATRIX_MARGIN && 96 | j > matrix.length - (QRCODE_MATRIX_MARGIN + 1)) 97 | ) 98 | ) { 99 | if ( 100 | !( 101 | i > matrixMiddleStart && 102 | i < matrixMiddleEnd && 103 | j > matrixMiddleStart && 104 | j < matrixMiddleEnd 105 | ) 106 | ) { 107 | const cx = i * cellSize + cellSize / 2; 108 | const cy = j * cellSize + cellSize / 2; 109 | circles.push([cx, cy]); 110 | } 111 | } 112 | } 113 | }); 114 | }); 115 | 116 | // Cx to multiple cys 117 | const circlesToConnect: Record = {}; 118 | 119 | // Mapping all dots cicles on the same x axis 120 | circles.forEach(([cx, cy]) => { 121 | if (circlesToConnect[cx]) { 122 | circlesToConnect[cx]?.push(cy); 123 | } else { 124 | circlesToConnect[cx] = [cy]; 125 | } 126 | }); 127 | 128 | // Drawing lonely dots 129 | Object.entries(circlesToConnect) 130 | // Only get dots that have neighbors 131 | .map(([cx, cys]) => { 132 | const newCys = cys.filter((cy) => 133 | cys.every((otherCy) => !isAdjecentDots(cy, otherCy, cellSize)) 134 | ); 135 | 136 | return [Number(cx), newCys] as CoordinateMapping; 137 | }) 138 | .forEach(([cx, cys]) => { 139 | cys.forEach((cy) => { 140 | dots.push( 141 | 148 | ); 149 | }); 150 | }); 151 | 152 | // Drawing lines for dots that are close to each other 153 | Object.entries(circlesToConnect) 154 | // Only get dots that have more than one dot on the x axis 155 | .filter(([_, cys]) => cys.length > 1) 156 | // Removing dots with no neighbors 157 | .map(([cx, cys]) => { 158 | const newCys = cys.filter((cy) => 159 | cys.some((otherCy) => isAdjecentDots(cy, otherCy, cellSize)) 160 | ); 161 | 162 | return [Number(cx), newCys] as CoordinateMapping; 163 | }) 164 | // Get the coordinates of the first and last dot of a line 165 | .map(([cx, cys]) => { 166 | cys.sort((a, b) => (a < b ? -1 : 1)); 167 | const groups: number[][] = []; 168 | 169 | for (const cy of cys) { 170 | const group = groups.find((item) => 171 | item.some((otherCy) => isAdjecentDots(cy, otherCy, cellSize)) 172 | ); 173 | if (group) { 174 | group.push(cy); 175 | } else { 176 | groups.push([cy]); 177 | } 178 | } 179 | 180 | return [cx, groups.map((item) => [item[0], item[item.length - 1]])] as [ 181 | number, 182 | number[][] 183 | ]; 184 | }) 185 | .forEach(([cx, groups]) => { 186 | groups.forEach(([y1, y2]) => { 187 | dots.push( 188 | 198 | ); 199 | }); 200 | }); 201 | 202 | return dots; 203 | }, 204 | }; 205 | -------------------------------------------------------------------------------- /src/utils/StorageUtil.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | import type { WcWallet } from '../types/controllerTypes'; 3 | 4 | export const StorageUtil = { 5 | WALLETCONNECT_DEEPLINK_CHOICE: 'WALLETCONNECT_DEEPLINK_CHOICE', 6 | W3M_RECENT_WALLET_INFO: 'W3M_RECENT_WALLET_INFO', 7 | 8 | setDeepLinkWallet(link: string) { 9 | return AsyncStorage.setItem( 10 | StorageUtil.WALLETCONNECT_DEEPLINK_CHOICE, 11 | JSON.stringify({ href: link }) 12 | ); 13 | }, 14 | 15 | removeDeepLinkWallet() { 16 | return AsyncStorage.removeItem(StorageUtil.WALLETCONNECT_DEEPLINK_CHOICE); 17 | }, 18 | 19 | setRecentWallet(wallet: WcWallet) { 20 | try { 21 | AsyncStorage.setItem( 22 | StorageUtil.W3M_RECENT_WALLET_INFO, 23 | JSON.stringify(wallet) 24 | ); 25 | } catch (error) { 26 | console.info('Unable to set recent wallet'); 27 | } 28 | }, 29 | 30 | async getRecentWallet(): Promise { 31 | try { 32 | const wallet = await AsyncStorage.getItem( 33 | StorageUtil.W3M_RECENT_WALLET_INFO 34 | ); 35 | 36 | if (wallet) { 37 | const parsedWallet = JSON.parse(wallet); 38 | if (typeof parsedWallet.app === 'object') { 39 | // Wallet from old api. Discard it 40 | await AsyncStorage.removeItem(StorageUtil.W3M_RECENT_WALLET_INFO); 41 | return undefined; 42 | } 43 | return parsedWallet; 44 | } 45 | 46 | return undefined; 47 | } catch (error) { 48 | console.info('Unable to get recent wallet'); 49 | } 50 | return undefined; 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /src/utils/UiUtil.ts: -------------------------------------------------------------------------------- 1 | import { LayoutAnimation } from 'react-native'; 2 | 3 | export const UiUtil = { 4 | layoutAnimation() { 5 | return LayoutAnimation.configureNext( 6 | LayoutAnimation.create(200, 'easeInEaseOut', 'opacity') 7 | ); 8 | }, 9 | 10 | getWalletName(name: string, short = false) { 11 | return short ? name?.split(' ')[0] : name; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/views/wcm-all-wallets-view/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react'; 2 | import { useSnapshot } from 'valtio'; 3 | import ModalHeader from '../../partials/wcm-modal-header'; 4 | import SearchBar from '../../components/SearchBar'; 5 | import { OptionsCtrl } from '../../controllers/OptionsCtrl'; 6 | import { WcConnectionCtrl } from '../../controllers/WcConnectionCtrl'; 7 | import { ThemeCtrl } from '../../controllers/ThemeCtrl'; 8 | import { RouterCtrl } from '../../controllers/RouterCtrl'; 9 | import { ApiCtrl } from '../../controllers/ApiCtrl'; 10 | import type { RouterProps } from '../../types/routerTypes'; 11 | import { useDebounceCallback } from '../../hooks/useDebounceCallback'; 12 | import { ConfigCtrl } from '../../controllers/ConfigCtrl'; 13 | import type { WcWallet } from '../../types/controllerTypes'; 14 | import { AllWalletsSearch } from '../../partials/wcm-all-wallets-search'; 15 | import { AllWalletsList } from '../../partials/wcm-all-wallets-list'; 16 | import styles from './styles'; 17 | 18 | function AllWalletsView({ 19 | isPortrait, 20 | windowHeight, 21 | windowWidth, 22 | }: RouterProps) { 23 | const { isDataLoaded } = useSnapshot(OptionsCtrl.state); 24 | const { pairingUri } = useSnapshot(WcConnectionCtrl.state); 25 | const { themeMode } = useSnapshot(ThemeCtrl.state); 26 | const { recentWallet } = useSnapshot(ConfigCtrl.state); 27 | const { wallets, recommended, installed, count, page } = useSnapshot( 28 | ApiCtrl.state 29 | ); 30 | const shouldLoadWallets = wallets.length === 0; 31 | const [walletsLoading, setWalletsLoading] = useState(false); 32 | const loading = !isDataLoaded || !pairingUri || walletsLoading; 33 | const [searchValue, setSearch] = useState(''); 34 | const [pageLoading, setPageLoading] = useState(false); 35 | const [searchLoading, setSearchLoading] = useState(false); 36 | const columns = isPortrait ? 4 : 7; 37 | const itemWidth = Math.trunc(windowWidth / columns); 38 | 39 | const filterOutRecentWallet = (list: WcWallet[]): WcWallet[] => { 40 | if (!recentWallet) return list; 41 | 42 | const filtered = list.filter((wallet) => wallet.id !== recentWallet.id); 43 | return filtered; 44 | }; 45 | 46 | const walletList = recentWallet 47 | ? [ 48 | recentWallet, 49 | ...filterOutRecentWallet([...installed, ...recommended, ...wallets]), 50 | ] 51 | : filterOutRecentWallet([...installed, ...recommended, ...wallets]); 52 | 53 | const searchWallets = useCallback(async (value: string) => { 54 | setSearch(value); 55 | if (value.length > 0) { 56 | setSearchLoading(true); 57 | await ApiCtrl.searchWallet({ search: value }); 58 | setSearchLoading(false); 59 | } 60 | }, []); 61 | 62 | const fetchNextPage = async () => { 63 | if (walletList.length < count && !pageLoading) { 64 | setPageLoading(true); 65 | await ApiCtrl.fetchWallets({ page: page + 1 }); 66 | setPageLoading(false); 67 | } 68 | }; 69 | 70 | const onWalletPress = (wallet: WcWallet) => { 71 | RouterCtrl.push('Connecting', { wallet }); 72 | }; 73 | 74 | const walletListTemplate = () => { 75 | if (searchValue.length > 0) { 76 | return ( 77 | 86 | ); 87 | } 88 | 89 | return ( 90 | 101 | ); 102 | }; 103 | 104 | const onChangeText = useDebounceCallback({ callback: searchWallets }); 105 | 106 | useEffect(() => { 107 | async function getWallets() { 108 | if (shouldLoadWallets) { 109 | setWalletsLoading(true); 110 | await ApiCtrl.fetchWallets({ page: 1 }); 111 | setWalletsLoading(false); 112 | } 113 | } 114 | getWallets(); 115 | }, [shouldLoadWallets]); 116 | 117 | return ( 118 | <> 119 | 120 | 121 | 122 | {walletListTemplate()} 123 | 124 | ); 125 | } 126 | 127 | export default AllWalletsView; 128 | -------------------------------------------------------------------------------- /src/views/wcm-all-wallets-view/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | searchbar: { marginLeft: 16 }, 5 | }); 6 | -------------------------------------------------------------------------------- /src/views/wcm-connect-view/index.tsx: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import { useSnapshot } from 'valtio'; 3 | import WalletItem, { 4 | WALLET_FULL_HEIGHT, 5 | WALLET_MARGIN, 6 | } from '../../components/WalletItem'; 7 | import ViewAllBox from '../../components/ViewAllBox'; 8 | import QRIcon from '../../assets/QRCode'; 9 | import ModalHeader from '../../partials/wcm-modal-header'; 10 | import type { WcWallet } from '../../types/controllerTypes'; 11 | import { RouterCtrl } from '../../controllers/RouterCtrl'; 12 | import { OptionsCtrl } from '../../controllers/OptionsCtrl'; 13 | import { WcConnectionCtrl } from '../../controllers/WcConnectionCtrl'; 14 | import { ConfigCtrl } from '../../controllers/ConfigCtrl'; 15 | import type { RouterProps } from '../../types/routerTypes'; 16 | import { ApiCtrl } from '../../controllers/ApiCtrl'; 17 | import useTheme from '../../hooks/useTheme'; 18 | import { AssetUtil } from '../../utils/AssetUtil'; 19 | import { WalletItemLoader } from '../../components/WalletItemLoader'; 20 | import styles from './styles'; 21 | 22 | function ConnectView({ isPortrait }: RouterProps) { 23 | const Theme = useTheme(); 24 | const { isDataLoaded } = useSnapshot(OptionsCtrl.state); 25 | const { pairingUri } = useSnapshot(WcConnectionCtrl.state); 26 | const { explorerExcludedWalletIds } = useSnapshot(ConfigCtrl.state); 27 | const { recentWallet } = useSnapshot(ConfigCtrl.state); 28 | const { recommended, installed } = useSnapshot(ApiCtrl.state); 29 | 30 | const loading = !isDataLoaded || !pairingUri; 31 | const viewHeight = isPortrait ? WALLET_FULL_HEIGHT * 2 : WALLET_FULL_HEIGHT; 32 | 33 | const showViewAllButton = 34 | installed.length + recommended.length >= 8 || 35 | explorerExcludedWalletIds !== 'ALL'; 36 | 37 | const viewAllTemplate = () => { 38 | if (!showViewAllButton) return null; 39 | 40 | return ( 41 | RouterCtrl.push('WalletExplorer')} 43 | wallets={recommended.slice(-4)} 44 | style={isPortrait ? styles.portraitItem : styles.landscapeItem} 45 | /> 46 | ); 47 | }; 48 | 49 | const filterOutRecentWallet = (wallets: WcWallet[]): WcWallet[] => { 50 | if (!recentWallet) return wallets; 51 | 52 | const filtered = wallets.filter((wallet) => wallet.id !== recentWallet.id); 53 | return filtered; 54 | }; 55 | 56 | const loadingTemplate = (items: number) => { 57 | return ( 58 | 59 | {Array.from({ length: items }).map((_, index) => ( 60 | 67 | ))} 68 | 69 | ); 70 | }; 71 | 72 | const walletTemplate = () => { 73 | const list = recentWallet 74 | ? [recentWallet, ...filterOutRecentWallet([...installed, ...recommended])] 75 | : filterOutRecentWallet([...installed, ...recommended]); 76 | return list 77 | .slice(0, showViewAllButton ? 7 : 8) 78 | .map((item: WcWallet) => ( 79 | RouterCtrl.push('Connecting', { wallet: item })} 84 | key={item.id} 85 | isRecent={item.id === recentWallet?.id} 86 | style={isPortrait ? styles.portraitItem : styles.landscapeItem} 87 | /> 88 | )); 89 | }; 90 | 91 | return ( 92 | <> 93 | RouterCtrl.push('Qrcode')} 96 | actionIcon={} 97 | /> 98 | {loading ? ( 99 | loadingTemplate(8) 100 | ) : ( 101 | 107 | {walletTemplate()} 108 | {viewAllTemplate()} 109 | 110 | )} 111 | 112 | ); 113 | } 114 | 115 | export default ConnectView; 116 | -------------------------------------------------------------------------------- /src/views/wcm-connect-view/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | explorerContainer: { 5 | flexDirection: 'row', 6 | flexWrap: 'wrap', 7 | alignItems: 'center', 8 | marginBottom: 16, 9 | }, 10 | portraitItem: { 11 | width: '25%', 12 | }, 13 | landscapeItem: { 14 | width: '12.5%', 15 | }, 16 | loaderContainer: { 17 | flexDirection: 'row', 18 | flexWrap: 'wrap', 19 | marginBottom: 16, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /src/views/wcm-connecting-view/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react'; 2 | import { Linking, Platform, View } from 'react-native'; 3 | import { useSnapshot } from 'valtio'; 4 | 5 | import ModalHeader from '../../partials/wcm-modal-header'; 6 | import CopyIcon from '../../assets/CopyLarge'; 7 | import useTheme from '../../hooks/useTheme'; 8 | import { ToastCtrl } from '../../controllers/ToastCtrl'; 9 | import { WcConnectionCtrl } from '../../controllers/WcConnectionCtrl'; 10 | import Text from '../../components/Text'; 11 | import type { RouterProps } from '../../types/routerTypes'; 12 | import { RouterCtrl } from '../../controllers/RouterCtrl'; 13 | import Touchable from '../../components/Touchable'; 14 | import { UiUtil } from '../../utils/UiUtil'; 15 | import RetryIcon from '../../assets/Retry'; 16 | import WalletImage from '../../components/WalletImage'; 17 | import WalletLoadingThumbnail from '../../components/WalletLoadingThumbnail'; 18 | import Chevron from '../../assets/Chevron'; 19 | import { CoreHelperUtil } from '../../utils/CoreHelperUtil'; 20 | import { StorageUtil } from '../../utils/StorageUtil'; 21 | import { AssetUtil } from '../../utils/AssetUtil'; 22 | import { ConfigCtrl } from '../../controllers/ConfigCtrl'; 23 | import { ApiCtrl } from '../../controllers/ApiCtrl'; 24 | import styles from './styles'; 25 | 26 | function ConnectingView({ onCopyClipboard }: RouterProps) { 27 | const Theme = useTheme(); 28 | const { pairingUri, pairingError } = useSnapshot(WcConnectionCtrl.state); 29 | const { data } = useSnapshot(RouterCtrl.state); 30 | const { installed } = useSnapshot(ApiCtrl.state); 31 | const walletName = UiUtil.getWalletName(data?.wallet?.name ?? 'Wallet', true); 32 | const imageUrl = AssetUtil.getWalletImage(data?.wallet); 33 | const isInstalled = !!installed.find((item) => item.id === data?.wallet?.id); 34 | 35 | const storeLink = Platform.select({ 36 | ios: data?.wallet?.app_store, 37 | android: data?.wallet?.play_store, 38 | }); 39 | 40 | const storeCaption = Platform.select({ 41 | ios: 'App Store', 42 | android: 'Play Store', 43 | }); 44 | 45 | const onCopy = async () => { 46 | if (onCopyClipboard) { 47 | onCopyClipboard(pairingUri); 48 | ToastCtrl.openToast('Link copied', 'success'); 49 | } 50 | }; 51 | 52 | const onRetry = async () => { 53 | WcConnectionCtrl.setPairingError(false); 54 | onConnect(); 55 | }; 56 | 57 | const onConnect = useCallback(async () => { 58 | try { 59 | if (!data?.wallet?.mobile_link) return; 60 | 61 | const mobileLink = CoreHelperUtil.formatNativeUrl( 62 | data?.wallet?.mobile_link, 63 | pairingUri 64 | ); 65 | await CoreHelperUtil.openLink(mobileLink); 66 | ConfigCtrl.setRecentWallet(data?.wallet); 67 | StorageUtil.setRecentWallet(data?.wallet); 68 | StorageUtil.setDeepLinkWallet(data?.wallet?.mobile_link); 69 | } catch (error) { 70 | StorageUtil.removeDeepLinkWallet(); 71 | ToastCtrl.openToast('Unable to open the wallet', 'error'); 72 | } 73 | }, [data?.wallet, pairingUri]); 74 | 75 | const retryButtonTemplate = () => { 76 | return ( 77 | 78 | 82 | Retry 83 | 84 | 85 | 86 | ); 87 | }; 88 | 89 | const storeButtonTemplate = () => { 90 | if (!storeLink || isInstalled) return null; 91 | 92 | return ( 93 | 94 | 95 | 96 | {`Get ${walletName}`} 99 | 100 | Linking.openURL(storeLink)} 103 | > 104 | 105 | {storeCaption} 106 | 107 | 112 | 113 | 114 | ); 115 | }; 116 | 117 | useEffect(() => { 118 | WcConnectionCtrl.setPairingError(false); 119 | onConnect(); 120 | }, [onConnect]); 121 | 122 | return ( 123 | <> 124 | } 127 | onActionPress={onCopyClipboard ? onCopy : undefined} 128 | /> 129 | 132 | 133 | 134 | 135 | 141 | {pairingError 142 | ? 'Connection declined' 143 | : `Continue in ${walletName}...`} 144 | 145 | 146 | 154 | {retryButtonTemplate()} 155 | {storeButtonTemplate()} 156 | 157 | 158 | ); 159 | } 160 | 161 | export default ConnectingView; 162 | -------------------------------------------------------------------------------- /src/views/wcm-connecting-view/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | walletContainer: { 5 | alignItems: 'center', 6 | justifyContent: 'center', 7 | paddingVertical: 40, 8 | }, 9 | image: { 10 | height: 96, 11 | width: 96, 12 | borderRadius: 28, 13 | }, 14 | continueText: { 15 | marginTop: 20, 16 | fontSize: 16, 17 | fontWeight: '600', 18 | }, 19 | retryButton: { 20 | flexDirection: 'row', 21 | alignItems: 'center', 22 | paddingVertical: 6, 23 | paddingHorizontal: 12, 24 | borderRadius: 16, 25 | }, 26 | retryIcon: { 27 | marginLeft: 4, 28 | }, 29 | text: { 30 | color: 'white', 31 | fontWeight: '500', 32 | fontSize: 14, 33 | }, 34 | alternateText: { 35 | marginTop: 16, 36 | }, 37 | getText: { 38 | fontSize: 16, 39 | fontWeight: '600', 40 | marginLeft: 8, 41 | }, 42 | storeText: { 43 | fontSize: 14, 44 | fontWeight: '600', 45 | }, 46 | footer: { 47 | width: '100%', 48 | }, 49 | row: { 50 | flexDirection: 'row', 51 | alignItems: 'center', 52 | }, 53 | upperFooter: { 54 | alignItems: 'center', 55 | paddingVertical: 16, 56 | borderTopWidth: StyleSheet.hairlineWidth, 57 | borderBottomWidth: StyleSheet.hairlineWidth, 58 | }, 59 | lowerFooter: { 60 | paddingHorizontal: 20, 61 | paddingTop: 14, 62 | paddingBottom: 24, 63 | flexDirection: 'row', 64 | justifyContent: 'space-between', 65 | alignItems: 'center', 66 | }, 67 | storeIcon: { 68 | marginLeft: 4, 69 | }, 70 | }); 71 | -------------------------------------------------------------------------------- /src/views/wcm-qr-code-view/index.tsx: -------------------------------------------------------------------------------- 1 | import { ActivityIndicator, StyleSheet, View } from 'react-native'; 2 | import { useSnapshot } from 'valtio'; 3 | 4 | import ModalHeader from '../../partials/wcm-modal-header'; 5 | import QRCode from '../../components/QRCode'; 6 | import CopyIcon from '../../assets/CopyLarge'; 7 | import { WcConnectionCtrl } from '../../controllers/WcConnectionCtrl'; 8 | import type { RouterProps } from '../../types/routerTypes'; 9 | import { ThemeCtrl } from '../../controllers/ThemeCtrl'; 10 | import useTheme from '../../hooks/useTheme'; 11 | import { ToastCtrl } from '../../controllers/ToastCtrl'; 12 | import { useEffect } from 'react'; 13 | 14 | function QRCodeView({ 15 | onCopyClipboard, 16 | isPortrait, 17 | windowHeight, 18 | windowWidth, 19 | }: RouterProps) { 20 | const Theme = useTheme(); 21 | const QRSize = isPortrait 22 | ? Math.round(windowWidth * 0.8) 23 | : Math.round(windowHeight * 0.6); 24 | const themeState = useSnapshot(ThemeCtrl.state); 25 | const { pairingUri, pairingError } = useSnapshot(WcConnectionCtrl.state); 26 | 27 | const onCopy = async () => { 28 | if (onCopyClipboard && pairingUri) { 29 | onCopyClipboard(pairingUri); 30 | ToastCtrl.openToast('Link copied', 'success'); 31 | } 32 | }; 33 | 34 | useEffect(() => { 35 | if (pairingError) { 36 | ToastCtrl.openToast('Connection request declined', 'error'); 37 | } 38 | }, [pairingError]); 39 | 40 | return ( 41 | <> 42 | 50 | } 51 | onActionPress={onCopyClipboard ? onCopy : undefined} 52 | actionDisabled={!pairingUri} 53 | /> 54 | 55 | {pairingUri ? ( 56 | 57 | ) : ( 58 | 64 | )} 65 | 66 | 67 | ); 68 | } 69 | 70 | const styles = StyleSheet.create({ 71 | container: { 72 | paddingBottom: 16, 73 | width: '100%', 74 | }, 75 | }); 76 | 77 | export default QRCodeView; 78 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig", 4 | "exclude": ["example"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@walletconnect/modal-react-native": ["./src/index"] 5 | }, 6 | "allowJs": false, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "ignoreDeprecations":"5.0", 11 | "importsNotUsedAsValues": "error", 12 | "forceConsistentCasingInFileNames": true, 13 | "jsx": "react-jsx", 14 | "lib": ["esnext"], 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitReturns": true, 19 | "noImplicitOverride": true, 20 | "noImplicitUseStrict": false, 21 | "noStrictGenericChecks": false, 22 | "noUncheckedIndexedAccess": true, 23 | "noPropertyAccessFromIndexSignature": false, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "resolveJsonModule": true, 27 | "skipLibCheck": true, 28 | "strict": true, 29 | "target": "esnext" 30 | }, 31 | "exclude": ["node_modules", "dist", "types", "lib"] 32 | } 33 | --------------------------------------------------------------------------------