diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a71681774..08237ff46 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,15 +18,15 @@ jobs: timeout-minutes: 40 # default is 360 strategy: matrix: - node-version: [14.x, 16.x] + node-version: [20.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Install dependencies - run: npm install + run: npm ci - name: Install gettext run: sudo apt-get install gettext - name: Check version diff --git a/QA_CUSTOM_NETWORK.md b/QA_CUSTOM_NETWORK.md index b9f90d104..c184d8ef8 100644 --- a/QA_CUSTOM_NETWORK.md +++ b/QA_CUSTOM_NETWORK.md @@ -1,80 +1,136 @@ # Custom Network 1. **Settings screen** - 1. Go to Settings - 1. Check you see "General Settings" as the title for the first collection of settings - 1. Check you see "Developer Settings" as the title for the second collection of settings + 1. [ ] Go to Settings + 1. [ ] Check you see "General Settings" as the title for the first collection of settings + 1. [ ] Check you see "Developer Settings" as the title for the second collection of settings 1. **Risk Disclaimer screen** - 1. Go to Network Settings - 1. Ensure you see the `RISK DISCLAIMER` screen - 1. Verify that there is a disclaimer text on a yellow background - 1. Confirm there is a button at the bottom of the page to acknowledge the disclaimer + 1. [ ] Go to Network Settings + 1. [ ] Ensure you see the `RISK DISCLAIMER` screen + 1. [ ] Verify that there is a disclaimer text on a yellow background + 1. [ ] Confirm there is a button at the bottom of the page to acknowledge the disclaimer 1. **Network Pre-Settings Screen** - 1. Click on "I UNDERSTAND" - 1. Check you see the `NETWORK PRE-SETTINGS` screen - 1. Check you see two pre-settings options: Mainnet and Testnet - 1. Check you see a button at the bottom of the page to customize the network + 1. [ ] Click on "I UNDERSTAND" + 1. [ ] Check you see the `NETWORK PRE-SETTINGS` screen + 1. [ ] Check you see two pre-settings options: Mainnet and Testnet + 1. [ ] Check you see a button at the bottom of the page to customize the network 1. **CUSTOM NETWORK SETTINGS Screen** - 1. Click on "CUSTOMIZE" - 1. Check you see the `CUSTOM NETWORK SETTINGS` screen - 1. Check you see a warning message text in a yellow background - 1. Check you see a form with the following values: + 1. [ ] Click on "CUSTOMIZE" + 1. [ ] Check you see the `CUSTOM NETWORK SETTINGS` screen + 1. [ ] Check you see a warning message text in a yellow background + 1. [ ] Check you see a form with the following values: - Node URL - Explorer URL - Explorer Service URL + - Transaction Mining Service URL - Wallet Service URL (optional) - Wallet Service WS URL (optional) - 1. Check you see a button at the bottom of the page to send the form + + >[!NOTE] + >Wallet Service fields only appear if the wallet is allowed on Wallet Service feature. + + 1. [ ] Check you see a button at the bottom of the page to send the form 1. **Send the default network settings** - 1. Click on "SEND" - 1. Verify that the wallet has successfully reloaded - 1. Ensure that a success feedback modal appears following the reload - 1. Dismiss the success feedback modal + 1. [ ] Click on "SEND" + 1. [ ] Verify that the wallet has successfully reloaded + 1. [ ] Ensure that a success feedback modal appears following the reload + 1. [ ] Dismiss the success feedback modal 1. **Send an invalid Node URL** - 1. Empty the Node URL field - 1. Check that an invalidation message appears under the field informing Node URL is required - 1. Fill the field with an invalid URL, e.g. "invalid" - 1. Check that an invalidation message appears under the field informing Node URL should be a valid URL + 1. [ ] Empty the Node URL field + 1. [ ] Check that an invalidation message appears under the field informing Node URL is required + 1. [ ] Fill the field with an invalid URL, e.g. "invalid" + 1. [ ] Check that an invalidation message appears under the field informing Node URL should be a valid URL 1. **Send a custom network** - 1. Navigate back to Network Pre-Settings screen - 1. Click on "CUSTOMIZE" again - 1. Fill the "Node URL" field with the value "https://node1.mainnet.hathor.network/v1a/" - 1. Click on "SEND" - 1. Wait the wallet reload - 1. Check if a success feedback modal has appeared - 1. Dismiss the success feedback modal - 1. Verify that a network status bar with a yellow background and the message "Custom network: mainnet" is visible at the top of the screen + 1. [ ] Navigate back to Network Pre-Settings screen + 1. [ ] Click on "CUSTOMIZE" again + 1. [ ] Fill the "Node URL" field with the value "https://node1.mainnet.hathor.network/v1a/" + 1. [ ] Click on "SEND" + 1. [ ] Wait the wallet reload + 1. [ ] Check if a success feedback modal has appeared + 1. [ ] Dismiss the success feedback modal + 1. [ ] Verify that a network status bar with a yellow background and the message "Custom network: mainnet" is visible at the top of the screen 1. **Restoring to mainnet** - 1. Navigate back to Network Pre-Settings screen - 1. Click on "Mainnet" option - 1. Wait for the wallet to reload - 1. Check a success feedback modal has appeared - 1. Dismiss the success feedback modal - 1. Ensure that the network status bar at the top of the screen has disappeared + 1. [ ] Navigate back to Network Pre-Settings screen + 1. [ ] Click on "Mainnet" option + 1. [ ] Wait for the wallet to reload + 1. [ ] Check a success feedback modal has appeared + 1. [ ] Dismiss the success feedback modal + 1. [ ] Ensure that the network status bar at the top of the screen has disappeared 1. **Check testnet balance** + >[!NOTE] >In this test we want to check the difference of transaction history and balance between mainnet and testnet of the same wallet. Therefore, you should have some transactions also in your testnet wallet. - 1. Click on "Testnet" option - 1. Wait for the wallet to reload - 1. Check a success feedback modal has appeared - 1. Dismiss the success feedback modal - 1. Verify that a network status bar with a yellow background and the message "Custom network: testnet" is visible at the top of the screen - 1. Go to the Home screen - 1. Check the balance of Hathor token - 1. Click on Hathor token - 1. Check you see different transactions from "mainnet" - 1. Click on a transaction - 1. Click on the "Public Explorer" item - 1. Verify that you were redirected to the transaction page on the testnet explorer + 1. [ ] Click on "Testnet" option + 1. [ ] Wait for the wallet to reload + 1. [ ] Check a success feedback modal has appeared + 1. [ ] Dismiss the success feedback modal + 1. [ ] Verify that a network status bar with a yellow background and the message "Custom network: testnet" is visible at the top of the screen + 1. [ ] Go to the Home screen + 1. [ ] Check the balance of Hathor token + 1. [ ] Click on Hathor token + 1. [ ] Check you see different transactions from "mainnet" + 1. [ ] Click on a transaction + 1. [ ] Click on the "Public Explorer" item + 1. [ ] Verify that you were redirected to the transaction page on the testnet explorer + +1. **Check registered tokens are cleaned when changing network settings** + 1. [ ] Register a token on Mainnet network + 1. [ ] Navigate to Network Pre-Settings screen + 1. [ ] Select the Testnet network + 1. [ ] Navigate to Mainscreen + 1. [ ] Check there is only the HTR token in the dashboard + +1. **Check Wallet Service fields didn't appear when Wallet Service is disabled for the wallet** + 1. [ ] Go to Unleash and disables the Wallet Service for the wallet + 1. You should set this configuration on `wallet-service-mobile.rollout` + 1. [ ] Close and reopens the App + 1. [ ] Navigate Custom Network Settings screen on Network Settings feature + 1. [ ] Check the Wallet Service fields do not appear + +1. **Check Wallet Service fields disable Push Notification when they are empty** + 1. [ ] Go to Unleash and enables the Wallet Service for the wallet + 1. You should set this configuration on `wallet-service-mobile.rollout` + 1. [ ] Close and reopens the App + 1. [ ] Navigate Custom Network Settings screen on Network Settings feature + 1. [ ] Check the Wallet Service fields do appear + 1. [ ] Make both Wallet Service fields empty + 1. [ ] Navigate to Settings screen + 1. [ ] Check the Push Notification option didn't appear +1. **Check Wallet Service fields can't be empty unilaterally** + 1. [ ] Go to Unleash and enables the Wallet Service for the wallet + 1. You should set this configuration on `wallet-service-mobile.rollout` + 1. [ ] Close and reopens the App + 1. [ ] Navigate Custom Network Settings screen on Network Settings feature + 1. [ ] Check the Wallet Service fields do appear + 1. [ ] Make one of the Wallet Service fields empty + 1. [ ] Send the custom settings + 1. You should see an error message under the field +1. **Check Push Notification aren't available on Testnet** + 1. [ ] Navigate to Network Pre-Settings screen + 1. [ ] Select Testnet pre-settings + 1. [ ] Navigate to Settings screen + 1. [ ] Check Push Notification didn't appear +1. **Check Push Notification aren't being received on Testnet** + 1. [ ] Go to Unleash and enables the Wallet Service for the wallet + 1. You should set this configuration on `wallet-service-mobile.rollout` + 1. [ ] Navigate to Network Pre-Settings screen + 1. [ ] Select Mainnet pre-settings + 1. [ ] Enables Push Notification + 1. [ ] Send a transaction to your wallet from another wallet + 1. You should receive a push notification + 1. [ ] Navigate to Network Pre-Settings screen + 1. [ ] Select Testnet pre-settings + 1. [ ] Send a transaction to your wallet from another wallet + 1. You should **not** receive a push notification diff --git a/QA_NANO_CONTRACT.md b/QA_NANO_CONTRACT.md new file mode 100644 index 000000000..7496ce16c --- /dev/null +++ b/QA_NANO_CONTRACT.md @@ -0,0 +1,175 @@ +# QA Nano Contract + +- CTA: call-to-action + +### Activation +- [ ] Make sure the wallet-service is disabled for the device +- [ ] Enable the nano contract feature for the device +- Close the wallet and reopen +- [ ] Check a toggle button with two options appear in the dashboard head + - Tokens + - Nano + +### Nano Contract Details +- [ ] Check the option "Tokens" is selected +- Select the option "Nano Contracts" +- [ ] Check the Nano Contract component is in focus with a title "No Nano Contracts" +- [ ] Check the *Nano Contract Details* component has: + - a message, and + - a call-to-action (CTA) "Register new" + +### Register Nano Contract +- Register a new Nano Contract by tapping over "Register new" on *Nano Contract Details* + - It may focus on a component of loading +- [ ] Check if the Nano Contract Registration screen is in focus +- [ ] Check if the Nano Contract ID is an input +- [ ] Check if the Wallet Address is loaded +- [ ] Check if the "REGISTER NANO CONTRACT" button is disabled +- Type the following Nano Contract ID `00004f1a3b07f0ffc7ced77a19ae6d4407991a15a281c0ddaf3d6e04a56eba78` + - It represents a Nano Contract in the `nano-testnet` network +- [ ] Check the "REGISTER NANO CONTRACT" button is now actionable +- Tap on "REGISTER NANO CONTRACT" button +- [ ] Check for a failure feedback modal to appear + - You can't register a Nano Contract from another network + - If the message "The informed address does not belong to the wallet" is the failure message, it happens when you are using the wallet service. Make sure to disable it and reload the wallet. +- Dismiss the modal by tapping outside its area +- Remove the input +- [ ] Check if a validation message of error appeared under the input: + - It should informe the field is required + - it should be in color red +- [ ] Check if the "REGISTER NANO CONTRACT" button is disabled +- Type any input +- [ ] Check if the validation error message disappeared + +### Change network +- Navigate to the *Settings* screen +- Go to "Network Settings" > Understand disclaimer > Customize +- [ ] Check if the following fields are hidden, you should not be able see them if Wallet Service is disabled: + - "Wallet Service URL (optional)", and + - "Wallet Service WS URL (optional)" +- [NOTE] If you see the mentioned fields because you have Wallet Service enabled, check they are empty or do it by your own before following the next instructions. +- Set "Node URL" the input "https://node1.nano-testnet.hathor.network/v1a/" +- Set "Explorer URL" the input "https://explorer.alpha.nano-testnet.hathor.network/" +- Set "Explorer Service URL" the input "https://explorer-service.nano-testnet.hathor.network/" +- Set "Transaction Mining Service URL" the input "https://txmining.nano-testnet.hathor.network/" +- Tap on "SEND" button +- [ ] Check for a success feedback modal message +- Dismiss the feedback modal +- [ ] Check there is a yellow bar at the head informing the custom network as *testnet* +- Go back to *Dashboard* screen + +### Register Nano Contract using the right network +- Select the option "Nano Contracts" of the toggle button +- Tap on "Register new" CTA +- Type `00004f1a3b07f0ffc7ced77a19ae6d4407991a15a281c0ddaf3d6e04a56eba78` as input to Nano Contract ID +- Tap on "REGISTER NANO CONTRACT" button +- [ ] Check for a success feedback modal to appear + - If a failure feedback modal appear with the message "The informed address does not belong to the wallet", it happens when you are using the wallet service. Make sure to disable it and reload the wallet +- [ ] Check the feedback modal has: + - A message informing the registration has happen with success, and + - A "SEE CONTRACT" button +- Tap on "SEE CONTRACT" button +- [ ] Check *Nano Contract Details* screen is in focus + +### Inspect Nano Contract Details +- From the *Nano Contract Details* screen follow the instructions +- [ ] Check there is at least one transaction listed, the "initialize" +- Tap on "Nano Contract" header to expand it +- Tap on "Nano Contract" header again to shrink it +- Tap on "Nano Contract" header to expand it again +- [ ] Check there is a "Nano Contract ID" field with eclipsed value +- [ ] Check there is a "Blueprint Name" field and value +- [ ] Check there ia a "Registered Address" field and value +- [ ] Check the field "Registered Address" is actionable +- [ ] Check there are two CTAs side-by-side: + - "See status details", and + - "Unregister contract" +- Tap on "See status details" CTA: + - A browser app should open in the explorer page of the Nano Contract and it should contains all Nano Contract details information, including the arguments of the method call +- Go back to the wallet +- Tap on "Registered Address" +- [ ] Check for a "Choose New Wallet Address" modal appears +- [ ] Check the address of *index 0* is selected +- Select any other indexed address +- [ ] Check for a "New Nano Contract Address" modal appears +- [ ] Check "New Nano Contract Address" modal has: + - An information content informing user this address is used to sign a transaction within the Nano Contract + - A mirror content informing the selected address and its index + - A "CONFIRM NEW ADDRESS" button + - A "GO BACK" button +- Tap on "GO BACK" button +- [ ] Check the modal disappear +- Select any other indexed address +- [ ] Check the modal appear +- Tap outside modal area +- [ ] Check the modal disappear +- Select any other indexed address +- Tap on "CONFIRM NEW ADDRESS" +- [ ] Check for the new address is set on "Registered Address" field +- Tap on "Registered Address" +- [ ] Check for the correct indexed address is selected +- [ ] Dismiss the modal +- Tap on "Unregister contract" CTA +- [ ] Check for a new "Unregister Nano Contract" modal appear +- [ ] Check "Unregister Nano Contract" modal has: + - A message asking the user to be sure of unregistration, + - A button "YES, UNREGISTER CONTRACT", and + - A button "NO, GO BACK" +- Tap on "NO, GO BACK" button +- [ ] Check the modal disappear +- Tap on "Unregister contract" CTA again +- Tap on "YES, UNREGISTER CONTRACT" +- [ ] Check the *Dashboard* screen is on focus +- [ ] Check the Nano Contract list is empty + - It shows title "No Nano Contracts" + +### Inspect Nano Contract Transaction +- [ ] Register the Nano Contract `00004f1a3b07f0ffc7ced77a19ae6d4407991a15a281c0ddaf3d6e04a56eba78` +- Go the *Nano Contract Details* screen +- [ ] Check there are many transactions +- [ ] Check a transaction item has: + - An eclipsed Transaction ID value in bold format + - A method name for the transaction + - A humanized timestamp +- [ ] Check any item is actionable +- [ ] Check the transaction list is ordered from newest to oldest +- [ ] Check the oldest transaction is the `initialize` +- Tap on `initialize` transaction +- [ ] Check the *Nano Contract Transaction* screen is on focus +- [ ] Check there is a "Transaction ID" field with eclipsed value as a header +- [ ] Check the header is expanded +- [ ] Check there is a label with value "Executed" in green +- [ ] Check there is a "Transaction ID" field with eclipsed value +- [ ] Check there is a "Nano Contract ID" field with eclipsed value +- [ ] Check there is a "Blueprint Method" field and value +- [ ] Check there ia a "Date and Time" field and value +- [ ] Check there is a "Caller" field with eclipsed value +- [ ] Check there is a "See transaction details" CTA +- Tap on any area of the header to shrink it +- Tap on "Transaction ID" header to expand it +- Tap on "See transaction details" CTA + - A browser app should open in the explorer page of the Transaction and it should contains all Transaction details information, including the arguments of the method call +- Go back to the wallet +- [ ] Check there is a content area under the header with title "No Actions" +- Go back to *Nano Contract Details* screen +- Tap on any bet +- [ ] Check the *Nano Contract Transaction* screen is on focus +- [ ] Check there is an action as a content that has: + - A "Deposit" text prefix + - An Input ID with eclipsed value + - An action amount + +### Change network and reset registered Nano Contracts +- Go to *Network Settings* screen +- Customize it to `testnet` network +- Go back to *Dashboard* screen +- Select "Nano Contracts" option if not selected +- [ ] Check there is no Nano Contract registered +- Customize network to `nano-testnet` network +- Set "Node URL" to value `https://node1.nano-testnet.hathor.network/v1a/` +- Set "Transaction Mining Service URL" to value `https://txmining.nano-testnet.hathor.network/` +- Clean "Wallet Service URL (optional)" and "Wallet Service WS URL (optional)" if present +- Tap on "SEND" +- Go back to *Dashboard* screen +- Select "Nano Contracts" option if not selected +- [ ] Check there is no Nano Contract registered diff --git a/QA_PUSH_NOTIFICATION.md b/QA_PUSH_NOTIFICATION.md index 4500d2bb9..543b93cbe 100644 --- a/QA_PUSH_NOTIFICATION.md +++ b/QA_PUSH_NOTIFICATION.md @@ -26,250 +26,278 @@ It's a second custom NFT token to test. ## Suggested test sequence 1. **Preparation** - 1. Clear the application storage - 1. Make sure the deviceId is not registered in the unleash **`push-notification.rollout`** feature toggle - 1. If testing the fullnode wallet, make sure the unleash wallet-service feature toggle is disabled + 1. [ ] Clear the application storage + 1. [ ] Make sure the deviceId is not registered in the unleash **`push-notification.rollout`** feature toggle + 1. [ ] If testing the fullnode wallet, make sure the unleash wallet-service feature toggle is disabled + 1. **Initialize a new wallet** - 1. You should **not** see a modal to opt-in the push notification yet - 1. Go to the **Settings** page + 1. [ ] You should **not** see a modal to opt-in the push notification yet + 1. [ ] Go to the **Settings** page 1. You should **not** see the **Push Notification** item yet + 1. **Turn on the `push-notification` feature toggle** - 1. Go to the settings page - 1. Get the `deviceId` and add it in the `UserIDs` for the stage and platform mobile in the unleash **`push-notification.rollout`** feature toggle. - 1. Wait until the **Push Notification** item shows up in the Settings page - 1. You should see a modal to opt-in the push notification - 1. Click on **Yes, enable** + 1. [ ] Go to the settings page + 1. [ ] Get the `deviceId` and add it in the `UserIDs` for the stage and platform mobile in the unleash **`push-notification.rollout`** feature toggle. + 1. [ ] Wait until the **Push Notification** item shows up in the Settings page + 1. [ ] You should see a modal to opt-in the push notification + 1. [ ] Click on **Yes, enable** 1. You should be redirected to **Push Notification** page 1. **Turn off the `push-notification` feature toggle** - 1. **Go to the Settings page** - 1. Remove your `deviceId` from the unleash **`push-notification.rollout`** feature toggle - 1. Wait until the Push Notification item disappears from the Settings page + 1. [ ] **Go to the Settings page** + 1. [ ] Remove your `deviceId` from the unleash **`push-notification.rollout`** feature toggle + 1. [ ] Wait until the Push Notification item disappears from the Settings page + 1. **Test push notification settings on/off** - 1. Turn on the `push-notification` feature toggle - 1. Go to the **Push Notification** page - 1. Turn on the `Enable Push Notification` - 1. Turn on the `Show amounts on notification` - 1. Turn off the `Enabled Push Notification` - 1. Try to turn on the `Show amounts on notification` + 1. [ ] Turn on the `push-notification` feature toggle + 1. [ ] Go to the **Push Notification** page + 1. [ ] Turn on the `Enable Push Notification` + 1. [ ] Turn on the `Show amounts on notification` + 1. [ ] Turn off the `Enabled Push Notification` + 1. [ ] Try to turn on the `Show amounts on notification` 1. it should not be possible - 1. Turn on the `Enabled Push Notification` + 1. [ ] Turn on the `Enabled Push Notification` 1. `Show amounts on notification` should be on - 1. Turn off the `Show amounts on notification` - 1. Turn off the `Enabled Push Notification` + 1. [ ] Turn off the `Show amounts on notification` + 1. [ ] Turn off the `Enabled Push Notification` + 1. **Try to send a notification with `push-notification` feature toggle turned off** - 1. Go to the **Settings** page - 1. Turn **off** the `push-notifiation` feature toggle - 1. Send HTR to this wallet + 1. [ ] Go to the **Settings** page + 1. [ ] Turn **off** the `push-notifiation` feature toggle + 1. [ ] Send HTR to this wallet 1. Wait some minutes to guarantee you won't receive any notifications for this tx + 1. **Try to send a notification with `push-notification` feature toggle turned on** - 1. Go to the **Settings** page - 1. Turn **on** the `push-notifiation` feature toggle - 1. Send HTR to this wallet + 1. [ ] Go to the **Settings** page + 1. [ ] Turn **on** the `push-notifiation` feature toggle + 1. [ ] Send HTR to this wallet 1. Wait some minutes to guarantee you won't receive any notifications for this tx (because the settings `Enable Push Notification` is disabled) + 1. **Send a token after turn on `Enable Push Notification` option** - 1. Turn on the `push-notification` feature toggle - 1. Go to the **Push Notification** page - 1. Turn **on** the `Enable Push Notification` option - 1. Send HTR to this wallet + 1. [ ] Turn on the `push-notification` feature toggle + 1. [ ] Go to the **Push Notification** page + 1. [ ] Turn **on** the `Enable Push Notification` option + 1. [ ] Send HTR to this wallet 1. Wait until the notification arrives 1. You should receive a notification of new transaction without show amounts > There is a new transaction in your wallet. - 1. Dismiss the notification + 1. [ ] Dismiss the notification + 1. **Send a token after turn on `Show amounts on notification` option** - 1. Turn on the `push-notification` feature toggle - 1. Go to the **Push Notification** page - 1. Turn **on** the `Enable Push Notification` option - 1. Turn **on** the `Show amounts on notification` option - 1. Send HTR to this wallet + 1. [ ] Turn on the `push-notification` feature toggle + 1. [ ] Go to the **Push Notification** page + 1. [ ] Turn **on** the `Enable Push Notification` option + 1. [ ] Turn **on** the `Show amounts on notification` option + 1. [ ] Send HTR to this wallet 1. Wait until the notification arrives 1. You should receive a notification of new transaction showing amounts in the message > You have received 0.04 HTR on a new transaction. - 1. Dismiss the notification + 1. [ ] Dismiss the notification + 1. **View the details of the transaction (foreground)** - 1. Send a token after turn on `Enable Push Notification` option - 1. **Keep the application open** - 1. Click on the notification - 1. Wait until the modal with tx details open + 1. [ ] Send a token after turn on `Enable Push Notification` option + 1. [ ] **Keep the application open** + 1. [ ] Click on the notification + 1. [ ] Wait until the modal with tx details open 1. The `HTR - HATHOR` name should be in the primary color (purple) - 1. Click on the `HTR - HATHOR` item + 1. [ ] Click on the `HTR - HATHOR` item 1. The **Balance** page should open + 1. **View the details of the transaction (quit)** - > INFO: Notifee v5.7.0 with Android API 32 has a [known issue regarding onBackgroundEvent](https://github.com/invertase/notifee/issues/404). - 1. Turn on the `push-notification` feature toggle - 1. Go to the **Push Notification** page - 1. Turn **on** the `Enable Push Notification` option - 1. **Close the application** - 1. Send HTR to this wallet + + >[!NOTE] + >Notifee v5.7.0 with Android API 32 has a [known issue regarding onBackgroundEvent](https://github.com/invertase/notifee/issues/404). + + 1. [ ] Turn on the `push-notification` feature toggle + 1. [ ] Go to the **Push Notification** page + 1. [ ] Turn **on** the `Enable Push Notification` option + 1. [ ] **Close the application** + 1. [ ] Send HTR to this wallet 1. Wait until the notification arrives 1. You should receive a notification of new transaction without show amounts > There is a new transaction in your wallet. - 1. Click on the notification - 1. Wait until the modal with tx details open + 1. [ ] Click on the notification + 1. [ ] Wait until the modal with tx details open 1. The `HTR - HATHOR` name should be in the primary color (purple) - 1. Click on the `HTR - HATHOR` item + 1. [ ] Click on the `HTR - HATHOR` item 1. The **Balance** page should open + 1. **View the details of the transaction while in foreground starting from lock screen** - 1. Send a token after turn on `Enable Push Notification` option - 1. **Keep the application open** - 1. Go to the Settings screen. - 1. Go to Security - 1. Click on Lock Wallet. - 1. Click on the notification - 1. Unlock the screen - 1. Wait until the modal with tx details open + 1. [ ] Send a token after turn on `Enable Push Notification` option + 1. [ ] **Keep the application open** + 1. [ ] Go to the Settings screen. + 1. [ ] Go to Security + 1. [ ] Click on Lock Wallet. + 1. [ ] Click on the notification + 1. [ ] Unlock the screen + 1. [ ] Wait until the modal with tx details open 1. The `HTR - HATHOR` name should be in the primary color (purple) - 1. Click on the `HTR - HATHOR` item + 1. [ ] Click on the `HTR - HATHOR` item 1. The **Balance** page should open + 1. **Reset wallet and send a token** - 1. Reset the wallet - 1. Send HTR to this wallet + 1. [ ] Reset the wallet + 1. [ ] Send HTR to this wallet 1. Wait until the notification arrives 1. You should **not** receive a notification - 1. Import the wallet - 1. Send HTR to this wallet + 1. [ ] Import the wallet + 1. [ ] Send HTR to this wallet 1. Wait until the notification arrives 1. You should **not** receive a notification - 1. Send a token after turn on `Enable Push Notification` option + 1. [ ] Send a token after turn on `Enable Push Notification` option 1. Wait until the notification arrives 1. You should receive a notification of new transaction without show amounts > There is a new transaction in your wallet. - 1. Dismiss the notification + 1. [ ] Dismiss the notification + 1. **Send 2 tokens after turn on `Show amounts on notification` option** - 1. Turn on the `push-notification` feature toggle - 1. Go to the **Push Notification** page - 1. Turn **on** the `Enable Push Notification` option - 1. Turn **on** the `Show amounts on notification` option - 1. Send HTR and TTT to this wallet + 1. [ ] Turn on the `push-notification` feature toggle + 1. [ ] Go to the **Push Notification** page + 1. [ ] Turn **on** the `Enable Push Notification` option + 1. [ ] Turn **on** the `Show amounts on notification` option + 1. [ ] Send HTR and TTT to this wallet 1. Wait until the notification arrives 1. You should receive a notification of new transaction showing 2 amounts in the message > You have received 0.09 HTR and 0.01 TTT on a new transaction. - 1. Click on the notification - 1. Wait until the modal with tx details open + 1. [ ] Click on the notification + 1. [ ] Wait until the modal with tx details open 1. The `HTR - HATHOR` name should be in the primary color (purple) 1. The `TTT - Test Token Test` name should be in gray - 1. Click on the `TTT - Test Token Test` item + 1. [ ] Click on the `TTT - Test Token Test` item 1. Nothing should happen - 1. Click on the `HTR - HATHOR` item + 1. [ ] Click on the `HTR - HATHOR` item 1. The **Balance** page should open + 1. **Send 3 tokens after turn on `Show amounts on notification` option** - 1. Turn on the `push-notification` feature toggle - 1. Go to the **Push Notification** page - 1. Turn **on** the `Enable Push Notification` option - 1. Turn **on** the `Show amounts on notification` option - 1. Send HTR, TTT and TN1 to this wallet + 1. [ ] Turn on the `push-notification` feature toggle + 1. [ ] Go to the **Push Notification** page + 1. [ ] Turn **on** the `Enable Push Notification` option + 1. [ ] Turn **on** the `Show amounts on notification` option + 1. [ ] Send HTR, TTT and TN1 to this wallet 1. Wait until the notification arrives 1. You should receive a notification of new transaction showing 2 amounts in the message > You have received 0.05 TN1, 0.03 TTT and 1 other token on a new transaction. - 1. Click on the notification - 1. Wait until the modal with tx details open + 1. [ ] Click on the notification + 1. [ ] Wait until the modal with tx details open 1. The `HTR - HATHOR` name should be in the primary color (purple) 1. The `TTT - Test Token Test` name should be in gray 1. The `TN1 - Test Nft` name should be in gray - 1. Click on the `TTT - Test Token Test` item + 1. [ ] Click on the `TTT - Test Token Test` item 1. Nothing should happen - 1. Click on the `TN1 - Test Nft` item + 1. [ ] Click on the `TN1 - Test Nft` item 1. Nothing should happen - 1. Click on the `HTR - HATHOR` item + 1. [ ] Click on the `HTR - HATHOR` item 1. The **Balance** page should open + 1. **Send 4 tokens after turn on `Show amounts on notification` option** - 1. Turn on the `push-notification` feature toggle - 1. Go to the **Push Notification** page - 1. Turn **on** the `Enable Push Notification` option - 1. Turn **on** the `Show amounts on notification` option - 1. Send HTR, TTT, TN1 and TNT to this wallet + 1. [ ] Turn on the `push-notification` feature toggle + 1. [ ] Go to the **Push Notification** page + 1. [ ] Turn **on** the `Enable Push Notification` option + 1. [ ] Turn **on** the `Show amounts on notification` option + 1. [ ] Send HTR, TTT, TN1 and TNT to this wallet 1. Wait until the notification arrives 1. You should receive a notification of new transaction showing 2 amounts in the message > You have received 0.08 TNT, 0.05 TN1 and 2 other tokens on a new transaction. - 1. Click on the notification - 1. Wait until the modal with tx details open + 1. [ ] Click on the notification + 1. [ ] Wait until the modal with tx details open 1. The `HTR - HATHOR` name should be in the primary color (purple) 1. The `TTT - Test Token Test` name should be in gray 1. The `TN1 - Test Nft` name should be in gray 1. The `TNT - Test Nft Test` name should be in gray - 1. Click on the `TTT - Test Token Test` item + 1. [ ] Click on the `TTT - Test Token Test` item 1. Nothing should happen - 1. Click on the `TN1 - Test Nft` item + 1. [ ] Click on the `TN1 - Test Nft` item 1. Nothing should happen - 1. Click on the `TNT - Test Nft Test` item + 1. [ ] Click on the `TNT - Test Nft Test` item 1. Nothing should happen - 1. Click on the `HTR - HATHOR` item + 1. [ ] Click on the `HTR - HATHOR` item 1. The **Balance** page should open + 1. **Register `TTT` token and send 2 tokens after turn on `Show amounts on notification` option** - > WARNING: Not possible using `wallet-service` in `testnet` due to a validation that consults the fullnode. - > Jump to **Test open the wallet 2 weeks later** (19) - 1. Register `TTT` token in the wallet - 1. Turn on the `push-notification` feature toggle - 1. Go to the **Push Notification** page - 1. Turn **on** the `Enable Push Notification` option - 1. Turn **on** the `Show amounts on notification` option - 1. Send HTR and TTT to this wallet + + >[!WARNING] + >Not possible using `wallet-service` in `testnet` due to a validation that consults the fullnode. + >Jump to **Test open the wallet 2 weeks later** + + 1. [ ] Register `TTT` token in the wallet + 1. [ ] Turn on the `push-notification` feature toggle + 1. [ ] Go to the **Push Notification** page + 1. [ ] Turn **on** the `Enable Push Notification` option + 1. [ ] Turn **on** the `Show amounts on notification` option + 1. [ ] Send HTR and TTT to this wallet 1. Wait until the notification arrives 1. You should receive a notification of new transaction showing 2 amounts in the message > You have received 0.02 TTT and 0.01 HTR on a new transaction. - 1. Click on the notification - 1. Wait until the modal with tx details open + 1. [ ] Click on the notification + 1. [ ] Wait until the modal with tx details open 1. The `HTR - HATHOR` name should be in the primary color (purple) 1. The `TTT - Test Token Test` name should be in the primary color (purple) - 1. Click on the `TTT - Test Token Test` item + 1. [ ] Click on the `TTT - Test Token Test` item 1. The **Balance** page for `TTT` token should open + 1. **Register `TN1` token and send 2 tokens after turn on `Show amounts on notification` option** - 1. Register `TTT` token in the wallet - 1. Turn on the `push-notification` feature toggle - 1. Go to the **Push Notification** page - 1. Turn **on** the `Enable Push Notification` option - 1. Turn **on** the `Show amounts on notification` option - 1. Send HTR and TN1 to this wallet + 1. [ ] Register `TTT` token in the wallet + 1. [ ] Turn on the `push-notification` feature toggle + 1. [ ] Go to the **Push Notification** page + 1. [ ] Turn **on** the `Enable Push Notification` option + 1. [ ] Turn **on** the `Show amounts on notification` option + 1. [ ] Send HTR and TN1 to this wallet 1. Wait until the notification arrives 1. You should receive a notification of new transaction showing 2 amounts in the message > You have received 0.03 TN1 and 0.02 HTR on a new transaction. - 1. Click on the notification - 1. Wait until the modal with tx details open + 1. [ ] Click on the notification + 1. [ ] Wait until the modal with tx details open 1. The `HTR - HATHOR` name should be in the primary color (purple) 1. The `TN1 - Test Nft` name should be in the primary color (purple) 1. The `TN1 - Test Nft` amount should be integer - 1. Click on the `TN1 - Test Nft` item + 1. [ ] Click on the `TN1 - Test Nft` item 1. The **Balance** page for `TN1` token should open -1. **Send token to self** + +1. [ ] **Send token to self** 1. Wait some minutes to guarantee you won't receive any notifications for this tx + 1. Test open the wallet 2 weeks later - > WARNING: Skip if running the wallet from store. - 1. Open the file `src/sagas/pushNotification.js` and search for the following assignment: + + >[!WARNING] + >Skip if running the wallet from store. + + 1. [ ] Open the file `src/sagas/pushNotification.js` and search for the following assignment: ```jsx const timeSinceLastRegistration = moment().diff(enabledAt, 'weeks'); ``` - 1. Assign the value `2` to `timeSinceLastRegistration` and save + 1. [ ] Assign the value `2` to `timeSinceLastRegistration` and save ```jsx const timeSinceLastRegistration = 2; ``` - 1. Reload the wallet - 1. You should see a modal asking for a registration refresh + 1. [ ] Reload the wallet + 1. You should see a modal asking for a registration refresh > This modal only shows up when the user is using the fullnode wallet. - 1. Click on **Refresh** - 1. Enter your pin - 1. Done! You will continue to receive the push notification. - 1. Reassign `timeSinceLastRegistration` with its previous expression: + 1. [ ] Click on **Refresh** + 1. [ ] Enter your pin + 1. [ ] Done! You will continue to receive the push notification. + 1. [ ] Reassign `timeSinceLastRegistration` with its previous expression: ```jsx const timeSinceLastRegistration = moment().diff(enabledAt, 'weeks'); ``` + 1. **Close test** - 1. Register TNT token - 1. Send back all the tokens to the source wallet - 1. Disable push notification settings - 1. Turn off push notification feature toggle - 1. Unregister the tokens - 1. Reset the wallet - 1. Close the app - 1. Clear the application storage + 1. [ ] Register TNT token + 1. [ ] Send back all the tokens to the source wallet + 1. [ ] Disable push notification settings + 1. [ ] Turn off push notification feature toggle + 1. [ ] Unregister the tokens + 1. [ ] Reset the wallet + 1. [ ] Close the app + 1. [ ] Clear the application storage ### Turn on the `wallet-service` feature toggle -1. Get the `deviceId` and add it in the `UserIDs` strategy in the unleash **`wallet-service-mobile-android-testnet.rollout`** feature toggle -1. Turn the feature toggle on +1. [ ] Get the `deviceId` and add it in the `UserIDs` strategy in the unleash **`wallet-service-mobile-android-testnet.rollout`** feature toggle +1. [ ] Turn the feature toggle on Run all the tests above with the wallet-service turned on. But as a quick test you can run the following test: 1. **Send token after turn on the `wallet-service` feature toggle** - 1. Turn **on** the `push-notification` feature toggle - 1. Turn **on** the `wallet-service` feature toggle - 1. View the details of the transaction (foreground) - 1. View the details of the transaction (quit) + 1. [ ] Turn **on** the `push-notification` feature toggle + 1. [ ] Turn **on** the `wallet-service` feature toggle + 1. [ ] View the details of the transaction (foreground) + 1. [ ] View the details of the transaction (quit) diff --git a/__tests__/sagas/nanoContracts/fixtures.js b/__tests__/sagas/nanoContracts/fixtures.js new file mode 100644 index 000000000..fa6b6e4b4 --- /dev/null +++ b/__tests__/sagas/nanoContracts/fixtures.js @@ -0,0 +1,145 @@ +import { jest } from '@jest/globals'; + +export const fixtures = { + address: 'HTeZeYTCv7cZ8u7pBGHkWsPwhZAuoq5j3V', + ncId: '3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595', + ncApi: { + getNanoContractState: { + successResponse: { + success: true, + nc_id: '3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595', + blueprint_name: 'Bet', + fields: { + token_uid: { value: '00' }, + total: { value: 300 }, + final_result: { value: '1x0' }, + oracle_script: { value: '76a91441c431ff7ad5d6ce5565991e3dcd5d9106cfd1e288ac' }, + 'withdrawals.a\'Wi8zvxdXHjaUVAoCJf52t3WovTZYcU9aX6\'': { value: 300 }, + 'address_details.a\'Wi8zvxdXHjaUVAoCJf52t3WovTZYcU9aX6\'': { value: { '1x0': 100 } }, + }, + }, + }, + getNanoContractHistory: { + failureResponse: { + success: false, + }, + errorResponse: { + error: new Error('API call error'), + }, + successResponse: { + success: true, + history: [ + { + hash: '000000203e87e8575f121de16d0eb347bd1473eedd9f46cc76c1bc8d4e5a5fce', + nonce: 2104638, + timestamp: 1708356261, + version: 4, + weight: 21.89480540500889, + signal_bits: 0, + parents: [ + '0000008fbebdf8d78be50c88aceebf3c6b9e92f4affd7dfc96d48a7a49f23e69', + '00000121c46366b19de5efa8e6c23f62895322486395a0e31e987f9073025989' + ], + inputs: [ + { + tx_id: '0000008fbebdf8d78be50c88aceebf3c6b9e92f4affd7dfc96d48a7a49f23e69', + index: 0, + data: 'RjBEAiBtWa0q8uzMvBfkh83Y+t4Tv5OeyJSD8NazaGp19Hc2UwIgbV2m5unBEHlTAcLJsZLsCBlnfpua8LrUkVORiW/t4OQhAolqAR4yFaeCBeu/kOG1SwWnRj2X62zT9mU+Deutnbqq' + } + ], + outputs: [ + { + value: 78500, + token_data: 1, + script: 'dqkU5W5CR9734WcxaZMJkuToO8XlD3mIrA==' + }, + { + value: 300, + token_data: 2, + script: 'dqkU5W5CR9734WcxaZMJkuToO8XlD3mIrA==' + } + ], + tokens: [ + '00000117b0502e9eef9ccbe987af65f153aa899d6eba88d50a6c89e78644713d', + '0000038c49253f86e6792006dd9124e2c50e6487fde3296b7bd637e3e1a497e7' + ], + nc_id: '000001342d3c5b858a4d4835baea93fcc683fa615ff5892bd044459621a0340a', + nc_method: 'swap', + nc_args: '', + nc_pubkey: '020b120c8ad037ceb2e5b51b3edda7cd15a44f843b56e49880f6647fa9aadadffa' + }, + ], + }, + }, + }, + ncSaga: { + getNanoContractState: { + errorResponse: { + error: new Error('API call error'), + }, + successResponse: { + ncState: { + success: true, + nc_id: '3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595', + blueprint_id: '000001342d3c5b858a4d4835baea93fcc683fa615ff5892bd044459621a0340a', + blueprint_name: 'Bet', + fields: { + token_uid: { value: '00' }, + total: { value: 300 }, + final_result: { value: '1x0' }, + oracle_script: { value: '76a91441c431ff7ad5d6ce5565991e3dcd5d9106cfd1e288ac' }, + 'withdrawals.a\'Wi8zvxdXHjaUVAoCJf52t3WovTZYcU9aX6\'': { value: 300 }, + 'address_details.a\'Wi8zvxdXHjaUVAoCJf52t3WovTZYcU9aX6\'': { value: { '1x0': 100 } }, + }, + }, + }, + }, + fetchHistory: { + successResponse: { + history: [ + { + txId: '000000203e87e8575f121de16d0eb347bd1473eedd9f46cc76c1bc8d4e5a5fce', + timestamp: 1708356261, + tokens: [ + '00000117b0502e9eef9ccbe987af65f153aa899d6eba88d50a6c89e78644713d', + '0000038c49253f86e6792006dd9124e2c50e6487fde3296b7bd637e3e1a497e7' + ], + isVoided: false, // review + ncId: '000001342d3c5b858a4d4835baea93fcc683fa615ff5892bd044459621a0340a', + ncMethod: 'swap', + }, + ], + next: null, + }, + }, + }, + wallet: { + notReady: { + isReady: () => false, + }, + addressNotMine: { + isReady: () => true, + isAddressMine: jest.fn().mockReturnValue(false), + storage: { + isNanoContractRegistered: jest.fn(), + registerNanoContract: jest.fn(), + }, + }, + readyAndMine: { + isReady: () => true, + isAddressMine: jest.fn().mockReturnValue(true), + storage: { + isNanoContractRegistered: jest.fn(), + registerNanoContract: jest.fn(), + getNanoContract: jest.fn(), + }, + }, + }, + store: { + nanoContractAddressAlreadyRegistered: { + ncId: '3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595', + blueprintName: 'Bet', + addresses: new Set(['HTeZeYTCv7cZ8u7pBGHkWsPwhZAuoq5j3V']), + }, + }, +}; diff --git a/__tests__/sagas/nanoContracts/historyNanoContract.test.js b/__tests__/sagas/nanoContracts/historyNanoContract.test.js new file mode 100644 index 000000000..a234b0863 --- /dev/null +++ b/__tests__/sagas/nanoContracts/historyNanoContract.test.js @@ -0,0 +1,167 @@ +import { put } from 'redux-saga/effects'; +import { ncApi, addressUtils, transactionUtils } from '@hathor/wallet-lib'; +import { jest, test, expect, beforeEach, describe } from '@jest/globals'; +import { + failureMessage, + requestHistoryNanoContract, + fetchHistory +} from '../../../src/sagas/nanoContract'; +import { + nanoContractHistoryFailure, + nanoContractHistoryRequest, + nanoContractHistorySuccess, + onExceptionCaptured +} from '../../../src/actions'; +import { STORE } from '../../../src/store'; +import { fixtures } from './fixtures'; + +jest.mock('@hathor/wallet-lib'); + +beforeEach(() => { + jest.clearAllMocks(); + STORE.clearItems(); +}); + +describe('sagas/nanoContract/fetchHistory', () => { + test('success', async () => { + // arrange wallet mock + const mockedWallet = { + getNetworkObject: jest.fn(), + isAddressMine: jest.fn(), + }; + // arrange ncApi mock + const mockedNcApi = jest.mocked(ncApi); + mockedNcApi.getNanoContractHistory + .mockReturnValue(fixtures.ncApi.getNanoContractHistory.successResponse); + // arrange addressUtils mock + const mockedAddressUtils = jest.mocked(addressUtils); + mockedAddressUtils.getAddressFromPubkey + .mockResolvedValue('123'); + // arrange transactionUtils + const mockedTransactionUtils = jest.mocked(transactionUtils); + mockedTransactionUtils.getTxBalance + .mockResolvedValue({}); + + // call fetchHistory + const count = 1; + const after = null; + const result = await fetchHistory(fixtures.ncId, count, after, mockedWallet); + + // assert result is defined + expect(result.history).toBeDefined(); + expect(result.next).toBeDefined(); + // assert next value is a txId from the last element of history + expect(result.next).toBe(fixtures.ncSaga.fetchHistory.successResponse.history[0].txId); + // assert call count to API + expect(mockedNcApi.getNanoContractHistory).toBeCalledTimes(1); + }); + + test('failure', async () => { + // arrange ncApi mock + const mockedNcApi = jest.mocked(ncApi); + mockedNcApi.getNanoContractHistory + .mockReturnValue(fixtures.ncApi.getNanoContractHistory.failureResponse); + + // call fetchHistory and assert exception + const count = 1; + const after = null; + await expect(fetchHistory(fixtures.ncId, count, after)).rejects.toThrow('Failed to fetch nano contract history'); + }); +}); + +describe('sagas/nanoContract/requestHistoryNanoContract', () => { + test('history loading', () => { + // arrange Nano Contract registration inputs + const { ncId } = fixtures; + + // call effect to request history + const gen = requestHistoryNanoContract(nanoContractHistoryRequest({ ncId })); + // select wallet + gen.next(); + // feed back historyMeta + gen.next({ [ncId]: { isLoading: true, after: null } }); + + // assert termination + expect(gen.next().value).toBeUndefined(); + }); + + test('history without registered contract', () => { + // arrange Nano Contract registration inputs + const { ncId } = fixtures; + + // call effect to request history + const gen = requestHistoryNanoContract(nanoContractHistoryRequest({ ncId })); + // select wallet + gen.next(); + // feed back historyMeta + gen.next({}); + // feed back wallet + gen.next(fixtures.wallet.readyAndMine); + + // expect failure + // feed back isNanoContractRegistered + expect(gen.next(false).value).toStrictEqual( + put(nanoContractHistoryFailure({ ncId, error: failureMessage.notRegistered })) + ); + }); + + test('fetch history fails', () => { + // arrange Nano Contract registration inputs + const { ncId } = fixtures; + const storage = STORE.getStorage(); + storage.registerNanoContract(ncId, { ncId }); + + // call effect to request history + const gen = requestHistoryNanoContract(nanoContractHistoryRequest({ ncId })); + // select historyMeta + gen.next(); + // feed back historyMeta + gen.next({ [ncId]: { isLoading: false, after: null } }); + // feed back wallet + gen.next(fixtures.wallet.readyAndMine); + // feed back isNanoContractRegistered + const fetchHistoryCall = gen.next(true).value; + + // throws on fetchHistory call + const failureCall = gen.throw(new Error('history')).value; + const onErrorCall = gen.next().value; + + // assert failure + expect(fetchHistoryCall.payload.fn).toBe(fetchHistory); + expect(failureCall).toStrictEqual( + put(nanoContractHistoryFailure({ ncId, error: failureMessage.nanoContractHistoryFailure })) + ); + expect(onErrorCall).toStrictEqual(put(onExceptionCaptured(new Error('history'), false))); + }); + + test('history with success', () => { + // arrange Nano Contract registration inputs + const { ncId } = fixtures; + + // call effect to request history + const gen = requestHistoryNanoContract(nanoContractHistoryRequest({ ncId })); + // select wallet + gen.next(); + // feed back historyMeta + gen.next({}); + // feed back wallet + gen.next(fixtures.wallet.readyAndMine); + // feed back isNanoContractRegistered + const fetchHistoryCall = gen.next(true).value; + // feed back fetchHistory + const sucessCall = gen.next(fixtures.ncSaga.fetchHistory.successResponse).value; + + // assert success + const expectedHistory = fixtures.ncSaga.fetchHistory.successResponse.history; + expect(fetchHistoryCall.payload.fn).toBe(fetchHistory); + expect(sucessCall.payload).toHaveProperty('action.payload.ncId'); + expect(sucessCall.payload).toHaveProperty('action.payload.history'); + expect(sucessCall.payload.action.payload.ncId).toStrictEqual(ncId); + expect(sucessCall.payload.action.payload.history).toStrictEqual(expectedHistory); + expect(sucessCall).toStrictEqual( + put(nanoContractHistorySuccess({ ncId, history: expectedHistory, after: null })) + ); + // assert termination + expect(gen.next().value).toBeUndefined(); + }); +}); diff --git a/__tests__/sagas/nanoContracts/registerNanoContract.test.js b/__tests__/sagas/nanoContracts/registerNanoContract.test.js new file mode 100644 index 000000000..a35e1571d --- /dev/null +++ b/__tests__/sagas/nanoContracts/registerNanoContract.test.js @@ -0,0 +1,132 @@ +import { put } from 'redux-saga/effects'; +import { jest, test, expect, beforeEach, describe } from '@jest/globals'; +import { registerNanoContract, failureMessage } from '../../../src/sagas/nanoContract'; +import { nanoContractRegisterFailure, nanoContractRegisterRequest, onExceptionCaptured, types } from '../../../src/actions'; +import { STORE } from '../../../src/store'; +import { fixtures } from './fixtures'; + +beforeEach(() => { + jest.clearAllMocks(); + STORE.clearItems(); +}); + +describe('sagas/nanoContract/registerNanoContract', () => { + test('contract already registered', async () => { + // arrange Nano Contract registration inputs + const { address, ncId } = fixtures; + + // call effect to register nano contract + const gen = registerNanoContract(nanoContractRegisterRequest({ address, ncId })); + // call select wallet + gen.next(); + // feed back the selector + gen.next(fixtures.wallet.addressNotMine); + + // assert failure + // feed back isNanoContractRegistered + expect(gen.next(true).value) + .toStrictEqual(put(nanoContractRegisterFailure(failureMessage.alreadyRegistered))) + // assert termination + expect(gen.next().value).toBeUndefined(); + }); + + test('wallet not ready', async () => { + // arrange Nano Contract registration inputs + const { address, ncId } = fixtures; + + // call effect to register nano contract + const gen = registerNanoContract(nanoContractRegisterRequest({ address, ncId })); + // call select wallet + gen.next(); + + // assert failure + // feed back the selector and advance generator to failure + // first emmit NANOCONTRACT_REGISTER_FAILURE + expect(gen.next(fixtures.wallet.notReady).value) + .toStrictEqual(put(nanoContractRegisterFailure(failureMessage.walletNotReadyError))) + // then emmit EXCEPTION_CAPTURED + expect(gen.next().value) + .toStrictEqual( + put(onExceptionCaptured(new Error(failureMessage.walletNotReadyError), false)) + ); + // assert termination + expect(gen.next().value).toBeUndefined(); + }); + + test('address not mine', async () => { + // arrange Nano Contract registration inputs + const { address, ncId } = fixtures; + + // call effect to register nano contract + const gen = registerNanoContract(nanoContractRegisterRequest({ address, ncId })); + // call select wallet + gen.next(); + // feed back the selector + gen.next(fixtures.wallet.addressNotMine); + // feed back isNanoContractRegistered + gen.next(false); + + // assert failure + // resume isAddressMine call and advance generator to failure + expect(gen.next(fixtures.wallet.addressNotMine.isAddressMine()).value) + .toStrictEqual(put(nanoContractRegisterFailure(failureMessage.addressNotMine))) + // assert termination + expect(gen.next().value).toBeUndefined(); + }); + + test('getNanoContractState error', async () => { + // arrange Nano Contract registration inputs + const { address, ncId } = fixtures; + + // call effect to register nano contract + const gen = registerNanoContract(nanoContractRegisterRequest({ address, ncId })); + // call select wallet + gen.next(); + // feed back the selector + gen.next(fixtures.wallet.readyAndMine); + // feed back isNanoContractRegistered + gen.next(false); + // feed back isAddressMine call + gen.next(fixtures.wallet.readyAndMine.isAddressMine()); + + // assert failure + // resume getNanoContractState call and advance generator to failure + expect(gen.throw(fixtures.ncSaga.getNanoContractState.errorResponse).value) + .toStrictEqual(put(nanoContractRegisterFailure(failureMessage.nanoContractStateFailure))) + // assert termination + expect(gen.next().value).toBeUndefined(); + }); + + test('register with success', async () => { + // arrange Nano Contract registration inputs + const { address, ncId } = fixtures; + + // call effect to register nano contract + const gen = registerNanoContract(nanoContractRegisterRequest({ address, ncId })); + // call select wallet + gen.next(); + // feed back the selector + gen.next(fixtures.wallet.readyAndMine); + // feed back isNanoContractRegistered + gen.next(false); + // feed back isAddressMine call + gen.next(fixtures.wallet.readyAndMine.isAddressMine()); + // feed back getNanoContractState call + gen.next(fixtures.ncSaga.getNanoContractState.successResponse); + // feed back registerNanoContract + const actionResult = gen.next().value; + + // assert success + expect(actionResult.payload.action.type) + .toBe(types.NANOCONTRACT_REGISTER_SUCCESS); + expect(actionResult.payload.action.payload.entryKey).toBe(ncId); + expect(actionResult.payload.action.payload.entryValue).toEqual(expect.objectContaining({ + address, + ncId, + blueprintId: expect.any(String), + blueprintName: expect.any(String), + })); + // assert termination + expect(gen.next().value).toBeUndefined(); + }); +}); diff --git a/__tests__/sagas/networkSettings.test.ts b/__tests__/sagas/networkSettings.test.ts index 33ca7839d..d8c9ec768 100644 --- a/__tests__/sagas/networkSettings.test.ts +++ b/__tests__/sagas/networkSettings.test.ts @@ -14,7 +14,7 @@ beforeEach(() => { jest.clearAllMocks(); }); -describe('updateNetworkSettings', () => { +describe.skip('updateNetworkSettings', () => { beforeAll(() => { jest.spyOn(config, 'getExplorerServiceBaseUrl').mockReturnValue(''); jest.spyOn(config, 'getServerUrl'); @@ -243,7 +243,7 @@ describe('updateNetworkSettings', () => { }); -describe('persistNetworkSettings', () => { +describe.skip('persistNetworkSettings', () => { it('should persist networkSettings and trigger feature toggle update', async () => { const actual: any[] = []; // simulates saga cluster in sagas/index.js @@ -300,7 +300,7 @@ describe('persistNetworkSettings', () => { }); }); -describe('cleanNetworkSettings', () => { +describe.skip('cleanNetworkSettings', () => { it('should clean persisted network settings', () => { const spyRemove = jest.spyOn(STORE, 'removeItem') runSaga( diff --git a/android/app/build.gradle b/android/app/build.gradle index f53d04b4e..7f72a16f3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -79,8 +79,8 @@ android { applicationId "network.hathor.wallet" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 72 - versionName "0.27.2" + versionCode 75 + versionName "0.28.0-rc.3" missingDimensionStrategy "react-native-camera", "general" } signingConfigs { diff --git a/android/build.gradle b/android/build.gradle index 306808b10..4fdc74708 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,7 +6,7 @@ buildscript { kotlin_version = "1.7.20" minSdkVersion = 23 compileSdkVersion = 33 - targetSdkVersion = 33 + targetSdkVersion = 34 // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. ndkVersion = "23.1.7779620" diff --git a/enable-walletconnect.patch b/enable-walletconnect.patch index 0b8141763..a28e7a5fe 100644 --- a/enable-walletconnect.patch +++ b/enable-walletconnect.patch @@ -1,53 +1,52 @@ -diff --git a/package.json b/package.json -index b5a16f3..8c13347 100644 ---- a/package.json -+++ b/package.json -@@ -26,6 +26,8 @@ - "@react-navigation/native": "6.1.7", - "@react-navigation/stack": "6.3.17", - "@sentry/react-native": "^5.6.0", -+ "@walletconnect/core": "^2.10.2", -+ "@walletconnect/web3wallet": "^1.9.2", - "assert": "^2.0.0", - "buffer": "^4.9.2", - "console-browserify": "^1.2.0", -@@ -67,6 +69,7 @@ - "redux-saga": "^1.2.0", - "redux-thunk": "^2.4.1", - "stream-browserify": "^1.0.0", -+ "text-encoding": "^0.7.0", - "ttag": "^1.7.24", - "unleash-proxy-client": "^1.11.0", - "url": "^0.11.0" diff --git a/shim.js b/shim.js -index 73c130d..10d894b 100644 +index 73c130d..deae975 100644 --- a/shim.js +++ b/shim.js -@@ -26,3 +26,8 @@ if (typeof localStorage !== 'undefined') { +@@ -26,3 +26,20 @@ if (typeof localStorage !== 'undefined') { // If using the crypto shim, uncomment the following line to ensure // crypto is loaded first, so it can populate global.crypto require('crypto') -+const TextEncoder = require('text-encoding').TextEncoder; -+const TextDecoder = require('text-encoding').TextDecoder; ++ ++const { TextEncoder, TextDecoder } = require('text-encoding'); + +global.TextDecoder = TextDecoder; +global.TextEncoder = TextEncoder; ++ ++if (typeof btoa === 'undefined') { ++ global.btoa = function (str) { ++ return Buffer.from(str, 'binary').toString('base64'); ++ }; ++} ++ ++if (typeof atob === 'undefined') { ++ global.atob = function (b64Encoded) { ++ return Buffer.from(b64Encoded, 'base64').toString('binary'); ++ }; ++} diff --git a/src/sagas/walletConnect.js b/src/sagas/walletConnect.js -index 5e954de..39ff15b 100644 +index 160cb11..07ccb34 100644 --- a/src/sagas/walletConnect.js +++ b/src/sagas/walletConnect.js -@@ -61,6 +61,8 @@ import { +@@ -45,6 +45,7 @@ + * loaded. + */ + ++import '@walletconnect/react-native-compat'; + import { + call, + fork, +@@ -62,6 +63,8 @@ import { } from 'redux-saga/effects'; import { eventChannel } from 'redux-saga'; import { get, values } from 'lodash'; +import { Core } from '@walletconnect/core'; +import { Web3Wallet } from '@walletconnect/web3wallet'; - - import { WalletConnectModalTypes } from '../components/WalletConnect/WalletConnectModal'; import { -@@ -95,12 +97,6 @@ const ERROR_CODES = { - INVALID_PAYLOAD: 5003, + TriggerTypes, + TriggerResponseTypes, +@@ -100,12 +103,6 @@ const AVAILABLE_METHODS = { }; + const AVAILABLE_EVENTS = []; -// We're mocking it here because we don't want to add the walletconnect -// libraries in our production build. If you really want to add it, just run the @@ -55,6 +54,21 @@ index 5e954de..39ff15b 100644 -const Core = class {}; -const Web3Wallet = class {}; - - function* isWalletConnectEnabled() { + /** + * Those are the only ones we are currently using, extracted from + * https://docs.walletconnect.com/2.0/specs/clients/sign/error-codes +@@ -118,13 +115,10 @@ const ERROR_CODES = { + INVALID_PAYLOAD: 5003, + }; + +-function isWalletConnectEnabled() { +- return false; +- /* ++function* isWalletConnectEnabled() { const walletConnectEnabled = yield call(checkForFeatureFlag, WALLET_CONNECT_FEATURE_TOGGLE); + return walletConnectEnabled; +- */ + } + + function* init() { diff --git a/flake.lock b/flake.lock index 9095299ca..e6693c853 100644 --- a/flake.lock +++ b/flake.lock @@ -6,11 +6,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1649691969, - "narHash": "sha256-nY1aUWIyh3TcGVo3sn+3vyCh+tOiEZL4JtMX3aOZSeY=", + "lastModified": 1717408969, + "narHash": "sha256-Q0OEFqe35fZbbRPPRdrjTUUChKVhhWXz3T9ZSKmaoVY=", "owner": "numtide", "repo": "devshell", - "rev": "e22633b05fec2fe196888c593d4d9b3f4f648a25", + "rev": "1ebbe68d57457c8cae98145410b164b5477761f4", "type": "github" }, "original": { @@ -20,12 +20,15 @@ } }, "flake-utils": { + "inputs": { + "systems": "systems" + }, "locked": { - "lastModified": 1642700792, - "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", "owner": "numtide", "repo": "flake-utils", - "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", "type": "github" }, "original": { @@ -35,12 +38,15 @@ } }, "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, "locked": { - "lastModified": 1649676176, - "narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=", + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "type": "github" }, "original": { @@ -51,11 +57,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1643381941, - "narHash": "sha256-pHTwvnN4tTsEKkWlXQ8JMY423epos8wUOhthpwJjtpc=", + "lastModified": 1704161960, + "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5efc8ca954272c4376ac929f4c5ffefcc20551d5", + "rev": "63143ac2c9186be6d9da6035fa22620018c85932", "type": "github" }, "original": { @@ -67,11 +73,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1705403940, - "narHash": "sha256-bl7E3w35Bleiexg01WsN0RuAQEL23HaQeNBC2zjt+9w=", + "lastModified": 1719082008, + "narHash": "sha256-jHJSUH619zBQ6WdC21fFAlDxHErKVDJ5fpN0Hgx4sjs=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f0326542989e1bdac955ad6269b334a8da4b0c95", + "rev": "9693852a2070b398ee123a329e68f0dab5526681", "type": "github" }, "original": { @@ -85,6 +91,36 @@ "flake-utils": "flake-utils_2", "nixpkgs": "nixpkgs_2" } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 37bba0214..d633f94b6 100644 --- a/flake.nix +++ b/flake.nix @@ -11,7 +11,7 @@ let pkgs = import nixpkgs { inherit system; - overlays = [ devshell.overlay ]; + overlays = [ devshell.overlays.default ]; }; in pkgs.devshell.mkShell { @@ -31,7 +31,7 @@ ]; packages = with pkgs; [ nixpkgs-fmt - nodejs-18_x + nodejs_20 ruby gnumake gettext diff --git a/ios/HathorMobile.xcodeproj/project.pbxproj b/ios/HathorMobile.xcodeproj/project.pbxproj index d2a8656ef..00cf2d980 100644 --- a/ios/HathorMobile.xcodeproj/project.pbxproj +++ b/ios/HathorMobile.xcodeproj/project.pbxproj @@ -512,7 +512,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = HathorMobile/HathorMobile.entitlements; - CURRENT_PROJECT_VERSION = 1.0.0; + CURRENT_PROJECT_VERSION = 0.3.0; DEVELOPMENT_TEAM = 55SHY647CG; ENABLE_BITCODE = NO; INFOPLIST_FILE = HathorMobile/Info.plist; @@ -521,7 +521,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.27.2; + MARKETING_VERSION = 0.28.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -542,7 +542,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = HathorMobile/HathorMobile.entitlements; - CURRENT_PROJECT_VERSION = 1.0.0; + CURRENT_PROJECT_VERSION = 0.3.0; DEVELOPMENT_TEAM = 55SHY647CG; INFOPLIST_FILE = HathorMobile/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; @@ -550,7 +550,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.27.2; + MARKETING_VERSION = 0.28.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a92cc1d68..8453d4181 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -20,9 +20,9 @@ PODS: - GoogleUtilities/Logger (~> 7.8) - FirebaseCoreExtension (10.3.0): - FirebaseCore (~> 10.0) - - FirebaseCoreInternal (10.18.0): + - FirebaseCoreInternal (10.29.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.18.0): + - FirebaseInstallations (10.29.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) @@ -38,43 +38,54 @@ PODS: - nanopb (< 2.30910.0, >= 2.30908.0) - fmt (6.2.1) - glog (0.3.5) - - GoogleDataTransport (9.3.0): + - GoogleDataTransport (9.4.1): - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) + - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities (7.12.0): - - GoogleUtilities/AppDelegateSwizzler (= 7.12.0) - - GoogleUtilities/Environment (= 7.12.0) - - GoogleUtilities/ISASwizzler (= 7.12.0) - - GoogleUtilities/Logger (= 7.12.0) - - GoogleUtilities/MethodSwizzler (= 7.12.0) - - GoogleUtilities/Network (= 7.12.0) - - "GoogleUtilities/NSData+zlib (= 7.12.0)" - - GoogleUtilities/Reachability (= 7.12.0) - - GoogleUtilities/SwizzlerTestHelpers (= 7.12.0) - - GoogleUtilities/UserDefaults (= 7.12.0) - - GoogleUtilities/AppDelegateSwizzler (7.12.0): + - GoogleUtilities (7.13.3): + - GoogleUtilities/AppDelegateSwizzler (= 7.13.3) + - GoogleUtilities/Environment (= 7.13.3) + - GoogleUtilities/ISASwizzler (= 7.13.3) + - GoogleUtilities/Logger (= 7.13.3) + - GoogleUtilities/MethodSwizzler (= 7.13.3) + - GoogleUtilities/Network (= 7.13.3) + - "GoogleUtilities/NSData+zlib (= 7.13.3)" + - GoogleUtilities/Privacy (= 7.13.3) + - GoogleUtilities/Reachability (= 7.13.3) + - GoogleUtilities/SwizzlerTestHelpers (= 7.13.3) + - GoogleUtilities/UserDefaults (= 7.13.3) + - GoogleUtilities/AppDelegateSwizzler (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/ISASwizzler (7.12.0) - - GoogleUtilities/Logger (7.12.0): + - GoogleUtilities/ISASwizzler (7.13.3): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (7.13.3): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/MethodSwizzler (7.13.3): - GoogleUtilities/Logger - - GoogleUtilities/Network (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Network (7.13.3): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.12.0)" - - GoogleUtilities/Reachability (7.12.0): + - "GoogleUtilities/NSData+zlib (7.13.3)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/Reachability (7.13.3): - GoogleUtilities/Logger - - GoogleUtilities/SwizzlerTestHelpers (7.12.0): + - GoogleUtilities/Privacy + - GoogleUtilities/SwizzlerTestHelpers (7.13.3): - GoogleUtilities/MethodSwizzler - - GoogleUtilities/UserDefaults (7.12.0): + - GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/Logger + - GoogleUtilities/Privacy - hermes-engine (0.72.5): - hermes-engine/Pre-built (= 0.72.5) - hermes-engine/Pre-built (0.72.5) @@ -88,7 +99,7 @@ PODS: - RNPermissions - Permission-Notifications (3.8.3): - RNPermissions - - PromisesObjC (2.3.1) + - PromisesObjC (2.4.0) - RCT-Folly (2021.07.22.00): - boost - DoubleConversion @@ -572,16 +583,17 @@ PODS: - RNScreens (3.22.1): - React-Core - React-RCTImage - - RNSentry (5.6.0): + - RNSentry (5.31.0): + - hermes-engine + - RCT-Folly (= 2021.07.22.00) - React-Core - - Sentry/HybridSDK (= 8.7.3) + - React-hermes + - Sentry/HybridSDK (= 8.36.0) - RNSVG (13.10.0): - React-Core - RNVectorIcons (9.2.0): - React-Core - - Sentry/HybridSDK (8.7.3): - - SentryPrivate (= 8.7.3) - - SentryPrivate (8.7.3) + - Sentry/HybridSDK (8.36.0) - SocketRocket (0.6.1) - Yoga (1.14.0) @@ -666,7 +678,6 @@ SPEC REPOS: - nanopb - PromisesObjC - Sentry - - SentryPrivate - SocketRocket EXTERNAL SOURCES: @@ -802,19 +813,19 @@ SPEC CHECKSUMS: Firebase: f92fc551ead69c94168d36c2b26188263860acd9 FirebaseCore: 988754646ab3bd4bdcb740f1bfe26b9f6c0d5f2a FirebaseCoreExtension: 93d252fabdc9696bf14a73b04d84877ab9b3a832 - FirebaseCoreInternal: 8eb002e564b533bdcf1ba011f33f2b5c10e2ed4a - FirebaseInstallations: e842042ec6ac1fd2e37d7706363ebe7f662afea4 + FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 + FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd FirebaseMessaging: e345b219fd15d325f0cf2fef28cb8ce00d851b3f fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b - GoogleDataTransport: 57c22343ab29bc686febbf7cbb13bad167c2d8fe - GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34 + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 hermes-engine: f6cf92a471053245614d9d8097736f6337d5b86c libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 Permission-Camera: 9c8b1a826770a6feea747cc8a4a89d2b39df4273 Permission-Notifications: 05a9c72e2ae989d28eb1eecf3d6a12daba73d375 - PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCTRequired: df81ab637d35fac9e6eb94611cfd20f0feb05455 RCTTypeSafety: 4636e4a36c7c2df332bda6d59b19b41c443d4287 @@ -864,14 +875,13 @@ SPEC CHECKSUMS: RNPermissions: d9db16f082ce2e09908e58c925189e2637d2786b RNReanimated: ab2e96c6d5591c3dfbb38a464f54c8d17fb34a87 RNScreens: 50ffe2fa2342eabb2d0afbe19f7c1af286bc7fb3 - RNSentry: 9f0447b3ce13806f544903748de423259ead8552 + RNSentry: b3f878fa0cef050836b302c0905e952d3e6e7452 RNSVG: 80584470ff1ffc7994923ea135a3e5ad825546b9 RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8 - Sentry: c7a86f43510a7d5678d4de28d78c28ab351d295b - SentryPrivate: 2eaabf598a46d4b9b8822aef766df2a84caf2e6f + Sentry: f8374b5415bc38dfb5645941b3ae31230fbeae57 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: 86fed2e4d425ee4c6eab3813ba1791101ee153c6 PODFILE CHECKSUM: c422aedd0c3b236578801088c8afef28030c3c83 -COCOAPODS: 1.13.0 +COCOAPODS: 1.15.2 diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 8eb675e9b..000000000 --- a/jest.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - preset: 'react-native', -}; diff --git a/locale/da/texts.po b/locale/da/texts.po index c22598be6..6022603fa 100644 --- a/locale/da/texts.po +++ b/locale/da/texts.po @@ -13,55 +13,55 @@ msgstr "" "X-Generator: Poedit 3.3.1\n" #. This should never happen! -#: src/models.js:24 +#: src/models.js:85 msgid "Unknown" msgstr "Ukendt" -#: src/models.js:27 +#: src/models.js:88 #, javascript-format msgid "Received ${ symbol }" msgstr "Modtaget ${ symbol }" -#: src/models.js:29 +#: src/models.js:90 msgid "Sent ${ symbol }" msgstr "Sendt ${ symbol }" -#: src/models.js:31 +#: src/models.js:92 #, javascript-format msgid "You sent ${ symbol } to yourself" msgstr "Du sendte ${ symbol } til dig selv" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:41 +#: src/models.js:102 msgid "[Today •] HH:mm" msgstr "[I dag •] HH:mm" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:42 +#: src/models.js:103 msgid "[Tomorrow •] HH:mm" msgstr "[I morgen •] HH:mm" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:43 +#: src/models.js:104 msgid "dddd [•] HH:mm" msgstr "dddd [•] HH:mm" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:44 +#: src/models.js:105 msgid "[Yesterday •] HH:mm" msgstr "[I går •] HH:mm" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:45 +#: src/models.js:106 msgid "[Last] dddd [•] HH:mm" msgstr "[Sidste] dddd [•] HH:mm" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:46 +#: src/models.js:107 src/utils.js:429 msgid "DD MMM YYYY [•] HH:mm" msgstr "DD MMM YYYY [•] HH:mm" -#: src/utils.js:146 +#: src/utils.js:164 msgid "Invalid address" msgstr "Ugyldig adresse" @@ -105,7 +105,7 @@ msgstr "" #: src/workers/pushNotificationHandler.js:118 msgid "New transaction received" -msgstr "Ingen transaktioner" +msgstr "" #: src/screens/About.js:83 msgid "ABOUT" @@ -228,7 +228,7 @@ msgstr "Du har ${ amountAvailableText } HTR tilgængelig" #: src/screens/CreateTokenAmount.js:149 src/screens/CreateTokenName.js:64 #: src/screens/CreateTokenSymbol.js:84 src/screens/InitWallet.js:220 -#: src/screens/InitWallet.js:341 src/screens/SendAddressInput.js:66 +#: src/screens/InitWallet.js:349 src/screens/SendAddressInput.js:66 #: src/screens/SendAmountInput.js:185 msgid "Next" msgstr "Næste" @@ -335,14 +335,21 @@ msgstr "Symbolet er en kortere version af token-navnet" msgid "E.g. HTR" msgstr "F.eks. HTR" -#: src/screens/Dashboard.js:67 src/screens/RegisterTokenManual.js:147 +#. Only show the toggle button when Nano Contract is enabled to the wallet +#: src/screens/Dashboard.js:130 src/screens/Dashboard.js:187 +msgid "Tokens" +msgstr "Tokens" + +#. Only show the toggle button when Nano Contract is enabled to the wallet +#: src/components/NanoContract/NanoContractsList.js:75 +#: src/screens/Dashboard.js:131 +msgid "Nano Contracts" +msgstr "" + +#: src/screens/Dashboard.js:178 src/screens/RegisterTokenManual.js:147 msgid "Register token" msgstr "Registrer token" -#: src/screens/Dashboard.js:74 -msgid "TOKENS" -msgstr "TOKENS" - #: src/screens/InitWallet.js:61 msgid "Welcome to Hathor Wallet!" msgstr "Velkommen til Hathor Wallet!" @@ -425,7 +432,11 @@ msgstr "Ord" msgid "Enter your seed words separated by space" msgstr "Indtast dine seed-ord adskilt med mellemrum" +#: src/components/NanoContract/NanoContractDetails.js:238 +#: src/components/WalletConnect/CreateTokenRequest.js:197 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:295 #: src/screens/LoadHistoryScreen.js:51 src/screens/LoadWalletErrorScreen.js:20 +#: src/screens/NanoContract/NanoContractRegisterScreen.js:168 msgid "Try again" msgstr "" @@ -448,7 +459,7 @@ msgid "There's been an error connecting to the server." msgstr "" #: src/screens/LoadWalletErrorScreen.js:21 src/screens/PinScreen.js:268 -#: src/screens/Settings.js:154 +#: src/screens/Settings.js:161 msgid "Reset wallet" msgstr "Nulstil wallet" @@ -472,14 +483,30 @@ msgstr "" msgid "Please |tryAgain:try again|" msgstr "" -#: src/screens/MainScreen.js:560 src/screens/MainScreen.js:592 +#: src/screens/MainScreen.js:535 src/screens/MainScreen.js:567 msgid "Available Balance" msgstr "Tilgængelig saldo" -#: src/screens/MainScreen.js:569 +#: src/screens/MainScreen.js:544 msgid "Locked" msgstr "Låst" +#. translator: Used when the QR Code Scanner is opened, and user will manually +#. enter the information. +#: src/screens/NanoContractRegisterQrCodeScreen.js:26 +#: src/screens/RegisterToken.js:27 src/screens/SendScanQRCode.js:87 +msgid "Manual info" +msgstr "Manuel information" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:251 +#: src/screens/NanoContractRegisterQrCodeScreen.js:48 +msgid "Nano Contract Registration" +msgstr "" + +#: src/screens/NanoContractRegisterQrCodeScreen.js:63 +msgid "Scan the nano contract ID QR code" +msgstr "" + #: src/screens/PaymentRequestDetail.js:88 #, javascript-format msgid "You've just received **${ amount } ${ symbol }**" @@ -489,11 +516,13 @@ msgstr "Du har lige modtaget **${ amount } ${ symbol }**" msgid "PAYMENT REQUEST" msgstr "BETALINGSANMODNING" -#: src/components/TxDetailsModal.js:59 src/screens/PaymentRequestDetail.js:123 +#: src/components/TxDetailsModal.js:101 src/screens/PaymentRequestDetail.js:123 msgid "Token" msgstr "Token" -#: src/components/TxDetailsModal.js:106 src/screens/PaymentRequestDetail.js:127 +#: src/components/TxDetailsModal.js:166 +#: src/components/WalletConnect/CreateTokenRequest.js:82 +#: src/screens/PaymentRequestDetail.js:127 msgid "Amount" msgstr "Antal" @@ -525,12 +554,15 @@ msgstr "Indtast din pinkode " msgid "Unlock Hathor Wallet" msgstr "Lås Hathor-wallet op" +#: src/components/WalletConnect/CreateTokenModal.js:60 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:64 +#: src/components/WalletConnect/SignMessageModal.js:72 #: src/screens/PinScreen.js:265 #: src/screens/WalletConnect/WalletConnectList.js:125 msgid "Cancel" msgstr "Annuller" -#: src/screens/PushNotification.js:58 src/screens/Settings.js:128 +#: src/screens/PushNotification.js:58 src/screens/Settings.js:135 msgid "Push Notification" msgstr "" @@ -563,12 +595,6 @@ msgstr "Betalingsanmodning" msgid "RECEIVE" msgstr "MODTAG" -#. translator: Used when the QR Code Scanner is opened, and user will manually -#. enter the information. -#: src/screens/RegisterToken.js:27 src/screens/SendScanQRCode.js:89 -msgid "Manual info" -msgstr "Manuel information" - #: src/screens/RegisterToken.js:42 src/screens/RegisterTokenManual.js:126 msgid "REGISTER TOKEN" msgstr "REGISTRER TOKEN" @@ -649,7 +675,7 @@ msgstr "Skift pinkode" msgid "Lock wallet" msgstr "Lås wallet" -#: src/screens/SendAddressInput.js:53 src/screens/SendScanQRCode.js:97 +#: src/screens/SendAddressInput.js:53 src/screens/SendScanQRCode.js:95 msgid "SEND" msgstr "SEND" @@ -677,11 +703,11 @@ msgstr "SEND ${ tokenNameUpperCase }" msgid "Your transfer is being processed" msgstr "Din overførsel behandles" -#: src/sagas/helpers.js:136 src/screens/SendConfirmScreen.js:104 +#: src/sagas/helpers.js:140 src/screens/SendConfirmScreen.js:104 msgid "Enter your 6-digit pin to authorize operation" msgstr "Indtast din 6-cifrede pin for at godkende overførslen" -#: src/sagas/helpers.js:137 src/screens/SendConfirmScreen.js:105 +#: src/sagas/helpers.js:141 src/screens/SendConfirmScreen.js:105 msgid "Authorize operation" msgstr "Autoriserer overførslen" @@ -689,11 +715,14 @@ msgstr "Autoriserer overførslen" msgid "Your transfer of **${ this.amountAndToken }** has been confirmed" msgstr "Din overførsel af **${ _this.amountAndToken }** er bekræftet" +#: src/components/NanoContract/EditAddressModal.js:60 +#: src/components/NanoContract/SelectAddressModal.js:117 +#: src/components/WalletConnect/SignMessageRequest.js:40 #: src/screens/SendConfirmScreen.js:161 msgid "Address" msgstr "Adresse" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:288 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:291 #: src/screens/SendConfirmScreen.js:168 msgid "Send" msgstr "Send" @@ -706,53 +735,53 @@ msgstr "Ugyldig QR-kode" msgid "OK" msgstr "Okay" -#: src/screens/SendScanQRCode.js:73 +#: src/screens/SendScanQRCode.js:71 #, javascript-format msgid "You don't have the requested token [${ tokenLabel }]" msgstr "Du har ikke den anmodede token [${ tokenLabel }]" -#: src/screens/SendScanQRCode.js:105 +#: src/screens/SendScanQRCode.js:103 #: src/screens/WalletConnect/WalletConnectScan.js:49 msgid "Scan the QR code" msgstr "Scan QR-koden" -#: src/screens/Settings.js:97 +#: src/screens/Settings.js:104 msgid "You are connected to" msgstr "Du er tilsluttet til" -#: src/screens/Settings.js:105 +#: src/screens/Settings.js:112 msgid "General Settings" msgstr "" -#: src/screens/Settings.js:109 +#: src/screens/Settings.js:116 msgid "Connected to" msgstr "Forbundet til" -#: src/screens/Settings.js:122 +#: src/screens/Settings.js:129 msgid "Security" msgstr "Sikkerhed" -#: src/screens/Settings.js:135 +#: src/screens/Settings.js:142 msgid "Create a new token" msgstr "Opret en ny token" -#: src/screens/Settings.js:142 +#: src/screens/Settings.js:149 msgid "Register a token" msgstr "Registrer en token" -#: src/screens/Settings.js:158 +#: src/screens/Settings.js:165 msgid "About" msgstr "Om" -#: src/screens/Settings.js:165 +#: src/screens/Settings.js:172 msgid "Unique app identifier" msgstr "" -#: src/screens/Settings.js:179 +#: src/screens/Settings.js:186 msgid "Developer Settings" msgstr "" -#: src/screens/Settings.js:181 +#: src/screens/Settings.js:188 msgid "Network Settings" msgstr "" @@ -786,6 +815,19 @@ msgstr "Jeg vil afregistrere token **${ tokenLabel }**" msgid "Unregister token" msgstr "Afregistrer token" +#: src/screens/WalletConnect/CreateTokenScreen.js:25 +msgid "Create Token Request" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:48 +#: src/screens/WalletConnect/NewNanoContractTransactionScreen.js:24 +msgid "New Nano Contract Transaction" +msgstr "" + +#: src/screens/WalletConnect/SignMessageRequestScreen.js:25 +msgid "Sign Message Request" +msgstr "" + #: src/screens/WalletConnect/WalletConnectList.js:33 msgid "There was an error connecting. Please try again later." msgstr "" @@ -842,17 +884,17 @@ msgid "" msgstr "" #: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:20 -#: src/screens/NetworkSettings/helper.js:4 +#: src/screens/NetworkSettings/helper.js:11 msgid "Updating custom network settings..." msgstr "" #: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:21 -#: src/screens/NetworkSettings/helper.js:5 +#: src/screens/NetworkSettings/helper.js:12 msgid "Network settings successfully customized." msgstr "" #: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:22 -#: src/screens/NetworkSettings/helper.js:6 +#: src/screens/NetworkSettings/helper.js:13 msgid "" "There was an error while customizing network settings. Please try again " "later." @@ -882,27 +924,27 @@ msgstr "" msgid "walletServiceWsUrl is required when walletServiceUrl is filled." msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:229 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:232 msgid "Node URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:239 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:242 msgid "Explorer URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:248 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:251 msgid "Explorer Service URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:257 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:260 msgid "Transaction Mining Service URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:268 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:271 msgid "Wallet Service URL (optional)" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:277 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:280 msgid "Wallet Service WS URL (optional)" msgstr "" @@ -925,71 +967,197 @@ msgid "" "this can potentially make you susceptible to fraudulent schemes." msgstr "" -#: src/sagas/networkSettings.js:88 +#: src/screens/NanoContract/NanoContractDetailsScreen.js:39 +msgid "Nano Contract Details" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:58 +msgid "Nano Contract ID is required." +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:158 +msgid "See contract" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:176 +msgid "Load First Addresses Error" +msgstr "" + +#: src/components/NanoContract/NanoContractDetails.js:202 +#: src/components/NanoContract/SelectAddressModal.js:105 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:215 +#: src/screens/NanoContract/NanoContractRegisterScreen.js:184 +msgid "Loading" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:185 +msgid "Loading first wallet address." +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:142 +#: src/components/NanoContract/NanoContractTransactionHeader.js:85 +#: src/components/NanoContract/NanoContractsListItem.js:57 +#: src/components/TxDetailsModal.js:106 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:83 +#: src/screens/NanoContract/NanoContractRegisterScreen.js:194 +msgid "Nano Contract ID" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:202 +msgid "Wallet Address" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:212 +msgid "If you want to change the wallet address, you will be able to do" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:214 +msgid "after the contract is registered." +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:233 +msgid "Register Nano Contract" +msgstr "" + +#: src/screens/NanoContract/NanoContractTransactionScreen.js:39 +msgid "Nano Contract Transaction" +msgstr "" + +#: src/screens/NanoContract/helper.js:11 +msgid "Contract successfully registered." +msgstr "" + +#: src/sagas/nanoContract.js:45 +msgid "Nano Contract already registered." +msgstr "" + +#: src/sagas/nanoContract.js:46 +msgid "Wallet is not ready yet to register a Nano Contract." +msgstr "" + +#: src/sagas/nanoContract.js:47 +msgid "The informed address does not belong to the wallet." +msgstr "" + +#: src/sagas/nanoContract.js:48 +msgid "Nano Contract not found." +msgstr "" + +#: src/sagas/nanoContract.js:49 +msgid "Error while trying to register Nano Contract." +msgstr "" + +#: src/sagas/nanoContract.js:50 +msgid "Invalid transaction to register as Nano Contract." +msgstr "" + +#: src/sagas/nanoContract.js:51 +msgid "Blueprint not found." +msgstr "" + +#: src/sagas/nanoContract.js:52 +msgid "Couldn't get Blueprint info." +msgstr "" + +#: src/sagas/nanoContract.js:53 +msgid "Nano Contract not registered." +msgstr "" + +#: src/sagas/nanoContract.js:54 +msgid "Error while trying to download Nano Contract transactions history." +msgstr "" + +#: src/sagas/networkSettings.js:85 msgid "Custom Network Settings cannot be empty." msgstr "" -#: src/sagas/networkSettings.js:95 +#: src/sagas/networkSettings.js:92 msgid "explorerUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:102 +#: src/sagas/networkSettings.js:99 msgid "explorerServiceUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:109 +#: src/sagas/networkSettings.js:106 msgid "txMiningServiceUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:116 +#: src/sagas/networkSettings.js:113 msgid "nodeUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:123 +#: src/sagas/networkSettings.js:120 msgid "walletServiceUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:130 +#: src/sagas/networkSettings.js:127 msgid "walletServiceWsUrl should be a valid URL." msgstr "" #. If we fall into this situation, the app should be killed #. for the custom new network settings take effect. -#: src/sagas/networkSettings.js:290 +#: src/sagas/networkSettings.js:279 msgid "Wallet not found while trying to persist the custom network settings." msgstr "" -#: src/sagas/pushNotification.js:59 +#: src/sagas/pushNotification.js:71 msgid "Transaction" -msgstr "Ingen transaktioner" +msgstr "Transaktion" -#: src/sagas/pushNotification.js:60 +#: src/sagas/pushNotification.js:72 msgid "Open" msgstr "Åben" -#: src/components/AskForPushNotification.js:22 +#: src/sagas/tokens.js:40 +msgid "Wallet is not ready yet." +msgstr "" + +#: src/sagas/tokens.js:41 +msgid "Error loading the details of some tokens." +msgstr "" + +#: src/sagas/wallet.js:780 +msgid "Wallet is not ready to load addresses." +msgstr "" + +#. This will show the message in the feedback content at SelectAddressModal +#: src/sagas/wallet.js:796 +msgid "There was an error while loading wallet addresses. Try again." +msgstr "" + +#: src/sagas/wallet.js:806 +msgid "Wallet is not ready to load the first address." +msgstr "" + +#. This will show the message in the feedback content +#: src/sagas/wallet.js:822 +msgid "There was an error while loading first wallet address. Try again." +msgstr "" + +#: src/components/AskForPushNotification.js:29 msgid "Do you want to enable push notifications for this wallet?" msgstr "" -#: src/components/AskForPushNotification.js:23 +#: src/components/AskForPushNotification.js:30 msgid "You can always change this later in the settings menu" msgstr "" -#: src/components/AskForPushNotification.js:24 +#: src/components/AskForPushNotification.js:31 msgid "Yes, enable" msgstr "" -#: src/components/AskForPushNotificationRefresh.js:28 +#: src/components/AskForPushNotificationRefresh.js:35 msgid "Refresh your push notification registration" msgstr "" -#: src/components/AskForPushNotificationRefresh.js:29 +#: src/components/AskForPushNotificationRefresh.js:36 msgid "" "In order to keep receiving push notifications, you need to refresh your " "registration. Do you want to do it now?" msgstr "" -#: src/components/AskForPushNotificationRefresh.js:30 +#: src/components/AskForPushNotificationRefresh.js:37 msgid "Refresh" msgstr "" @@ -1066,21 +1234,21 @@ msgstr "Opret betalingsanmodning" msgid "No internet connection" msgstr "Ingen internetforbindelse" -#: src/components/PublicExplorerListButton.js:19 +#: src/components/PublicExplorerListButton.js:17 msgid "Public Explorer" msgstr "Public Explorer" -#: src/components/PushTxDetailsModal.js:69 src/components/TxDetailsModal.js:61 +#: src/components/PushTxDetailsModal.js:76 src/components/TxDetailsModal.js:103 msgid "Date & Time" msgstr "Dato & Tid" -#: src/components/PushTxDetailsModal.js:70 src/components/TxDetailsModal.js:62 +#: src/components/PushTxDetailsModal.js:77 msgid "ID" msgstr "ID" -#: src/components/PushTxDetailsModal.js:94 +#: src/components/PushTxDetailsModal.js:101 msgid "New Transaction" -msgstr "Ingen transaktioner" +msgstr "" #: src/components/ReceiveMyAddress.js:34 #, javascript-format @@ -1099,16 +1267,16 @@ msgstr "Del" msgid "Propagating transaction to the network." msgstr "" -#: src/components/ShowPushNotificationTxDetails.js:8 +#: src/components/ShowPushNotificationTxDetails.js:16 msgid "Transation not found" -msgstr "Ingen transaktioner" +msgstr "" -#: src/components/ShowPushNotificationTxDetails.js:9 +#: src/components/ShowPushNotificationTxDetails.js:17 msgid "" "The transaction has not arrived yet in your wallet. Do you want to retry?" msgstr "" -#: src/components/ShowPushNotificationTxDetails.js:10 +#: src/components/ShowPushNotificationTxDetails.js:18 msgid "Retry" msgstr "" @@ -1119,10 +1287,46 @@ msgid "" msgstr "" "Her er konfigurationsstrengen for token ${ tokenLabel }: ${ configString }" -#: src/components/TxDetailsModal.js:60 +#: src/components/TransactionStatusLabel.js:71 +msgid "Executed" +msgstr "" + +#: src/components/TransactionStatusLabel.js:76 +msgid "Processing" +msgstr "" + +#: src/components/TransactionStatusLabel.js:81 +msgid "Voided" +msgstr "" + +#: src/components/TxDetailsModal.js:102 msgid "Description" msgstr "Beskrivelse" +#: src/components/NanoContract/NanoContractTransactionHeader.js:44 +#: src/components/TxDetailsModal.js:104 +msgid "Transaction ID" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:89 +#: src/components/TxDetailsModal.js:105 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:116 +msgid "Blueprint Method" +msgstr "" + +#: src/components/TxDetailsModal.js:107 +msgid "Nano Contract Caller" +msgstr "" + +#: src/components/TxDetailsModal.js:111 +msgid "Nano Contract Status" +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:79 +#: src/components/TxDetailsModal.js:120 +msgid "Nano Contract" +msgstr "" + #: src/components/WalletConnect/ApproveRejectModal.js:63 msgid "Reject" msgstr "" @@ -1142,19 +1346,397 @@ msgid "" "security step to protect your data from potential phishing risks." msgstr "" -#: src/components/WalletConnect/SignMessageModal.js:50 -msgid "Sign this message?" +#: src/components/WalletConnect/CreateTokenModal.js:44 +msgid "New Create Token Request" +msgstr "" + +#: src/components/WalletConnect/CreateTokenModal.js:48 +msgid "You have received a new Create Token Request. Please" msgstr "" -#: src/components/WalletConnect/SignMessageModal.js:57 +#: src/components/WalletConnect/CreateTokenModal.js:50 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:54 +#: src/components/WalletConnect/SignMessageModal.js:62 +msgid "carefully review the details" +msgstr "" + +#: src/components/WalletConnect/CreateTokenModal.js:52 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:56 +#: src/components/WalletConnect/SignMessageModal.js:64 +msgid "before deciding to accept or decline." +msgstr "" + +#: src/components/WalletConnect/CreateTokenModal.js:56 +msgid "Review Create Token Request details" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:74 +msgid "Yes" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:74 +msgid "No" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:80 +msgid "Name" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:81 +msgid "Symbol" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:83 +#, javascript-format +msgid "Address to send newly minted ${ data.symbol }" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:84 +msgid "Address to send change ${ DEFAULT_TOKEN.uid }" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:85 +msgid "Create mint authority?" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:86 +msgid "Create melt authority?" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:87 +msgid "Address to send the mint authority" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:88 +msgid "Address to send the melt authority" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:92 +msgid "Allow external mint authority addresses?" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:99 +msgid "Allow external melt authority addresses?" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:103 +msgid "Token data" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:158 +#: src/components/WalletConnect/SignMessageRequest.js:81 +msgid "Accept Request" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:162 +#: src/components/WalletConnect/SignMessageRequest.js:85 +msgid "Decline Request" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:172 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:258 +msgid "Sending transaction" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:173 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:259 +msgid "Please wait." +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:186 +msgid "Create Token Transaction successfully sent." +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:188 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:287 +msgid "Ok, close" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:195 +msgid "Error while sending create token transaction." +msgstr "" + +#: src/components/WalletConnect/SignMessageModal.js:56 +msgid "New Sign Message Request" +msgstr "" + +#: src/components/WalletConnect/SignMessageModal.js:60 +msgid "You have received a new Sign Message Request. Please" +msgstr "" + +#: src/components/WalletConnect/SignMessageModal.js:68 +msgid "Review Sign Message Request details" +msgstr "" + +#: src/components/WalletConnect/SignMessageRequest.js:35 +msgid "Message to sign" +msgstr "" + +#: src/components/WalletConnect/SignMessageRequest.js:45 +msgid "Address Path" +msgstr "" + +#: src/components/WalletConnect/WarnDisclaimer.js:27 msgid "" -"By clicking approve, you will sign the requested message using the first " -"address derived from your root key on the m/44'/280'/0'/0/0 derivation path." +"Caution: There are risks associated with signing dapp transaction requests." +msgstr "" + +#: src/components/WalletConnect/WarnDisclaimer.js:33 +msgid "Read More." +msgstr "" + +#: src/components/WalletConnect/NanoContract/DappContainer.js:41 +msgid "Review your transaction from this dApp" +msgstr "" + +#: src/components/WalletConnect/NanoContract/DappContainer.js:44 +msgid "Stay vigilant and protect your data from potential phishing attempts." +msgstr "" + +#: src/components/WalletConnect/NanoContract/DeclineModal.js:26 +msgid "Decline transaction" +msgstr "" + +#: src/components/WalletConnect/NanoContract/DeclineModal.js:29 +msgid "Are you sure you want to decline this transaction?" +msgstr "" + +#: src/components/WalletConnect/NanoContract/DeclineModal.js:33 +msgid "Yes, decline transaction" +msgstr "" + +#: src/components/NanoContract/UnregisterNanoContractModal.js:50 +#: src/components/WalletConnect/NanoContract/DeclineModal.js:39 +msgid "No, go back" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:36 +#, javascript-format +msgid "${ tokenSymbol } Deposit" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:37 +msgid "${ tokenSymbol } Withdrawal" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:101 +msgid "Action List" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:150 +msgid "To Address:" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:88 +msgid "Blueprint ID" +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:146 +#: src/components/NanoContract/NanoContractsListItem.js:59 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:93 +msgid "Blueprint Name" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:109 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:140 +msgid "Loading..." +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:103 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:124 +msgid "Caller" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:143 +msgid "Couldn't determine address, select one" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractMethodArgs.js:51 +#, javascript-format +msgid "Position ${ idx }" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractMethodArgs.js:100 +msgid "Arguments" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractMethodArgs.js:105 +msgid "Loading arguments." +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:52 +msgid "You have received a new Nano Contract Transaction. Please" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:60 +msgid "Review transaction details" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:197 +msgid "Nano Contract Not Found" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:198 +msgid "" +"The Nano Contract requested is not registered. First register the Nano " +"Contract to interact with it." +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:201 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:248 +msgid "Decline Transaction" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:216 +msgid "Loading transaction information." +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:244 +msgid "Accept Transaction" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:285 +msgid "Transaction successfully sent." +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:293 +msgid "Error while sending transaction." msgstr "" #: src/components/NetworkSettings/NetworkStatusBar.js:14 msgid "Custom network" msgstr "" -#~ msgid "I understand the risks of using a mobile wallet" -#~ msgstr "Jeg forstår risikoen ved at bruge en mobil wallet" +#: src/components/NanoContract/EditAddressModal.js:41 +msgid "New Nano Contract Address" +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:49 +msgid "" +"This address signs any transaction you create with Nano Contracts method. " +"Switching to a new one means" +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:51 +msgid "all future transactions will use this address." +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:57 +msgid "Selected Information" +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:67 +msgid "Index" +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:74 +msgid "Confirm new address" +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:78 +msgid "Go back" +msgstr "" + +#: src/components/NanoContract/NanoContractDetails.js:177 +msgid "Load More" +msgstr "" + +#: src/components/NanoContract/NanoContractDetails.js:203 +msgid "Loading Nano Contract transactions." +msgstr "" + +#: src/components/NanoContract/NanoContractDetails.js:217 +msgid "Nano Contract Transactions Error" +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:150 +msgid "Registered Address" +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:153 +msgid "See status details" +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:154 +msgid "Unregister contract" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionActionList.js:44 +msgid "No Actions" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionActionList.js:45 +msgid "See full transaction details on Public Explorer." +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionActionListItem.js:108 +#, javascript-format +msgid "Deposit ${ tokenSymbol }" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionActionListItem.js:109 +msgid "Withdrawal ${ tokenSymbol }" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:93 +msgid "Date and Time" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:100 +#: src/components/NanoContract/NanoContractTransactionsListItem.js:62 +msgid "From this wallet" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:106 +msgid "See transaction details" +msgstr "" + +#: src/components/NanoContract/NanoContractsList.js:85 +msgid "No Nano Contracts" +msgstr "" + +#: src/components/NanoContract/NanoContractsList.js:86 +msgid "" +"You can keep track of your registered Nano Contracts here once you have " +"registered them." +msgstr "" + +#: src/components/NanoContract/RegisterNewNanoContractButton.js:22 +msgid "Register new" +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:90 +msgid "Choose New Wallet Address" +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:97 +msgid "Load Addresses Error" +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:106 +msgid "Loading wallet addresses." +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:114 +msgid "Current Information" +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:115 +msgid "To change, select other address on the list below." +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:178 +msgid "index" +msgstr "" + +#: src/components/NanoContract/UnregisterNanoContractModal.js:39 +msgid "Unregister Nano Contract" +msgstr "" + +#: src/components/NanoContract/UnregisterNanoContractModal.js:41 +msgid "Are you sure you want to unregister this Nano Contract?" +msgstr "" + +#: src/components/NanoContract/UnregisterNanoContractModal.js:44 +msgid "Yes, unregister contract" +msgstr "" diff --git a/locale/pt-br/texts.po b/locale/pt-br/texts.po index 3901f0f91..9634cc78b 100644 --- a/locale/pt-br/texts.po +++ b/locale/pt-br/texts.po @@ -10,58 +10,58 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n!=1);\n" -"X-Generator: Poedit 3.3.1\n" +"X-Generator: Poedit 3.4.4\n" #. This should never happen! -#: src/models.js:24 +#: src/models.js:85 msgid "Unknown" msgstr "Desconhecido" -#: src/models.js:27 +#: src/models.js:88 #, javascript-format msgid "Received ${ symbol }" msgstr "Recebido ${ symbol }" -#: src/models.js:29 +#: src/models.js:90 msgid "Sent ${ symbol }" msgstr "Enviado ${ symbol }" -#: src/models.js:31 +#: src/models.js:92 #, javascript-format msgid "You sent ${ symbol } to yourself" msgstr "Você enviou ${ symbol } para você mesmo" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:41 +#: src/models.js:102 msgid "[Today •] HH:mm" msgstr "[Hoje •] HH:mm" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:42 +#: src/models.js:103 msgid "[Tomorrow •] HH:mm" msgstr "[Amanhã •] HH:mm" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:43 +#: src/models.js:104 msgid "dddd [•] HH:mm" msgstr "dddd [•] HH:mm" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:44 +#: src/models.js:105 msgid "[Yesterday •] HH:mm" msgstr "[Ontem •] HH:mm" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:45 +#: src/models.js:106 msgid "[Last] dddd [•] HH:mm" msgstr "[Última] dddd [•] HH:mm" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:46 +#: src/models.js:107 src/utils.js:429 msgid "DD MMM YYYY [•] HH:mm" msgstr "DD MMM YYYY [•] HH:mm" -#: src/utils.js:146 +#: src/utils.js:164 msgid "Invalid address" msgstr "Endereço inválido" @@ -236,7 +236,7 @@ msgstr "Você tem ${ amountAvailableText } HTR disponíveis" #: src/screens/CreateTokenAmount.js:149 src/screens/CreateTokenName.js:64 #: src/screens/CreateTokenSymbol.js:84 src/screens/InitWallet.js:220 -#: src/screens/InitWallet.js:341 src/screens/SendAddressInput.js:66 +#: src/screens/InitWallet.js:349 src/screens/SendAddressInput.js:66 #: src/screens/SendAmountInput.js:185 msgid "Next" msgstr "Próximo" @@ -344,14 +344,21 @@ msgstr "O símbolo é a versão reduzida do nome do seu token" msgid "E.g. HTR" msgstr "Por exemplo, HTR" -#: src/screens/Dashboard.js:67 src/screens/RegisterTokenManual.js:147 +#. Only show the toggle button when Nano Contract is enabled to the wallet +#: src/screens/Dashboard.js:130 src/screens/Dashboard.js:187 +msgid "Tokens" +msgstr "Tokens" + +#. Only show the toggle button when Nano Contract is enabled to the wallet +#: src/components/NanoContract/NanoContractsList.js:75 +#: src/screens/Dashboard.js:131 +msgid "Nano Contracts" +msgstr "Nano Contracts" + +#: src/screens/Dashboard.js:178 src/screens/RegisterTokenManual.js:147 msgid "Register token" msgstr "Registrar um token" -#: src/screens/Dashboard.js:74 -msgid "TOKENS" -msgstr "TOKENS" - #: src/screens/InitWallet.js:61 msgid "Welcome to Hathor Wallet!" msgstr "Bem vindo à Hathor Wallet!" @@ -436,7 +443,11 @@ msgstr "Palavras" msgid "Enter your seed words separated by space" msgstr "Digite suas palavras separadas por espaços" +#: src/components/NanoContract/NanoContractDetails.js:238 +#: src/components/WalletConnect/CreateTokenRequest.js:197 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:295 #: src/screens/LoadHistoryScreen.js:51 src/screens/LoadWalletErrorScreen.js:20 +#: src/screens/NanoContract/NanoContractRegisterScreen.js:168 msgid "Try again" msgstr "Tente novamente" @@ -459,7 +470,7 @@ msgid "There's been an error connecting to the server." msgstr "Ocorreu um erro ao conectar com o servidor." #: src/screens/LoadWalletErrorScreen.js:21 src/screens/PinScreen.js:268 -#: src/screens/Settings.js:154 +#: src/screens/Settings.js:161 msgid "Reset wallet" msgstr "Resetar Wallet" @@ -485,14 +496,30 @@ msgstr "Ocorreu um erro ao carregar seu histórico de transações" msgid "Please |tryAgain:try again|" msgstr "Por favor |tryAgain:tente novamente|" -#: src/screens/MainScreen.js:560 src/screens/MainScreen.js:592 +#: src/screens/MainScreen.js:535 src/screens/MainScreen.js:567 msgid "Available Balance" msgstr "Saldo Disponível" -#: src/screens/MainScreen.js:569 +#: src/screens/MainScreen.js:544 msgid "Locked" msgstr "Bloqueado" +#. translator: Used when the QR Code Scanner is opened, and user will manually +#. enter the information. +#: src/screens/NanoContractRegisterQrCodeScreen.js:26 +#: src/screens/RegisterToken.js:27 src/screens/SendScanQRCode.js:87 +msgid "Manual info" +msgstr "Digitar" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:251 +#: src/screens/NanoContractRegisterQrCodeScreen.js:48 +msgid "Nano Contract Registration" +msgstr "Registro do Nano Contract" + +#: src/screens/NanoContractRegisterQrCodeScreen.js:63 +msgid "Scan the nano contract ID QR code" +msgstr "Escaneie o QR code do ID do Nano Contract" + #: src/screens/PaymentRequestDetail.js:88 #, javascript-format msgid "You've just received **${ amount } ${ symbol }**" @@ -502,11 +529,13 @@ msgstr "Você recebeu **${ amount } ${ symbol }**" msgid "PAYMENT REQUEST" msgstr "REQUISIÇÃO DE PAGAMENTO" -#: src/components/TxDetailsModal.js:59 src/screens/PaymentRequestDetail.js:123 +#: src/components/TxDetailsModal.js:101 src/screens/PaymentRequestDetail.js:123 msgid "Token" msgstr "Token" -#: src/components/TxDetailsModal.js:106 src/screens/PaymentRequestDetail.js:127 +#: src/components/TxDetailsModal.js:166 +#: src/components/WalletConnect/CreateTokenRequest.js:82 +#: src/screens/PaymentRequestDetail.js:127 msgid "Amount" msgstr "Quantidade" @@ -538,12 +567,15 @@ msgstr "Digite seu PIN " msgid "Unlock Hathor Wallet" msgstr "Desbloqueie sua Hathor Wallet" +#: src/components/WalletConnect/CreateTokenModal.js:60 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:64 +#: src/components/WalletConnect/SignMessageModal.js:72 #: src/screens/PinScreen.js:265 #: src/screens/WalletConnect/WalletConnectList.js:125 msgid "Cancel" msgstr "Cancelar" -#: src/screens/PushNotification.js:58 src/screens/Settings.js:128 +#: src/screens/PushNotification.js:58 src/screens/Settings.js:135 msgid "Push Notification" msgstr "Notificação" @@ -578,12 +610,6 @@ msgstr "Requisição de Pagamento" msgid "RECEIVE" msgstr "RECEBER" -#. translator: Used when the QR Code Scanner is opened, and user will manually -#. enter the information. -#: src/screens/RegisterToken.js:27 src/screens/SendScanQRCode.js:89 -msgid "Manual info" -msgstr "Digitar" - #: src/screens/RegisterToken.js:42 src/screens/RegisterTokenManual.js:126 msgid "REGISTER TOKEN" msgstr "REGISTRAR UM TOKEN" @@ -665,7 +691,7 @@ msgstr "Mudar PIN" msgid "Lock wallet" msgstr "Bloquear a wallet" -#: src/screens/SendAddressInput.js:53 src/screens/SendScanQRCode.js:97 +#: src/screens/SendAddressInput.js:53 src/screens/SendScanQRCode.js:95 msgid "SEND" msgstr "ENVIAR" @@ -693,11 +719,11 @@ msgstr "ENVIAR ${ tokenNameUpperCase }" msgid "Your transfer is being processed" msgstr "Sua transferência está sendo processada" -#: src/sagas/helpers.js:136 src/screens/SendConfirmScreen.js:104 +#: src/sagas/helpers.js:140 src/screens/SendConfirmScreen.js:104 msgid "Enter your 6-digit pin to authorize operation" msgstr "Digite seu PIN de 6 dígitos para autorizar a operação" -#: src/sagas/helpers.js:137 src/screens/SendConfirmScreen.js:105 +#: src/sagas/helpers.js:141 src/screens/SendConfirmScreen.js:105 msgid "Authorize operation" msgstr "Autorizar operação" @@ -705,11 +731,14 @@ msgstr "Autorizar operação" msgid "Your transfer of **${ this.amountAndToken }** has been confirmed" msgstr "Sua transferência de **${ this.amountAndToken }** foi confirmada" +#: src/components/NanoContract/EditAddressModal.js:60 +#: src/components/NanoContract/SelectAddressModal.js:117 +#: src/components/WalletConnect/SignMessageRequest.js:40 #: src/screens/SendConfirmScreen.js:161 msgid "Address" msgstr "Endereço" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:288 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:291 #: src/screens/SendConfirmScreen.js:168 msgid "Send" msgstr "Enviar" @@ -722,53 +751,53 @@ msgstr "QR code inválido" msgid "OK" msgstr "OK" -#: src/screens/SendScanQRCode.js:73 +#: src/screens/SendScanQRCode.js:71 #, javascript-format msgid "You don't have the requested token [${ tokenLabel }]" msgstr "Você não tem o token requisitado [${ tokenLabel }]" -#: src/screens/SendScanQRCode.js:105 +#: src/screens/SendScanQRCode.js:103 #: src/screens/WalletConnect/WalletConnectScan.js:49 msgid "Scan the QR code" msgstr "Leia o QR code" -#: src/screens/Settings.js:97 +#: src/screens/Settings.js:104 msgid "You are connected to" msgstr "Você está conectado à" -#: src/screens/Settings.js:105 +#: src/screens/Settings.js:112 msgid "General Settings" msgstr "Configurações Gerais" -#: src/screens/Settings.js:109 +#: src/screens/Settings.js:116 msgid "Connected to" msgstr "Conectado ao servidor" -#: src/screens/Settings.js:122 +#: src/screens/Settings.js:129 msgid "Security" msgstr "Segurança" -#: src/screens/Settings.js:135 +#: src/screens/Settings.js:142 msgid "Create a new token" msgstr "Criar um novo token" -#: src/screens/Settings.js:142 +#: src/screens/Settings.js:149 msgid "Register a token" msgstr "Registrar um token" -#: src/screens/Settings.js:158 +#: src/screens/Settings.js:165 msgid "About" msgstr "Sobre" -#: src/screens/Settings.js:165 +#: src/screens/Settings.js:172 msgid "Unique app identifier" msgstr "Identificador único do aplicativo" -#: src/screens/Settings.js:179 +#: src/screens/Settings.js:186 msgid "Developer Settings" msgstr "Configurações do Desenvolvedor" -#: src/screens/Settings.js:181 +#: src/screens/Settings.js:188 msgid "Network Settings" msgstr "Configurações de Rede" @@ -804,6 +833,19 @@ msgstr "Eu quero desregistrar o token **${ tokenLabel }**" msgid "Unregister token" msgstr "Desregistrar token" +#: src/screens/WalletConnect/CreateTokenScreen.js:25 +msgid "Create Token Request" +msgstr "Requisição de Criação de Token" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:48 +#: src/screens/WalletConnect/NewNanoContractTransactionScreen.js:24 +msgid "New Nano Contract Transaction" +msgstr "Nova Transação de Nano Contract" + +#: src/screens/WalletConnect/SignMessageRequestScreen.js:25 +msgid "Sign Message Request" +msgstr "Solicitação de Assinatura de Mensagem" + #: src/screens/WalletConnect/WalletConnectList.js:33 msgid "There was an error connecting. Please try again later." msgstr "" @@ -863,17 +905,17 @@ msgstr "" "souber o que está fazendo." #: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:20 -#: src/screens/NetworkSettings/helper.js:4 +#: src/screens/NetworkSettings/helper.js:11 msgid "Updating custom network settings..." msgstr "Atualizando configurações de rede..." #: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:21 -#: src/screens/NetworkSettings/helper.js:5 +#: src/screens/NetworkSettings/helper.js:12 msgid "Network settings successfully customized." msgstr "Configuração de rede atualizada com sucesso." #: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:22 -#: src/screens/NetworkSettings/helper.js:6 +#: src/screens/NetworkSettings/helper.js:13 msgid "" "There was an error while customizing network settings. Please try again " "later." @@ -899,33 +941,35 @@ msgstr "txMiningServiceUrl é obrigatório." #: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:62 msgid "walletServiceUrl is required when walletServiceWsUrl is filled." -msgstr "walletServiceUrl é obrigatório quando walletServiceWsUrl está preenchido." +msgstr "" +"walletServiceUrl é obrigatório quando walletServiceWsUrl está preenchido." #: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:65 msgid "walletServiceWsUrl is required when walletServiceUrl is filled." -msgstr "walletServiceWsUrl é obrigatório quando walletServiceUrl está preenchido" +msgstr "" +"walletServiceWsUrl é obrigatório quando walletServiceUrl está preenchido." -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:229 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:232 msgid "Node URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:239 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:242 msgid "Explorer URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:248 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:251 msgid "Explorer Service URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:257 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:260 msgid "Transaction Mining Service URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:268 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:271 msgid "Wallet Service URL (optional)" msgstr "Wallet Service URL (opcional)" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:277 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:280 msgid "Wallet Service WS URL (optional)" msgstr "Wallet Service WS URL (opcional)" @@ -951,66 +995,193 @@ msgstr "" "circunstância altere a rede por sugestão de terceiros, uma vez que ao " "alterar a rede você pode ser vítima de fraude." -#: src/sagas/networkSettings.js:88 +#: src/screens/NanoContract/NanoContractDetailsScreen.js:39 +msgid "Nano Contract Details" +msgstr "Detalhes do Nano Contract" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:58 +msgid "Nano Contract ID is required." +msgstr "ID do Nano Contract é obrigatório." + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:158 +msgid "See contract" +msgstr "Ver contrato" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:176 +msgid "Load First Addresses Error" +msgstr "Erro ao carregar primeiro endereço da wallet" + +#: src/components/NanoContract/NanoContractDetails.js:202 +#: src/components/NanoContract/SelectAddressModal.js:105 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:215 +#: src/screens/NanoContract/NanoContractRegisterScreen.js:184 +msgid "Loading" +msgstr "Carregando" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:185 +msgid "Loading first wallet address." +msgstr "Carregando primeiro endereço da wallet." + +#: src/components/NanoContract/NanoContractDetailsHeader.js:142 +#: src/components/NanoContract/NanoContractTransactionHeader.js:85 +#: src/components/NanoContract/NanoContractsListItem.js:57 +#: src/components/TxDetailsModal.js:106 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:83 +#: src/screens/NanoContract/NanoContractRegisterScreen.js:194 +msgid "Nano Contract ID" +msgstr "ID do Nano Contract" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:202 +msgid "Wallet Address" +msgstr "Endereço da Carteira" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:212 +msgid "If you want to change the wallet address, you will be able to do" +msgstr "Se deseja alterar o endereço de assinatura, você pode alterar" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:214 +msgid "after the contract is registered." +msgstr "depois do contrato ser registrado." + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:233 +msgid "Register Nano Contract" +msgstr "Registrar Nano Contract" + +#: src/screens/NanoContract/NanoContractTransactionScreen.js:39 +msgid "Nano Contract Transaction" +msgstr "Transação do Nano Contract" + +#: src/screens/NanoContract/helper.js:11 +msgid "Contract successfully registered." +msgstr "Nano Contract registrado com sucesso." + +#: src/sagas/nanoContract.js:45 +msgid "Nano Contract already registered." +msgstr "Nano Contract já registrado." + +#: src/sagas/nanoContract.js:46 +msgid "Wallet is not ready yet to register a Nano Contract." +msgstr "A wallet não está pronta ainda para registrar Nano Contracts." + +#: src/sagas/nanoContract.js:47 +msgid "The informed address does not belong to the wallet." +msgstr "O endereço informado não pertence a esta carteira." + +#: src/sagas/nanoContract.js:48 +msgid "Nano Contract not found." +msgstr "Nano Contract não encontrado." + +#: src/sagas/nanoContract.js:49 +msgid "Error while trying to register Nano Contract." +msgstr "Erro ao tentar registrar o Nano Contract." + +#: src/sagas/nanoContract.js:50 +msgid "Invalid transaction to register as Nano Contract." +msgstr "Nano Contract inválido." + +#: src/sagas/nanoContract.js:51 +msgid "Blueprint not found." +msgstr "Blueprint não encontrado." + +#: src/sagas/nanoContract.js:52 +msgid "Couldn't get Blueprint info." +msgstr "Não foi possível carregar informações do Blueprint." + +#: src/sagas/nanoContract.js:53 +msgid "Nano Contract not registered." +msgstr "Nano Contract não registrado." + +#: src/sagas/nanoContract.js:54 +msgid "Error while trying to download Nano Contract transactions history." +msgstr "Error ao fazer download do histórico de transações do Nano Contract." + +#: src/sagas/networkSettings.js:85 msgid "Custom Network Settings cannot be empty." msgstr "As Configurações de Rede não podem estar vazias." -#: src/sagas/networkSettings.js:95 +#: src/sagas/networkSettings.js:92 msgid "explorerUrl should be a valid URL." msgstr "explorerUrl deve ser uma URL válida." -#: src/sagas/networkSettings.js:102 +#: src/sagas/networkSettings.js:99 msgid "explorerServiceUrl should be a valid URL." msgstr "explorerServiceUrl deve ser uma URL válida." -#: src/sagas/networkSettings.js:109 +#: src/sagas/networkSettings.js:106 msgid "txMiningServiceUrl should be a valid URL." msgstr "txMiningServiceUrl deve ser uma URL válida." -#: src/sagas/networkSettings.js:116 +#: src/sagas/networkSettings.js:113 msgid "nodeUrl should be a valid URL." msgstr "nodeUrl deve ser uma URL válida." -#: src/sagas/networkSettings.js:123 +#: src/sagas/networkSettings.js:120 msgid "walletServiceUrl should be a valid URL." msgstr "walletServiceUrl deve ser uma URL válida." -#: src/sagas/networkSettings.js:130 +#: src/sagas/networkSettings.js:127 msgid "walletServiceWsUrl should be a valid URL." msgstr "walletServiceWsUrl deve ser uma URL válida." #. If we fall into this situation, the app should be killed #. for the custom new network settings take effect. -#: src/sagas/networkSettings.js:290 +#: src/sagas/networkSettings.js:279 msgid "Wallet not found while trying to persist the custom network settings." msgstr "" "Wallet não encontrada ao persistir a configuração personalizada da rede." -#: src/sagas/pushNotification.js:59 +#: src/sagas/pushNotification.js:71 msgid "Transaction" msgstr "Transação" -#: src/sagas/pushNotification.js:60 +#: src/sagas/pushNotification.js:72 msgid "Open" msgstr "Abrir" -#: src/components/AskForPushNotification.js:22 +#: src/sagas/tokens.js:40 +msgid "Wallet is not ready yet." +msgstr "A wallet não está pronta ainda." + +#: src/sagas/tokens.js:41 +msgid "Error loading the details of some tokens." +msgstr "Ocorreu um erro durante o carregamento de detalhes de alguns tokens." + +#: src/sagas/wallet.js:780 +msgid "Wallet is not ready to load addresses." +msgstr "A wallet não está pronta para carregar os endereços." + +#. This will show the message in the feedback content at SelectAddressModal +#: src/sagas/wallet.js:796 +msgid "There was an error while loading wallet addresses. Try again." +msgstr "Ocorreu um erro ao carregar os endereços da wallet. Tente novamente." + +#: src/sagas/wallet.js:806 +msgid "Wallet is not ready to load the first address." +msgstr "A wallet não está pronta para carregar o primeiro endereço." + +#. This will show the message in the feedback content +#: src/sagas/wallet.js:822 +msgid "There was an error while loading first wallet address. Try again." +msgstr "" +"Ocorreu um erro ao carregar o primeiro endereço da wallet. Tente novamente." + +#: src/components/AskForPushNotification.js:29 msgid "Do you want to enable push notifications for this wallet?" msgstr "Você deseja habilitar as notificações para esta wallet?" -#: src/components/AskForPushNotification.js:23 +#: src/components/AskForPushNotification.js:30 msgid "You can always change this later in the settings menu" msgstr "Você sempre pode alterar as configurações depois no menu" -#: src/components/AskForPushNotification.js:24 +#: src/components/AskForPushNotification.js:31 msgid "Yes, enable" msgstr "Sim, habilitar" -#: src/components/AskForPushNotificationRefresh.js:28 +#: src/components/AskForPushNotificationRefresh.js:35 msgid "Refresh your push notification registration" msgstr "Atualize sua configuração de notificação" -#: src/components/AskForPushNotificationRefresh.js:29 +#: src/components/AskForPushNotificationRefresh.js:36 msgid "" "In order to keep receiving push notifications, you need to refresh your " "registration. Do you want to do it now?" @@ -1018,7 +1189,7 @@ msgstr "" "Para continuar recebendo notificações, é necessário atualizar sua " "configuração de notificação. Deseja atualizar agora?" -#: src/components/AskForPushNotificationRefresh.js:30 +#: src/components/AskForPushNotificationRefresh.js:37 msgid "Refresh" msgstr "Atualizar" @@ -1096,19 +1267,19 @@ msgstr "Criar requisição de pagamento" msgid "No internet connection" msgstr "Sem conexão com a internet" -#: src/components/PublicExplorerListButton.js:19 +#: src/components/PublicExplorerListButton.js:17 msgid "Public Explorer" msgstr "Explorer Público" -#: src/components/PushTxDetailsModal.js:69 src/components/TxDetailsModal.js:61 +#: src/components/PushTxDetailsModal.js:76 src/components/TxDetailsModal.js:103 msgid "Date & Time" msgstr "Data & Hora" -#: src/components/PushTxDetailsModal.js:70 src/components/TxDetailsModal.js:62 +#: src/components/PushTxDetailsModal.js:77 msgid "ID" msgstr "ID" -#: src/components/PushTxDetailsModal.js:94 +#: src/components/PushTxDetailsModal.js:101 msgid "New Transaction" msgstr "Transação" @@ -1129,16 +1300,16 @@ msgstr "Compartilhar" msgid "Propagating transaction to the network." msgstr "Propagando a transação para a rede." -#: src/components/ShowPushNotificationTxDetails.js:8 +#: src/components/ShowPushNotificationTxDetails.js:16 msgid "Transation not found" msgstr "Transação não encontrada" -#: src/components/ShowPushNotificationTxDetails.js:9 +#: src/components/ShowPushNotificationTxDetails.js:17 msgid "" "The transaction has not arrived yet in your wallet. Do you want to retry?" msgstr "A transação ainda não chegou na sua carteira. Deseja tentar novamente?" -#: src/components/ShowPushNotificationTxDetails.js:10 +#: src/components/ShowPushNotificationTxDetails.js:18 msgid "Retry" msgstr "Tentar novamente" @@ -1148,10 +1319,46 @@ msgid "" "Here is the configuration string of token ${ tokenLabel }: ${ configString }" msgstr "Essa é a configuração do seu token ${ tokenLabel }: ${ configString }" -#: src/components/TxDetailsModal.js:60 +#: src/components/TransactionStatusLabel.js:71 +msgid "Executed" +msgstr "Executada" + +#: src/components/TransactionStatusLabel.js:76 +msgid "Processing" +msgstr "Processando" + +#: src/components/TransactionStatusLabel.js:81 +msgid "Voided" +msgstr "Inválida" + +#: src/components/TxDetailsModal.js:102 msgid "Description" msgstr "Descrição" +#: src/components/NanoContract/NanoContractTransactionHeader.js:44 +#: src/components/TxDetailsModal.js:104 +msgid "Transaction ID" +msgstr "ID da Transação" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:89 +#: src/components/TxDetailsModal.js:105 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:116 +msgid "Blueprint Method" +msgstr "Método do Blueprint" + +#: src/components/TxDetailsModal.js:107 +msgid "Nano Contract Caller" +msgstr "Endereço de assinatura do Nano Contract" + +#: src/components/TxDetailsModal.js:111 +msgid "Nano Contract Status" +msgstr "Status do Nano Contract" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:79 +#: src/components/TxDetailsModal.js:120 +msgid "Nano Contract" +msgstr "Nano Contract" + #: src/components/WalletConnect/ApproveRejectModal.js:63 msgid "Reject" msgstr "Rejeitar" @@ -1175,31 +1382,407 @@ msgstr "" "importante de segurança para proteger seus dados de potenciais riscos de " "phishing." -#: src/components/WalletConnect/SignMessageModal.js:50 -msgid "Sign this message?" -msgstr "Assinar essa mensagem?" +#: src/components/WalletConnect/CreateTokenModal.js:44 +msgid "New Create Token Request" +msgstr "Nova Requisição de Criação de Token" + +#: src/components/WalletConnect/CreateTokenModal.js:48 +msgid "You have received a new Create Token Request. Please" +msgstr "Você recebeu uma nova Solicitação de Criação de Token. Por favor," + +#: src/components/WalletConnect/CreateTokenModal.js:50 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:54 +#: src/components/WalletConnect/SignMessageModal.js:62 +msgid "carefully review the details" +msgstr "revise os detalhes com cuidado" + +#: src/components/WalletConnect/CreateTokenModal.js:52 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:56 +#: src/components/WalletConnect/SignMessageModal.js:64 +msgid "before deciding to accept or decline." +msgstr "antes de decidir aceitar ou recusar." -#: src/components/WalletConnect/SignMessageModal.js:57 +#: src/components/WalletConnect/CreateTokenModal.js:56 +msgid "Review Create Token Request details" +msgstr "Revisar detalhes da Solicitação de Criação de Token" + +#: src/components/WalletConnect/CreateTokenRequest.js:74 +msgid "Yes" +msgstr "Sim" + +#: src/components/WalletConnect/CreateTokenRequest.js:74 +msgid "No" +msgstr "Não" + +#: src/components/WalletConnect/CreateTokenRequest.js:80 +msgid "Name" +msgstr "Nome" + +#: src/components/WalletConnect/CreateTokenRequest.js:81 +msgid "Symbol" +msgstr "Símbolo" + +#: src/components/WalletConnect/CreateTokenRequest.js:83 +#, javascript-format +msgid "Address to send newly minted ${ data.symbol }" +msgstr "Endereço para enviar os tokens ${ data.symbol } criados" + +#: src/components/WalletConnect/CreateTokenRequest.js:84 +msgid "Address to send change ${ DEFAULT_TOKEN.uid }" +msgstr "Endereço para enviar o ${ DEFAULT_TOKEN.uid } de troco" + +#: src/components/WalletConnect/CreateTokenRequest.js:85 +msgid "Create mint authority?" +msgstr "Criar mint authority?" + +#: src/components/WalletConnect/CreateTokenRequest.js:86 +msgid "Create melt authority?" +msgstr "Criar melt authority?" + +#: src/components/WalletConnect/CreateTokenRequest.js:87 +msgid "Address to send the mint authority" +msgstr "Endereço para enviar o mint authority" + +#: src/components/WalletConnect/CreateTokenRequest.js:88 +msgid "Address to send the melt authority" +msgstr "Endereço para enviar o melt authority" + +#: src/components/WalletConnect/CreateTokenRequest.js:92 +msgid "Allow external mint authority addresses?" +msgstr "Permitir endereços externos de mint authority?" + +#: src/components/WalletConnect/CreateTokenRequest.js:99 +msgid "Allow external melt authority addresses?" +msgstr "Permitir endereços externos de melt authority?" + +#: src/components/WalletConnect/CreateTokenRequest.js:103 +msgid "Token data" +msgstr "Dados do Token" + +#: src/components/WalletConnect/CreateTokenRequest.js:158 +#: src/components/WalletConnect/SignMessageRequest.js:81 +msgid "Accept Request" +msgstr "Aceitar Solicitação" + +#: src/components/WalletConnect/CreateTokenRequest.js:162 +#: src/components/WalletConnect/SignMessageRequest.js:85 +msgid "Decline Request" +msgstr "Recusar Solicitação" + +#: src/components/WalletConnect/CreateTokenRequest.js:172 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:258 +msgid "Sending transaction" +msgstr "Enviando transação" + +#: src/components/WalletConnect/CreateTokenRequest.js:173 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:259 +msgid "Please wait." +msgstr "Por favor, espere." + +#: src/components/WalletConnect/CreateTokenRequest.js:186 +msgid "Create Token Transaction successfully sent." +msgstr "Transação de Criação de Token enviada com sucesso." + +#: src/components/WalletConnect/CreateTokenRequest.js:188 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:287 +msgid "Ok, close" +msgstr "Ok, fechar" + +#: src/components/WalletConnect/CreateTokenRequest.js:195 +msgid "Error while sending create token transaction." +msgstr "Erro ao enviar transação de criação de token." + +#: src/components/WalletConnect/SignMessageModal.js:56 +msgid "New Sign Message Request" +msgstr "Nova Solicitação de Assinatura de Mensagem" + +#: src/components/WalletConnect/SignMessageModal.js:60 +msgid "You have received a new Sign Message Request. Please" +msgstr "" +"Você recebeu uma nova Solicitação de Assinatura de Mensagem. Por favor," + +#: src/components/WalletConnect/SignMessageModal.js:68 +msgid "Review Sign Message Request details" +msgstr "Revisar detalhes da Solicitação de Assinatura de Mensagem" + +#: src/components/WalletConnect/SignMessageRequest.js:35 +msgid "Message to sign" +msgstr "Mensagem para assinar" + +#: src/components/WalletConnect/SignMessageRequest.js:45 +msgid "Address Path" +msgstr "Path do Endereço" + +#: src/components/WalletConnect/WarnDisclaimer.js:27 msgid "" -"By clicking approve, you will sign the requested message using the first " -"address derived from your root key on the m/44'/280'/0'/0/0 derivation path." +"Caution: There are risks associated with signing dapp transaction requests." msgstr "" -"Ao clicar em aprovar, você assinará a mensagem solicitada usando o primeiro " -"endereço derivado da sua chave raiz no caminho de derivação " -"m/44'/280'/0'/0/0." +"Cuidado: Há riscos associados a pedidos de assinatura de transação via dApp." + +#: src/components/WalletConnect/WarnDisclaimer.js:33 +msgid "Read More." +msgstr "Ler mais." + +#: src/components/WalletConnect/NanoContract/DappContainer.js:41 +msgid "Review your transaction from this dApp" +msgstr "Revise sua transação originada neste dApp" + +#: src/components/WalletConnect/NanoContract/DappContainer.js:44 +msgid "Stay vigilant and protect your data from potential phishing attempts." +msgstr "Fique atento e proteja seus dados de potenciais tentativas de phising." + +#: src/components/WalletConnect/NanoContract/DeclineModal.js:26 +msgid "Decline transaction" +msgstr "Recusar transação" + +#: src/components/WalletConnect/NanoContract/DeclineModal.js:29 +msgid "Are you sure you want to decline this transaction?" +msgstr "Tem certeza que deseja recusar esta transação?" + +#: src/components/WalletConnect/NanoContract/DeclineModal.js:33 +msgid "Yes, decline transaction" +msgstr "Sim, recusar transação" + +#: src/components/NanoContract/UnregisterNanoContractModal.js:50 +#: src/components/WalletConnect/NanoContract/DeclineModal.js:39 +msgid "No, go back" +msgstr "Não, voltar" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:36 +#, javascript-format +msgid "${ tokenSymbol } Deposit" +msgstr "${ tokenSymbol } Depósito" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:37 +msgid "${ tokenSymbol } Withdrawal" +msgstr "${ tokenSymbol } Saque" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:101 +msgid "Action List" +msgstr "Lista de Actions" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:150 +msgid "To Address:" +msgstr "Para o endereço:" + +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:88 +msgid "Blueprint ID" +msgstr "ID do Blueprint" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:146 +#: src/components/NanoContract/NanoContractsListItem.js:59 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:93 +msgid "Blueprint Name" +msgstr "Nome do Blueprint" + +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:109 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:140 +msgid "Loading..." +msgstr "Carregando..." + +#: src/components/NanoContract/NanoContractTransactionHeader.js:103 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:124 +msgid "Caller" +msgstr "Caller" + +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:143 +msgid "Couldn't determine address, select one" +msgstr "Não foi possível determinar um endereço, selecione um" + +#: src/components/WalletConnect/NanoContract/NanoContractMethodArgs.js:51 +#, javascript-format +msgid "Position ${ idx }" +msgstr "Posição ${ idx }" + +#: src/components/WalletConnect/NanoContract/NanoContractMethodArgs.js:100 +msgid "Arguments" +msgstr "Argumentos" + +#: src/components/WalletConnect/NanoContract/NanoContractMethodArgs.js:105 +msgid "Loading arguments." +msgstr "Carregando argumentos." + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:52 +msgid "You have received a new Nano Contract Transaction. Please" +msgstr "" +"Você recebeu um pedido para criar uma transação de Nano Contract. Por favor," + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:60 +msgid "Review transaction details" +msgstr "Revisar detalhes da transação" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:197 +msgid "Nano Contract Not Found" +msgstr "Nano Contract não encontrado" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:198 +msgid "" +"The Nano Contract requested is not registered. First register the Nano " +"Contract to interact with it." +msgstr "" +"O Nano Contract solicitado não está registrado. Primeiro registre o Nano " +"Contract para interagir com ele." + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:201 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:248 +msgid "Decline Transaction" +msgstr "Recusar transação" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:216 +msgid "Loading transaction information." +msgstr "Carregando informações da transação." + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:244 +msgid "Accept Transaction" +msgstr "Aceitar transação" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:285 +msgid "Transaction successfully sent." +msgstr "Transação enviada com sucesso." + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:293 +msgid "Error while sending transaction." +msgstr "Ocorreu um erro durante o envio da transação." #: src/components/NetworkSettings/NetworkStatusBar.js:14 msgid "Custom network" msgstr "Rede personalizada" -#~ msgid "There has been an error upgrading your wallet." -#~ msgstr "Ocorreu um erro ao atualizar sua wallet." +#: src/components/NanoContract/EditAddressModal.js:41 +msgid "New Nano Contract Address" +msgstr "Novo endereço para o Nano Contract" + +#: src/components/NanoContract/EditAddressModal.js:49 +msgid "" +"This address signs any transaction you create with Nano Contracts method. " +"Switching to a new one means" +msgstr "" +"Este endereço assina toda transação criada com os métodos do Nano Contract. " +"Ao alterar para um novo endereço significa que" + +#: src/components/NanoContract/EditAddressModal.js:51 +msgid "all future transactions will use this address." +msgstr "todas as transações futuras usarão este endereço." + +#: src/components/NanoContract/EditAddressModal.js:57 +msgid "Selected Information" +msgstr "Informação Selecionada" + +#: src/components/NanoContract/EditAddressModal.js:67 +msgid "Index" +msgstr "Índice" + +#: src/components/NanoContract/EditAddressModal.js:74 +msgid "Confirm new address" +msgstr "Confirmar novo endereço" + +#: src/components/NanoContract/EditAddressModal.js:78 +msgid "Go back" +msgstr "Voltar" + +#: src/components/NanoContract/NanoContractDetails.js:177 +msgid "Load More" +msgstr "Carregar mais" + +#: src/components/NanoContract/NanoContractDetails.js:203 +msgid "Loading Nano Contract transactions." +msgstr "Carregando transações do Nano Contract." + +#: src/components/NanoContract/NanoContractDetails.js:217 +msgid "Nano Contract Transactions Error" +msgstr "Erro em Transações do Nano Contract" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:150 +msgid "Registered Address" +msgstr "Endereço Registrado" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:153 +msgid "See status details" +msgstr "Ver detalhes" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:154 +msgid "Unregister contract" +msgstr "Desregistrar contract" + +#: src/components/NanoContract/NanoContractTransactionActionList.js:44 +msgid "No Actions" +msgstr "Nenhuma Action" + +#: src/components/NanoContract/NanoContractTransactionActionList.js:45 +msgid "See full transaction details on Public Explorer." +msgstr "Ver detalhes completos da transação no Explorer Público." + +#: src/components/NanoContract/NanoContractTransactionActionListItem.js:108 +#, javascript-format +msgid "Deposit ${ tokenSymbol }" +msgstr "Depósito ${ tokenSymbol }" + +#: src/components/NanoContract/NanoContractTransactionActionListItem.js:109 +msgid "Withdrawal ${ tokenSymbol }" +msgstr "Saque ${ tokenSymbol }" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:93 +msgid "Date and Time" +msgstr "Data & Hora" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:100 +#: src/components/NanoContract/NanoContractTransactionsListItem.js:62 +msgid "From this wallet" +msgstr "Desta wallet" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:106 +msgid "See transaction details" +msgstr "Ver detalhes" + +#: src/components/NanoContract/NanoContractsList.js:85 +msgid "No Nano Contracts" +msgstr "Nenhum Nano Contract" + +#: src/components/NanoContract/NanoContractsList.js:86 +msgid "" +"You can keep track of your registered Nano Contracts here once you have " +"registered them." +msgstr "Você pode acompanhar os Nano Contracts registrados nesta tela." + +#: src/components/NanoContract/RegisterNewNanoContractButton.js:22 +msgid "Register new" +msgstr "Registrar novo" + +#: src/components/NanoContract/SelectAddressModal.js:90 +msgid "Choose New Wallet Address" +msgstr "Escolha o Novo Endereço" + +#: src/components/NanoContract/SelectAddressModal.js:97 +msgid "Load Addresses Error" +msgstr "Erro ao carregar endereços" + +#: src/components/NanoContract/SelectAddressModal.js:106 +msgid "Loading wallet addresses." +msgstr "Carregando endereços da wallet." + +#: src/components/NanoContract/SelectAddressModal.js:114 +msgid "Current Information" +msgstr "Informação Atual" + +#: src/components/NanoContract/SelectAddressModal.js:115 +msgid "To change, select other address on the list below." +msgstr "Para alterar, selecione outro endereço da lista abaixo." + +#: src/components/NanoContract/SelectAddressModal.js:178 +msgid "index" +msgstr "índice" + +#: src/components/NanoContract/UnregisterNanoContractModal.js:39 +msgid "Unregister Nano Contract" +msgstr "Desregistrar Nano Contract" -#~ msgid "Please reset it and restore it with your seed." -#~ msgstr "Por favor resete sua wallet e restaure utilizando sua seed." +#: src/components/NanoContract/UnregisterNanoContractModal.js:41 +msgid "Are you sure you want to unregister this Nano Contract?" +msgstr "Confirma que deseja desregistrar este Nano Contract?" -#~ msgid "Upgrading your wallet, please hang on." -#~ msgstr "Atualizando sua wallet, por favor aguarde." +#: src/components/NanoContract/UnregisterNanoContractModal.js:44 +msgid "Yes, unregister contract" +msgstr "Sim, desregistrar" -#~ msgid "I understand the risks of using a mobile wallet" -#~ msgstr "Eu entendo os riscos de usar uma wallet de celular" +#~ msgid "Title" +#~ msgstr "Título" diff --git a/locale/ru-ru/texts.po b/locale/ru-ru/texts.po index 598ae15b2..1fb48d314 100644 --- a/locale/ru-ru/texts.po +++ b/locale/ru-ru/texts.po @@ -14,55 +14,55 @@ msgstr "" "X-Generator: Poedit 3.3.1\n" #. This should never happen! -#: src/models.js:24 +#: src/models.js:85 msgid "Unknown" msgstr "Неизвестно" -#: src/models.js:27 +#: src/models.js:88 #, javascript-format msgid "Received ${ symbol }" msgstr "Получено ${ symbol }" -#: src/models.js:29 +#: src/models.js:90 msgid "Sent ${ symbol }" msgstr "Отправлено ${ symbol }" -#: src/models.js:31 +#: src/models.js:92 #, javascript-format msgid "You sent ${ symbol } to yourself" msgstr "Вы отправили себе ${ symbol }" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:41 +#: src/models.js:102 msgid "[Today •] HH:mm" msgstr "[Сегодня •] HH:mm" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:42 +#: src/models.js:103 msgid "[Tomorrow •] HH:mm" msgstr "[Завтра •] HH:mm" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:43 +#: src/models.js:104 msgid "dddd [•] HH:mm" msgstr "dddd [•] HH:mm" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:44 +#: src/models.js:105 msgid "[Yesterday •] HH:mm" msgstr "[Вчера •] HH:mm" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:45 +#: src/models.js:106 msgid "[Last] dddd [•] HH:mm" msgstr "[Последний] dddd [•] HH:mm" #. See https://momentjs.com/docs/#/displaying/calendar-time/ -#: src/models.js:46 +#: src/models.js:107 src/utils.js:429 msgid "DD MMM YYYY [•] HH:mm" msgstr "DD MMM YYYY [•] HH:mm" -#: src/utils.js:146 +#: src/utils.js:164 msgid "Invalid address" msgstr "Неправильный адрес" @@ -107,7 +107,7 @@ msgstr "" #: src/workers/pushNotificationHandler.js:118 msgid "New transaction received" -msgstr "Нет транзакций" +msgstr "" #: src/screens/About.js:83 msgid "ABOUT" @@ -229,7 +229,7 @@ msgstr "У вас ${ amountAvailableText } HTR" #: src/screens/CreateTokenAmount.js:149 src/screens/CreateTokenName.js:64 #: src/screens/CreateTokenSymbol.js:84 src/screens/InitWallet.js:220 -#: src/screens/InitWallet.js:341 src/screens/SendAddressInput.js:66 +#: src/screens/InitWallet.js:349 src/screens/SendAddressInput.js:66 #: src/screens/SendAmountInput.js:185 msgid "Next" msgstr "Далее" @@ -336,14 +336,21 @@ msgstr "Символ является сокращенной версией им msgid "E.g. HTR" msgstr "Например, HTR" -#: src/screens/Dashboard.js:67 src/screens/RegisterTokenManual.js:147 +#. Only show the toggle button when Nano Contract is enabled to the wallet +#: src/screens/Dashboard.js:130 src/screens/Dashboard.js:187 +msgid "Tokens" +msgstr "ТокенЫ" + +#. Only show the toggle button when Nano Contract is enabled to the wallet +#: src/components/NanoContract/NanoContractsList.js:75 +#: src/screens/Dashboard.js:131 +msgid "Nano Contracts" +msgstr "" + +#: src/screens/Dashboard.js:178 src/screens/RegisterTokenManual.js:147 msgid "Register token" msgstr "Зарегистрировать токен" -#: src/screens/Dashboard.js:74 -msgid "TOKENS" -msgstr "ТОКЕНЫ" - #: src/screens/InitWallet.js:61 msgid "Welcome to Hathor Wallet!" msgstr "Добро пожаловать в Hathor Wallet!" @@ -426,7 +433,11 @@ msgstr "Слова" msgid "Enter your seed words separated by space" msgstr "Введите seed-фразу" +#: src/components/NanoContract/NanoContractDetails.js:238 +#: src/components/WalletConnect/CreateTokenRequest.js:197 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:295 #: src/screens/LoadHistoryScreen.js:51 src/screens/LoadWalletErrorScreen.js:20 +#: src/screens/NanoContract/NanoContractRegisterScreen.js:168 msgid "Try again" msgstr "" @@ -449,7 +460,7 @@ msgid "There's been an error connecting to the server." msgstr "" #: src/screens/LoadWalletErrorScreen.js:21 src/screens/PinScreen.js:268 -#: src/screens/Settings.js:154 +#: src/screens/Settings.js:161 msgid "Reset wallet" msgstr "Сбросить кошелек" @@ -474,14 +485,30 @@ msgstr "" msgid "Please |tryAgain:try again|" msgstr "" -#: src/screens/MainScreen.js:560 src/screens/MainScreen.js:592 +#: src/screens/MainScreen.js:535 src/screens/MainScreen.js:567 msgid "Available Balance" msgstr "Доступный Баланс" -#: src/screens/MainScreen.js:569 +#: src/screens/MainScreen.js:544 msgid "Locked" msgstr "Заблокированный" +#. translator: Used when the QR Code Scanner is opened, and user will manually +#. enter the information. +#: src/screens/NanoContractRegisterQrCodeScreen.js:26 +#: src/screens/RegisterToken.js:27 src/screens/SendScanQRCode.js:87 +msgid "Manual info" +msgstr "Ручной ввод" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:251 +#: src/screens/NanoContractRegisterQrCodeScreen.js:48 +msgid "Nano Contract Registration" +msgstr "" + +#: src/screens/NanoContractRegisterQrCodeScreen.js:63 +msgid "Scan the nano contract ID QR code" +msgstr "" + #: src/screens/PaymentRequestDetail.js:88 #, javascript-format msgid "You've just received **${ amount } ${ symbol }**" @@ -491,11 +518,13 @@ msgstr "Вы только что получили **${ amount } ${ symbol }**" msgid "PAYMENT REQUEST" msgstr "ЗАПРОС СРЕДСТВ" -#: src/components/TxDetailsModal.js:59 src/screens/PaymentRequestDetail.js:123 +#: src/components/TxDetailsModal.js:101 src/screens/PaymentRequestDetail.js:123 msgid "Token" msgstr "Токен" -#: src/components/TxDetailsModal.js:106 src/screens/PaymentRequestDetail.js:127 +#: src/components/TxDetailsModal.js:166 +#: src/components/WalletConnect/CreateTokenRequest.js:82 +#: src/screens/PaymentRequestDetail.js:127 msgid "Amount" msgstr "Количество" @@ -527,12 +556,15 @@ msgstr "Введите свой PIN-код " msgid "Unlock Hathor Wallet" msgstr "Разблокировать Hathor Wallet" +#: src/components/WalletConnect/CreateTokenModal.js:60 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:64 +#: src/components/WalletConnect/SignMessageModal.js:72 #: src/screens/PinScreen.js:265 #: src/screens/WalletConnect/WalletConnectList.js:125 msgid "Cancel" msgstr "Отмена" -#: src/screens/PushNotification.js:58 src/screens/Settings.js:128 +#: src/screens/PushNotification.js:58 src/screens/Settings.js:135 msgid "Push Notification" msgstr "" @@ -565,12 +597,6 @@ msgstr "Запрос Средств" msgid "RECEIVE" msgstr "ПОЛУЧИТЬ" -#. translator: Used when the QR Code Scanner is opened, and user will manually -#. enter the information. -#: src/screens/RegisterToken.js:27 src/screens/SendScanQRCode.js:89 -msgid "Manual info" -msgstr "Ручной ввод" - #: src/screens/RegisterToken.js:42 src/screens/RegisterTokenManual.js:126 msgid "REGISTER TOKEN" msgstr "РЕГИСТРАЦИЯ ТОКЕНА" @@ -650,7 +676,7 @@ msgstr "Изменить PIN-код" msgid "Lock wallet" msgstr "Заблокировать кошелек" -#: src/screens/SendAddressInput.js:53 src/screens/SendScanQRCode.js:97 +#: src/screens/SendAddressInput.js:53 src/screens/SendScanQRCode.js:95 msgid "SEND" msgstr "ОТПРАВИТЬ" @@ -679,11 +705,11 @@ msgstr "ОТПРАВИТЬ ${ tokenNameUpperCase }" msgid "Your transfer is being processed" msgstr "Ваш перевод обрабатывается" -#: src/sagas/helpers.js:136 src/screens/SendConfirmScreen.js:104 +#: src/sagas/helpers.js:140 src/screens/SendConfirmScreen.js:104 msgid "Enter your 6-digit pin to authorize operation" msgstr "Введите 6-значный PIN-код для авторизации операции" -#: src/sagas/helpers.js:137 src/screens/SendConfirmScreen.js:105 +#: src/sagas/helpers.js:141 src/screens/SendConfirmScreen.js:105 msgid "Authorize operation" msgstr "Авторизовать операцию" @@ -691,11 +717,14 @@ msgstr "Авторизовать операцию" msgid "Your transfer of **${ this.amountAndToken }** has been confirmed" msgstr "Ваш перевод **${ this.amountAndToken }** был подтвержден" +#: src/components/NanoContract/EditAddressModal.js:60 +#: src/components/NanoContract/SelectAddressModal.js:117 +#: src/components/WalletConnect/SignMessageRequest.js:40 #: src/screens/SendConfirmScreen.js:161 msgid "Address" msgstr "Адрес" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:288 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:291 #: src/screens/SendConfirmScreen.js:168 msgid "Send" msgstr "Отправить" @@ -708,53 +737,53 @@ msgstr "Неверный QR-код" msgid "OK" msgstr "OK" -#: src/screens/SendScanQRCode.js:73 +#: src/screens/SendScanQRCode.js:71 #, javascript-format msgid "You don't have the requested token [${ tokenLabel }]" msgstr "У вас нет запрошенного токена [${ tokenLabel }]" -#: src/screens/SendScanQRCode.js:105 +#: src/screens/SendScanQRCode.js:103 #: src/screens/WalletConnect/WalletConnectScan.js:49 msgid "Scan the QR code" msgstr "Сканировать QR-код" -#: src/screens/Settings.js:97 +#: src/screens/Settings.js:104 msgid "You are connected to" msgstr "Вы подключены к" -#: src/screens/Settings.js:105 +#: src/screens/Settings.js:112 msgid "General Settings" msgstr "" -#: src/screens/Settings.js:109 +#: src/screens/Settings.js:116 msgid "Connected to" msgstr "Подключены к" -#: src/screens/Settings.js:122 +#: src/screens/Settings.js:129 msgid "Security" msgstr "Безопасность" -#: src/screens/Settings.js:135 +#: src/screens/Settings.js:142 msgid "Create a new token" msgstr "Создать новый токен" -#: src/screens/Settings.js:142 +#: src/screens/Settings.js:149 msgid "Register a token" msgstr "Зарегистрировать токен" -#: src/screens/Settings.js:158 +#: src/screens/Settings.js:165 msgid "About" msgstr "О нас" -#: src/screens/Settings.js:165 +#: src/screens/Settings.js:172 msgid "Unique app identifier" msgstr "" -#: src/screens/Settings.js:179 +#: src/screens/Settings.js:186 msgid "Developer Settings" msgstr "" -#: src/screens/Settings.js:181 +#: src/screens/Settings.js:188 msgid "Network Settings" msgstr "" @@ -790,6 +819,19 @@ msgstr "Я хочу отменить регистрацию токена **${ to msgid "Unregister token" msgstr "Отменить регистрацию токена" +#: src/screens/WalletConnect/CreateTokenScreen.js:25 +msgid "Create Token Request" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:48 +#: src/screens/WalletConnect/NewNanoContractTransactionScreen.js:24 +msgid "New Nano Contract Transaction" +msgstr "" + +#: src/screens/WalletConnect/SignMessageRequestScreen.js:25 +msgid "Sign Message Request" +msgstr "" + #: src/screens/WalletConnect/WalletConnectList.js:33 msgid "There was an error connecting. Please try again later." msgstr "" @@ -846,17 +888,17 @@ msgid "" msgstr "" #: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:20 -#: src/screens/NetworkSettings/helper.js:4 +#: src/screens/NetworkSettings/helper.js:11 msgid "Updating custom network settings..." msgstr "" #: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:21 -#: src/screens/NetworkSettings/helper.js:5 +#: src/screens/NetworkSettings/helper.js:12 msgid "Network settings successfully customized." msgstr "" #: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:22 -#: src/screens/NetworkSettings/helper.js:6 +#: src/screens/NetworkSettings/helper.js:13 msgid "" "There was an error while customizing network settings. Please try again " "later." @@ -886,27 +928,27 @@ msgstr "" msgid "walletServiceWsUrl is required when walletServiceUrl is filled." msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:229 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:232 msgid "Node URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:239 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:242 msgid "Explorer URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:248 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:251 msgid "Explorer Service URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:257 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:260 msgid "Transaction Mining Service URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:268 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:271 msgid "Wallet Service URL (optional)" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:277 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:280 msgid "Wallet Service WS URL (optional)" msgstr "" @@ -929,71 +971,197 @@ msgid "" "this can potentially make you susceptible to fraudulent schemes." msgstr "" -#: src/sagas/networkSettings.js:88 +#: src/screens/NanoContract/NanoContractDetailsScreen.js:39 +msgid "Nano Contract Details" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:58 +msgid "Nano Contract ID is required." +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:158 +msgid "See contract" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:176 +msgid "Load First Addresses Error" +msgstr "" + +#: src/components/NanoContract/NanoContractDetails.js:202 +#: src/components/NanoContract/SelectAddressModal.js:105 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:215 +#: src/screens/NanoContract/NanoContractRegisterScreen.js:184 +msgid "Loading" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:185 +msgid "Loading first wallet address." +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:142 +#: src/components/NanoContract/NanoContractTransactionHeader.js:85 +#: src/components/NanoContract/NanoContractsListItem.js:57 +#: src/components/TxDetailsModal.js:106 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:83 +#: src/screens/NanoContract/NanoContractRegisterScreen.js:194 +msgid "Nano Contract ID" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:202 +msgid "Wallet Address" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:212 +msgid "If you want to change the wallet address, you will be able to do" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:214 +msgid "after the contract is registered." +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:233 +msgid "Register Nano Contract" +msgstr "" + +#: src/screens/NanoContract/NanoContractTransactionScreen.js:39 +msgid "Nano Contract Transaction" +msgstr "" + +#: src/screens/NanoContract/helper.js:11 +msgid "Contract successfully registered." +msgstr "" + +#: src/sagas/nanoContract.js:45 +msgid "Nano Contract already registered." +msgstr "" + +#: src/sagas/nanoContract.js:46 +msgid "Wallet is not ready yet to register a Nano Contract." +msgstr "" + +#: src/sagas/nanoContract.js:47 +msgid "The informed address does not belong to the wallet." +msgstr "" + +#: src/sagas/nanoContract.js:48 +msgid "Nano Contract not found." +msgstr "" + +#: src/sagas/nanoContract.js:49 +msgid "Error while trying to register Nano Contract." +msgstr "" + +#: src/sagas/nanoContract.js:50 +msgid "Invalid transaction to register as Nano Contract." +msgstr "" + +#: src/sagas/nanoContract.js:51 +msgid "Blueprint not found." +msgstr "" + +#: src/sagas/nanoContract.js:52 +msgid "Couldn't get Blueprint info." +msgstr "" + +#: src/sagas/nanoContract.js:53 +msgid "Nano Contract not registered." +msgstr "" + +#: src/sagas/nanoContract.js:54 +msgid "Error while trying to download Nano Contract transactions history." +msgstr "" + +#: src/sagas/networkSettings.js:85 msgid "Custom Network Settings cannot be empty." msgstr "" -#: src/sagas/networkSettings.js:95 +#: src/sagas/networkSettings.js:92 msgid "explorerUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:102 +#: src/sagas/networkSettings.js:99 msgid "explorerServiceUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:109 +#: src/sagas/networkSettings.js:106 msgid "txMiningServiceUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:116 +#: src/sagas/networkSettings.js:113 msgid "nodeUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:123 +#: src/sagas/networkSettings.js:120 msgid "walletServiceUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:130 +#: src/sagas/networkSettings.js:127 msgid "walletServiceWsUrl should be a valid URL." msgstr "" #. If we fall into this situation, the app should be killed #. for the custom new network settings take effect. -#: src/sagas/networkSettings.js:290 +#: src/sagas/networkSettings.js:279 msgid "Wallet not found while trying to persist the custom network settings." msgstr "" -#: src/sagas/pushNotification.js:59 +#: src/sagas/pushNotification.js:71 msgid "Transaction" -msgstr "Нет транзакций" +msgstr "" -#: src/sagas/pushNotification.js:60 +#: src/sagas/pushNotification.js:72 msgid "Open" msgstr "Открыть" -#: src/components/AskForPushNotification.js:22 +#: src/sagas/tokens.js:40 +msgid "Wallet is not ready yet." +msgstr "" + +#: src/sagas/tokens.js:41 +msgid "Error loading the details of some tokens." +msgstr "" + +#: src/sagas/wallet.js:780 +msgid "Wallet is not ready to load addresses." +msgstr "" + +#. This will show the message in the feedback content at SelectAddressModal +#: src/sagas/wallet.js:796 +msgid "There was an error while loading wallet addresses. Try again." +msgstr "" + +#: src/sagas/wallet.js:806 +msgid "Wallet is not ready to load the first address." +msgstr "" + +#. This will show the message in the feedback content +#: src/sagas/wallet.js:822 +msgid "There was an error while loading first wallet address. Try again." +msgstr "" + +#: src/components/AskForPushNotification.js:29 msgid "Do you want to enable push notifications for this wallet?" msgstr "" -#: src/components/AskForPushNotification.js:23 +#: src/components/AskForPushNotification.js:30 msgid "You can always change this later in the settings menu" msgstr "" -#: src/components/AskForPushNotification.js:24 +#: src/components/AskForPushNotification.js:31 msgid "Yes, enable" msgstr "" -#: src/components/AskForPushNotificationRefresh.js:28 +#: src/components/AskForPushNotificationRefresh.js:35 msgid "Refresh your push notification registration" msgstr "" -#: src/components/AskForPushNotificationRefresh.js:29 +#: src/components/AskForPushNotificationRefresh.js:36 msgid "" "In order to keep receiving push notifications, you need to refresh your " "registration. Do you want to do it now?" msgstr "" -#: src/components/AskForPushNotificationRefresh.js:30 +#: src/components/AskForPushNotificationRefresh.js:37 msgid "Refresh" msgstr "" @@ -1056,21 +1224,21 @@ msgstr "Создать запрос на получение средств" msgid "No internet connection" msgstr "Нет соединения с интернетом" -#: src/components/PublicExplorerListButton.js:19 +#: src/components/PublicExplorerListButton.js:17 msgid "Public Explorer" msgstr "Public Explorer" -#: src/components/PushTxDetailsModal.js:69 src/components/TxDetailsModal.js:61 +#: src/components/PushTxDetailsModal.js:76 src/components/TxDetailsModal.js:103 msgid "Date & Time" msgstr "Дата и Время" -#: src/components/PushTxDetailsModal.js:70 src/components/TxDetailsModal.js:62 +#: src/components/PushTxDetailsModal.js:77 msgid "ID" msgstr "ID" -#: src/components/PushTxDetailsModal.js:94 +#: src/components/PushTxDetailsModal.js:101 msgid "New Transaction" -msgstr "Нет транзакций" +msgstr "" #: src/components/ReceiveMyAddress.js:34 #, javascript-format @@ -1089,16 +1257,16 @@ msgstr "Поделиться" msgid "Propagating transaction to the network." msgstr "" -#: src/components/ShowPushNotificationTxDetails.js:8 +#: src/components/ShowPushNotificationTxDetails.js:16 msgid "Transation not found" -msgstr "Нет транзакций" +msgstr "" -#: src/components/ShowPushNotificationTxDetails.js:9 +#: src/components/ShowPushNotificationTxDetails.js:17 msgid "" "The transaction has not arrived yet in your wallet. Do you want to retry?" msgstr "" -#: src/components/ShowPushNotificationTxDetails.js:10 +#: src/components/ShowPushNotificationTxDetails.js:18 msgid "Retry" msgstr "" @@ -1108,10 +1276,46 @@ msgid "" "Here is the configuration string of token ${ tokenLabel }: ${ configString }" msgstr "Это конфигурация вашего токена ${ tokenLabel }: ${ configString }" -#: src/components/TxDetailsModal.js:60 +#: src/components/TransactionStatusLabel.js:71 +msgid "Executed" +msgstr "" + +#: src/components/TransactionStatusLabel.js:76 +msgid "Processing" +msgstr "" + +#: src/components/TransactionStatusLabel.js:81 +msgid "Voided" +msgstr "" + +#: src/components/TxDetailsModal.js:102 msgid "Description" msgstr "Описание" +#: src/components/NanoContract/NanoContractTransactionHeader.js:44 +#: src/components/TxDetailsModal.js:104 +msgid "Transaction ID" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:89 +#: src/components/TxDetailsModal.js:105 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:116 +msgid "Blueprint Method" +msgstr "" + +#: src/components/TxDetailsModal.js:107 +msgid "Nano Contract Caller" +msgstr "" + +#: src/components/TxDetailsModal.js:111 +msgid "Nano Contract Status" +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:79 +#: src/components/TxDetailsModal.js:120 +msgid "Nano Contract" +msgstr "" + #: src/components/WalletConnect/ApproveRejectModal.js:63 msgid "Reject" msgstr "" @@ -1131,19 +1335,397 @@ msgid "" "security step to protect your data from potential phishing risks." msgstr "" -#: src/components/WalletConnect/SignMessageModal.js:50 -msgid "Sign this message?" +#: src/components/WalletConnect/CreateTokenModal.js:44 +msgid "New Create Token Request" +msgstr "" + +#: src/components/WalletConnect/CreateTokenModal.js:48 +msgid "You have received a new Create Token Request. Please" +msgstr "" + +#: src/components/WalletConnect/CreateTokenModal.js:50 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:54 +#: src/components/WalletConnect/SignMessageModal.js:62 +msgid "carefully review the details" +msgstr "" + +#: src/components/WalletConnect/CreateTokenModal.js:52 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:56 +#: src/components/WalletConnect/SignMessageModal.js:64 +msgid "before deciding to accept or decline." +msgstr "" + +#: src/components/WalletConnect/CreateTokenModal.js:56 +msgid "Review Create Token Request details" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:74 +msgid "Yes" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:74 +msgid "No" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:80 +msgid "Name" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:81 +msgid "Symbol" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:83 +#, javascript-format +msgid "Address to send newly minted ${ data.symbol }" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:84 +msgid "Address to send change ${ DEFAULT_TOKEN.uid }" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:85 +msgid "Create mint authority?" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:86 +msgid "Create melt authority?" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:87 +msgid "Address to send the mint authority" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:88 +msgid "Address to send the melt authority" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:92 +msgid "Allow external mint authority addresses?" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:99 +msgid "Allow external melt authority addresses?" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:103 +msgid "Token data" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:158 +#: src/components/WalletConnect/SignMessageRequest.js:81 +msgid "Accept Request" msgstr "" -#: src/components/WalletConnect/SignMessageModal.js:57 +#: src/components/WalletConnect/CreateTokenRequest.js:162 +#: src/components/WalletConnect/SignMessageRequest.js:85 +msgid "Decline Request" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:172 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:258 +msgid "Sending transaction" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:173 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:259 +msgid "Please wait." +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:186 +msgid "Create Token Transaction successfully sent." +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:188 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:287 +msgid "Ok, close" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:195 +msgid "Error while sending create token transaction." +msgstr "" + +#: src/components/WalletConnect/SignMessageModal.js:56 +msgid "New Sign Message Request" +msgstr "" + +#: src/components/WalletConnect/SignMessageModal.js:60 +msgid "You have received a new Sign Message Request. Please" +msgstr "" + +#: src/components/WalletConnect/SignMessageModal.js:68 +msgid "Review Sign Message Request details" +msgstr "" + +#: src/components/WalletConnect/SignMessageRequest.js:35 +msgid "Message to sign" +msgstr "" + +#: src/components/WalletConnect/SignMessageRequest.js:45 +msgid "Address Path" +msgstr "" + +#: src/components/WalletConnect/WarnDisclaimer.js:27 +msgid "" +"Caution: There are risks associated with signing dapp transaction requests." +msgstr "" + +#: src/components/WalletConnect/WarnDisclaimer.js:33 +msgid "Read More." +msgstr "" + +#: src/components/WalletConnect/NanoContract/DappContainer.js:41 +msgid "Review your transaction from this dApp" +msgstr "" + +#: src/components/WalletConnect/NanoContract/DappContainer.js:44 +msgid "Stay vigilant and protect your data from potential phishing attempts." +msgstr "" + +#: src/components/WalletConnect/NanoContract/DeclineModal.js:26 +msgid "Decline transaction" +msgstr "" + +#: src/components/WalletConnect/NanoContract/DeclineModal.js:29 +msgid "Are you sure you want to decline this transaction?" +msgstr "" + +#: src/components/WalletConnect/NanoContract/DeclineModal.js:33 +msgid "Yes, decline transaction" +msgstr "" + +#: src/components/NanoContract/UnregisterNanoContractModal.js:50 +#: src/components/WalletConnect/NanoContract/DeclineModal.js:39 +msgid "No, go back" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:36 +#, javascript-format +msgid "${ tokenSymbol } Deposit" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:37 +msgid "${ tokenSymbol } Withdrawal" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:101 +msgid "Action List" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:150 +msgid "To Address:" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:88 +msgid "Blueprint ID" +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:146 +#: src/components/NanoContract/NanoContractsListItem.js:59 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:93 +msgid "Blueprint Name" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:109 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:140 +msgid "Loading..." +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:103 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:124 +msgid "Caller" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:143 +msgid "Couldn't determine address, select one" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractMethodArgs.js:51 +#, javascript-format +msgid "Position ${ idx }" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractMethodArgs.js:100 +msgid "Arguments" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractMethodArgs.js:105 +msgid "Loading arguments." +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:52 +msgid "You have received a new Nano Contract Transaction. Please" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:60 +msgid "Review transaction details" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:197 +msgid "Nano Contract Not Found" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:198 msgid "" -"By clicking approve, you will sign the requested message using the first " -"address derived from your root key on the m/44'/280'/0'/0/0 derivation path." +"The Nano Contract requested is not registered. First register the Nano " +"Contract to interact with it." +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:201 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:248 +msgid "Decline Transaction" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:216 +msgid "Loading transaction information." +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:244 +msgid "Accept Transaction" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:285 +msgid "Transaction successfully sent." +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:293 +msgid "Error while sending transaction." msgstr "" #: src/components/NetworkSettings/NetworkStatusBar.js:14 msgid "Custom network" msgstr "" -#~ msgid "I understand the risks of using a mobile wallet" -#~ msgstr "Я осознаю риски использования мобильного кошелька" +#: src/components/NanoContract/EditAddressModal.js:41 +msgid "New Nano Contract Address" +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:49 +msgid "" +"This address signs any transaction you create with Nano Contracts method. " +"Switching to a new one means" +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:51 +msgid "all future transactions will use this address." +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:57 +msgid "Selected Information" +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:67 +msgid "Index" +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:74 +msgid "Confirm new address" +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:78 +msgid "Go back" +msgstr "" + +#: src/components/NanoContract/NanoContractDetails.js:177 +msgid "Load More" +msgstr "" + +#: src/components/NanoContract/NanoContractDetails.js:203 +msgid "Loading Nano Contract transactions." +msgstr "" + +#: src/components/NanoContract/NanoContractDetails.js:217 +msgid "Nano Contract Transactions Error" +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:150 +msgid "Registered Address" +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:153 +msgid "See status details" +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:154 +msgid "Unregister contract" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionActionList.js:44 +msgid "No Actions" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionActionList.js:45 +msgid "See full transaction details on Public Explorer." +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionActionListItem.js:108 +#, javascript-format +msgid "Deposit ${ tokenSymbol }" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionActionListItem.js:109 +msgid "Withdrawal ${ tokenSymbol }" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:93 +msgid "Date and Time" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:100 +#: src/components/NanoContract/NanoContractTransactionsListItem.js:62 +msgid "From this wallet" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:106 +msgid "See transaction details" +msgstr "" + +#: src/components/NanoContract/NanoContractsList.js:85 +msgid "No Nano Contracts" +msgstr "" + +#: src/components/NanoContract/NanoContractsList.js:86 +msgid "" +"You can keep track of your registered Nano Contracts here once you have " +"registered them." +msgstr "" + +#: src/components/NanoContract/RegisterNewNanoContractButton.js:22 +msgid "Register new" +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:90 +msgid "Choose New Wallet Address" +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:97 +msgid "Load Addresses Error" +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:106 +msgid "Loading wallet addresses." +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:114 +msgid "Current Information" +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:115 +msgid "To change, select other address on the list below." +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:178 +msgid "index" +msgstr "" + +#: src/components/NanoContract/UnregisterNanoContractModal.js:39 +msgid "Unregister Nano Contract" +msgstr "" + +#: src/components/NanoContract/UnregisterNanoContractModal.js:41 +msgid "Are you sure you want to unregister this Nano Contract?" +msgstr "" + +#: src/components/NanoContract/UnregisterNanoContractModal.js:44 +msgid "Yes, unregister contract" +msgstr "" diff --git a/locale/texts.pot b/locale/texts.pot index 497ab9564..72c4316a2 100644 --- a/locale/texts.pot +++ b/locale/texts.pot @@ -3,56 +3,57 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Plural-Forms: nplurals=2; plural=(n!=1);\n" -#: src/models.js:24 +#: src/models.js:85 #. This should never happen! msgid "Unknown" msgstr "" -#: src/models.js:27 +#: src/models.js:88 #, javascript-format msgid "Received ${ symbol }" msgstr "" -#: src/models.js:29 +#: src/models.js:90 msgid "Sent ${ symbol }" msgstr "" -#: src/models.js:31 +#: src/models.js:92 #, javascript-format msgid "You sent ${ symbol } to yourself" msgstr "" -#: src/models.js:41 +#: src/models.js:102 #. See https://momentjs.com/docs/#/displaying/calendar-time/ msgid "[Today •] HH:mm" msgstr "" -#: src/models.js:42 +#: src/models.js:103 #. See https://momentjs.com/docs/#/displaying/calendar-time/ msgid "[Tomorrow •] HH:mm" msgstr "" -#: src/models.js:43 +#: src/models.js:104 #. See https://momentjs.com/docs/#/displaying/calendar-time/ msgid "dddd [•] HH:mm" msgstr "" -#: src/models.js:44 +#: src/models.js:105 #. See https://momentjs.com/docs/#/displaying/calendar-time/ msgid "[Yesterday •] HH:mm" msgstr "" -#: src/models.js:45 +#: src/models.js:106 #. See https://momentjs.com/docs/#/displaying/calendar-time/ msgid "[Last] dddd [•] HH:mm" msgstr "" -#: src/models.js:46 +#: src/models.js:107 +#: src/utils.js:429 #. See https://momentjs.com/docs/#/displaying/calendar-time/ msgid "DD MMM YYYY [•] HH:mm" msgstr "" -#: src/utils.js:146 +#: src/utils.js:164 msgid "Invalid address" msgstr "" @@ -222,7 +223,7 @@ msgstr "" #: src/screens/CreateTokenName.js:64 #: src/screens/CreateTokenSymbol.js:84 #: src/screens/InitWallet.js:220 -#: src/screens/InitWallet.js:341 +#: src/screens/InitWallet.js:349 #: src/screens/SendAddressInput.js:66 #: src/screens/SendAmountInput.js:185 msgid "Next" @@ -328,13 +329,21 @@ msgstr "" msgid "E.g. HTR" msgstr "" -#: src/screens/Dashboard.js:67 -#: src/screens/RegisterTokenManual.js:147 -msgid "Register token" +#: src/screens/Dashboard.js:130 +#: src/screens/Dashboard.js:187 +#. Only show the toggle button when Nano Contract is enabled to the wallet +msgid "Tokens" msgstr "" -#: src/screens/Dashboard.js:74 -msgid "TOKENS" +#: src/components/NanoContract/NanoContractsList.js:75 +#: src/screens/Dashboard.js:131 +#. Only show the toggle button when Nano Contract is enabled to the wallet +msgid "Nano Contracts" +msgstr "" + +#: src/screens/Dashboard.js:178 +#: src/screens/RegisterTokenManual.js:147 +msgid "Register token" msgstr "" #: src/screens/InitWallet.js:61 @@ -412,8 +421,12 @@ msgstr "" msgid "Enter your seed words separated by space" msgstr "" +#: src/components/NanoContract/NanoContractDetails.js:238 +#: src/components/WalletConnect/CreateTokenRequest.js:197 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:295 #: src/screens/LoadHistoryScreen.js:51 #: src/screens/LoadWalletErrorScreen.js:20 +#: src/screens/NanoContract/NanoContractRegisterScreen.js:168 msgid "Try again" msgstr "" @@ -437,7 +450,7 @@ msgstr "" #: src/screens/LoadWalletErrorScreen.js:21 #: src/screens/PinScreen.js:268 -#: src/screens/Settings.js:154 +#: src/screens/Settings.js:161 msgid "Reset wallet" msgstr "" @@ -461,15 +474,32 @@ msgstr "" msgid "Please |tryAgain:try again|" msgstr "" -#: src/screens/MainScreen.js:560 -#: src/screens/MainScreen.js:592 +#: src/screens/MainScreen.js:535 +#: src/screens/MainScreen.js:567 msgid "Available Balance" msgstr "" -#: src/screens/MainScreen.js:569 +#: src/screens/MainScreen.js:544 msgid "Locked" msgstr "" +#: src/screens/NanoContractRegisterQrCodeScreen.js:26 +#: src/screens/RegisterToken.js:27 +#: src/screens/SendScanQRCode.js:87 +#. translator: Used when the QR Code Scanner is opened, and user will manually +#. enter the information. +msgid "Manual info" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:251 +#: src/screens/NanoContractRegisterQrCodeScreen.js:48 +msgid "Nano Contract Registration" +msgstr "" + +#: src/screens/NanoContractRegisterQrCodeScreen.js:63 +msgid "Scan the nano contract ID QR code" +msgstr "" + #: src/screens/PaymentRequestDetail.js:88 #, javascript-format msgid "You've just received **${ amount } ${ symbol }**" @@ -479,12 +509,13 @@ msgstr "" msgid "PAYMENT REQUEST" msgstr "" -#: src/components/TxDetailsModal.js:59 +#: src/components/TxDetailsModal.js:101 #: src/screens/PaymentRequestDetail.js:123 msgid "Token" msgstr "" -#: src/components/TxDetailsModal.js:106 +#: src/components/TxDetailsModal.js:166 +#: src/components/WalletConnect/CreateTokenRequest.js:82 #: src/screens/PaymentRequestDetail.js:127 msgid "Amount" msgstr "" @@ -518,13 +549,16 @@ msgstr "" msgid "Unlock Hathor Wallet" msgstr "" +#: src/components/WalletConnect/CreateTokenModal.js:60 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:64 +#: src/components/WalletConnect/SignMessageModal.js:72 #: src/screens/PinScreen.js:265 #: src/screens/WalletConnect/WalletConnectList.js:125 msgid "Cancel" msgstr "" #: src/screens/PushNotification.js:58 -#: src/screens/Settings.js:128 +#: src/screens/Settings.js:135 msgid "Push Notification" msgstr "" @@ -557,13 +591,6 @@ msgstr "" msgid "RECEIVE" msgstr "" -#: src/screens/RegisterToken.js:27 -#: src/screens/SendScanQRCode.js:89 -#. translator: Used when the QR Code Scanner is opened, and user will manually -#. enter the information. -msgid "Manual info" -msgstr "" - #: src/screens/RegisterToken.js:42 #: src/screens/RegisterTokenManual.js:126 msgid "REGISTER TOKEN" @@ -641,7 +668,7 @@ msgid "Lock wallet" msgstr "" #: src/screens/SendAddressInput.js:53 -#: src/screens/SendScanQRCode.js:97 +#: src/screens/SendScanQRCode.js:95 msgid "SEND" msgstr "" @@ -671,12 +698,12 @@ msgstr "" msgid "Your transfer is being processed" msgstr "" -#: src/sagas/helpers.js:136 +#: src/sagas/helpers.js:140 #: src/screens/SendConfirmScreen.js:104 msgid "Enter your 6-digit pin to authorize operation" msgstr "" -#: src/sagas/helpers.js:137 +#: src/sagas/helpers.js:141 #: src/screens/SendConfirmScreen.js:105 msgid "Authorize operation" msgstr "" @@ -685,11 +712,14 @@ msgstr "" msgid "Your transfer of **${ this.amountAndToken }** has been confirmed" msgstr "" +#: src/components/NanoContract/EditAddressModal.js:60 +#: src/components/NanoContract/SelectAddressModal.js:117 +#: src/components/WalletConnect/SignMessageRequest.js:40 #: src/screens/SendConfirmScreen.js:161 msgid "Address" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:288 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:291 #: src/screens/SendConfirmScreen.js:168 msgid "Send" msgstr "" @@ -702,53 +732,53 @@ msgstr "" msgid "OK" msgstr "" -#: src/screens/SendScanQRCode.js:73 +#: src/screens/SendScanQRCode.js:71 #, javascript-format msgid "You don't have the requested token [${ tokenLabel }]" msgstr "" -#: src/screens/SendScanQRCode.js:105 +#: src/screens/SendScanQRCode.js:103 #: src/screens/WalletConnect/WalletConnectScan.js:49 msgid "Scan the QR code" msgstr "" -#: src/screens/Settings.js:97 +#: src/screens/Settings.js:104 msgid "You are connected to" msgstr "" -#: src/screens/Settings.js:105 +#: src/screens/Settings.js:112 msgid "General Settings" msgstr "" -#: src/screens/Settings.js:109 +#: src/screens/Settings.js:116 msgid "Connected to" msgstr "" -#: src/screens/Settings.js:122 +#: src/screens/Settings.js:129 msgid "Security" msgstr "" -#: src/screens/Settings.js:135 +#: src/screens/Settings.js:142 msgid "Create a new token" msgstr "" -#: src/screens/Settings.js:142 +#: src/screens/Settings.js:149 msgid "Register a token" msgstr "" -#: src/screens/Settings.js:158 +#: src/screens/Settings.js:165 msgid "About" msgstr "" -#: src/screens/Settings.js:165 +#: src/screens/Settings.js:172 msgid "Unique app identifier" msgstr "" -#: src/screens/Settings.js:179 +#: src/screens/Settings.js:186 msgid "Developer Settings" msgstr "" -#: src/screens/Settings.js:181 +#: src/screens/Settings.js:188 msgid "Network Settings" msgstr "" @@ -781,6 +811,19 @@ msgstr "" msgid "Unregister token" msgstr "" +#: src/screens/WalletConnect/CreateTokenScreen.js:25 +msgid "Create Token Request" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:48 +#: src/screens/WalletConnect/NewNanoContractTransactionScreen.js:24 +msgid "New Nano Contract Transaction" +msgstr "" + +#: src/screens/WalletConnect/SignMessageRequestScreen.js:25 +msgid "Sign Message Request" +msgstr "" + #: src/screens/WalletConnect/WalletConnectList.js:33 msgid "There was an error connecting. Please try again later." msgstr "" @@ -837,17 +880,17 @@ msgid "" msgstr "" #: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:20 -#: src/screens/NetworkSettings/helper.js:4 +#: src/screens/NetworkSettings/helper.js:11 msgid "Updating custom network settings..." msgstr "" #: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:21 -#: src/screens/NetworkSettings/helper.js:5 +#: src/screens/NetworkSettings/helper.js:12 msgid "Network settings successfully customized." msgstr "" #: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:22 -#: src/screens/NetworkSettings/helper.js:6 +#: src/screens/NetworkSettings/helper.js:13 msgid "" "There was an error while customizing network settings. Please try again " "later." @@ -877,27 +920,27 @@ msgstr "" msgid "walletServiceWsUrl is required when walletServiceUrl is filled." msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:229 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:232 msgid "Node URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:239 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:242 msgid "Explorer URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:248 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:251 msgid "Explorer Service URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:257 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:260 msgid "Transaction Mining Service URL" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:268 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:271 msgid "Wallet Service URL (optional)" msgstr "" -#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:277 +#: src/screens/NetworkSettings/CustomNetworkSettingsScreen.js:280 msgid "Wallet Service WS URL (optional)" msgstr "" @@ -920,71 +963,197 @@ msgid "" "this can potentially make you susceptible to fraudulent schemes." msgstr "" -#: src/sagas/networkSettings.js:88 +#: src/screens/NanoContract/NanoContractDetailsScreen.js:39 +msgid "Nano Contract Details" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:58 +msgid "Nano Contract ID is required." +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:158 +msgid "See contract" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:176 +msgid "Load First Addresses Error" +msgstr "" + +#: src/components/NanoContract/NanoContractDetails.js:202 +#: src/components/NanoContract/SelectAddressModal.js:105 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:215 +#: src/screens/NanoContract/NanoContractRegisterScreen.js:184 +msgid "Loading" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:185 +msgid "Loading first wallet address." +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:142 +#: src/components/NanoContract/NanoContractTransactionHeader.js:85 +#: src/components/NanoContract/NanoContractsListItem.js:57 +#: src/components/TxDetailsModal.js:106 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:83 +#: src/screens/NanoContract/NanoContractRegisterScreen.js:194 +msgid "Nano Contract ID" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:202 +msgid "Wallet Address" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:212 +msgid "If you want to change the wallet address, you will be able to do" +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:214 +msgid "after the contract is registered." +msgstr "" + +#: src/screens/NanoContract/NanoContractRegisterScreen.js:233 +msgid "Register Nano Contract" +msgstr "" + +#: src/screens/NanoContract/NanoContractTransactionScreen.js:39 +msgid "Nano Contract Transaction" +msgstr "" + +#: src/screens/NanoContract/helper.js:11 +msgid "Contract successfully registered." +msgstr "" + +#: src/sagas/nanoContract.js:45 +msgid "Nano Contract already registered." +msgstr "" + +#: src/sagas/nanoContract.js:46 +msgid "Wallet is not ready yet to register a Nano Contract." +msgstr "" + +#: src/sagas/nanoContract.js:47 +msgid "The informed address does not belong to the wallet." +msgstr "" + +#: src/sagas/nanoContract.js:48 +msgid "Nano Contract not found." +msgstr "" + +#: src/sagas/nanoContract.js:49 +msgid "Error while trying to register Nano Contract." +msgstr "" + +#: src/sagas/nanoContract.js:50 +msgid "Invalid transaction to register as Nano Contract." +msgstr "" + +#: src/sagas/nanoContract.js:51 +msgid "Blueprint not found." +msgstr "" + +#: src/sagas/nanoContract.js:52 +msgid "Couldn't get Blueprint info." +msgstr "" + +#: src/sagas/nanoContract.js:53 +msgid "Nano Contract not registered." +msgstr "" + +#: src/sagas/nanoContract.js:54 +msgid "Error while trying to download Nano Contract transactions history." +msgstr "" + +#: src/sagas/networkSettings.js:85 msgid "Custom Network Settings cannot be empty." msgstr "" -#: src/sagas/networkSettings.js:95 +#: src/sagas/networkSettings.js:92 msgid "explorerUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:102 +#: src/sagas/networkSettings.js:99 msgid "explorerServiceUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:109 +#: src/sagas/networkSettings.js:106 msgid "txMiningServiceUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:116 +#: src/sagas/networkSettings.js:113 msgid "nodeUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:123 +#: src/sagas/networkSettings.js:120 msgid "walletServiceUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:130 +#: src/sagas/networkSettings.js:127 msgid "walletServiceWsUrl should be a valid URL." msgstr "" -#: src/sagas/networkSettings.js:290 +#: src/sagas/networkSettings.js:279 #. If we fall into this situation, the app should be killed #. for the custom new network settings take effect. msgid "Wallet not found while trying to persist the custom network settings." msgstr "" -#: src/sagas/pushNotification.js:59 +#: src/sagas/pushNotification.js:71 msgid "Transaction" msgstr "" -#: src/sagas/pushNotification.js:60 +#: src/sagas/pushNotification.js:72 msgid "Open" msgstr "" -#: src/components/AskForPushNotification.js:22 +#: src/sagas/tokens.js:40 +msgid "Wallet is not ready yet." +msgstr "" + +#: src/sagas/tokens.js:41 +msgid "Error loading the details of some tokens." +msgstr "" + +#: src/sagas/wallet.js:780 +msgid "Wallet is not ready to load addresses." +msgstr "" + +#: src/sagas/wallet.js:796 +#. This will show the message in the feedback content at SelectAddressModal +msgid "There was an error while loading wallet addresses. Try again." +msgstr "" + +#: src/sagas/wallet.js:806 +msgid "Wallet is not ready to load the first address." +msgstr "" + +#: src/sagas/wallet.js:822 +#. This will show the message in the feedback content +msgid "There was an error while loading first wallet address. Try again." +msgstr "" + +#: src/components/AskForPushNotification.js:29 msgid "Do you want to enable push notifications for this wallet?" msgstr "" -#: src/components/AskForPushNotification.js:23 +#: src/components/AskForPushNotification.js:30 msgid "You can always change this later in the settings menu" msgstr "" -#: src/components/AskForPushNotification.js:24 +#: src/components/AskForPushNotification.js:31 msgid "Yes, enable" msgstr "" -#: src/components/AskForPushNotificationRefresh.js:28 +#: src/components/AskForPushNotificationRefresh.js:35 msgid "Refresh your push notification registration" msgstr "" -#: src/components/AskForPushNotificationRefresh.js:29 +#: src/components/AskForPushNotificationRefresh.js:36 msgid "" "In order to keep receiving push notifications, you need to refresh your " "registration. Do you want to do it now?" msgstr "" -#: src/components/AskForPushNotificationRefresh.js:30 +#: src/components/AskForPushNotificationRefresh.js:37 msgid "Refresh" msgstr "" @@ -1047,21 +1216,20 @@ msgstr "" msgid "No internet connection" msgstr "" -#: src/components/PublicExplorerListButton.js:19 +#: src/components/PublicExplorerListButton.js:17 msgid "Public Explorer" msgstr "" -#: src/components/PushTxDetailsModal.js:69 -#: src/components/TxDetailsModal.js:61 +#: src/components/PushTxDetailsModal.js:76 +#: src/components/TxDetailsModal.js:103 msgid "Date & Time" msgstr "" -#: src/components/PushTxDetailsModal.js:70 -#: src/components/TxDetailsModal.js:62 +#: src/components/PushTxDetailsModal.js:77 msgid "ID" msgstr "" -#: src/components/PushTxDetailsModal.js:94 +#: src/components/PushTxDetailsModal.js:101 msgid "New Transaction" msgstr "" @@ -1083,15 +1251,15 @@ msgstr "" msgid "Propagating transaction to the network." msgstr "" -#: src/components/ShowPushNotificationTxDetails.js:8 +#: src/components/ShowPushNotificationTxDetails.js:16 msgid "Transation not found" msgstr "" -#: src/components/ShowPushNotificationTxDetails.js:9 +#: src/components/ShowPushNotificationTxDetails.js:17 msgid "The transaction has not arrived yet in your wallet. Do you want to retry?" msgstr "" -#: src/components/ShowPushNotificationTxDetails.js:10 +#: src/components/ShowPushNotificationTxDetails.js:18 msgid "Retry" msgstr "" @@ -1100,10 +1268,46 @@ msgstr "" msgid "Here is the configuration string of token ${ tokenLabel }: ${ configString }" msgstr "" -#: src/components/TxDetailsModal.js:60 +#: src/components/TransactionStatusLabel.js:71 +msgid "Executed" +msgstr "" + +#: src/components/TransactionStatusLabel.js:76 +msgid "Processing" +msgstr "" + +#: src/components/TransactionStatusLabel.js:81 +msgid "Voided" +msgstr "" + +#: src/components/TxDetailsModal.js:102 msgid "Description" msgstr "" +#: src/components/NanoContract/NanoContractTransactionHeader.js:44 +#: src/components/TxDetailsModal.js:104 +msgid "Transaction ID" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:89 +#: src/components/TxDetailsModal.js:105 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:116 +msgid "Blueprint Method" +msgstr "" + +#: src/components/TxDetailsModal.js:107 +msgid "Nano Contract Caller" +msgstr "" + +#: src/components/TxDetailsModal.js:111 +msgid "Nano Contract Status" +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:79 +#: src/components/TxDetailsModal.js:120 +msgid "Nano Contract" +msgstr "" + #: src/components/WalletConnect/ApproveRejectModal.js:63 msgid "Reject" msgstr "" @@ -1123,16 +1327,396 @@ msgid "" "security step to protect your data from potential phishing risks." msgstr "" -#: src/components/WalletConnect/SignMessageModal.js:50 -msgid "Sign this message?" +#: src/components/WalletConnect/CreateTokenModal.js:44 +msgid "New Create Token Request" +msgstr "" + +#: src/components/WalletConnect/CreateTokenModal.js:48 +msgid "You have received a new Create Token Request. Please" +msgstr "" + +#: src/components/WalletConnect/CreateTokenModal.js:50 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:54 +#: src/components/WalletConnect/SignMessageModal.js:62 +msgid "carefully review the details" +msgstr "" + +#: src/components/WalletConnect/CreateTokenModal.js:52 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:56 +#: src/components/WalletConnect/SignMessageModal.js:64 +msgid "before deciding to accept or decline." +msgstr "" + +#: src/components/WalletConnect/CreateTokenModal.js:56 +msgid "Review Create Token Request details" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:74 +msgid "Yes" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:74 +msgid "No" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:80 +msgid "Name" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:81 +msgid "Symbol" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:83 +#, javascript-format +msgid "Address to send newly minted ${ data.symbol }" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:84 +msgid "Address to send change ${ DEFAULT_TOKEN.uid }" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:85 +msgid "Create mint authority?" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:86 +msgid "Create melt authority?" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:87 +msgid "Address to send the mint authority" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:88 +msgid "Address to send the melt authority" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:92 +msgid "Allow external mint authority addresses?" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:99 +msgid "Allow external melt authority addresses?" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:103 +msgid "Token data" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:158 +#: src/components/WalletConnect/SignMessageRequest.js:81 +msgid "Accept Request" msgstr "" -#: src/components/WalletConnect/SignMessageModal.js:57 +#: src/components/WalletConnect/CreateTokenRequest.js:162 +#: src/components/WalletConnect/SignMessageRequest.js:85 +msgid "Decline Request" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:172 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:258 +msgid "Sending transaction" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:173 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:259 +msgid "Please wait." +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:186 +msgid "Create Token Transaction successfully sent." +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:188 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:287 +msgid "Ok, close" +msgstr "" + +#: src/components/WalletConnect/CreateTokenRequest.js:195 +msgid "Error while sending create token transaction." +msgstr "" + +#: src/components/WalletConnect/SignMessageModal.js:56 +msgid "New Sign Message Request" +msgstr "" + +#: src/components/WalletConnect/SignMessageModal.js:60 +msgid "You have received a new Sign Message Request. Please" +msgstr "" + +#: src/components/WalletConnect/SignMessageModal.js:68 +msgid "Review Sign Message Request details" +msgstr "" + +#: src/components/WalletConnect/SignMessageRequest.js:35 +msgid "Message to sign" +msgstr "" + +#: src/components/WalletConnect/SignMessageRequest.js:45 +msgid "Address Path" +msgstr "" + +#: src/components/WalletConnect/WarnDisclaimer.js:27 +msgid "Caution: There are risks associated with signing dapp transaction requests." +msgstr "" + +#: src/components/WalletConnect/WarnDisclaimer.js:33 +msgid "Read More." +msgstr "" + +#: src/components/WalletConnect/NanoContract/DappContainer.js:41 +msgid "Review your transaction from this dApp" +msgstr "" + +#: src/components/WalletConnect/NanoContract/DappContainer.js:44 +msgid "Stay vigilant and protect your data from potential phishing attempts." +msgstr "" + +#: src/components/WalletConnect/NanoContract/DeclineModal.js:26 +msgid "Decline transaction" +msgstr "" + +#: src/components/WalletConnect/NanoContract/DeclineModal.js:29 +msgid "Are you sure you want to decline this transaction?" +msgstr "" + +#: src/components/WalletConnect/NanoContract/DeclineModal.js:33 +msgid "Yes, decline transaction" +msgstr "" + +#: src/components/NanoContract/UnregisterNanoContractModal.js:50 +#: src/components/WalletConnect/NanoContract/DeclineModal.js:39 +msgid "No, go back" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:36 +#, javascript-format +msgid "${ tokenSymbol } Deposit" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:37 +msgid "${ tokenSymbol } Withdrawal" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:101 +msgid "Action List" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractActions.js:150 +msgid "To Address:" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:88 +msgid "Blueprint ID" +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:146 +#: src/components/NanoContract/NanoContractsListItem.js:59 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:93 +msgid "Blueprint Name" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:109 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:140 +msgid "Loading..." +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:103 +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:124 +msgid "Caller" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractExecInfo.js:143 +msgid "Couldn't determine address, select one" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractMethodArgs.js:51 +#, javascript-format +msgid "Position ${ idx }" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractMethodArgs.js:100 +msgid "Arguments" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NanoContractMethodArgs.js:105 +msgid "Loading arguments." +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:52 +msgid "You have received a new Nano Contract Transaction. Please" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js:60 +msgid "Review transaction details" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:197 +msgid "Nano Contract Not Found" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:198 msgid "" -"By clicking approve, you will sign the requested message using the first " -"address derived from your root key on the m/44'/280'/0'/0/0 derivation path." +"The Nano Contract requested is not registered. First register the Nano " +"Contract to interact with it." +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:201 +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:248 +msgid "Decline Transaction" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:216 +msgid "Loading transaction information." +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:244 +msgid "Accept Transaction" +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:285 +msgid "Transaction successfully sent." +msgstr "" + +#: src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js:293 +msgid "Error while sending transaction." msgstr "" #: src/components/NetworkSettings/NetworkStatusBar.js:14 msgid "Custom network" msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:41 +msgid "New Nano Contract Address" +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:49 +msgid "" +"This address signs any transaction you create with Nano Contracts method. " +"Switching to a new one means" +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:51 +msgid "all future transactions will use this address." +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:57 +msgid "Selected Information" +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:67 +msgid "Index" +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:74 +msgid "Confirm new address" +msgstr "" + +#: src/components/NanoContract/EditAddressModal.js:78 +msgid "Go back" +msgstr "" + +#: src/components/NanoContract/NanoContractDetails.js:177 +msgid "Load More" +msgstr "" + +#: src/components/NanoContract/NanoContractDetails.js:203 +msgid "Loading Nano Contract transactions." +msgstr "" + +#: src/components/NanoContract/NanoContractDetails.js:217 +msgid "Nano Contract Transactions Error" +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:150 +msgid "Registered Address" +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:153 +msgid "See status details" +msgstr "" + +#: src/components/NanoContract/NanoContractDetailsHeader.js:154 +msgid "Unregister contract" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionActionList.js:44 +msgid "No Actions" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionActionList.js:45 +msgid "See full transaction details on Public Explorer." +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionActionListItem.js:108 +#, javascript-format +msgid "Deposit ${ tokenSymbol }" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionActionListItem.js:109 +msgid "Withdrawal ${ tokenSymbol }" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:93 +msgid "Date and Time" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:100 +#: src/components/NanoContract/NanoContractTransactionsListItem.js:62 +msgid "From this wallet" +msgstr "" + +#: src/components/NanoContract/NanoContractTransactionHeader.js:106 +msgid "See transaction details" +msgstr "" + +#: src/components/NanoContract/NanoContractsList.js:85 +msgid "No Nano Contracts" +msgstr "" + +#: src/components/NanoContract/NanoContractsList.js:86 +msgid "" +"You can keep track of your registered Nano Contracts here once you have " +"registered them." +msgstr "" + +#: src/components/NanoContract/RegisterNewNanoContractButton.js:22 +msgid "Register new" +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:90 +msgid "Choose New Wallet Address" +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:97 +msgid "Load Addresses Error" +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:106 +msgid "Loading wallet addresses." +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:114 +msgid "Current Information" +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:115 +msgid "To change, select other address on the list below." +msgstr "" + +#: src/components/NanoContract/SelectAddressModal.js:178 +msgid "index" +msgstr "" + +#: src/components/NanoContract/UnregisterNanoContractModal.js:39 +msgid "Unregister Nano Contract" +msgstr "" + +#: src/components/NanoContract/UnregisterNanoContractModal.js:41 +msgid "Are you sure you want to unregister this Nano Contract?" +msgstr "" + +#: src/components/NanoContract/UnregisterNanoContractModal.js:44 +msgid "Yes, unregister contract" +msgstr "" diff --git a/package-lock.json b/package-lock.json index af6cf78e8..becca3ca1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,21 @@ { "name": "HathorMobile", - "version": "0.27.2", + "version": "0.28.0-rc.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "HathorMobile", - "version": "0.27.2", + "version": "0.28.0-rc.3", "hasInstallScript": true, "dependencies": { "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-regular-svg-icons": "6.4.0", "@fortawesome/free-solid-svg-icons": "6.4.0", "@fortawesome/react-native-fontawesome": "0.2.7", + "@hathor/hathor-rpc-handler": "0.0.1-experimental-alpha", "@hathor/unleash-client": "0.1.0", - "@hathor/wallet-lib": "1.0.1", + "@hathor/wallet-lib": "1.10.0", "@notifee/react-native": "5.7.0", "@react-native-async-storage/async-storage": "1.19.0", "@react-native-firebase/app": "16.7.0", @@ -22,7 +23,7 @@ "@react-navigation/bottom-tabs": "6.5.8", "@react-navigation/native": "6.1.7", "@react-navigation/stack": "6.3.17", - "@sentry/react-native": "5.6.0", + "@sentry/react-native": "5.31.0", "assert": "2.0.0", "buffer": "4.9.2", "console-browserify": "1.2.0", @@ -2541,6 +2542,86 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@hathor/hathor-rpc-handler": { + "version": "0.0.1-experimental-alpha", + "resolved": "https://registry.npmjs.org/@hathor/hathor-rpc-handler/-/hathor-rpc-handler-0.0.1-experimental-alpha.tgz", + "integrity": "sha512-/8zz7HUFYOYd1k0ZIyJMyNBB1mdhZ0VoU09yAVkYFtJFJlScCPD8gNvlWmtjC8CdBnQAAk7ozKW4k0UbYd6/zQ==", + "dependencies": { + "@hathor/wallet-lib": "1.8.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@hathor/hathor-rpc-handler/node_modules/@hathor/wallet-lib": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@hathor/wallet-lib/-/wallet-lib-1.8.0.tgz", + "integrity": "sha512-G1kLlxZ4Ev3S7hPiq/y9wUl4ns4pndOvuK37q+xqdxieZKCl2/O7lXiiHVOgRN9xOntL/TR66n2UZPpe0p29zQ==", + "dependencies": { + "axios": "1.7.2", + "bitcore-lib": "8.25.10", + "bitcore-mnemonic": "8.25.10", + "buffer": "6.0.3", + "crypto-js": "4.2.0", + "isomorphic-ws": "5.0.0", + "level": "8.0.1", + "lodash": "4.17.21", + "long": "5.2.3", + "ws": "8.17.0" + }, + "engines": { + "node": ">=20.0.0", + "npm": ">=10.0.0" + } + }, + "node_modules/@hathor/hathor-rpc-handler/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/@hathor/hathor-rpc-handler/node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, + "node_modules/@hathor/hathor-rpc-handler/node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@hathor/unleash-client": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@hathor/unleash-client/-/unleash-client-0.1.0.tgz", @@ -2550,20 +2631,26 @@ } }, "node_modules/@hathor/wallet-lib": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@hathor/wallet-lib/-/wallet-lib-1.0.1.tgz", - "integrity": "sha512-Pfjq0oks3qxD2BJXNYv7X6jv0oIW0vdSBvpkLMVsKQ/VikXV2RHP+av2vQdnkC2lMdkvBeG/gtoREF21tQzing==", - "dependencies": { - "axios": "^0.21.4", - "bitcore-lib": "^8.25.10", - "bitcore-mnemonic": "^8.25.10", - "buffer": "^6.0.3", - "crypto-js": "^3.1.9-1", - "isomorphic-ws": "^4.0.1", - "level": "^8.0.0", - "lodash": "^4.17.21", - "long": "^4.0.0", - "ws": "^7.5.9" + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@hathor/wallet-lib/-/wallet-lib-1.10.0.tgz", + "integrity": "sha512-oxQoxnwZNDrgjnvblx0ipVOxMNeM8W2PJkQpyzZJyl1QpRyhqHqqyYD0iSn1bhQEq8C1IRoSrvjsd3Af5eHdWQ==", + "license": "MIT", + "dependencies": { + "abstract-level": "1.0.4", + "axios": "1.7.2", + "bitcore-lib": "8.25.10", + "bitcore-mnemonic": "8.25.10", + "buffer": "6.0.3", + "crypto-js": "4.2.0", + "isomorphic-ws": "5.0.0", + "level": "8.0.1", + "lodash": "4.17.21", + "long": "5.2.3", + "ws": "8.17.1" + }, + "engines": { + "node": ">=20.0.0", + "npm": ">=10.0.0" } }, "node_modules/@hathor/wallet-lib/node_modules/buffer": { @@ -2590,9 +2677,30 @@ } }, "node_modules/@hathor/wallet-lib/node_modules/crypto-js": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", - "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, + "node_modules/@hathor/wallet-lib/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", @@ -6319,41 +6427,83 @@ "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" }, + "node_modules/@sentry-internal/feedback": { + "version": "7.119.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.119.0.tgz", + "integrity": "sha512-om8TkAU5CQGO8nkmr7qsSBVkP+/vfeS4JgtW3sjoTK0fhj26+DljR6RlfCGWtYQdPSP6XV7atcPTjbSnsmG9FQ==", + "license": "MIT", + "dependencies": { + "@sentry/core": "7.119.0", + "@sentry/types": "7.119.0", + "@sentry/utils": "7.119.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "7.119.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.119.0.tgz", + "integrity": "sha512-NL02VQx6ekPxtVRcsdp1bp5Tb5w6vnfBKSIfMKuDRBy5A10Uc3GSoy/c3mPyHjOxB84452A+xZSx6bliEzAnuA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "7.119.0", + "@sentry/replay": "7.119.0", + "@sentry/types": "7.119.0", + "@sentry/utils": "7.119.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@sentry-internal/tracing": { - "version": "7.54.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.54.0.tgz", - "integrity": "sha512-JsyhZ0wWZ+VqbHJg+azqRGdYJDkcI5R9+pnkO6SzbzxrRewqMAIwzkpPee3oI7vG99uhMEkOkMjHu0nQGwkOQw==", + "version": "7.119.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.119.0.tgz", + "integrity": "sha512-oKdFJnn+56f0DHUADlL8o9l8jTib3VDLbWQBVkjD9EprxfaCwt2m8L5ACRBdQ8hmpxCEo4I8/6traZ7qAdBUqA==", + "license": "MIT", "dependencies": { - "@sentry/core": "7.54.0", - "@sentry/types": "7.54.0", - "@sentry/utils": "7.54.0", - "tslib": "^1.9.3" + "@sentry/core": "7.119.0", + "@sentry/types": "7.119.0", + "@sentry/utils": "7.119.0" }, "engines": { "node": ">=8" } }, + "node_modules/@sentry/babel-plugin-component-annotate": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.20.1.tgz", + "integrity": "sha512-4mhEwYTK00bIb5Y9UWIELVUfru587Vaeg0DQGswv4aIRHIiMKLyNqCEejaaybQ/fNChIZOKmvyqXk430YVd7Qg==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/@sentry/browser": { - "version": "7.54.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.54.0.tgz", - "integrity": "sha512-EvLAw03N9WE2m1CMl2/1YMeIs1icw9IEOVJhWmf3uJEysNJOFWXu6ZzdtHEz1E6DiJYhc1HzDya0ExZeJxNARA==", + "version": "7.119.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.119.0.tgz", + "integrity": "sha512-WwmW1Y4D764kVGeKmdsNvQESZiAn9t8LmCWO0ucBksrjL2zw9gBPtOpRcO6l064sCLeSxxzCN+kIxhRm1gDFEA==", + "license": "MIT", "dependencies": { - "@sentry-internal/tracing": "7.54.0", - "@sentry/core": "7.54.0", - "@sentry/replay": "7.54.0", - "@sentry/types": "7.54.0", - "@sentry/utils": "7.54.0", - "tslib": "^1.9.3" + "@sentry-internal/feedback": "7.119.0", + "@sentry-internal/replay-canvas": "7.119.0", + "@sentry-internal/tracing": "7.119.0", + "@sentry/core": "7.119.0", + "@sentry/integrations": "7.119.0", + "@sentry/replay": "7.119.0", + "@sentry/types": "7.119.0", + "@sentry/utils": "7.119.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/cli": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.18.1.tgz", - "integrity": "sha512-lc/dX/cvcmznWNbLzDbzxn224vwY5zLIDBe3yOO6Usg3CDgkZZ3xfjN4AIUZwkiTEPIOELodrOfdoMxqpXyYDw==", + "version": "2.31.2", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.31.2.tgz", + "integrity": "sha512-2aKyUx6La2P+pplL8+2vO67qJ+c1C79KYWAyQBE0JIT5kvKK9JpwtdNoK1F0/2mRpwhhYPADCz3sVIRqmL8cQQ==", "hasInstallScript": true, + "license": "BSD-3-Clause", "dependencies": { "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.7", @@ -6366,59 +6516,185 @@ }, "engines": { "node": ">= 10" + }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.31.2", + "@sentry/cli-linux-arm": "2.31.2", + "@sentry/cli-linux-arm64": "2.31.2", + "@sentry/cli-linux-i686": "2.31.2", + "@sentry/cli-linux-x64": "2.31.2", + "@sentry/cli-win32-i686": "2.31.2", + "@sentry/cli-win32-x64": "2.31.2" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.31.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.31.2.tgz", + "integrity": "sha512-BHA/JJXj1dlnoZQdK4efRCtHRnbBfzbIZUKAze7oRR1RfNqERI84BVUQeKateD3jWSJXQfEuclIShc61KOpbKw==", + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm": { + "version": "2.31.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.31.2.tgz", + "integrity": "sha512-W8k5mGYYZz/I/OxZH65YAK7dCkQAl+wbuoASGOQjUy5VDgqH0QJ8kGJufXvFPM+f3ZQGcKAnVsZ6tFqZXETBAw==", + "cpu": [ + "arm" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.31.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.31.2.tgz", + "integrity": "sha512-FLVKkJ/rWvPy/ka7OrUdRW63a/z8HYI1Gt8Pr6rWs50hb7YJja8lM8IO10tYmcFE/tODICsnHO9HTeUg2g2d1w==", + "cpu": [ + "arm64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.31.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.31.2.tgz", + "integrity": "sha512-A64QtzaPi3MYFpZ+Fwmi0mrSyXgeLJ0cWr4jdeTGrzNpeowSteKgd6tRKU+LVq0k5shKE7wdnHk+jXnoajulMA==", + "cpu": [ + "x86", + "ia32" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-x64": { + "version": "2.31.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.31.2.tgz", + "integrity": "sha512-YL/r+15R4mOEiU3mzn7iFQOeFEUB6KxeKGTTrtpeOGynVUGIdq4nV5rHow5JDbIzOuBS3SpOmcIMluvo1NCh0g==", + "cpu": [ + "x64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.31.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.31.2.tgz", + "integrity": "sha512-Az/2bmW+TFI059RE0mSBIxTBcoShIclz7BDebmIoCkZ+retrwAzpmBnBCDAHow+Yi43utOow+3/4idGa2OxcLw==", + "cpu": [ + "x86", + "ia32" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.31.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.31.2.tgz", + "integrity": "sha512-XIzyRnJu539NhpFa+JYkotzVwv3NrZ/4GfHB/JWA2zReRvsk39jJG8D5HOmm0B9JA63QQT7Dt39RW8g3lkmb6w==", + "cpu": [ + "x64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" } }, "node_modules/@sentry/core": { - "version": "7.54.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.54.0.tgz", - "integrity": "sha512-MAn0E2EwgNn1pFQn4qxhU+1kz6edullWg6VE5wCmtpXWOVw6sILBUsQpeIG5djBKMcneJCdOlz5jeqcKPrLvZQ==", + "version": "7.119.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.119.0.tgz", + "integrity": "sha512-CS2kUv9rAJJEjiRat6wle3JATHypB0SyD7pt4cpX5y0dN5dZ1JrF57oLHRMnga9fxRivydHz7tMTuBhSSwhzjw==", + "license": "MIT", "dependencies": { - "@sentry/types": "7.54.0", - "@sentry/utils": "7.54.0", - "tslib": "^1.9.3" + "@sentry/types": "7.119.0", + "@sentry/utils": "7.119.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/hub": { - "version": "7.54.0", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-7.54.0.tgz", - "integrity": "sha512-GePswxz0rzSaCGB0QR2FgH7Hht9SfxsVyX271FtPH3V5hUIZOHlftXggqmNy5XyyiGf27zsWM+DYgQUFJwMcjQ==", + "version": "7.119.0", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-7.119.0.tgz", + "integrity": "sha512-183h5B/rZosLxpB+ZYOvFdHk0rwZbKskxqKFtcyPbDAfpCUgCass41UTqyxF6aH1qLgCRxX8GcLRF7frIa/SOg==", + "license": "MIT", "dependencies": { - "@sentry/core": "7.54.0", - "@sentry/types": "7.54.0", - "@sentry/utils": "7.54.0", - "tslib": "^1.9.3" + "@sentry/core": "7.119.0", + "@sentry/types": "7.119.0", + "@sentry/utils": "7.119.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/integrations": { - "version": "7.54.0", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.54.0.tgz", - "integrity": "sha512-RolGsQzJChJzjHTJcCKSZ1HanmY33floc5o13WgU9NoDqJbLGLNcOIrAu+WynqPe8P5VTVrVb8NiwhLqWrKp4g==", + "version": "7.119.0", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.119.0.tgz", + "integrity": "sha512-OHShvtsRW0A+ZL/ZbMnMqDEtJddPasndjq+1aQXw40mN+zeP7At/V1yPZyFaURy86iX7Ucxw5BtmzuNy7hLyTA==", + "license": "MIT", "dependencies": { - "@sentry/types": "7.54.0", - "@sentry/utils": "7.54.0", - "localforage": "^1.8.1", - "tslib": "^1.9.3" + "@sentry/core": "7.119.0", + "@sentry/types": "7.119.0", + "@sentry/utils": "7.119.0", + "localforage": "^1.8.1" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/react": { - "version": "7.54.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.54.0.tgz", - "integrity": "sha512-qUbwmRRpTh05m2rbC8A2zAFQYsoHhwIpxT5UXxh0P64ZlA3cSg1/DmTTgwnd1l+7gzKrc31UikXQ4y0YDbMNKg==", + "version": "7.119.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.119.0.tgz", + "integrity": "sha512-cf8Cei+qdSA26gx+IMAuc/k44PeBImNzIpXi3930SLhUe44ypT5OZ/44L6xTODHZzTIyMSJPduf59vT2+eW9yg==", + "license": "MIT", "dependencies": { - "@sentry/browser": "7.54.0", - "@sentry/types": "7.54.0", - "@sentry/utils": "7.54.0", - "hoist-non-react-statics": "^3.3.2", - "tslib": "^1.9.3" + "@sentry/browser": "7.119.0", + "@sentry/core": "7.119.0", + "@sentry/types": "7.119.0", + "@sentry/utils": "7.119.0", + "hoist-non-react-statics": "^3.3.2" }, "engines": { "node": ">=8" @@ -6428,52 +6704,66 @@ } }, "node_modules/@sentry/react-native": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sentry/react-native/-/react-native-5.6.0.tgz", - "integrity": "sha512-qmu1WzgairYNE/dlGQrjWAZA0OcTv/31yaPUDhX0Yrf1GT+KSerSlimiHGqvle81NJcpSi/K1+7EBfPiK57GmA==", - "dependencies": { - "@sentry/browser": "7.54.0", - "@sentry/cli": "2.18.1", - "@sentry/core": "7.54.0", - "@sentry/hub": "7.54.0", - "@sentry/integrations": "7.54.0", - "@sentry/react": "7.54.0", - "@sentry/types": "7.54.0", - "@sentry/utils": "7.54.0" + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@sentry/react-native/-/react-native-5.31.0.tgz", + "integrity": "sha512-iflj+SfH2k/vSybC5APTjwdBuW53rQtitD99vRbsMgMTnhcvWMy+ASTf6Shr9czv9uzlYrS83iF/COSyDNwiPQ==", + "license": "MIT", + "dependencies": { + "@sentry/babel-plugin-component-annotate": "2.20.1", + "@sentry/browser": "7.119.0", + "@sentry/cli": "2.31.2", + "@sentry/core": "7.119.0", + "@sentry/hub": "7.119.0", + "@sentry/integrations": "7.119.0", + "@sentry/react": "7.119.0", + "@sentry/types": "7.119.0", + "@sentry/utils": "7.119.0" + }, + "bin": { + "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" }, "peerDependencies": { + "expo": ">=49.0.0", "react": ">=17.0.0", "react-native": ">=0.65.0" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } } }, "node_modules/@sentry/replay": { - "version": "7.54.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.54.0.tgz", - "integrity": "sha512-C0F0568ybphzGmKGe23duB6n5wJcgM7WLYhoeqW3o2bHeqpj1dGPSka/K3s9KzGaAgzn1zeOUYXJsOs+T/XdsA==", + "version": "7.119.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.119.0.tgz", + "integrity": "sha512-BnNsYL+X5I4WCH6wOpY6HQtp4MgVt0NVlhLUsEyrvMUiTs0bPkDBrulsgZQBUKJsbOr3l9nHrFoNVB/0i6WNLA==", + "license": "MIT", "dependencies": { - "@sentry/core": "7.54.0", - "@sentry/types": "7.54.0", - "@sentry/utils": "7.54.0" + "@sentry-internal/tracing": "7.119.0", + "@sentry/core": "7.119.0", + "@sentry/types": "7.119.0", + "@sentry/utils": "7.119.0" }, "engines": { "node": ">=12" } }, "node_modules/@sentry/types": { - "version": "7.54.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.54.0.tgz", - "integrity": "sha512-D+i9xogBeawvQi2r0NOrM7zYcUaPuijeME4O9eOTrDF20tj71hWtJLilK+KTGLYFtpGg1h+9bPaz7OHEIyVopg==", + "version": "7.119.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.119.0.tgz", + "integrity": "sha512-27qQbutDBPKGbuJHROxhIWc1i0HJaGLA90tjMu11wt0E4UNxXRX+UQl4Twu68v4EV3CPvQcEpQfgsViYcXmq+w==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@sentry/utils": { - "version": "7.54.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.54.0.tgz", - "integrity": "sha512-3Yf5KlKjIcYLddOexSt2ovu2TWlR4Fi7M+aCK8yUTzwNzf/xwFSWOstHlD/WiDy9HvfhWAOB/ukNTuAeJmtasw==", + "version": "7.119.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.119.0.tgz", + "integrity": "sha512-ZwyXexWn2ZIe2bBoYnXJVPc2esCSbKpdc6+0WJa8eutXfHq3FRKg4ohkfCBpfxljQGEfP1+kfin945lA21Ka+A==", + "license": "MIT", "dependencies": { - "@sentry/types": "7.54.0", - "tslib": "^1.9.3" + "@sentry/types": "7.119.0" }, "engines": { "node": ">=8" @@ -6977,9 +7267,9 @@ "integrity": "sha512-HQiug4c+/s3WOvEnDRxXVmNtSG5s2gJM9r19BTcqjp7BWcE48PB+Y2G6jE65kqI0LpsQeMZygt/b60Gi4KxGyA==" }, "node_modules/abstract-level": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-1.0.3.tgz", - "integrity": "sha512-t6jv+xHy+VYwc4xqZMn2Pa9DjcdzvzZmQGRjTFc8spIbRGHgBrEKbPq+rYXc7CCo0lxgYvSgKVg9qZAhpVQSjA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-1.0.4.tgz", + "integrity": "sha512-eUP/6pbXBkMbXFdx4IH2fVgvB7M0JvR7/lIL33zcs0IBcwjdzSSl31TOJsaCzmKSSDF9h8QYSOJux4Nd4YJqFg==", "dependencies": { "buffer": "^6.0.3", "catering": "^2.1.0", @@ -7052,6 +7342,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", "dependencies": { "debug": "4" }, @@ -7340,6 +7631,11 @@ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -7361,11 +7657,13 @@ } }, "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "dependencies": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/axobject-query": { @@ -7843,9 +8141,9 @@ ] }, "node_modules/bech32": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.3.tgz", + "integrity": "sha512-yuVFUvrNcoJi0sv5phmqc6P+Fl1HjRDRNOOkHY2X/3LBy2bIGNSFx4fZ95HMaXHupuS7cZR15AsvtmCIF4UEyg==" }, "node_modules/big-integer": { "version": "1.6.51", @@ -7855,33 +8153,12 @@ "node": ">=0.6" } }, - "node_modules/bigi": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/bigi/-/bigi-1.4.2.tgz", - "integrity": "sha512-ddkU+dFIuEIW8lE7ZwdIAf2UPoM90eaprg5m3YXAVVTmKlqV/9BX4A2M8BOK2yOq6/VgZFVhK6QAxJebhlbhzw==" - }, - "node_modules/bip-schnorr": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/bip-schnorr/-/bip-schnorr-0.6.4.tgz", - "integrity": "sha512-dNKw7Lea8B0wMIN4OjEmOk/Z5qUGqoPDY0P2QttLqGk1hmDPytLWW8PR5Pb6Vxy6CprcdEgfJpOjUu+ONQveyg==", - "dependencies": { - "bigi": "^1.4.2", - "ecurve": "^1.0.6", - "js-sha256": "^0.9.0", - "randombytes": "^2.1.0", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/bitcore-lib": { - "version": "8.25.47", - "resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-8.25.47.tgz", - "integrity": "sha512-qDZr42HuP4P02I8kMGZUx/vvwuDsz8X3rQxXLfM0BtKzlQBcbSM7ycDkDN99Xc5jzpd4fxNQyyFXOmc6owUsrQ==", + "version": "8.25.10", + "resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-8.25.10.tgz", + "integrity": "sha512-MyHpSg7aFRHe359RA/gdkaQAal3NswYZTLEuu0tGX1RGWXAYN9i/24fsjPqVKj+z0ua+gzAT7aQs0KiKXWCgKA==", "dependencies": { - "bech32": "=2.0.0", - "bip-schnorr": "=0.6.4", + "bech32": "=1.1.3", "bn.js": "=4.11.8", "bs58": "^4.0.1", "buffer-compare": "=1.1.1", @@ -7891,11 +8168,11 @@ } }, "node_modules/bitcore-mnemonic": { - "version": "8.25.47", - "resolved": "https://registry.npmjs.org/bitcore-mnemonic/-/bitcore-mnemonic-8.25.47.tgz", - "integrity": "sha512-wTa0imZZpFTqwlpyokvU8CNl+YdaIvQIrWKp/0AEL9gPX2vuzBnE+U8Ok6D5lHCnbG6dvmoesmtyf6R3aYI86A==", + "version": "8.25.10", + "resolved": "https://registry.npmjs.org/bitcore-mnemonic/-/bitcore-mnemonic-8.25.10.tgz", + "integrity": "sha512-FeXxO37BLV5JRvxPmVFB91zRHalavV8H4TdQGt1/hz0AkoPymIV68OkuB+TptpjeYgatcgKPoPvPhglJkTzFQQ==", "dependencies": { - "bitcore-lib": "^8.25.47", + "bitcore-lib": "^8.25.10", "unorm": "^1.4.1" }, "peerDependencies": { @@ -8351,9 +8628,9 @@ "dev": true }, "node_modules/classic-level": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.3.0.tgz", - "integrity": "sha512-iwFAJQYtqRTRM0F6L8h4JCt00ZSGdOyqh7yVrhhjrOpFhmBjNlRUey64MCiyo6UmQHMJ+No3c81nujPv+n9yrg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.4.1.tgz", + "integrity": "sha512-qGx/KJl3bvtOHrGau2WklEZuXhS3zme+jf+fsu6Ej7W7IP/C49v7KNlWIsT1jZu0YnfzSIYDGcEWpCa1wKGWXQ==", "hasInstallScript": true, "dependencies": { "abstract-level": "^1.0.2", @@ -8502,6 +8779,17 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/command-exists": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", @@ -9016,6 +9304,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -9180,15 +9476,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/ecurve": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/ecurve/-/ecurve-1.0.6.tgz", - "integrity": "sha512-/BzEjNfiSuB7jIWKcS/z8FK9jNjmEWvUV2YZ4RLSmcDtP7Lq0m6FvDuSnJpBlDpGRpfRQeTLGLBI8H+kEv0r+w==", - "dependencies": { - "bigi": "^1.1.0", - "safe-buffer": "^5.0.1" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -10522,9 +10809,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -10573,6 +10860,19 @@ "integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==", "dev": true }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/formidable": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", @@ -11178,6 +11478,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", "dependencies": { "agent-base": "6", "debug": "4" @@ -11262,7 +11563,8 @@ "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" }, "node_modules/import-fresh": { "version": "3.3.0", @@ -11767,9 +12069,9 @@ } }, "node_modules/isomorphic-ws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", - "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", "peerDependencies": { "ws": "*" } @@ -14665,11 +14967,6 @@ "@sideway/pinpoint": "^2.0.0" } }, - "node_modules/js-sha256": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", - "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -15077,10 +15374,11 @@ } }, "node_modules/level": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/level/-/level-8.0.0.tgz", - "integrity": "sha512-ypf0jjAk2BWI33yzEaaotpq7fkOPALKAgDBxggO6Q9HGX2MRXn0wbP1Jn/tJv1gtL867+YOjOB49WaUF3UoJNQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/level/-/level-8.0.1.tgz", + "integrity": "sha512-oPBGkheysuw7DmzFQYyFe8NAia5jFLAgEnkgWnK3OXAuJr8qFT+xBQIwokAZPME2bhPFzS8hlYcL16m8UZrtwQ==", "dependencies": { + "abstract-level": "^1.0.4", "browser-level": "^1.0.1", "classic-level": "^1.2.0" }, @@ -15160,6 +15458,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", "dependencies": { "immediate": "~3.0.5" } @@ -15174,6 +15473,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", "dependencies": { "lie": "3.1.1" } @@ -15312,9 +15612,9 @@ } }, "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" }, "node_modules/loose-envify": { "version": "1.4.0", @@ -16683,9 +16983,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", - "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -17450,6 +17750,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -19594,7 +19895,8 @@ "node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "node_modules/tsscmp": { "version": "1.0.6", diff --git a/package.json b/package.json index c723ba9c1..fc1d80f64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "HathorMobile", - "version": "0.27.2", + "version": "0.28.0-rc.3", "engines": { "node": ">=18.0.0", "npm": ">=9.0.0" @@ -21,8 +21,9 @@ "@fortawesome/free-regular-svg-icons": "6.4.0", "@fortawesome/free-solid-svg-icons": "6.4.0", "@fortawesome/react-native-fontawesome": "0.2.7", + "@hathor/hathor-rpc-handler": "0.0.1-experimental-alpha", "@hathor/unleash-client": "0.1.0", - "@hathor/wallet-lib": "1.0.1", + "@hathor/wallet-lib": "1.10.0", "@notifee/react-native": "5.7.0", "@react-native-async-storage/async-storage": "1.19.0", "@react-native-firebase/app": "16.7.0", @@ -30,7 +31,7 @@ "@react-navigation/bottom-tabs": "6.5.8", "@react-navigation/native": "6.1.7", "@react-navigation/stack": "6.3.17", - "@sentry/react-native": "5.6.0", + "@sentry/react-native": "5.31.0", "assert": "2.0.0", "buffer": "4.9.2", "console-browserify": "1.2.0", @@ -131,6 +132,9 @@ "setupFiles": [ "/jestMockSetup.js" ], - "cacheDirectory": ".jest/cache" + "cacheDirectory": ".jest/cache", + "testMatch": [ + "**/*.test.js" + ] } } diff --git a/pre_release.sh b/pre_release.sh index 5c22eb3fd..ffbc6ae05 100755 --- a/pre_release.sh +++ b/pre_release.sh @@ -10,6 +10,8 @@ set -e # Exit on any command failure. set -u # Exit on unset variables. set -v # Verbose mode for better debugging +git log -n1 + rm -rf node_modules/ rm -f ./android/app/google-services.json diff --git a/src/App.js b/src/App.js index 7296d6e4d..20e93ac41 100644 --- a/src/App.js +++ b/src/App.js @@ -87,6 +87,13 @@ import { COLORS, HathorTheme } from './styles/themes'; import { NetworkSettingsFlowNav, NetworkSettingsFlowStack } from './screens/NetworkSettings'; import { NetworkStatusBar } from './components/NetworkSettings/NetworkStatusBar'; import ShowPushNotificationTxDetails from './components/ShowPushNotificationTxDetails'; +import { NanoContractDetailsScreen } from './screens/NanoContract/NanoContractDetailsScreen'; +import { NanoContractTransactionScreen } from './screens/NanoContract/NanoContractTransactionScreen'; +import { NanoContractRegisterScreen } from './screens/NanoContract/NanoContractRegisterScreen'; +import { NewNanoContractTransactionScreen } from './screens/WalletConnect/NewNanoContractTransactionScreen'; +import { NanoContractRegisterQrCodeScreen } from './screens/NanoContractRegisterQrCodeScreen'; +import { SignMessageRequestScreen } from './screens/WalletConnect/SignMessageRequestScreen'; +import { CreateTokenRequestScreen } from './screens/WalletConnect/CreateTokenScreen'; /** * This Stack Navigator is exhibited when there is no wallet initialized on the local storage. @@ -283,6 +290,72 @@ const RegisterTokenStack = ({ navigation }) => { ); }; +/** + * Stack of screens dedicated to the nano contract registration process + */ +const RegisterNanoContractStack = ({ navigation }) => { + const Stack = createStackNavigator(); + const dispatch = useDispatch(); + const isCameraAvailable = useSelector((state) => state.isCameraAvailable); + + /** + * Defines which screen will be the initial one, according to app camera permissions + * @param {null|boolean} cameraStatus + * @returns {string} Route name + */ + const decideRouteByCameraAvailablity = (cameraStatus) => { + switch (isCameraAvailable) { + case true: + return 'NanoContractRegisterQrCodeScreen'; + case false: + return 'NanoContractRegisterScreen'; + default: + return 'RegisterCameraPermissionScreen'; + } + }; + + // Initial screen set on component initial rendering + const [initialRoute, setInitialRoute] = useState( + decideRouteByCameraAvailablity(isCameraAvailable) + ); + + /* + * Request camera permission on initialization only if permission is not already set + */ + useEffect(() => { + if (isCameraAvailable === null) { + dispatch(requestCameraPermission()); + } + }, []); + + // Listen to camera permission changes from user input and navigate to the relevant screen + useEffect(() => { + const newScreenName = decideRouteByCameraAvailablity(isCameraAvailable); + + // Navigator screen already correct: no further action. + if (initialRoute === newScreenName) { + return; + } + + // Set initial route and navigate there according to new permission set + setInitialRoute(newScreenName); + navigation.replace(newScreenName); + }, [isCameraAvailable]); + + return ( + + + + + + ); +}; + const tabBarIconMap = { Home: 'icDashboard', Send: 'icSend', @@ -377,11 +450,17 @@ const AppStack = () => { initialParams={{ hName: 'Main' }} component={TabNavigator} /> + + + + + + { /> + ( + - diff --git a/src/actions.js b/src/actions.js index f188dd893..707650670 100644 --- a/src/actions.js +++ b/src/actions.js @@ -12,8 +12,10 @@ import { } from '@hathor/wallet-lib'; import { METADATA_CONCURRENT_DOWNLOAD, + WALLETCONNECT_CREATE_TOKEN_STATUS, + WALLETCONNECT_NEW_NANOCONTRACT_TX_STATUS, } from './constants'; -import { mapTokenHistory } from './utils'; +import { mapToTxHistory } from './utils'; // TODO: We should apply the agreed taxonomy to all the actions. // See: https://github.com/HathorNetwork/hathor-wallet-mobile/issues/334 @@ -113,6 +115,9 @@ export const types = { SET_WALLET_CONNECT: 'SET_WALLET_CONNECT', SET_WALLET_CONNECT_MODAL: 'SET_WALLET_CONNECT_MODAL', SET_WALLET_CONNECT_SESSIONS: 'SET_WALLET_CONNECT_SESSIONS', + WALLET_CONNECT_ACCEPT: 'WALLET_CONNECT_ACCEPT', + WALLET_CONNECT_REJECT: 'WALLET_CONNECT_REJECT', + SET_NEW_NANO_CONTRACT_TRANSACTION: 'SET_NEW_NANO_CONTRACT_TRANSACTION', SET_UNLEASH_CLIENT: 'SET_UNLEASH_CLIENT', WC_URI_INPUTTED: 'WC_URI_INPUTTED', WC_CANCEL_SESSION: 'WC_CANCEL_SESSION', @@ -137,6 +142,68 @@ export const types = { NETWORKSETTINGS_UPDATE_FAILURE: 'NETWORK_SETTINGS_UPDATE_FAILURE', /* It updates the redux state of network settings status. */ NETWORKSETTINGS_UPDATE_READY: 'NETWORK_SETTINGS_UPDATE_READY', + /* It signals Nano Contract initialization. */ + NANOCONTRACT_INIT: 'NANOCONTRACT_INIT', + /* It initiates a registration process of a Nano Contract. */ + NANOCONTRACT_REGISTER_REQUEST: 'NANOCONTRACT_REGISTER_REQUEST', + /* It indicates a Nano Contract registration is couldn't complete. */ + NANOCONTRACT_REGISTER_FAILURE: 'NANOCONTRACT_REGISTER_FAILURE', + /* It indicates a Nano Contract registration is complete. */ + NANOCONTRACT_REGISTER_SUCCESS: 'NANOCONTRACT_REGISTER_SUCCESS', + /* It updates the redux state of nano contract register status to ready. */ + NANOCONTRACT_REGISTER_READY: 'NANOCONTRACT_REGISTER_READY', + /* It indicates a Nano Contract hitory was requested to load. */ + NANOCONTRACT_HISTORY_REQUEST: 'NANOCONTRACT_HISTORY_REQUEST', + /* It indicates a Nano Contract history is processing. */ + NANOCONTRACT_HISTORY_LOADING: 'NANOCONTRACT_HISTORY_LOADING', + /* It indicates a Nano Contract history was successfully loaded. */ + NANOCONTRACT_HISTORY_SUCCESS: 'NANOCONTRACT_HISTORY_SUCCESS', + /* It indicates a Nano Contract history failed to load. */ + NANOCONTRACT_HISTORY_FAILURE: 'NANOCONTRACT_HISTORY_FAILURE', + /* It indicates a Nano Contract history clean. */ + NANOCONTRACT_HISTORY_CLEAN: 'NANOCONTRACT_HISTORY_CLEAN', + /* It initiates an unregistration process of a Nano Contract. */ + NANOCONTRACT_UNREGISTER_REQUEST: 'NANOCONTRACT_UNREGISTER_REQUEST', + /* It signals a successful completion of unregistration process. */ + NANOCONTRACT_UNREGISTER_SUCCESS: 'NANOCONTRACT_UNREGISTER_SUCCESS', + /* It initiates a process to change the address on registered Nano Contract. */ + NANOCONTRACT_ADDRESS_CHANGE_REQUEST: 'NANOCONTRACT_ADDRESS_CHANGE_REQUEST', + /* It triggers a process to fetch blueprint info. */ + NANOCONTRACT_BLUEPRINTINFO_REQUEST: 'NANOCONTRACT_BLUEPRINTINFO_REQUEST', + /* It signals a failure on fetch blueprint info. */ + NANOCONTRACT_BLUEPRINTINFO_FAILURE: 'NANOCONTRACT_BLUEPRINTINFO_FAILURE', + /* It signals a success on fetch blueprint info. */ + NANOCONTRACT_BLUEPRINTINFO_SUCCESS: 'NANOCONTRACT_BLUEPRINTINFO_SUCCESS', + /* It triggers a process to fetch all wallet addresses. */ + SELECTADDRESS_ADDRESSES_REQUEST: 'SELECTADDRESS_ADDRESSES_REQUEST', + /* It signals the fetch has loaded all the addresses with success. */ + SELECTADDRESS_ADDRESSES_SUCCESS: 'SELECTADDRESS_ADDRESSES_SUCCESS', + /* It signals a fetch failure due to an error. */ + SELECTADDRESS_ADDRESSES_FAILURE: 'SELECTADDRESS_ADDRESSES_FAILURE', + /* It triggers a process to fetch the first wallet address. */ + FIRSTADDRESS_REQUEST: 'FIRSTADDRESS_REQUEST', + /* It signals the fetch has loaded the first address with success. */ + FIRSTADDRESS_SUCCESS: 'FIRSTADDRESS_SUCCESS', + /* It signals a fetch failure due to an error. */ + FIRSTADDRESS_FAILURE: 'FIRSTADDRESS_FAILURE', + /* It updates the redux state of new nano contract transaction status on wallet connect register. */ + WALLETCONNECT_NEW_NANOCONTRACT_STATUS: 'WALLETCONNECT_NEW_NANOCONTRACT_STATUS', + /* It triggers a process to fetch token details for a list of unregistered tokens. */ + UNREGISTEREDTOKENS_DOWNLOAD_REQUEST: 'UNREGISTEREDTOKENS_DOWNLOAD_REQUEST', + /* It signals the process has loaded at least one token details with success. */ + UNREGISTEREDTOKENS_DOWNLOAD_SUCCESS: 'UNREGISTEREDTOKENS_DOWNLOAD_SUCCESS', + /* It signals the process has failed to load at least one token details. */ + UNREGISTEREDTOKENS_DOWNLOAD_FAILURE: 'UNREGISTEREDTOKENS_DOWNLOAD_FAILURE', + /* It signals the end of the process. */ + UNREGISTEREDTOKENS_DOWNLOAD_END: 'UNREGISTEREDTOKENS_DOWNLOAD_END', + WALLETCONNECT_NEW_NANOCONTRACT_RETRY: 'WALLETCONNECT_NEW_NANOCONTRACT_RETRY', + WALLETCONNECT_NEW_NANOCONTRACT_RETRY_DISMISS: 'WALLETCONNECT_NEW_NANOCONTRACT_RETRY_DISMISS', + SHOW_SIGN_MESSAGE_REQUEST_MODAL: 'SHOW_SIGN_MESSAGE_REQUEST_MODAL', + SHOW_NANO_CONTRACT_SEND_TX_MODAL: 'SHOW_NANO_CONTRACT_SEND_TX_MODAL', + SHOW_CREATE_TOKEN_REQUEST_MODAL: 'SHOW_CREATE_TOKEN_REQUEST_MODAL', + WALLETCONNECT_CREATE_TOKEN_STATUS: 'WALLETCONNECT_CREATE_TOKEN_STATUS', + WALLETCONNECT_CREATE_TOKEN_RETRY: 'WALLETCONNECT_CREATE_TOKEN_RETRY', + WALLETCONNECT_CREATE_TOKEN_RETRY_DISMISS: 'WALLETCONNECT_CREATE_TOKEN_RETRY_DISMISS', }; export const featureToggleInitialized = () => ({ @@ -197,6 +264,30 @@ export const walletConnectCancelSession = (sessionKey) => ({ payload: sessionKey, }); +/** + * @param {Object} data Data that the user has accepted. + */ +export const walletConnectAccept = (data) => ({ + type: types.WALLET_CONNECT_ACCEPT, + payload: data, +}); + +export const walletConnectReject = () => ({ + type: types.WALLET_CONNECT_REJECT, +}); + +/** + * @param {Object} ncRequest + * @param {boolean} ncRequest.show + * @param {Object} ncRequest.data + * @param {Object} ncRequest.data.nc + * @param {Object} ncRequest.data.dapp + */ +export const setNewNanoContractTransaction = (ncRequest) => ({ + type: types.SET_NEW_NANO_CONTRACT_TRANSACTION, + payload: ncRequest +}); + /** * isShowingPinScreen {bool} * */ @@ -344,7 +435,7 @@ export const fetchHistoryAndBalance = async (wallet) => { const tokenBalance = balance[0].balance; tokensBalance[token] = { available: tokenBalance.unlocked, locked: tokenBalance.locked }; const history = await wallet.getTxHistory({ token_id: token }); - tokensHistory[token] = history.map((element) => mapTokenHistory(element, token)); + tokensHistory[token] = history.map(mapToTxHistory(token)); /* eslint-enable no-await-in-loop */ } @@ -360,7 +451,7 @@ export const fetchHistoryAndBalance = async (wallet) => { */ export const fetchMoreHistory = async (wallet, token, history) => { const newHistory = await wallet.getTxHistory({ token_id: token, skip: history.length }); - const newHistoryObjects = newHistory.map((element) => mapTokenHistory(element, token)); + const newHistoryObjects = newHistory.map(mapToTxHistory(token)); return newHistoryObjects; }; @@ -382,7 +473,7 @@ export const fetchTokensMetadata = async (tokens, network) => { for (const tokenChunk of tokenChunks) { /* eslint-disable no-await-in-loop */ await Promise.all(tokenChunk.map(async (token) => { - if (token === hathorLibConstants.HATHOR_TOKEN_CONFIG.uid) { + if (token === hathorLibConstants.NATIVE_TOKEN_UID) { return; } @@ -453,7 +544,7 @@ export const fetchTokenBalance = async (wallet, uid) => { * wallet {HathorWallet | HathorWalletServiceWallet} wallet object */ export const fetchNewHTRBalance = async (wallet) => { - const { uid } = hathorLibConstants.HATHOR_TOKEN_CONFIG; + const uid = hathorLibConstants.NATIVE_TOKEN_UID; return fetchTokenBalance(wallet, uid); }; @@ -534,8 +625,8 @@ export const tokenFetchHistoryRequested = (tokenId, force) => ({ }); /** - * tokenId: The tokenId to store history data - * data: The downloaded history data + * @param {string} tokenId: The tokenId to store history data + * @param {TxHistory} data: The downloaded history data */ export const tokenFetchHistorySuccess = (tokenId, data) => ({ type: types.TOKEN_FETCH_HISTORY_SUCCESS, @@ -975,3 +1066,398 @@ export const networkSettingsUpdateInvalid = (errors) => ({ export const networkSettingsUpdateReady = () => ({ type: types.NETWORKSETTINGS_UPDATE_READY, }); + +/** + * It signals Nano Contract initialization. + */ +export const nanoContractInit = () => ({ + type: types.NANOCONTRACT_INIT, +}); + +/** + * Request a Nano Contract to be registered. + * @param {{ + * address: string, + * ncId: string, + * }} registry Inputs to register a Nano Contract + */ +export const nanoContractRegisterRequest = (registerRequest) => ({ + type: types.NANOCONTRACT_REGISTER_REQUEST, + payload: registerRequest, +}); + +/** + * Nano Contract registration has failed. + * @param {string} error Registration failure reason. + */ +export const nanoContractRegisterFailure = (error) => ({ + type: types.NANOCONTRACT_REGISTER_FAILURE, + payload: { error } +}); + +/** + * Nano Contract registration has finished with success. + * @param {{ + * entryKey: string; + * entryValue: Object; + * hasFeedback?: boolean; + * }} ncEntry basic information of Nano Contract registered. + */ +export const nanoContractRegisterSuccess = (ncEntry) => ({ + type: types.NANOCONTRACT_REGISTER_SUCCESS, + payload: ncEntry, +}); + +/** + * Request a change on Nano Contract register status to ready. + */ +export const nanoContractRegisterReady = () => ({ + type: types.NANOCONTRACT_REGISTER_READY, +}); + +/** + * Nano Contract request fetch history. + * @param {{ + * ncId: string; + * after: string; + * }} ncEntry Basic information of Nano Contract registered. + */ +export const nanoContractHistoryRequest = (ncEntry) => ({ + type: types.NANOCONTRACT_HISTORY_REQUEST, + payload: ncEntry, +}); + +/** + * Nano Contract fetch history is loading. + * @param {{ + * ncId: string; + * }} + */ +export const nanoContractHistoryLoading = (ncEntry) => ({ + type: types.NANOCONTRACT_HISTORY_LOADING, + payload: ncEntry, +}); + +/** + * Nano Contract history has loaded success. + * @param {Object} payload + * @param {string} payload.ncId Nano Contract ID. + * @param {Object[]?} payload.history A chunk of txs to initialize history + * @param {Object[]?} payload.beforeHistory A chunk of newer txs. + * @param {Object[]?} payload.afterHistory A chunk of older txs. + * + * @description + * The history options are mutually exclusive. + */ +export const nanoContractHistorySuccess = (payload) => ({ + type: types.NANOCONTRACT_HISTORY_SUCCESS, + payload, +}); + +/** + * Nano Contract history clean signal. + * @param {Object} payload + * @param {string} payload.ncId Nano Contract ID. + */ +export const nanoContractHistoryClean = (payload) => ({ + type: types.NANOCONTRACT_HISTORY_CLEAN, + payload, +}); + +/** + * Nano Contract history has failed. + * @param {Object} payload + * @param {string} payload.ncId Nano Contract ID. + * @param {string} payload.error History failure reason. + */ +export const nanoContractHistoryFailure = (payload) => ({ + type: types.NANOCONTRACT_HISTORY_FAILURE, + payload, +}); + +/** + * Request unregistration of a Nano Contract by its key. + * @param {{ + * ncId: string, + * }} Nano Contract ID to unregister. + */ +export const nanoContractUnregisterRequest = (unregisterRequest) => ({ + type: types.NANOCONTRACT_UNREGISTER_REQUEST, + payload: unregisterRequest, +}); + +/** + * Unregistration of a Nano Contract has finished with success. + * @param {{ + * ncId: string, + * }} Nano Contract ID unregistered. + */ +export const nanoContractUnregisterSuccess = (unregistered) => ({ + type: types.NANOCONTRACT_UNREGISTER_SUCCESS, + payload: unregistered, +}); + +/** + * Request a change on the Nano Contract registered. + * @param {{ + * ncId: string; + * newAddress: string; + * }} changeAddressRequest + */ +export const nanoContractAddressChangeRequest = (changeAddressRequest) => ({ + type: types.NANOCONTRACT_ADDRESS_CHANGE_REQUEST, + payload: changeAddressRequest, +}); + +/** + * Request to load all wallet addresses. + */ +export const selectAddressAddressesRequest = () => ({ + type: types.SELECTADDRESS_ADDRESSES_REQUEST, +}); + +/** + * Signals all wallet addresses were loaded with success. + * @param {Object} successPayload + * @param {string[]} successPayload.addresses + */ +export const selectAddressAddressesSuccess = (successPayload) => ({ + type: types.SELECTADDRESS_ADDRESSES_SUCCESS, + payload: successPayload, +}); + +/** + * Signals a failure on wallet addresses loading due to an error. + * @param {Object} failurePayload + * @param {string} failurePayload.error + */ +export const selectAddressAddressesFailure = (failurePayload) => ({ + type: types.SELECTADDRESS_ADDRESSES_FAILURE, + payload: failurePayload, +}); + +/** + * Request to load first wallet address. + */ +export const firstAddressRequest = () => ({ + type: types.FIRSTADDRESS_REQUEST, +}); + +/** + * Signals first wallet address was loaded with success. + * @param {Object} successPayload + * @param {string} successPayload.address + */ +export const firstAddressSuccess = (successPayload) => ({ + type: types.FIRSTADDRESS_SUCCESS, + payload: successPayload, +}); + +/** + * Signals a failure on first wallet address loading due to an error. + * @param {Object} failurePayload + * @param {string} failurePayload.error + */ +export const firstAddressFailure = (failurePayload) => ({ + type: types.FIRSTADDRESS_FAILURE, + payload: failurePayload, +}); + +/** + * Signals that the user wants to attempt to retry the nano contract tx send + */ +export const newNanoContractRetry = () => ({ + type: types.WALLETCONNECT_NEW_NANOCONTRACT_RETRY, +}); + +/** + * Signals that the user doesn't want to retry the new nano contract tx send + */ +export const newNanoContractRetryDismiss = () => ({ + type: types.WALLETCONNECT_NEW_NANOCONTRACT_RETRY_DISMISS, +}); + +/** + * Signals update on new nano contract status to ready. + */ +export const setNewNanoContractStatusReady = () => ({ + type: types.WALLETCONNECT_NEW_NANOCONTRACT_STATUS, + payload: WALLETCONNECT_NEW_NANOCONTRACT_TX_STATUS.READY, +}); + +/** + * Signals update on new nano contract status to loading. + */ +export const setNewNanoContractStatusLoading = () => ({ + type: types.WALLETCONNECT_NEW_NANOCONTRACT_STATUS, + payload: WALLETCONNECT_NEW_NANOCONTRACT_TX_STATUS.LOADING, +}); + +/** + * Signals update on new nano contract status to failed. + */ +export const setNewNanoContractStatusFailure = () => ({ + type: types.WALLETCONNECT_NEW_NANOCONTRACT_STATUS, + payload: WALLETCONNECT_NEW_NANOCONTRACT_TX_STATUS.FAILED, +}); + +/** + * Signals update on new nano contract status to successful. + */ +export const setNewNanoContractStatusSuccess = () => ({ + type: types.WALLETCONNECT_NEW_NANOCONTRACT_STATUS, + payload: WALLETCONNECT_NEW_NANOCONTRACT_TX_STATUS.SUCCESSFUL, +}); + +/** + * Signals that the user wants to attempt to retry the create token request + */ +export const createTokenRetry = () => ({ + type: types.WALLETCONNECT_CREATE_TOKEN_RETRY, +}); + +/** + * Signals that the user doesn't want to retry the create token request + */ +export const createTokenRetryDismiss = () => ({ + type: types.WALLETCONNECT_CREATE_TOKEN_RETRY_DISMISS, +}); + +/** + * Signals update on create token status to ready. + */ +export const setCreateTokenStatusReady = () => ({ + type: types.WALLETCONNECT_CREATE_TOKEN_STATUS, + payload: WALLETCONNECT_CREATE_TOKEN_STATUS.READY, +}); + +/** + * Signals update on create token status to loading. + */ +export const setCreateTokenStatusLoading = () => ({ + type: types.WALLETCONNECT_CREATE_TOKEN_STATUS, + payload: WALLETCONNECT_CREATE_TOKEN_STATUS.LOADING, +}); + +/** + * Signals update on create token status to failed. + */ +export const setCreateTokenStatusFailed = () => ({ + type: types.WALLETCONNECT_CREATE_TOKEN_STATUS, + payload: WALLETCONNECT_CREATE_TOKEN_STATUS.FAILED, +}); + +/** + * Signals update on create token status to successful. + */ +export const setCreateTokenStatusSuccessful = () => ({ + type: types.WALLETCONNECT_CREATE_TOKEN_STATUS, + payload: WALLETCONNECT_CREATE_TOKEN_STATUS.SUCCESSFUL, +}); + +/** + * Blueprint Info request in the context of a Nano Contract. + * @param {string} id Blueprint ID. + */ +export const nanoContractBlueprintInfoRequest = (id) => ({ + type: types.NANOCONTRACT_BLUEPRINTINFO_REQUEST, + payload: { id }, +}); + +/** + * Signals the bluprint info request has failed. + * @param {string} id Blueprint ID. + * @param {string} error Request failure reason. + */ +export const nanoContractBlueprintInfoFailure = (id, error) => ({ + type: types.NANOCONTRACT_BLUEPRINTINFO_FAILURE, + payload: { id, error }, +}); + +/** + * Signals the blueprint info was fetched with success. + * @param {string} id Blueprint ID. + * @param {{ + * id: string; + * name: string; + * public_methods: { + * [methodName: string]: { + * args: { + * type: string; + * name: string; + * }[]; + * }; + * }; + * }} blueprintInfo Raw data response from fullnode. + */ +export const nanoContractBlueprintInfoSuccess = (id, blueprintInfo) => ({ + type: types.NANOCONTRACT_BLUEPRINTINFO_SUCCESS, + payload: { id, data: { ...blueprintInfo } }, +}); + +/** + * Signals a request to load a collection of token data by a collection of token UID. + * @param {Object} payload + * @param {string[]} payload.uids A list of token UID. + */ +export const unregisteredTokensDownloadRequest = (payload) => ({ + type: types.UNREGISTEREDTOKENS_DOWNLOAD_REQUEST, + payload, +}); + +/** + * Signals the success of unregistered tokens request. + * @param {Object} payload + * @param {Object} payload.tokens A map of token data by its UID. + */ +export const unregisteredTokensDownloadSuccess = (payload) => ({ + type: types.UNREGISTEREDTOKENS_DOWNLOAD_SUCCESS, + payload, +}); + +/** + * Signals a failure on unregistered tokens request. + * @param {Object} payload + * @param {string} payload.error The error message as feedback to user + */ +export const unregisteredTokensDownloadFailure = (payload) => ({ + type: types.UNREGISTEREDTOKENS_DOWNLOAD_FAILURE, + payload, +}); + +/** + * Signals the unregistered tokens request has ended. + */ +export const unregisteredTokensDownloadEnd = () => ({ + type: types.UNREGISTEREDTOKENS_DOWNLOAD_END, +}); + +export const showSignMessageWithAddressModal = (accept, deny, data, dapp) => ({ + type: types.SHOW_SIGN_MESSAGE_REQUEST_MODAL, + payload: { + accept, + deny, + data, + dapp, + }, +}); + +export const showNanoContractSendTxModal = (accept, deny, nc, dapp) => ({ + type: types.SHOW_NANO_CONTRACT_SEND_TX_MODAL, + payload: { + accept, + deny, + nc, + dapp, + }, +}); + +export const showCreateTokenModal = (accept, deny, data, dapp) => ({ + type: types.SHOW_CREATE_TOKEN_REQUEST_MODAL, + payload: { + accept, + deny, + data, + dapp, + }, +}); diff --git a/src/components/AskForPushNotification.js b/src/components/AskForPushNotification.js index f1a468d89..b091c30a9 100644 --- a/src/components/AskForPushNotification.js +++ b/src/components/AskForPushNotification.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { t } from 'ttag'; diff --git a/src/components/AskForPushNotificationRefresh.js b/src/components/AskForPushNotificationRefresh.js index a613ccf88..e8cbf0173 100644 --- a/src/components/AskForPushNotificationRefresh.js +++ b/src/components/AskForPushNotificationRefresh.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { t } from 'ttag'; diff --git a/src/components/EditInfoContainer.js b/src/components/EditInfoContainer.js new file mode 100644 index 000000000..202e49ccc --- /dev/null +++ b/src/components/EditInfoContainer.js @@ -0,0 +1,72 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { + StyleSheet, + View, + TouchableOpacity +} from 'react-native'; +import { COLORS } from '../styles/themes'; + +import { PenIcon } from './Icons/Pen.icon'; + +export const EditInfoContainer = ({ center, onPress, children }) => ( + + + + {children} + + + + + + +); + +const styles = StyleSheet.create({ + editInfoWrapperCentered: { + alignSelf: 'center', + }, + editInfoContainer: { + paddingVertical: 8, + paddingLeft: 8, + paddingRight: 40, + borderRadius: 8, + backgroundColor: COLORS.white, + }, + editInfoContainerCentered: { + paddingLeft: 40, + }, + alignCenter: { + alignItems: 'center', + }, + editIcon: { + position: 'absolute', + display: 'flex', + justifyContent: 'center', + top: 0, + right: 0, + bottom: 0, + width: 24, + height: '100%', + marginVertical: 8, + marginRight: 8, + }, +}); diff --git a/src/components/FeedbackContent.js b/src/components/FeedbackContent.js new file mode 100644 index 000000000..87c47f200 --- /dev/null +++ b/src/components/FeedbackContent.js @@ -0,0 +1,119 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { + StyleSheet, + View, + Text, +} from 'react-native'; +import { COLORS } from '../styles/themes'; + +/** + * It presents a feedback content to user with title, message, icon and a call to action + * if needed. + * + * @param {Objects} props + * @param {string} props.title A bold, centered text + * @param {string} props.message A regular text underneath title + * @param {Object?} props.icon A react component or react element containing an icon, + * if provided, it renders uppon the title + * @param {Object?} props.action A react component or react element containing a call to action, + * if provided, it renders underneath the content + * @param {boolean} props.offcard Renders a feedback without card style + * @param {boolean} props.offmargin Renders a feedback without margins + * + * @example + * } + * action={} + * /> + */ +export const FeedbackContent = ({ + title, + message, + icon, + action, + offcard, + offmargin, + offbackground +}) => ( + + + + {icon + && ({icon})} + {title} + {message} + {action && (action)} + + + +); + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignSelf: 'stretch', + marginTop: 16, + marginBottom: 45, + marginHorizontal: 16, + backgroundColor: COLORS.backgroundColor, + }, + card: { + borderRadius: 16, + shadowOffset: { height: 2, width: 0 }, + shadowRadius: 4, + shadowColor: COLORS.textColor, + shadowOpacity: 0.08, + }, + offMargin: { + marginTop: 0, + marginBottom: 0, + marginRight: 0, + marginLeft: 0, + }, + offBackground: { + backgroundColor: COLORS.lowContrastDetail, + }, + wrapper: { + overflow: 'scroll', + }, + content: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 45, + /* Play the role of a minimum vertical padding for small screens */ + paddingVertical: 90, + }, + title: { + fontSize: 16, + lineHeight: 20, + fontWeight: 'bold', + paddingBottom: 16, + textAlign: 'center', + }, + message: { + fontSize: 14, + lineHeight: 20, + paddingBottom: 16, + textAlign: 'center', + }, + icon: { + paddingBottom: 16, + }, +}); diff --git a/src/components/FeedbackModal.js b/src/components/FeedbackModal.js index f6b39202d..4509bb187 100644 --- a/src/components/FeedbackModal.js +++ b/src/components/FeedbackModal.js @@ -6,19 +6,53 @@ */ import React from 'react'; -import { Text } from 'react-native'; +import { Text, View } from 'react-native'; import PropTypes from 'prop-types'; import HathorModal from './HathorModal'; +/** + * It renders a modal with some feedback to user. + * + * @param {Object} props + * + * @example + * )} + * text={t`Error while sending transaction.`} + * onDismiss={handleFeedbackModalDismiss} + * action={()} + * /> + */ const FeedbackModal = (props) => ( - + {props.icon} - + {props.text} + {props.action + && ( + + {props.action} + + )} ); +const styles = { + content: { + fontSize: 18, + lineHeight: 21, + paddingTop: 36, + textAlign: 'center', + }, + action: { + width: '100%', + paddingTop: 8, + }, +}; + FeedbackModal.propTypes = { // Icon used on this modal. Usually an image or the Spinner component icon: PropTypes.element.isRequired, @@ -28,6 +62,8 @@ FeedbackModal.propTypes = { // Function to execute on dismissing the modal onDismiss: PropTypes.func, + + action: PropTypes.element, }; export default FeedbackModal; diff --git a/src/components/FrozenTextValue.js b/src/components/FrozenTextValue.js new file mode 100644 index 000000000..8f4056e4b --- /dev/null +++ b/src/components/FrozenTextValue.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { TextValue } from './TextValue'; + +/** + * @param {Object} props + * @param {boolean} props.title It sets font weight to bold and a larger font size + * @param {boolean} props.bold It sets font weight to bold + * @param {boolean} props.oneline It sets numberOfLines to 1 + * @param {boolean} props.shrink It sets flexShrink to 1 + * @param {boolean} props.pb4 It sets padding bottom to 4 + */ +export const FrozenTextValue = ({ title, bold, oneline, shrink, pb4, children }) => ( + + {children} + +); diff --git a/src/components/HathorFlatList.js b/src/components/HathorFlatList.js new file mode 100644 index 000000000..2d7cf5aac --- /dev/null +++ b/src/components/HathorFlatList.js @@ -0,0 +1,61 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* eslint no-unused-vars: ["warn", { "varsIgnorePattern": "Props" }] -- + * Properties like `FlatListProps` are not counted as used, + * however they are important for coding completion during development. + */ + +import React from 'react'; +import { + FlatList, StyleSheet, View, FlatListProps +} from 'react-native'; +import { COLORS } from '../styles/themes'; + +/** + * @param {FlatListProps} props + * @param {Object} props.wrapperStyle A custom style object used to control the wrapper component. + */ +export const HathorFlatList = (props) => ( + + + +); + +const ItemSeparator = () => ( + +); + +const styles = StyleSheet.create({ + wrapper: { + flexShrink: 1, + alignSelf: 'stretch', + marginTop: 16, + marginBottom: 24, + backgroundColor: COLORS.backgroundColor, + marginHorizontal: 16, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + borderBottomLeftRadius: 16, + borderBottomRightRadius: 16, + shadowOffset: { height: 2, width: 0 }, + shadowRadius: 4, + shadowColor: COLORS.textColor, + shadowOpacity: 0.08, + paddingTop: 16, + paddingBottom: 16, + }, + itemSeparator: { + width: '100%', + height: 1, + backgroundColor: COLORS.borderColor + }, +}); diff --git a/src/components/HathorHeader.js b/src/components/HathorHeader.js index d6952d7e3..f6a174b2a 100644 --- a/src/components/HathorHeader.js +++ b/src/components/HathorHeader.js @@ -16,71 +16,125 @@ import chevronLeft from '../assets/icons/chevron-left.png'; import closeIcon from '../assets/icons/icCloseActive.png'; import { COLORS, STYLE } from '../styles/themes'; -const HathorHeader = (props) => { - const renderBackButton = () => { - if (props.onBackPress) { - return ( - - - - - - ); - } - return ; - }; - - const CancelButton = () => ( - +const HathorHeader = ({ + title, + rightElement, + withLogo, + withBorder, + onBackPress, + onCancel, + wrapperStyle, + children, +}) => { + const hasChildren = children != null; + const left = React.Children.toArray(children).find( + (child) => child.type.displayName === HathorHeaderLeft.displayName ); + const central = React.Children.toArray(children).find( + (child) => child.type.displayName === HathorHeaderCentral.displayName + ); + const right = React.Children.toArray(children).find( + (child) => child.type.displayName === HathorHeaderRight.displayName + ); + + return ( + + {hasChildren + && ( + + {left} + {central} + {right} + + )} + {!hasChildren + && ( + + + + + + )} + + ); +}; + +const Wrapper = ({ withBorder, style, children }) => ( + + {children} + +); + +const InnerWrapper = ({ children }) => ( + + {children} + +); + +const HathorHeaderLeft = ({ children }) => ({children}); +HathorHeaderLeft.displayName = 'HathorHeaderLeft'; + +const HathorHeaderCentral = ({ style, children }) => {children}; +HathorHeaderCentral.displayName = 'HathorHeaderCentral'; + +const HathorHeaderRight = ({ children }) => {children}; +HathorHeaderRight.displayName = 'HathorHeaderRight'; + +HathorHeader.Left = HathorHeaderLeft; +HathorHeader.Central = HathorHeaderCentral; +HathorHeader.Right = HathorHeaderRight; + +const CancelButton = ({ onCancel }) => ( + +); - const renderHeaderRight = () => { - const element = (props.onCancel ? : props.rightElement); +const LeftComponent = ({ onBackPress }) => { + if (onBackPress) { return ( - - {element} + + + + ); - }; - - const renderHeaderCentral = () => { - if (props.withLogo) { - return ( - - ); - } - return {props.title}; - }; - - let extraStyle = {}; - if (props.withBorder) { - extraStyle = { borderBottomWidth: 1 }; } + return ; +}; + +const CentralComponent = ({ title, withLogo }) => { + if (withLogo) { + return ( + + ); + } + return {title}; +}; +const RightComponent = ({ rightElement, onCancel }) => { + const element = (onCancel ? : rightElement); return ( - - - {renderBackButton()} - {renderHeaderCentral()} - {renderHeaderRight()} - + + {element} ); }; const styles = StyleSheet.create({ wrapper: { - height: STYLE.headerHeight, + minHeight: STYLE.headerHeight, flexDirection: 'row', alignItems: 'flex-end', borderColor: COLORS.borderColor, paddingHorizontal: 16, }, + wrapperWithBorder: { + borderBottomWidth: 1, + }, innerWrapper: { flex: 1, flexDirection: 'row', @@ -93,5 +147,16 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', }, + iconWrapperStart: { + justifyContent: 'flex-start', + }, + iconWrapperEnd: { + justifyContent: 'flex-end', + }, + centralComponentLogo: { + height: 22, + width: 100, + }, }); + export default HathorHeader; diff --git a/src/components/HathorModal.js b/src/components/HathorModal.js index 9ee9610e2..3f70cdca4 100644 --- a/src/components/HathorModal.js +++ b/src/components/HathorModal.js @@ -23,7 +23,7 @@ const HathorModal = (props) => ( onBackdropPress={props.onDismiss} style={styles.modal} > - + {props.children} @@ -38,9 +38,9 @@ const styles = StyleSheet.create({ borderRadius: 8, alignItems: 'center', paddingHorizontal: 16, - paddingBottom: 56, - paddingTop: 48, - height: 290, + paddingBottom: 24, + paddingTop: 42, + minHeight: 290, }, }); diff --git a/src/components/Icons/ActionDot.icon.js b/src/components/Icons/ActionDot.icon.js new file mode 100644 index 000000000..5680cbb8f --- /dev/null +++ b/src/components/Icons/ActionDot.icon.js @@ -0,0 +1,42 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react' +import Svg, { G, Path, Defs, ClipPath } from 'react-native-svg' +import { COLORS } from '../../styles/themes' +import { DEFAULT_ICON_SIZE } from './constants' +import { getScale, getViewBox } from './helper' + +/** + * @param {object} props + * @property {number} props.size + * @property {StyleSheet} props.color + */ +export const ActionDot = ({ size = DEFAULT_ICON_SIZE, color = COLORS.black }) => ( + + + + + + + + + + +) diff --git a/src/components/Icons/ArrowDown.icon.js b/src/components/Icons/ArrowDown.icon.js new file mode 100644 index 000000000..5c28d5f82 --- /dev/null +++ b/src/components/Icons/ArrowDown.icon.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react' +import { View, Image, StyleSheet } from 'react-native'; +import chevronDown from '../../assets/icons/chevron-down.png'; +import { DEFAULT_ICON_SIZE } from './constants'; + +/** + * @param {object} props + * @property {number} props.size + * @property {StyleSheet} props.style + */ +export const ArrowDownIcon = ({ size = DEFAULT_ICON_SIZE, style }) => ( + + + +); + +const styles = StyleSheet.create({ + /* This wrapper adjusts the icon's size to 24x24, otherwise it would be 12x7. */ + wrapper: { + paddingVertical: 8.5, + paddingHorizontal: 6, + }, +}); diff --git a/src/components/Icons/ArrowUp.icon.js b/src/components/Icons/ArrowUp.icon.js new file mode 100644 index 000000000..1be5fcd89 --- /dev/null +++ b/src/components/Icons/ArrowUp.icon.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react' +import { View, Image, StyleSheet } from 'react-native'; +import chevronUp from '../../assets/icons/chevron-up.png'; +import { DEFAULT_ICON_SIZE } from './constants'; + +/** + * @param {object} props + * @property {number} props.size + * @property {StyleSheet} props.style + */ +export const ArrowUpIcon = ({ size = DEFAULT_ICON_SIZE, style }) => ( + + + +); + +const styles = StyleSheet.create({ + /* This wrapper adjusts the icon's size to 24x24, otherwise it would be 12x7. */ + wrapper: { + paddingVertical: 8.5, + paddingHorizontal: 6, + }, +}); diff --git a/src/components/Icons/Base.icon.js b/src/components/Icons/Base.icon.js new file mode 100644 index 000000000..c505a5c5c --- /dev/null +++ b/src/components/Icons/Base.icon.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { StyleSheet, View } from 'react-native'; +import { COLORS } from '../../styles/themes'; + +/** + * @param {object} props + * @property {'default'|'outline'|'fill'} props.type + * @property {StyleSheet} props.style + * @property {ReactNode} props.children + */ +export const BaseIcon = ({ type = 'default', style, children }) => ( + + {children} + +); + +const styles = StyleSheet.create({ + default: {}, + outline: { + borderRadius: 8, + borderWidth: 1.3, + padding: 4, + }, + fill: { + borderRadius: 8, + borderWidth: 1.3, + borderColor: COLORS.primary, + padding: 4, + backgroundColor: COLORS.primary, + }, +}); diff --git a/src/components/Icons/CircleCheck.icon.js b/src/components/Icons/CircleCheck.icon.js new file mode 100644 index 000000000..48f6895d7 --- /dev/null +++ b/src/components/Icons/CircleCheck.icon.js @@ -0,0 +1,50 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react' +import Svg, { Mask, Path } from 'react-native-svg' +import { COLORS } from '../../styles/themes' +import { DEFAULT_ICON_SIZE } from './constants' +import { getScale, getViewBox } from './helper' + +/** + * @param {object} props + * @property {number} props.size + * @property {StyleSheet} props.color + * + * @description + * Svg converted from Figma using transaformer at https://react-svgr.com/playground/?native=true + */ +export const CircleCheck = ({ size = DEFAULT_ICON_SIZE, color = COLORS.black }) => ( + + + + + + + +) diff --git a/src/components/Icons/CircleClock.icon.js b/src/components/Icons/CircleClock.icon.js new file mode 100644 index 000000000..ec5d03d6b --- /dev/null +++ b/src/components/Icons/CircleClock.icon.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react' +import Svg, { Path } from 'react-native-svg' +import { COLORS } from '../../styles/themes' +import { DEFAULT_ICON_SIZE } from './constants' +import { getScale, getViewBox } from './helper' + +/** + * @param {object} props + * @property {number} props.size + * @property {StyleSheet} props.color + * + * @description + * Svg converted from Figma using transaformer at https://react-svgr.com/playground/?native=true + */ +export const CircleClock = ({ size = DEFAULT_ICON_SIZE, color = COLORS.black }) => ( + + + +) diff --git a/src/components/Icons/CircleError.icon.js b/src/components/Icons/CircleError.icon.js new file mode 100644 index 000000000..383d9b16d --- /dev/null +++ b/src/components/Icons/CircleError.icon.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react' +import Svg, { Path } from 'react-native-svg' +import { COLORS } from '../../styles/themes' +import { DEFAULT_ICON_SIZE } from './constants' +import { getScale, getViewBox } from './helper' + +/** + * @param {object} props + * @property {number} props.size + * @property {StyleSheet} props.color + * + * @description + * Svg converted from Figma using transaformer at https://react-svgr.com/playground/?native=true + */ +export const CircleError = ({ size = DEFAULT_ICON_SIZE, color = COLORS.black }) => ( + + + +) diff --git a/src/components/Icons/CircleInfo.icon.js b/src/components/Icons/CircleInfo.icon.js new file mode 100644 index 000000000..3d934a1f2 --- /dev/null +++ b/src/components/Icons/CircleInfo.icon.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react' +import Svg, { G, Path, Defs, ClipPath } from 'react-native-svg' +import { COLORS } from '../../styles/themes'; + +/** + * @param {object} props + * @property {number} props.size + * @property {StyleSheet} props.color + * + * @description + * Svg converted from Figma using transaformer at https://react-svgr.com/playground/?native=true + */ +export const CircleInfoIcon = ({ size = 20, color = COLORS.white }) => ( + + + + + + + + + + +); diff --git a/src/components/Icons/NanoContract.icon.js b/src/components/Icons/NanoContract.icon.js new file mode 100644 index 000000000..570fec539 --- /dev/null +++ b/src/components/Icons/NanoContract.icon.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react' +import Svg, { Path } from 'react-native-svg' +import { COLORS } from '../../styles/themes'; +import { BaseIcon } from './Base.icon'; +import { DEFAULT_ICON_SIZE } from './constants'; +import { getScale, getViewBox } from './helper'; + +/** + * @param {object} props + * @property {SvgProps|{type: 'default'|'outline'|'fill'}} props.type + * @property {number} props.size + * @property {StyleSheet} props.color + * + * @description + * Svg converted from Figma using transaformer at https://react-svgr.com/playground/?native=true + */ +export const NanoContractIcon = ({ type = 'default', size = DEFAULT_ICON_SIZE, color = COLORS.black }) => ( + + + + + +); diff --git a/src/components/Icons/Oracle.icon.js b/src/components/Icons/Oracle.icon.js new file mode 100644 index 000000000..500177a53 --- /dev/null +++ b/src/components/Icons/Oracle.icon.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react' +import Svg, { Path } from 'react-native-svg' +import { COLORS } from '../../styles/themes'; +import { BaseIcon } from './Base.icon'; +import { DEFAULT_ICON_SIZE } from './constants'; +import { getScale, getViewBox } from './helper'; + +/** + * @param {object} props + * @property {SvgProps|{type: 'default'|'outline'|'fill'}} props.type + * @property {number} props.size + * @property {StyleSheet} props.color + * + * @description + * Svg converted from Figma using transaformer at https://react-svgr.com/playground/?native=true + */ +export const OracleIcon = ({ type = 'default', size = DEFAULT_ICON_SIZE, color = COLORS.black }) => ( + + + + + + +); diff --git a/src/components/Icons/Pen.icon.js b/src/components/Icons/Pen.icon.js new file mode 100644 index 000000000..03d4204c8 --- /dev/null +++ b/src/components/Icons/Pen.icon.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react' +import Svg, { Path } from 'react-native-svg' +import { COLORS } from '../../styles/themes'; +import { BaseIcon } from './Base.icon'; +import { DEFAULT_ICON_SIZE } from './constants'; + +/** + * @param {object} props + * @property {SvgProps|{type: 'default'|'outline'|'fill'}} props.type + * @property {number} props.size + * @property {StyleSheet} props.color + * + * @description + * Svg converted from Figma using transaformer at https://react-svgr.com/playground/?native=true + */ +export const PenIcon = ({ type = 'default', size = DEFAULT_ICON_SIZE, color = COLORS.black }) => ( + + + + + +); diff --git a/src/components/Icons/RandomTx.icon.js b/src/components/Icons/RandomTx.icon.js new file mode 100644 index 000000000..8cc228334 --- /dev/null +++ b/src/components/Icons/RandomTx.icon.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react' +import Svg, { Path } from 'react-native-svg' +import { COLORS } from '../../styles/themes'; +import { BaseIcon } from './Base.icon'; +import { DEFAULT_ICON_SIZE } from './constants'; +import { getScale, getViewBox } from './helper'; + +/** + * @param {object} props + * @property {SvgProps|{type: 'default'|'outline'|'fill'}} props.type + * @property {number} props.size + * @property {StyleSheet} props.color + * + * @description + * Svg converted from Figma using transaformer at https://react-svgr.com/playground/?native=true + */ +export const RandomTxIcon = ({ type = 'default', size = DEFAULT_ICON_SIZE, color = COLORS.black }) => ( + + + + + +); diff --git a/src/components/Icons/Received.icon.js b/src/components/Icons/Received.icon.js new file mode 100644 index 000000000..cf71232b0 --- /dev/null +++ b/src/components/Icons/Received.icon.js @@ -0,0 +1,53 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react' +import Svg, { G, Path, Defs, ClipPath } from 'react-native-svg' +import { COLORS } from '../../styles/themes'; +import { BaseIcon } from './Base.icon'; +import { DEFAULT_ICON_SIZE } from './constants'; +import { getScale, getViewBox } from './helper'; + +/** + * @param {object} props + * @param {'default'|'outline'|'fill'} props.type + * @property {number} props.size + * @property {string} props.color + * @property {string} props.backgroundColor + * + * @description + * Svg converted from Figma using transaformer at https://react-svgr.com/playground/?native=true + */ +export const ReceivedIcon = ({ type = 'default', size = DEFAULT_ICON_SIZE, color = 'hsla(180, 85%, 34%, 1)', backgroundColor = COLORS.white }) => ( + + + + + + + + + + + + +); diff --git a/src/components/Icons/Registered.icon.js b/src/components/Icons/Registered.icon.js new file mode 100644 index 000000000..31f741588 --- /dev/null +++ b/src/components/Icons/Registered.icon.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react' +import Svg, { Path } from 'react-native-svg' +import { COLORS } from '../../styles/themes'; +import { BaseIcon } from './Base.icon'; +import { DEFAULT_ICON_SIZE } from './constants'; +import { getScale, getViewBox } from './helper'; + +/** + * @param {object} props + * @property {SvgProps|{type: 'default'|'outline'|'fill'}} props.type + * @property {number} props.size + * @property {StyleSheet} props.color + * + * @description + * Svg converted from Figma using transaformer at https://react-svgr.com/playground/?native=true + */ +export const RegisteredIcon = ({ type = 'default', size = DEFAULT_ICON_SIZE, color = COLORS.black }) => ( + + + + + + +); diff --git a/src/components/Icons/Sent.icon.js b/src/components/Icons/Sent.icon.js new file mode 100644 index 000000000..ce8ce3129 --- /dev/null +++ b/src/components/Icons/Sent.icon.js @@ -0,0 +1,53 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react' +import Svg, { G, Path, Defs, ClipPath } from 'react-native-svg' +import { COLORS } from '../../styles/themes'; +import { BaseIcon } from './Base.icon'; +import { DEFAULT_ICON_SIZE } from './constants'; +import { getScale, getViewBox } from './helper'; + +/** + * @param {object} props + * @param {'default'|'outline'|'fill'} props.type + * @property {number} props.size + * @property {string} props.color + * @property {string} props.backgroundColor + * + * @description + * Svg converted from Figma using transaformer at https://react-svgr.com/playground/?native=true + */ +export const SentIcon = ({ type = 'default', size = DEFAULT_ICON_SIZE, color = COLORS.black, backgroundColor = COLORS.white }) => ( + + + + + + + + + + + + +); diff --git a/src/components/Icons/Wallet.icon.js b/src/components/Icons/Wallet.icon.js new file mode 100644 index 000000000..a769fbce0 --- /dev/null +++ b/src/components/Icons/Wallet.icon.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from 'react' +import Svg, { Path } from 'react-native-svg' +import { COLORS } from '../../styles/themes'; +import { BaseIcon } from './Base.icon'; +import { DEFAULT_ICON_SIZE } from './constants'; +import { getScale, getViewBox } from './helper'; + +/** + * @param {object} props + * @property {SvgProps|{type: 'default'|'outline'|'fill'}} props.type + * @property {number} props.size + * @property {StyleSheet} props.color + * + * @description + * Svg converted from Figma using transaformer at https://react-svgr.com/playground/?native=true + */ +export const WalletIcon = ({ type = 'default', size = DEFAULT_ICON_SIZE, color = COLORS.black }) => ( + + + + + +); diff --git a/src/components/Icons/constants.js b/src/components/Icons/constants.js new file mode 100644 index 000000000..eacdd0110 --- /dev/null +++ b/src/components/Icons/constants.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export const DEFAULT_ICON_SIZE = 24; diff --git a/src/components/Icons/helper.js b/src/components/Icons/helper.js new file mode 100644 index 000000000..0610ad1c0 --- /dev/null +++ b/src/components/Icons/helper.js @@ -0,0 +1,23 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * @param {number} size + * @example + * getViewBox(24) + * // return `0 0 24 24` + */ +export const getViewBox = (size) => `0 0 ${size} ${size}`; + +/** + * @param {number} size + * @param {number} baseSize + * @example + * getScale(48, 24) + * // return `scale(2)` + */ +export const getScale = (size, baseSize) => `scale(${size / baseSize})`; diff --git a/src/components/ModalBase.js b/src/components/ModalBase.js new file mode 100644 index 000000000..2b24b7acf --- /dev/null +++ b/src/components/ModalBase.js @@ -0,0 +1,116 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { + StyleSheet, + View, + Text, +} from 'react-native'; +import Modal from 'react-native-modal'; + +import { COLORS } from '../styles/themes'; +import NewHathorButton from './NewHathorButton'; + +const ModalBase = ({ styleModal, styleWrapper, show, onDismiss, children }) => { + const hasChildren = children != null; + + const title = hasChildren && React.Children.toArray(children).find( + (child) => child.type.displayName === Title.displayName + ); + const body = hasChildren && React.Children.toArray(children).find( + (child) => child.type.displayName === Body.displayName + ); + const button = hasChildren && React.Children.toArray(children).find( + (child) => child.type.displayName === Button.displayName + ); + const discreteButton = hasChildren && React.Children.toArray(children).find( + (child) => child.type.displayName === DiscreteButton.displayName + ); + + return ( + + + {title && title} + {body && body} + {button && button} + {discreteButton && discreteButton} + + + ); +}; + +const Title = ({ children }) => ( + + + {children} + + +); +Title.displayName = 'ModalBaseTitle'; + +/** + * @param {Object} props + * @property {ReactNode} props.children + * @property {StyleProp} props.style + */ +const Body = ({ style, children }) => ( + + {children} + +); +Body.displayName = 'ModalBaseBody'; + +const Button = ({ title, disabled, secondary, danger, onPress }) => ( + +); +Button.displayName = 'ModalBaseButton'; + +const DiscreteButton = ({ title, onPress }) => ( + +); +DiscreteButton.displayName = 'ModalBaseDiscreteButton'; + +ModalBase.Title = Title; +ModalBase.Body = Body; +ModalBase.Button = Button; +ModalBase.DiscreteButton = DiscreteButton; + +const styles = StyleSheet.create({ + wrapper: { + borderRadius: 8, + paddingVertical: 24, + paddingHorizontal: 16, + backgroundColor: COLORS.white, + }, + titleWrapper: { + paddingBottom: 20, + }, + title: { + color: 'black', + fontSize: 18, + lineHeight: 20, + }, + discreteButton: { + marginTop: 8, + }, +}); + +export { ModalBase } diff --git a/src/components/NanoContract/EditAddressModal.js b/src/components/NanoContract/EditAddressModal.js new file mode 100644 index 000000000..736e5c0bf --- /dev/null +++ b/src/components/NanoContract/EditAddressModal.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { + StyleSheet, + View, + Text, +} from 'react-native'; +import { t } from 'ttag'; +import { COLORS } from '../../styles/themes'; +import { CircleInfoIcon } from '../Icons/CircleInfo.icon'; +import { ModalBase } from '../ModalBase'; +import { TextValue } from '../TextValue'; + +/** + * Use this modal to edit an address. + * + * @param {Object} props + * @param {string} props.item It refers to the address selected + * @param {boolean} props.show It determines if modal must show or hide + * @param {(address:string) => {}} props.onAddressChange + * Function called when an address is selected + * @param {() => {}} props.onDismiss + * Function called to dismiss the modal on user's interaction + * + * @example + * + */ +export const EditAddressModal = ({ item, show, onAddressChange, onDismiss }) => ( + + {t`New Nano Contract Address`} + + + + + + + + {t`This address signs any transaction you create with Nano Contracts method. Switching to a new one means`} + + {' '}{t`all future transactions will use this address.`} + + + + + + {t`Selected Information`} + + + {t`Address`} + {/* the unicode character u00A0 means no-break space. */} + {`:${'\u00A0'}${item.address}`} + + + + + {t`Index`} + {`:${'\u00A0'}${item.index}`} + + + + + onAddressChange(item.address)} + /> + + +); + +/** + * Container for label and value pair components. + * + * @param {Object} props + * @param {object} props.last It determines bottom padding application. + * @param {object} props.children + */ +const FieldContainer = ({ last, children }) => ( + + {children} + +); + +const styles = StyleSheet.create({ + pd0: { + paddingBottom: 0, + }, + pd8: { + paddingBottom: 8, + }, + body: { + paddingBottom: 20, + }, + fieldContainer: { + width: '100%', + paddingBottom: 4, + }, + text: { + fontSize: 14, + lineHeight: 20, + }, + bold: { + fontWeight: 'bold', + }, + infoContainer: { + flexShrink: 1, + flexDirection: 'row', + alignItems: 'center', + marginBottom: 24, + borderRadius: 8, + paddingVertical: 8, + paddingHorizontal: 16, + backgroundColor: 'hsla(203, 100%, 93%, 1)', + }, + infoContent: { + paddingLeft: 8, + }, + selectionContainer: { + borderRadius: 8, + paddingVertical: 8, + paddingHorizontal: 16, + backgroundColor: COLORS.freeze100, + }, +}); diff --git a/src/components/NanoContract/NanoContractDetails.js b/src/components/NanoContract/NanoContractDetails.js new file mode 100644 index 000000000..df3a2dafe --- /dev/null +++ b/src/components/NanoContract/NanoContractDetails.js @@ -0,0 +1,254 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { useNavigation } from '@react-navigation/native'; +import React, { useCallback, useEffect, useState } from 'react'; +import { + StyleSheet, + View, + Image, +} from 'react-native'; +import { useDispatch, useSelector } from 'react-redux'; +import { t } from 'ttag'; +import { NanoContractDetailsHeader } from './NanoContractDetailsHeader'; +import { NanoContractTransactionsListItem } from './NanoContractTransactionsListItem'; +import { COLORS } from '../../styles/themes'; +import { nanoContractAddressChangeRequest, nanoContractHistoryRequest } from '../../actions'; +import { HathorFlatList } from '../HathorFlatList'; +import Spinner from '../Spinner'; +import errorIcon from '../../assets/images/icErrorBig.png'; +import SimpleButton from '../SimpleButton'; +import { FeedbackContent } from '../FeedbackContent'; +import NewHathorButton from '../NewHathorButton'; + +/** + * Retrieves Nano Contract details from Redux. + * + * @param {string} ncId Nano Contract ID + * + * @returns {{ + * txHistory: Object[]; + * isLoading: boolean; + * error: string; + * }} + */ +const getNanoContractDetails = (ncId) => (state) => { + /* Without this default the app breaks after the current Nano Contract unregistration. + * By having a default value the app can render the component normally after unregistration + * and let it step aside while coming back to Dashboard screen. This transition happens + * quickly, therefore the user will not have time to see the default state. + */ + const defaultMeta = { isLoading: false, error: null }; + const txHistory = state.nanoContract.history[ncId] || []; + const { isLoading, error } = state.nanoContract.historyMeta[ncId] || defaultMeta; + return { + txHistory, + isLoading, + error, + }; +} + +/** + * It presents a list of transactions from selected Nano Contract. + * + * @param {Object} props + * @param {Object} props.nc Nano Contract data + * @param {string} props.nc.ncId Nano Contract ID + * @param {string} props.nc.address Default caller address for Nano Contract interaction + */ +export const NanoContractDetails = ({ nc }) => { + const dispatch = useDispatch(); + const navigation = useNavigation(); + + const { + txHistory, + isLoading, + error, + } = useSelector(getNanoContractDetails(nc.ncId)); + const [ncAddress, changeNcAddress] = useState(nc.address); + + const onAddressChange = (newAddress) => { + changeNcAddress(newAddress); + dispatch(nanoContractAddressChangeRequest({ newAddress, ncId: nc.ncId })); + } + + const navigatesToNanoContractTransaction = (tx) => { + navigation.navigate('NanoContractTransactionScreen', { tx }); + }; + + // This effect runs only once when the component is first built. + useEffect(() => { + if (txHistory.length === 0) { + /* The first time we load the Nano Contract details its transaction history is empty. + * The second time it is garanteed that its transaction history is not empty, + * because a Nano Contract must have at least the 'initialize' transaction to exists. + * For the first transaction history load we don't need to specify the `after` param, + * it will be set during the load. + */ + dispatch(nanoContractHistoryRequest({ ncId: nc.ncId })); + } else { + // Fetch new transactions when there are some transactions in the history. + dispatch(nanoContractHistoryRequest({ ncId: nc.ncId, before: txHistory[0].txId })); + } + }, []); + + /** + * Triggered when a user makes the pull gesture on the transaction history content. + */ + const handleNewerTransactions = () => { + dispatch(nanoContractHistoryRequest({ ncId: nc.ncId, before: txHistory[0].txId })); + }; + + /* If an error happens on loading transactions history, an error feedback + * content must be presented to the user, and must have precedence over + * all other content. + */ + const hasError = () => error != null; + const isEmpty = () => txHistory.length === 0 && !hasError(); + const notEmpty = () => !isEmpty() && !hasError(); + + return ( + + + {isLoading && ( + + )} + {hasError() + && ()} + {isEmpty() + && ()} + {notEmpty() + && ( + ( + navigatesToNanoContractTransaction(item)} + /> + )} + keyExtractor={(item) => item.txId} + refreshing={isLoading} + // Enables the pull gesture to get newer transactions + onRefresh={handleNewerTransactions} + // Enables a button to load more of older transactions until the end + // By reaching the end, the button ceases to render + ListFooterComponent={} + extraData={[isLoading, error]} + /> + )} + + ); +}; + +/** + * It shows a button to 'Load More' transactions after the last one. + * It hides the button when the last transaction is the initialize. + * + * @param {Object} prop Properties object + * @param {{ + * ncId: string; + * txId: string; + * }} prop.lastTx A transaction item from transaction history + */ +const LoadMoreButton = ({ lastTx }) => { + const dispatch = useDispatch(); + const isInitializeTx = lastTx.ncMethod === 'initialize'; + + /** + * This handling will dispatch an action to request for + * older transactions after a txId. + */ + const handleLoadMore = () => useCallback(dispatch(nanoContractHistoryRequest({ + ncId: lastTx.ncId, + after: lastTx.txId, + })), [lastTx]); + + return !isInitializeTx && ( + + ) +}; + +/** + * @param {Object} props + * @param {Object?} props.children Either a react component or a react element + */ +const Wrapper = ({ children }) => ( + + {children} + +); + +/** + * Renders a feedback for the lack of transactions. + */ +export const NoNanoContractTransaction = () => ( + +); + +/** + * Renders an error feedback for when the history load request goes wrong, + * and provides a call to action to try fetch the history again. + * + * @param {Object} props + * @param {string} props.ncId Nano Contract ID + * @param {string} props.error + */ +export const ErrorLoadingTransaction = ({ ncId, error }) => ( + } + action={} + /> +); + +/** + * Renders a call to action to request history loading again. + * + * @param {Object} props + * @param {string} props.ncId Nano Contract ID + */ +export const TryAgain = ({ ncId }) => { + const dispatch = useDispatch(); + const fetchTransactionsHistory = () => { + dispatch(nanoContractHistoryRequest({ ncId, after: null })); + }; + + return ( + + ); +}; + +const styles = StyleSheet.create({ + wrapper: { + flex: 1, + justifyContent: 'flex-start', + alignSelf: 'stretch', + backgroundColor: COLORS.lowContrastDetail, // Defines an outer area on the main list content + }, + center: { + alignSelf: 'center', + }, +}); diff --git a/src/components/NanoContract/NanoContractDetailsHeader.js b/src/components/NanoContract/NanoContractDetailsHeader.js new file mode 100644 index 000000000..19e676623 --- /dev/null +++ b/src/components/NanoContract/NanoContractDetailsHeader.js @@ -0,0 +1,267 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useState } from 'react'; +import { + StyleSheet, + View, + Text, + TouchableWithoutFeedback, + Linking, +} from 'react-native'; +import { useSelector } from 'react-redux'; +import { t } from 'ttag'; +import { COLORS } from '../../styles/themes'; +import { combineURLs, getShortContent, getShortHash } from '../../utils'; +import SimpleButton from '../SimpleButton'; +import HathorHeader from '../HathorHeader'; +import { NanoContractIcon } from '../Icons/NanoContract.icon'; +import { ArrowDownIcon } from '../Icons/ArrowDown.icon'; +import { ArrowUpIcon } from '../Icons/ArrowUp.icon'; +import { TextValue } from '../TextValue'; +import { TextLabel } from '../TextLabel'; +import { EditInfoContainer } from '../EditInfoContainer'; +import { SelectAddressModal } from './SelectAddressModal'; +import { UnregisterNanoContractModal } from './UnregisterNanoContractModal'; + +/** + * It presents the header for Nano Contract Details screen and provides the following + * actions to users: + * - Open Nano Contract details at the Explorer + * - Edit the registered address for the Nano Contract + * - Unregister the Nano Contract + * + * @param {Object} props + * @param {Object} props.nc Nano Contract data + * @param {string} props.address Default address selected + * @param {(address:string) => {}} props.onAddressChange Function called when address changes + */ +export const NanoContractDetailsHeader = ({ nc, address, onAddressChange }) => { + const [isShrank, toggleShrank] = useState(true); + const [selectedAddress, setSelectedAddress] = useState(address); + const [showSelectAddressModal, setShowSelectAddressModal] = useState(false); + const [showUnregisterNanoContractModal, setShowUnregisterNanoContractModal] = useState(false); + + const isExpanded = () => !isShrank; + + const onEditAddress = () => { + setShowSelectAddressModal(true); + }; + + const toggleSelectAddressModal = () => { + setShowSelectAddressModal(!showSelectAddressModal); + }; + + const onUnregisterNanoContract = () => { + setShowUnregisterNanoContractModal(true); + }; + + const toggleUnregisterNanoContractModal = () => { + setShowUnregisterNanoContractModal(!showUnregisterNanoContractModal); + }; + + const handleSelectAddress = (pickedAddress) => { + setSelectedAddress(pickedAddress); + toggleSelectAddressModal(); + onAddressChange(pickedAddress); + }; + + return ( + + + toggleShrank(!isShrank)}> + + + {t`Nano Contract`} + {isShrank + && } + {isExpanded() + && ( + + )} + + + + + + + ) +}; + +const HeaderIcon = () => ( + + + +); + +const HeaderShrank = () => ( + +); + +/** + * Presents all the information. + * + * @param {Object} props + * @param {Object} props.nc Nano Contract data + * @param {string} props.address Current address + * @param {() => void} props.onEditAddress Function called on address edition trigger + * @param {() => void} props.onUnregisterNanoContract Function called on unregister trigger + */ +const HeaderExpanded = ({ nc, address, onEditAddress, onUnregisterNanoContract }) => { + const baseExplorerUrl = useSelector((state) => state.networkSettings.explorerUrl); + + const navigatesToExplorer = () => { + const txUrl = `transaction/${nc.ncId}`; + const explorerLink = combineURLs(baseExplorerUrl, txUrl); + Linking.openURL(explorerLink); + }; + + return ( + <> + + + {getShortHash(nc.ncId, 7)} + {t`Nano Contract ID`} + + + {nc.blueprintName} + {t`Blueprint Name`} + + + {getShortContent(address, 7)} + {t`Registered Address`} + + + + + + + + + ) +}; + +/** + * Container for value and label pair components. + * + * @param {Object} props + * @param {Object} props.children + */ +const InfoContainer = ({ children }) => ( + + {children} + +); + +/** + * It presents two button options inline. + * + * @param {Object} props + * @param {Object} props.children + */ +const TwoActionsWrapper = ({ children }) => ( + + {children} + +); + +/** + * Text button in primary color and style. + * + * @param {Object} props + * @param {string} props.title Text content + * @param {() => void} props.onPress Callback for interaction + */ +const PrimaryTextButton = ({ title, onPress }) => ( + +); + +/** + * Text button in red. + * + * @param {Object} props + * @param {string} props.title Text content + * @param {() => void} props.onPress Callback for interaction + */ +const DenyTextButton = ({ title, onPress }) => ( + +); + +const styles = StyleSheet.create({ + headerCentral: { + flex: 1, + alignItems: 'center', + paddingTop: 24, + }, + headerWrapper: { + alignItems: 'center', + }, + headerTitle: { + fontSize: 18, + lineHeight: 20, + fontWeight: 'bold', + paddingVertical: 16, + }, + wrapper: { + paddingHorizontal: 16, + paddingBottom: 16, + }, + infoContainer: { + alignItems: 'center', + paddingBottom: 16, + }, + lastElement: { + paddingBottom: 0, + }, + TwoActionsWrapper: { + flexDirection: 'row', + flexWrap: 'nowrap', + }, + buttonWrapper: { + /* It also increases touch area. */ + paddingTop: 24, + }, + buttonText: { + fontWeight: 'bold', + }, + buttonUnregister: { + marginStart: 24, + color: 'hsla(0, 100%, 41%, 1)', + }, + buttonDetails: { + display: 'inline-block', + /* We are using negative margin here to correct the text position + * and create an optic effect of alignment. */ + marginBottom: -2, + borderBottomWidth: 1, + borderColor: COLORS.primary, + }, +}); diff --git a/src/components/NanoContract/NanoContractTransactionActionList.js b/src/components/NanoContract/NanoContractTransactionActionList.js new file mode 100644 index 000000000..2fee5092c --- /dev/null +++ b/src/components/NanoContract/NanoContractTransactionActionList.js @@ -0,0 +1,61 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { StyleSheet, View } from 'react-native'; +import { t } from 'ttag'; +import { COLORS } from '../../styles/themes'; +import { NanoContractTransactionActionListItem } from './NanoContractTransactionActionListItem'; +import { HathorFlatList } from '../HathorFlatList'; +import { FeedbackContent } from '../FeedbackContent'; + +/** + * It presents a list of actions of a transaction. + * + * @param {Object} props + * @param {Object} props.tx Transaction data + */ +export const NanoContractTransactionActionList = ({ tx }) => { + const isEmpty = () => tx.actions.length === 0; + const notEmpty = () => !isEmpty(); + + return ( + + {isEmpty() + && ()} + {notEmpty() + && ( + ( + + )} + /> + )} + + ); +}; + +const NoActions = () => ( + +); + +const Wrapper = ({ children }) => ( + + {children} + +); + +const styles = StyleSheet.create({ + wrapper: { + flex: 1, + alignSelf: 'stretch', + backgroundColor: COLORS.lowContrastDetail, // Defines an outer area on the main list content + }, +}); diff --git a/src/components/NanoContract/NanoContractTransactionActionListItem.js b/src/components/NanoContract/NanoContractTransactionActionListItem.js new file mode 100644 index 000000000..747ef1608 --- /dev/null +++ b/src/components/NanoContract/NanoContractTransactionActionListItem.js @@ -0,0 +1,184 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { + TouchableHighlight, + StyleSheet, + View, + Text, +} from 'react-native'; +import { useSelector } from 'react-redux'; +import { t } from 'ttag'; +import { NANO_CONTRACT_ACTION } from '../../constants'; +import { COLORS } from '../../styles/themes'; +import { getShortHash, isTokenNFT, renderValue } from '../../utils'; +import { ReceivedIcon } from '../Icons/Received.icon'; +import { SentIcon } from '../Icons/Sent.icon'; + +/** + * Retrieves token symbol, otherwise returns a shortened token hash. + * + * @param {string} tokenUid Token hash + * @returns {(state) => boolean} Callback that takes state as input + * + * Remarks: + * This function should be used combined with `useSelector`. + */ +function getTokenSymbol(tokenUid) { + return (state) => { + const tokens = state.tokens || {}; + if (tokenUid in tokens) { + return tokens[tokenUid].symbol; + } + return getShortHash(tokenUid, 7); + }; +} + +/** + * Checks if the referred token is an NFT. + * + * @param {string} tokenUid Token hash + * @returns {(state) => boolean} Callback that takes state as input + * + * Remarks: + * This function should be used combined with `useSelector`. + */ +function checkIsTokenNft(tokenUid) { + return (state) => isTokenNFT(tokenUid, state.tokenMetadata || {}); +} + +/** + * It renders the item of actions list of a Nano Contract transaction. + * + * @param {Object} props + * @param {{ + * type: string; + * uid: string; + * amount: number; + * }} props.item A transaction action + */ +export const NanoContractTransactionActionListItem = ({ item }) => { + const tokenSymbol = useSelector(getTokenSymbol(item.uid)); + const isNft = useSelector(checkIsTokenNft(item.uid)); + + return ( + + + + + + ); +}; + +const Wrapper = ({ children }) => ( + + {children} + +); + +/** + * It renders the balance icon, either sent or received. + * + * @param {Object} props + * @param {'deposit'|'withdrawal'} props.type An action type + */ +const Icon = ({ type }) => { + const iconMap = { + deposit: SentIcon({ type: 'default' }), + withdrawal: ReceivedIcon({ type: 'default' }), + }; + + return (iconMap[type]); +}; + +/** + * Renders item core content. + * + * @param {Object} props + * @property {string} props.tokenSymbol The symbol that represents a token + * @property {'deposit'|'withdrawal'} props.type An action type + */ +const ContentWrapper = ({ tokenSymbol, type }) => { + const contentMap = { + deposit: t`Deposit ${tokenSymbol}`, + withdrawal: t`Withdrawal ${tokenSymbol}`, + }; + + return ( + + {contentMap[type]} + + ); +}; + +/** + * It presents the token's amount using the right style. + * + * @param {Object} props + * @param {number} props.amount Action amount as integer + * @param {boolean} props.isNft True when it is an NFT, false otherwise + * @param {'deposit'|'withdrawal'} props.type An action type + */ +const TokenAmount = ({ amount, isNft, type }) => { + const isReceivingToken = type === NANO_CONTRACT_ACTION.withdrawal; + const amountToRender = renderValue(amount, isNft); + + return ( + + + {amountToRender} + + + ) +}; + +const styles = StyleSheet.create({ + wrapper: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + flexWrap: 'wrap', + width: '100%', + paddingVertical: 24, + paddingHorizontal: 16, + }, + contentWrapper: { + maxWidth: '80%', + flexDirection: 'column', + alignItems: 'flex-start', + justifyContent: 'space-between', + marginRight: 'auto', + paddingHorizontal: 16, + }, + text: { + fontSize: 14, + lineHeight: 20, + paddingBottom: 6, + color: 'hsla(0, 0%, 38%, 1)', + }, + property: { + paddingBottom: 4, + fontWeight: 'bold', + color: 'black', + }, + amountWrapper: { + marginLeft: 'auto', + }, + amount: { + fontSize: 16, + lineHeight: 20, + }, + amountReceived: { + color: 'hsla(180, 85%, 34%, 1)', + fontWeight: 'bold', + }, +}); diff --git a/src/components/NanoContract/NanoContractTransactionHeader.js b/src/components/NanoContract/NanoContractTransactionHeader.js new file mode 100644 index 000000000..f662638ce --- /dev/null +++ b/src/components/NanoContract/NanoContractTransactionHeader.js @@ -0,0 +1,204 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useState } from 'react'; +import { + StyleSheet, + View, + TouchableWithoutFeedback, + Linking, + Text, +} from 'react-native'; +import { t } from 'ttag'; +import { useSelector } from 'react-redux'; +import HathorHeader from '../HathorHeader'; +import { COLORS } from '../../styles/themes'; +import { combineURLs, getShortContent, getShortHash, getTimestampFormat } from '../../utils'; +import SimpleButton from '../SimpleButton'; +import { ArrowDownIcon } from '../Icons/ArrowDown.icon'; +import { ArrowUpIcon } from '../Icons/ArrowUp.icon'; +import { TextValue } from '../TextValue'; +import { TextLabel } from '../TextLabel'; +import { TransactionStatusLabel } from '../TransactionStatusLabel'; + +/** + * It presents the header of Nano Contract Transaction screen. + * + * @param {Obejct} props + * @param {Obejct} props.tx Transaction data + */ +export const NanoContractTransactionHeader = ({ tx }) => { + const [isShrank, toggleShrank] = useState(false); // shows expanded header by default + + return ( + + + toggleShrank(!isShrank)}> + + + {getShortHash(tx.txId, 7)} + {t`Transaction ID`} + + {isShrank ? : } + + + + + ) +}; + +const HeaderShrank = () => ( + +); + +/** + * It presents the expanded header of Nano Contract Transaction screen + * containing contextual information about the Nano Contract and the transaction. + * + * @param {Obejct} props + * @param {Obejct} props.tx Transaction data + */ +const HeaderExpanded = ({ tx }) => { + const baseExplorerUrl = useSelector((state) => state.networkSettings.explorerUrl); + const ncId = getShortHash(tx.ncId, 7); + const callerAddr = getShortContent(tx.caller, 7); + const hasFirstBlock = tx.firstBlock != null; + + const navigatesToExplorer = () => { + const txUrl = `transaction/${tx.txId}`; + const explorerLink = combineURLs(baseExplorerUrl, txUrl); + Linking.openURL(explorerLink); + }; + + return ( + <> + + + + + + {ncId} + {t`Nano Contract ID`} + + + {tx.ncMethod} + {t`Blueprint Method`} + + + {getTimestampFormat(tx.timestamp)} + {t`Date and Time`} + + + {callerAddr} + {tx.isMine + && ( + + {t`From this wallet`} + + )} + {t`Caller`} + + + + + + + + ) +}; + +/** + * Container for value and label pair components. + * + * @param {Object} props + * @param {Object} props.children + */ +const InfoContainer = ({ lastElement, children }) => ( + + {children} + +); + +/** + * It presents the action button as inline text. It can contain at maximum two actions. + * + * @param {Object} props + * @param {Object} props.children + */ +const ActionsWrapper = ({ children }) => ( + + {children} + +); + +/** + * Text button in primary color and style. + * + * @param {Object} props + * @param {string} props.title Text content + * @param {() => void} props.onPress Callback for interaction + */ +const PrimaryTextButton = ({ title, onPress }) => ( + +); + +const styles = StyleSheet.create({ + headerCentral: { + flex: 1, + alignItems: 'center', + paddingTop: 24, + }, + headerWrapper: { + alignItems: 'center', + }, + wrapper: { + paddingHorizontal: 16, + paddingBottom: 16, + }, + infoContainer: { + alignItems: 'center', + paddingBottom: 16, + }, + lastElement: { + paddingBottom: 0, + }, + actionsWrapper: { + flexDirection: 'row', + justifyContent: 'center', + flexWrap: 'nowrap', + }, + buttonWrapper: { + paddingTop: 24, + }, + buttonText: { + fontWeight: 'bold', + }, + buttonDetails: { + display: 'inline-block', + /* We are using negative margin here to correct the text position + * and create an optic effect of alignment. */ + marginBottom: -2, + borderBottomWidth: 1, + borderColor: COLORS.primary, + }, + headlineLabel: { + marginVertical: 6, + borderRadius: 20, + paddingHorizontal: 12, + paddingVertical: 2, + backgroundColor: COLORS.freeze100, + }, + isMineLabel: { + fontSize: 12, + lineHeight: 20, + }, +}); diff --git a/src/components/NanoContract/NanoContractTransactionsListItem.js b/src/components/NanoContract/NanoContractTransactionsListItem.js new file mode 100644 index 000000000..7f5cb67af --- /dev/null +++ b/src/components/NanoContract/NanoContractTransactionsListItem.js @@ -0,0 +1,124 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { TouchableHighlight, StyleSheet, View, Text, Image } from 'react-native'; +import { t } from 'ttag'; + +import chevronRight from '../../assets/icons/chevron-right.png'; +import { COLORS } from '../../styles/themes'; +import { getShortHash, getTimestampFormat } from '../../utils'; + +/** + * It renders an item of Nano Contract Transactions list. + * + * @param {Object} props + * @param {Object} props.item Nano Contract transaction data + * @param {() => void} props.onPress Callback function called on press + */ +export const NanoContractTransactionsListItem = ({ item, onPress }) => ( + + + + +); + +const Wrapper = ({ onPress, children }) => ( + + {children} + +); + +/** + * It presents summarized transaction information. + * + * @param {Object} props + * @param {Object} props.tx Nano Contract transaction data + * @param {string} props.tx.txId + * @param {string} props.tx.ncMethod + * @param {boolean} props.tx.isMine + * @param {number} props.tx.timestamp + */ +const TransactionSummary = ({ tx }) => ( + + + + {getShortHash(tx.txId, 7)} + + {tx.isMine + && ( + + {t`From this wallet`} + + )} + + {tx.ncMethod} + {getTimestampFormat(tx.timestamp)} + +); + +const ArrowRight = () => ( + + + +); + +const styles = StyleSheet.create({ + wrapper: { + paddingVertical: 16, + paddingHorizontal: 16, + flexDirection: 'row', + alignItems: 'center', + }, + contentWrapper: { + flexShrink: 1, + maxWidth: '80%', + flexDirection: 'column', + alignItems: 'flex-start', + justifyContent: 'space-between', + marginRight: 'auto', + /* Add optical effect of simmetry with array icon */ + paddingLeft: 8, + }, + contentHeadline: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + paddingBottom: 4, + }, + headlineLabel: { + marginLeft: 8, + borderRadius: 20, + paddingHorizontal: 12, + paddingVertical: 2, + backgroundColor: COLORS.freeze100, + }, + isMineLabel: { + fontSize: 12, + lineHeight: 20, + }, + text: { + fontSize: 14, + lineHeight: 20, + paddingBottom: 6, + color: 'hsla(0, 0%, 38%, 1)', + }, + property: { + fontWeight: 'bold', + color: 'black', + }, + padding0: { + paddingBottom: 0, + }, +}); diff --git a/src/components/NanoContract/NanoContractsList.js b/src/components/NanoContract/NanoContractsList.js new file mode 100644 index 000000000..418797083 --- /dev/null +++ b/src/components/NanoContract/NanoContractsList.js @@ -0,0 +1,103 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { StyleSheet, View, Text } from 'react-native'; +import { t } from 'ttag'; +import { useNavigation } from '@react-navigation/native'; +import { useSelector } from 'react-redux'; +import { COLORS } from '../../styles/themes'; +import HathorHeader from '../HathorHeader'; +import { HathorFlatList } from '../HathorFlatList'; +import { RegisterNanoContract } from './RegisterNewNanoContractButton'; +import { NanoContractsListItem } from './NanoContractsListItem'; +import { FeedbackContent } from '../FeedbackContent'; + +/** + * It selects the list of registered Nano Contracts. + * + * @param {Object} state Redux root state + * @returns {Object} Array of registered Nano Contract with basic information + */ +const getRegisteredNanoContracts = (state) => { + const { registered } = state.nanoContract; + return Object.values(registered); +} + +/** + * It presents a list of Nano Contracts or an empty content. + */ +export const NanoContractsList = () => { + const registeredNanoContracts = useSelector(getRegisteredNanoContracts); + const navigation = useNavigation(); + + const navigatesToNanoContractTransactions = (nc) => { + navigation.navigate('NanoContractDetailsScreen', { ncId: nc.ncId }); + }; + const isEmpty = () => registeredNanoContracts.length === 0; + const notEmpty = () => !isEmpty(); + + return ( + +
+ {isEmpty() + && } + {notEmpty() + && ( + ( + navigatesToNanoContractTransactions(item)} + /> + )} + keyExtractor={(nc) => nc.ncId} + /> + )} + + ); +}; + +const Wrapper = ({ children }) => ( + + {children} + +); + +const Header = () => ( + + + {t`Nano Contracts`} + + + + + +); + +export const NoNanoContracts = () => ( + } + /> +); + +const styles = StyleSheet.create({ + wrapper: { + flex: 1, + justifyContent: 'flex-start', + alignItems: 'center', + backgroundColor: COLORS.lowContrastDetail, // Defines an outer area on the main list content + }, + headerTitle: { + fontSize: 24, + lineHeight: 24, + fontWeight: 'bold', + }, +}); diff --git a/src/components/NanoContract/NanoContractsListItem.js b/src/components/NanoContract/NanoContractsListItem.js new file mode 100644 index 000000000..4506530b2 --- /dev/null +++ b/src/components/NanoContract/NanoContractsListItem.js @@ -0,0 +1,103 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { TouchableHighlight, StyleSheet, View, Text, Image } from 'react-native'; +import { t } from 'ttag'; + +import chevronRight from '../../assets/icons/chevron-right.png'; +import { COLORS } from '../../styles/themes'; +import { getShortHash } from '../../utils'; +import { NanoContractIcon } from '../Icons/NanoContract.icon'; + +/** + * It renders an item of Nano Contracts list. + * + * @param {Object} props + * @param {Object} props.item Registered Nano Contract data + * @param {() => {}} props.onPress Callback function called on press + */ +export const NanoContractsListItem = ({ item, onPress }) => ( + + + + + +); + +const Wrapper = ({ onPress, children }) => ( + + {children} + +); + +const Icon = () => ( + + + +); + +/** + * It presents summarized Nano Contract information. + * + * @param {Object} props + * @param {Object} props.nc Registered Nano Contract data + * @param {string} props.nc.ncId + * @param {string} props.nc.blueprintName + */ +const NanoContractSummary = ({ nc }) => ( + + {t`Nano Contract ID`} + {getShortHash(nc.ncId, 7)} + {t`Blueprint Name`} + {nc.blueprintName} + +); + +const ArrowRight = () => ( + + + +); + +const styles = StyleSheet.create({ + wrapper: { + paddingVertical: 16, + paddingHorizontal: 16, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + contentWrapper: { + maxWidth: '80%', + flexDirection: 'column', + alignItems: 'flex-start', + justifyContent: 'space-between', + marginRight: 'auto', + paddingHorizontal: 16, + }, + icon: { + alignSelf: 'flex-start', + }, + text: { + fontSize: 14, + lineHeight: 20, + paddingBottom: 6, + color: COLORS.textLabel, + }, + property: { + paddingBottom: 4, + fontWeight: 'bold', + color: 'black', + }, + padding0: { + paddingBottom: 0, + }, +}); diff --git a/src/components/NanoContract/RegisterNewNanoContractButton.js b/src/components/NanoContract/RegisterNewNanoContractButton.js new file mode 100644 index 000000000..a924dbced --- /dev/null +++ b/src/components/NanoContract/RegisterNewNanoContractButton.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { t } from 'ttag'; +import { useNavigation } from '@react-navigation/native'; + +import SimpleButton from '../SimpleButton'; + +export const RegisterNanoContract = () => { + const navigation = useNavigation(); + const navigatesToRegisterNanoContract = () => { + navigation.navigate('RegisterNanoContract'); + }; + + return ( + + ); +}; diff --git a/src/components/NanoContract/SelectAddressModal.js b/src/components/NanoContract/SelectAddressModal.js new file mode 100644 index 000000000..ac98a902e --- /dev/null +++ b/src/components/NanoContract/SelectAddressModal.js @@ -0,0 +1,233 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useEffect, useState } from 'react'; +import { + StyleSheet, + View, + FlatList, + TouchableHighlight, + Text, + Image, +} from 'react-native'; +import { t } from 'ttag'; +import { useDispatch, useSelector } from 'react-redux'; +import { COLORS } from '../../styles/themes'; +import { ModalBase } from '../ModalBase'; +import { TextValue } from '../TextValue'; +import { TextLabel } from '../TextLabel'; +import { EditAddressModal } from './EditAddressModal'; +import { FeedbackContent } from '../FeedbackContent'; +import errorIcon from '../../assets/images/icErrorBig.png'; +import { selectAddressAddressesRequest } from '../../actions'; + +/** + * Use this modal to select an address from the wallet. + * + * @param {Object} props + * @param {string} props.address It refers to the address selected + * @param {boolean} props.show It determines if modal must show or hide + * @param {(address:string) => {}} props.onSelectAddress + * Callback function called when an address is selected + * @param {() => {}} props.onDismiss + * Callback function called to dismiss the modal + * + * @example + * + */ +export const SelectAddressModal = ({ address, show, onSelectAddress, onDismiss }) => { + const dispatch = useDispatch(); + const { addresses, error } = useSelector((state) => state.selectAddressModal); + + const [selectedItem, setSelectedItem] = useState({ address }); + const [showEditAddressModal, setShowEditAddressModal] = useState(false); + + const toggleEditAddressModal = () => { + setShowEditAddressModal(!showEditAddressModal); + }; + + const onDismissEditAddressModal = () => { + setSelectedItem({ address }); + toggleEditAddressModal(); + }; + + // This method is only called by AddressItem if the selected address + // is different from the current one. + const onSelectItem = (item) => { + setSelectedItem(item); + toggleEditAddressModal(); + }; + + const hookAddressChange = (selectedAddress) => { + toggleEditAddressModal(); + onSelectAddress(selectedAddress); + }; + + useEffect(() => { + dispatch(selectAddressAddressesRequest()); + }, []); + + const hasFailed = () => error; + const isLoading = () => !error && addresses.length === 0; + const hasLoaded = () => !error && addresses.length > 0; + + return ( + + {t`Choose New Wallet Address`} + + + {hasFailed() + && ( + )} + title={t`Load Addresses Error`} + message={error} + offcard + /> + )} + {isLoading() + && ( + + )} + {hasLoaded() + && ( + <> + + {t`Current Information`} + {t`To change, select other address on the list below.`} + + {t`Address`} + {/* the unicode character u00A0 means no-break space. */} + {`:${'\u00A0'}${address}`} + + + ( + + )} + keyExtractor={(item) => item.address} + /> + + )} + + {showEditAddressModal + && ( + + )} + + + ); +}; + +/** + * It renders and address as an item of the list, also it indicates a match + * between the current address and the item's address. + * + * @param {Object} props + * @param {string} props.currentAddress It refers to the address selected + * @param {Object} props.item Address of the item + * @param {string} props.item.address + * @param {number} props.item.index + * @param {(address:string) => void} props.onSelectItem + * Callback function called when an address is selected + */ +const AddressItem = ({ currentAddress, item, onSelectItem }) => { + const onPress = () => { + if (currentAddress === item.address) { + return; + } + onSelectItem(item); + }; + return ( + + + + {item.address} + {t`index`} {item.index} + + + + ) +}; + +const styles = StyleSheet.create({ + modal: { + justifyContent: 'flex-end', + marginHorizontal: 0, + }, + wrapper: { + height: '90%', + }, + body: { + flex: 1, + paddingBottom: 20, + }, + bodyWrapper: { + flex: 1, + }, + infoWrapper: { + borderRadius: 8, + backgroundColor: COLORS.freeze100, + paddingVertical: 8, + paddingHorizontal: 16, + marginBottom: 16, + }, + infoText: { + fontSize: 14, + lineHeight: 20, + color: COLORS.black, + paddingBottom: 8, + }, + textBold: { + fontWeight: 'bold', + }, + feedbackContentIcon: { + height: 36, + width: 36, + }, +}); + +const addressItemStyle = StyleSheet.create({ + wrapper: { + paddingVertical: 16, + paddingHorizontal: 16, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + selected: { + backgroundColor: COLORS.freeze100, + }, +}); diff --git a/src/components/NanoContract/UnregisterNanoContractModal.js b/src/components/NanoContract/UnregisterNanoContractModal.js new file mode 100644 index 000000000..2ce5ba1c4 --- /dev/null +++ b/src/components/NanoContract/UnregisterNanoContractModal.js @@ -0,0 +1,69 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { + StyleSheet, + Text, +} from 'react-native'; +import { useDispatch } from 'react-redux'; +import { t } from 'ttag'; +import { useNavigation } from '@react-navigation/native'; +import { nanoContractUnregisterRequest } from '../../actions'; +import { ModalBase } from '../ModalBase'; + +/** + * Presents a modal to confirm unregistration of the Nano Contract. + * + * @param {Object} props + * @param {Object} props.ncId Nano Contract ID + * @param {Object} props.show It determines if modal must show or hide + * @param {() => void} props.onDimiss Callback function called to dismiss the modal + */ +export const UnregisterNanoContractModal = ({ ncId, show, onDismiss }) => { + const dispatch = useDispatch(); + const navigation = useNavigation(); + + const onUnregisterContract = () => { + onDismiss(); + dispatch(nanoContractUnregisterRequest({ ncId })); + navigation.navigate('Dashboard'); + }; + + return ( + + {t`Unregister Nano Contract`} + + {t`Are you sure you want to unregister this Nano Contract?`} + + + + + ); +}; + +const styles = StyleSheet.create({ + body: { + paddingBottom: 20, + }, + fieldContainer: { + width: '100%', + paddingBottom: 4, + }, + text: { + fontSize: 14, + lineHeight: 20, + }, +}); diff --git a/src/components/NewHathorButton.js b/src/components/NewHathorButton.js index a1603e949..1afe5e5d1 100644 --- a/src/components/NewHathorButton.js +++ b/src/components/NewHathorButton.js @@ -21,6 +21,11 @@ const NewHathorButton = (props) => { textStyle.push(style.textDisabled); } + if (props.discrete) { + wrapperViewStyle.push(style.wrapperDiscrete); + textStyle.push(style.textDiscrete); + } + if (props.secondary) { wrapperViewStyle.push(style.wrapperSecondary); textStyle.push(style.textSecondary); @@ -34,6 +39,11 @@ const NewHathorButton = (props) => { } } + if (props.danger) { + wrapperViewStyle.push(style.wrapperSecondaryDanger); + textStyle.push(style.textSecondaryDanger); + } + return ( @@ -79,6 +89,14 @@ const style = StyleSheet.create({ wrapperSecondaryDisabled: { borderColor: COLORS.textColorShadow, }, + wrapperDiscrete: { + backgroundColor: COLORS.backgroundColor, + borderColor: COLORS.backgroundColor, + borderWidth: 1.5, + }, + wrapperSecondaryDanger: { + borderColor: COLORS.errorBgColor, + }, touchable: { flex: 1, flexDirection: 'row', @@ -101,6 +119,12 @@ const style = StyleSheet.create({ textDisabled: { color: COLORS.textColorShadow, }, + textDiscrete: { + color: COLORS.freeze300, + }, + textSecondaryDanger: { + color: COLORS.errorBgColor, + }, }); export default NewHathorButton; diff --git a/src/components/PublicExplorerListButton.js b/src/components/PublicExplorerListButton.js index 827b33c32..338ed6c55 100644 --- a/src/components/PublicExplorerListButton.js +++ b/src/components/PublicExplorerListButton.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import React from 'react'; import { t } from 'ttag'; import { Image, Linking } from 'react-native'; @@ -7,8 +14,9 @@ import { ListButton } from './HathorList'; import { COLORS } from '../styles/themes'; import { combineURLs } from '../utils'; -export function PublicExplorerListButton(props) { - const { txId } = props; +const DEFAULT_TITLE = t`Public Explorer`; + +export function PublicExplorerListButton({ txId, title }) { const explorerIcon = ; const baseExplorerUrl = useSelector((state) => state.networkSettings.explorerUrl); const txUrl = `transaction/${txId}`; @@ -16,6 +24,11 @@ export function PublicExplorerListButton(props) { const explorerLink = combineURLs(baseExplorerUrl, txUrl); return ( - { Linking.openURL(explorerLink); }} titleStyle={{ color: COLORS.textColorShadow }} isLast /> + { Linking.openURL(explorerLink) }} + titleStyle={{ color: COLORS.textColorShadow }} + /> ); } diff --git a/src/components/PushTxDetailsModal.js b/src/components/PushTxDetailsModal.js index 6a8eee367..2b9bfdb53 100644 --- a/src/components/PushTxDetailsModal.js +++ b/src/components/PushTxDetailsModal.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import React from 'react'; import { Text, StyleSheet, View } from 'react-native'; import { t } from 'ttag'; diff --git a/src/components/ShowPushNotificationTxDetails.js b/src/components/ShowPushNotificationTxDetails.js index 7989465ec..c80a97212 100644 --- a/src/components/ShowPushNotificationTxDetails.js +++ b/src/components/ShowPushNotificationTxDetails.js @@ -1,5 +1,12 @@ -import { useNavigation } from '@react-navigation/native'; +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import React from 'react'; +import { useNavigation } from '@react-navigation/native'; import { useSelector, useDispatch } from 'react-redux'; import { t } from 'ttag'; import { pushCleanTxDetails, pushTxDetailsRequested } from '../actions'; diff --git a/src/components/SimpleButton.js b/src/components/SimpleButton.js index 4c8e87e3e..c7adc79f1 100644 --- a/src/components/SimpleButton.js +++ b/src/components/SimpleButton.js @@ -11,24 +11,48 @@ import { } from 'react-native'; import { PRIMARY_COLOR } from '../constants'; -const SimpleButton = (props) => { +/** + * Simple button component. + * + * @typedef {Object} Props + * @property {string} [title] - The title text of the button. + * @property {Object} [textStyle] - The style object for the text of the button. + * @property {string} [color] - The color of button's text. + * @property {string} [icon] - The icon component to be displayed in the button. + * @property {Object} [iconStyle] - The style object for the icon component. + * @property {Object} [containerStyle] - The style object for the container of the button. + * @property {Function} onPress - The function to be called when the button is pressed. + * @property {Object} children - The children component to be rendered. + * + * @param {Props} props - The props for the SimpleButton component. + */ +const SimpleButton = ({ + title, + textStyle, + color, + icon, + iconStyle, + containerStyle, + onPress, + children +}) => { const renderTitle = () => { - if (props.title) { - const textStyles = [styles.text, props.textStyle]; - if (props.color) { - textStyles.push({ color: props.color }); + if (title) { + const textStyles = [styles.text, textStyle]; + if (color) { + textStyles.push({ color }); } - return {props.title}; + return {title}; } return null; }; const renderIcon = () => { - if (props.icon) { + if (icon) { return ( - - + + ); } @@ -37,9 +61,10 @@ const SimpleButton = (props) => { }; return ( - + {renderTitle()} {renderIcon()} + {children} ); }; diff --git a/src/components/TextLabel.js b/src/components/TextLabel.js new file mode 100644 index 000000000..702e17d2d --- /dev/null +++ b/src/components/TextLabel.js @@ -0,0 +1,35 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { + StyleSheet, + Text, +} from 'react-native'; + +export const TextLabel = ({ pb8, bold, children }) => ( + {children} +); + +const styles = StyleSheet.create({ + textLabel: { + fontSize: 12, + lineHeight: 20, + color: 'hsla(0, 0%, 38%, 1)', + }, + pb8: { + paddingBottom: 8, + }, + bold: { + fontWeight: 'bold', + }, +}); diff --git a/src/components/TextValue.js b/src/components/TextValue.js new file mode 100644 index 000000000..efcf00c71 --- /dev/null +++ b/src/components/TextValue.js @@ -0,0 +1,65 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { + StyleSheet, + Text, +} from 'react-native'; +import { commonStyles } from './WalletConnect/theme'; + +/** + * @param {Object} props + * @param {boolean} props.title It sets font weight to bold and a larger font size + * @param {boolean} props.label It sets font weight to bold and a bottom margin + * @param {boolean} props.bold It sets font weight to bold + * @param {boolean} props.oneline It sets numberOfLines to 1 + * @param {boolean} props.shrink It sets flexShrink to 1 + * @param {boolean} props.pb4 It sets padding bottom to 4 + * @param {string} props.color It sets text color + */ +export const TextValue = ({ title, label, bold, oneline, shrink, pb4, color, children }) => ( + {children} +); + +const styles = StyleSheet.create({ + textValue: { + fontSize: 14, + lineHeight: 20, + color: 'black', + }, + title: { + fontSize: 18, + fontWeight: 'bold', + }, + label: [ + commonStyles.bold, + commonStyles.mb4, + ], + pb4: { + paddingBottom: 4, + }, + bold: { + fontWeight: 'bold', + }, + oneline: { + numberOfLines: 1, + }, + shrink: { + flexShrink: 1, + }, +}); diff --git a/src/components/TokenSelect.js b/src/components/TokenSelect.js index d3d151a01..06c391627 100644 --- a/src/components/TokenSelect.js +++ b/src/components/TokenSelect.js @@ -7,7 +7,7 @@ import React from 'react'; import { - FlatList, Image, StyleSheet, View, Text, TouchableHighlight, + Image, StyleSheet, View, Text, TouchableHighlight, } from 'react-native'; import { get } from 'lodash'; import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; @@ -18,6 +18,7 @@ import Spinner from './Spinner'; import { renderValue, isTokenNFT } from '../utils'; import { TOKEN_DOWNLOAD_STATUS } from '../sagas/tokens'; import { COLORS } from '../styles/themes'; +import { HathorFlatList } from './HathorFlatList'; /** * @typedef TokenBalance @@ -37,7 +38,7 @@ import { COLORS } from '../styles/themes'; */ const TokenSelect = (props) => { const tokens = Object.values(props.tokens); - const renderItem = ({ item, index }) => { + const renderItem = ({ item }) => { const symbolWrapperStyle = [styles.symbolWrapper]; const symbolTextStyle = [styles.text, styles.leftText, styles.symbolText]; if (props.selectedToken && props.selectedToken.uid === item.uid) { @@ -50,7 +51,6 @@ const TokenSelect = (props) => { return ( { props.onItemPress(item); }} underlayColor={COLORS.primaryOpacity30} > @@ -94,51 +94,35 @@ const TokenSelect = (props) => { return ( {props.header} - - item.uid} - /> - + item.uid} + ItemSeparatorComponent={ItemSeparator} + /> ); }; +const ItemSeparator = () => ( + +); + const styles = StyleSheet.create({ wrapper: { flex: 1, - justifyContent: 'center', + justifyContent: 'flex-start', alignItems: 'center', backgroundColor: COLORS.lowContrastDetail, // Defines an outer area on the main list content }, - listWrapper: { - alignSelf: 'stretch', - flex: 1, - marginTop: 16, - backgroundColor: COLORS.backgroundColor, - marginHorizontal: 16, - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - shadowOffset: { height: 2, width: 0 }, - shadowRadius: 4, - shadowColor: COLORS.textColor, - shadowOpacity: 0.08, - }, itemWrapper: { height: 80, paddingHorizontal: 16, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - borderBottomWidth: 1, - borderColor: COLORS.borderColor, - }, - firstItemWrapper: { - borderTopLeftRadius: 16, - borderTopRightRadius: 16, }, itemLeftWrapper: { flexDirection: 'row', diff --git a/src/components/TransactionStatusLabel.js b/src/components/TransactionStatusLabel.js new file mode 100644 index 000000000..e3e0e8efa --- /dev/null +++ b/src/components/TransactionStatusLabel.js @@ -0,0 +1,104 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { StyleSheet, Text, View, } from 'react-native'; +import { t } from 'ttag'; +import { COLORS } from '../styles/themes'; +import { CircleCheck } from './Icons/CircleCheck.icon'; +import { CircleClock } from './Icons/CircleClock.icon'; +import { CircleError } from './Icons/CircleError.icon'; + +const styles = StyleSheet.create({ + wrapper: { + flexShrink: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: 6, + borderRadius: 100, + paddingVertical: 4, + paddingLeft: 12, + // Adds optical effect of centralization for the whole object + paddingRight: 14, + }, + label: { + fontSize: 12, + lineHeight: 20, + }, + feedbackSuccess: { + backgroundColor: COLORS.feedbackSuccess100, + color: COLORS.feedbackSuccess400, + }, + feedbackWarning: { + backgroundColor: COLORS.feedbackWarning100, + color: COLORS.feedbackWarning300, + }, + feedbackError: { + backgroundColor: COLORS.feedbackError200, + color: COLORS.feedbackError600, + }, + freeze: { + backgroundColor: COLORS.freeze100, + color: COLORS.freeze300, + }, +}); + +/** + * @param {Object} param + * @param {string} param.label Status text as label + * @param {Object} param.style Style props to customize the base component + * @param {Object} param.children Icon component to compose with the label + */ +const TransactionStatusBase = ({ label, style, children: icon }) => ( + + + {icon} + + + + {label.toUpperCase()} + + + +); + +const Executed = () => ( + + + +); +const Processing = () => ( + + + +); +const Voided = () => ( + + + +); + +/** + * @description + * This component was devised to be used in Nano Contract context + * to provide a visual indication for transaction "status". + * @param {Object} param + * @param {boolean} param.hasFirstBlock It indicates if a transaction has a first block + * @param {boolean} param.isVoided Transaction's void flag + */ +export const TransactionStatusLabel = ({ hasFirstBlock, isVoided = false }) => { + if (isVoided) { + return Voided(); + } + + if (hasFirstBlock) { + return Executed(); + } + + return Processing(); +}; diff --git a/src/components/TwoOptionsToggle.js b/src/components/TwoOptionsToggle.js new file mode 100644 index 000000000..af02df398 --- /dev/null +++ b/src/components/TwoOptionsToggle.js @@ -0,0 +1,86 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useState } from 'react'; +import { StyleSheet, View, Text, TouchableOpacity } from 'react-native'; +import { COLORS } from '../styles/themes'; + +/** + * @param {{ + * options: Map<'first'|'second', { value: string; onTap: Function; }>; + * defaultOption: 'first'|'second'; + * }} + */ +export const TwoOptionsToggle = ({ options, defaultOption }) => { + const [currOption, setCurrOption] = useState(defaultOption); + const isFirst = currOption === 'first'; + const isSecond = currOption === 'second'; + + const onTapFirst = () => onTap('first'); + const onTapSecond = () => onTap('second'); + const onTap = (option) => { + if (option === currOption) { + // do nothing and halt. + return; + } + setCurrOption(option); + // Execute the callback assigned to the option + options[option].onTap(); + }; + + return ( + + + ); +}; + +/** + * @param {{ + * optionValue: string; + * isActive: boolean; + * onTap: (option: string) => void; + * }} + */ +const Option = ({ optionValue, isActive, onTap }) => ( + + {optionValue} + +); + +const styles = StyleSheet.create({ + wrapper: { + display: 'flex', + flexDirection: 'row', + width: '80%', + marginTop: 16, + borderRadius: 24, + backgroundColor: 'hsla(220, 10%, 94%, 1)', + }, + button: { + width: '50%', + borderRadius: 24, + paddingTop: 9, + paddingBottom: 10, + color: COLORS.textColor, + }, + buttonFocus: { + backgroundColor: COLORS.backgroundColor, + }, + text: { + fontSize: 14, + lineHeight: 20, + textAlign: 'center', + }, + textFocus: { + fontWeight: 'bold', + }, +}); diff --git a/src/components/TxDetailsModal.js b/src/components/TxDetailsModal.js index 52ca9b16d..4e2a3aac3 100644 --- a/src/components/TxDetailsModal.js +++ b/src/components/TxDetailsModal.js @@ -6,41 +6,80 @@ */ import React, { Component } from 'react'; -import { Text, StyleSheet, View } from 'react-native'; +import { Text, StyleSheet, View, ScrollView, TouchableWithoutFeedback } from 'react-native'; import Modal from 'react-native-modal'; import { t } from 'ttag'; -import { getShortHash, getTokenLabel, renderValue } from '../utils'; +import { getShortContent, getShortHash, getTokenLabel, renderValue } from '../utils'; import { ListItem } from './HathorList'; import SlideIndicatorBar from './SlideIndicatorBar'; import CopyClipboard from './CopyClipboard'; import { PublicExplorerListButton } from './PublicExplorerListButton'; import { COLORS } from '../styles/themes'; +import { TransactionStatusLabel } from './TransactionStatusLabel'; class TxDetailsModal extends Component { style = StyleSheet.create({ modal: { justifyContent: 'flex-end', + marginVertical: 0, + marginHorizontal: 0, + }, + wrapper: { + flex: 1, + paddingTop: 96, + paddingHorizontal: 8, }, inner: { - backgroundColor: COLORS.backgroundColor, + flexShrink: 1, + marginTop: 'auto', borderRadius: 8, + paddingBottom: 24, + backgroundColor: COLORS.backgroundColor, }, }); + getCopyClipboard = ({ text, copyText }) => ( + + ); + render() { + /** + * @type {{ + * token: unknown; + * tx: TxHistory; + * isNFT: boolean; + * }} TxDetailsModal properties + */ const { token, tx, isNFT } = this.props; + const { txId, ncId, ncMethod, ncCaller, isVoided } = tx; + const ncCallerAddr = ncCaller && ncCaller.base58; + const fullTokenStr = getTokenLabel(token); const description = tx.getDescription(token); const timestampStr = tx.getTimestampFormat(); - const idStr = getShortHash(tx.txId, 12); - const txIdComponent = ( - - ); + const shortTxId = getShortHash(txId, 7); + const shortNcId = ncId && getShortHash(ncId, 7); + const shortNcCallerAddr = ncCallerAddr && getShortContent(ncCallerAddr, 7); + const txIdComponent = this.getCopyClipboard({ + text: shortTxId, + copyText: txId + }); + const ncIdComponent = ncId && this.getCopyClipboard({ + text: shortNcId, + copyText: ncId + }); + const ncCallerAddrComponent = ncCaller && this.getCopyClipboard({ + text: shortNcCallerAddr, + copyText: ncCallerAddr + }); + const isNc = tx.isNanoContract(); + const hasFirstBlock = tx.hasFirstBlock(); + return ( - + - - - - - - - + + + + + + + + {isNc && } + {isNc && } + {isNc && } + {isNc + && ( + + )} + /> + )} + {isNc && } + + + + diff --git a/src/components/WalletConnect/CreateTokenModal.js b/src/components/WalletConnect/CreateTokenModal.js new file mode 100644 index 000000000..98afb2ada --- /dev/null +++ b/src/components/WalletConnect/CreateTokenModal.js @@ -0,0 +1,65 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigation } from '@react-navigation/native'; +import { t } from 'ttag'; +import { Text } from 'react-native'; +import { ModalBase } from '../ModalBase'; +import { WarnDisclaimer } from './WarnDisclaimer'; +import { walletConnectReject } from '../../actions'; +import { commonStyles } from './theme'; + +export default ({ + onDismiss, + data, +}) => { + const navigation = useNavigation(); + const dispatch = useDispatch(); + const isRetrying = useSelector((state) => state.walletConnect.createToken.retrying); + + const onReject = () => { + onDismiss(); + dispatch(walletConnectReject()); + }; + + const navigateToCreateTokenRequestScreen = () => { + onDismiss(); + navigation.navigate('CreateTokenRequest', { createTokenRequest: data }); + }; + + useEffect(() => { + if (isRetrying) { + navigateToCreateTokenRequestScreen(); + } + }, [isRetrying]); + + return ( + + {t`New Create Token Request`} + + + + {t`You have received a new Create Token Request. Please`} + + {' '}{t`carefully review the details`}{' '} + + {t`before deciding to accept or decline.`} + + + + + + ); +}; diff --git a/src/components/WalletConnect/CreateTokenRequest.js b/src/components/WalletConnect/CreateTokenRequest.js new file mode 100644 index 000000000..087610aed --- /dev/null +++ b/src/components/WalletConnect/CreateTokenRequest.js @@ -0,0 +1,225 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { useEffect } from 'react'; +import { + StyleSheet, + View, + ScrollView, + Text, + Image, +} from 'react-native'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigation } from '@react-navigation/native'; +import { numberUtils } from '@hathor/wallet-lib'; +import { t } from 'ttag'; +import { + createTokenRetry, + createTokenRetryDismiss, + setCreateTokenStatusReady, + walletConnectAccept, + walletConnectReject +} from '../../actions'; +import { COLORS } from '../../styles/themes'; +import NewHathorButton from '../NewHathorButton'; +import { DappContainer } from './NanoContract/DappContainer'; +import { commonStyles } from './theme'; +import { FeedbackContent } from '../FeedbackContent'; +import { DEFAULT_TOKEN, WALLETCONNECT_CREATE_TOKEN_STATUS } from '../../constants'; +import FeedbackModal from '../FeedbackModal'; +import Spinner from '../Spinner'; +import errorIcon from '../../assets/images/icErrorBig.png'; +import checkIcon from '../../assets/images/icCheckBig.png'; + +const condRenderData = ( + attribute, + title, + separator, + formatter = (val) => val, +) => { + if (attribute != null) { + return ( + <> + { separator && } + + + { title } + + + { formatter(attribute) } + + + + ); + } + + return null; +}; + +/** + * Renders translated values for boolean inputs + * @param {boolean} bool + */ +function renderBooleanFormatter(bool) { + return bool ? t`Yes` : t`No`; +} + +export const CreateTokenRequestData = ({ data }) => ( + + + { condRenderData(data.name, t`Name`, false) } + { condRenderData(data.symbol, t`Symbol`, true) } + { condRenderData(data.amount, t`Amount`, true, numberUtils.prettyValue) } + { condRenderData(data.address, t`Address to send newly minted ${data.symbol}`, true) } + { condRenderData(data.changeAddress, t`Address to send change ${DEFAULT_TOKEN.uid}`, true) } + { condRenderData(data.createMint, t`Create mint authority?`, true, renderBooleanFormatter) } + { condRenderData(data.createMelt, t`Create melt authority?`, true, renderBooleanFormatter) } + { condRenderData(data.mintAuthorityAddress, t`Address to send the mint authority`, true) } + { condRenderData(data.meltAuthorityAddress, t`Address to send the melt authority`, true) } + { data.mintAuthorityAddress != null + && condRenderData( + data.allowExternalMintAuthorityAddress, + t`Allow external mint authority addresses?`, + true, + renderBooleanFormatter, + )} + { data.meltAuthorityAddress != null + && condRenderData( + data.allowExternalMeltAuthorityAddress, + t`Allow external melt authority addresses?`, + true, + renderBooleanFormatter, + )} + { condRenderData(data.data, t`Token data`, true, (tokenData) => tokenData.join('\n')) } + + +); + +export const CreateTokenRequest = ({ createTokenRequest }) => { + const { dapp, data } = createTokenRequest; + const { status } = useSelector((state) => state.walletConnect.createToken); + const dispatch = useDispatch(); + const navigation = useNavigation(); + + useEffect(() => () => { + dispatch(setCreateTokenStatusReady()); + }, []); + + const onAcceptCreateTokenRequest = () => { + const acceptedCreateToken = data; + + dispatch(walletConnectAccept(acceptedCreateToken)); + }; + + const onDeclineTransaction = () => { + dispatch(walletConnectReject()); + navigation.goBack(); + }; + + const isTxReady = status === WALLETCONNECT_CREATE_TOKEN_STATUS.READY; + const isTxProcessing = status === WALLETCONNECT_CREATE_TOKEN_STATUS.LOADING; + const isTxSuccessful = status === WALLETCONNECT_CREATE_TOKEN_STATUS.SUCCESSFUL; + const isTxFailed = status === WALLETCONNECT_CREATE_TOKEN_STATUS.FAILED; + + const onFeedbackModalDismiss = () => { + dispatch(createTokenRetryDismiss()); + navigation.goBack(); + }; + + const onNavigateToDashboard = () => { + navigation.navigate('Dashboard'); + }; + + const onTryAgain = () => { + dispatch(createTokenRetry()); + }; + + return ( + <> + + + {isTxReady && ( + + + + {/* User actions */} + + + + + + )} + {isTxProcessing && ( + } + offmargin + offcard + offbackground + /> + )} + + + + {isTxSuccessful && ( + )} + text={t`Create Token Transaction successfully sent.`} + onDismiss={onFeedbackModalDismiss} + action={()} + /> + )} + + {isTxFailed && ( + )} + text={t`Error while sending create token transaction.`} + onDismiss={onFeedbackModalDismiss} + action={()} + /> + )} + + ); +}; + +const styles = StyleSheet.create({ + wide: { + width: '100%' + }, + wrapper: { + flex: 1, + paddingHorizontal: 16, + backgroundColor: COLORS.lowContrastDetail, // Defines an outer area on the main list content + }, + content: { + flex: 1, + rowGap: 24, + width: '100%', + paddingVertical: 16, + }, + actionContainer: { + flexDirection: 'column', + gap: 8, + paddingBottom: 48, + }, + value: [commonStyles.text, commonStyles.value], +}); diff --git a/src/components/WalletConnect/ModalButton.js b/src/components/WalletConnect/ModalButton.js index 85c9222b9..c78b19695 100644 --- a/src/components/WalletConnect/ModalButton.js +++ b/src/components/WalletConnect/ModalButton.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import React from 'react'; import { Text, TouchableOpacity, StyleSheet } from 'react-native'; import { COLORS } from '../../styles/themes'; diff --git a/src/components/WalletConnect/NanoContract/DappContainer.js b/src/components/WalletConnect/NanoContract/DappContainer.js new file mode 100644 index 000000000..7e46f6828 --- /dev/null +++ b/src/components/WalletConnect/NanoContract/DappContainer.js @@ -0,0 +1,84 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { + StyleSheet, + View, + Text, + Image +} from 'react-native'; +import { t } from 'ttag'; +import { commonStyles } from '../theme'; + +/** + * Renders DApp information. + * + * @param {Object} props + * @param {Object} props.dapp + */ +export const DappContainer = ({ dapp }) => ( + + + + + + + {dapp.proposer} + {'• '}{dapp.chain} + + + + {t`Review your transaction from this dApp`} + + + {t`Stay vigilant and protect your data from potential phishing attempts.`} + + +); + +const styles = StyleSheet.create({ + container: { + gap: 16, + paddingVertical: 16, + paddingHorizontal: 16, + }, + header: { + flexDirection: 'row', + gap: 16, + }, + avatar: { + flexShrink: 1, + alignSelf: 'flex-start', + maxWidth: 48, + maxHeight: 48, + }, + avatarIcon: { + width: 48, + height: 48, + backgroundColor: 'hsla(0, 0%, 85%, 1)', + borderRadius: 24, + }, + proposer: [ + commonStyles.text, + commonStyles.bold, + commonStyles.mb4, + ], + network: [ + commonStyles.text, + { color: 'hsla(263, 100%, 64%, 1)', } + ], + emphasis: [ + commonStyles.text, + commonStyles.bold + ] +}); diff --git a/src/components/WalletConnect/NanoContract/DeclineModal.js b/src/components/WalletConnect/NanoContract/DeclineModal.js new file mode 100644 index 000000000..f93959237 --- /dev/null +++ b/src/components/WalletConnect/NanoContract/DeclineModal.js @@ -0,0 +1,54 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { + StyleSheet, + Text, +} from 'react-native'; +import { t } from 'ttag'; +import { ModalBase } from '../../ModalBase'; + +/** + * It renders a confirmation modal to decline the tranaction creation. + * + * @param {Object} props + * @param {boolean} props.show Flag that determines the if the modal should appear or not. + * @param {() => void} props.onDecline Callback fn for decline action. + * @param {() => void} props.onDismiss Callback fn for dismiss action. + */ +export const DeclineModal = ({ show, onDecline, onDismiss }) => ( + + {t`Decline transaction`} + + + {t`Are you sure you want to decline this transaction?`} + + + + + +); + +const styles = StyleSheet.create({ + declineModalBody: { + paddingBottom: 24, + }, + text: { + fontSize: 16, + lineHeight: 20, + textAlign: 'center', + }, +}); diff --git a/src/components/WalletConnect/NanoContract/NanoContractActions.js b/src/components/WalletConnect/NanoContract/NanoContractActions.js new file mode 100644 index 000000000..9b38d0d70 --- /dev/null +++ b/src/components/WalletConnect/NanoContract/NanoContractActions.js @@ -0,0 +1,203 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useCallback } from 'react'; +import { + StyleSheet, + View, + Text, +} from 'react-native'; +import { t } from 'ttag'; +import { useSelector } from 'react-redux'; +import { HathorFlatList } from '../../HathorFlatList'; +import { commonStyles } from '../theme'; +import { getShortHash, isTokenNFT, renderValue } from '../../../utils'; +import { ReceivedIcon } from '../../Icons/Received.icon'; +import { SentIcon } from '../../Icons/Sent.icon'; +import { AlertUI, COLORS } from '../../../styles/themes'; +import { DEFAULT_TOKEN } from '../../../constants'; +import { WarnTextValue } from '../../WarnTextValue'; +import { CircleError } from '../../Icons/CircleError.icon'; + +/** + * It returns the title template for each action type, + * which is either 'deposit' or 'withdrawal'. + * + * @param {string} tokenSymbol The token symbol fetched from metadata, + * or a shortened token hash. + * + * @returns {string} A title template by action type. + */ +const actionTitleMap = (tokenSymbol) => ({ + deposit: t`${tokenSymbol} Deposit`, + withdrawal: t`${tokenSymbol} Withdrawal`, +}); + +/** + * Get action title depending on the action type. + * @param {Object} tokens A map of token metadata by token uid + * @param {Object} action An action object + * + * @returns {string} A formatted title to be used in the action card + * + * @example + * getActionTitle({ '123': { ..., symbol: 'STR' }}, { ..., token: '123', type: 'deposit' }) + * >>> 'STR Deposit' + * + * @example + * getActionTitle({}, { ..., token: '1234...5678', type: 'deposit' }) + * >>> '1234...5678 Deposit' + */ +const getActionTitle = (tokens, action) => { + const tokenMetadata = tokens[action.token]; + if (tokenMetadata) { + return actionTitleMap(tokenMetadata.symbol)[action.type]; + } + + if (action.token === DEFAULT_TOKEN.uid) { + return actionTitleMap(DEFAULT_TOKEN.symbol)[action.type] + } + + return actionTitleMap(getShortHash(action.token))[action.type]; +}; + +/** + * It renders a list of actions with a proper title for each one. + * It renders nothing if there aren't actions to render. + * + * @param {Object} props + * @param {Object[]} props.ncActions A list of Nano Contract actions. + * @param {Object} props.tokens A map of token metadata by token uid. + * @param {string} props.error A feedback error for tokens not loaded. + */ +export const NanoContractActions = ({ ncActions, tokens, error }) => { + if (!ncActions || ncActions.length < 1) { + return null; + } + + const tokenMetadata = useSelector((state) => state.tokenMetadata); + // A callback to check if the action token is an NFT. + const isNft = useCallback( + (token) => isTokenNFT(token, tokenMetadata), + [tokenMetadata] + ); + // A callback to retrieve the action title by its token symbol of hash. + const getTitle = useCallback( + (action) => getActionTitle(tokens, action), + [tokens] + ); + + const styles = StyleSheet.create({ + wrapper: { marginTop: 0, marginBottom: 0, marginHorizontal: 0 }, + }); + + return ( + + + {t`Action List`} + + ( + + )} + // If has error, shows the feedback error message in the list header. + ListHeaderComponent={error && ( + + + + {error} + + + )} + /> + + ); +}; + +/** + * @param {Object} props + * @param {{ + * type: 'deposit'|'withdrawal'; + * token: string; + * amount: number; + * address: string; + * }} props.action A transaction's action object + * @param {boolean} props.isNft A flag to inform if the token is an NFT or not + * @param {string} props.title The card title for the action + */ +const ActionItem = ({ action, title, isNft }) => { + const styles = StyleSheet.create({ + action: [commonStyles.text, commonStyles.bold], + valueLabel: [commonStyles.text, commonStyles.field, commonStyles.bold, commonStyles.mb4], + value: [commonStyles.text, commonStyles.field], + }); + + return ( + + + + {title} + {action.address + && ( + + {t`To Address:`} + {action.address} + + )} + + + + ) +} + +/** + * It renders an icon by action type, either 'deposit' or 'withdrawal'. + * + * @param {Object} props + * @param {'deposit'|'withdrawal'} props.type Action type. + */ +const Icon = ({ type }) => { + const iconMap = { + deposit: SentIcon({ type: 'default' }), + withdrawal: ReceivedIcon({ type: 'default' }), + }; + + return (iconMap[type]); +}; + +/** + * It renders an amount with the right format. + * + * @param {Object} props + * @param {number} props.amount + * @param {boolean} props.isNft + */ +const Amount = ({ amount, isNft }) => { + const amountToRender = renderValue(amount, isNft); + + const styles = StyleSheet.create({ + wrapper: { + marginLeft: 'auto', + }, + amount: { + fontSize: 16, + lineHeight: 20, + color: COLORS.black, + }, + }); + + return ( + + + {amountToRender} + + + ) +}; diff --git a/src/components/WalletConnect/NanoContract/NanoContractExecInfo.js b/src/components/WalletConnect/NanoContract/NanoContractExecInfo.js new file mode 100644 index 000000000..564d38802 --- /dev/null +++ b/src/components/WalletConnect/NanoContract/NanoContractExecInfo.js @@ -0,0 +1,170 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useEffect, useMemo } from 'react'; +import { + StyleSheet, + View, + TouchableOpacity, +} from 'react-native'; +import { useDispatch, useSelector } from 'react-redux'; +import { t } from 'ttag'; +import { firstAddressRequest } from '../../../actions'; +import { NANOCONTRACT_BLUEPRINTINFO_STATUS as STATUS } from '../../../constants'; +import { COLORS } from '../../../styles/themes'; +import { FrozenTextValue } from '../../FrozenTextValue'; +import { CircleError } from '../../Icons/CircleError.icon'; +import { NanoContractIcon } from '../../Icons/NanoContract.icon'; +import { PenIcon } from '../../Icons/Pen.icon'; +import Spinner from '../../Spinner'; +import { TextValue } from '../../TextValue'; +import { WarnTextValue } from '../../WarnTextValue'; +import { commonStyles } from '../theme'; + +/** + * It renders a card with basic information to execute the Nano Contract creation. + * + * @param {Object} props + * @param {Object} props.nc Nano Contract info. + * @param {() => void} props.onSelectAddress Callback fn for tap on caller address component. + */ +export const NanoContractExecInfo = ({ nc, onSelectAddress }) => { + const dispatch = useDispatch(); + const registeredNc = useSelector((state) => state.nanoContract.registered[nc.ncId]); + const blueprintInfo = useSelector((state) => state.nanoContract.blueprint[nc.blueprintId]); + const firstAddress = useSelector((state) => state.firstAddress); + + const isInitialize = nc.method === 'initialize'; + const notInitialize = !isInitialize; + + const blueprintName = useMemo(() => { + if (notInitialize && registeredNc) { + return registeredNc.blueprintName; + } + + if (blueprintInfo?.status === STATUS.SUCCESSFUL) { + return blueprintInfo.data.name; + } + return null; + }, [blueprintInfo]); + + useEffect(() => { + if (isInitialize) { + // Load firstAddress if not loaded + if (!firstAddress.address) { + dispatch(firstAddressRequest()); + } + } + }, [nc]); + + const isBlueprintInfoLoading = !registeredNc + && blueprintInfo?.status === STATUS.LOADING; + const hasBlueprintInfoFailed = !registeredNc + && blueprintInfo?.status === STATUS.FAILED; + + const hasCaller = nc.caller != null; + const hasFirstAddressFailed = !hasCaller && isInitialize && firstAddress.error; + const isFirstAddressLoading = !hasCaller + && isInitialize + && !hasFirstAddressFailed; + + return ( + + + + + + {notInitialize && ( + + {t`Nano Contract ID`} + {nc.ncId} + + )} + + {t`Blueprint ID`} + {nc.blueprintId} + + + + {t`Blueprint Name`} + {isBlueprintInfoLoading && ( + + {' '} + + )} + {hasBlueprintInfoFailed && ( + + {' '} + + )} + + {blueprintName && ( + {blueprintName} + )} + {isBlueprintInfoLoading && ( + {t`Loading...`} + )} + {hasBlueprintInfoFailed && ( + {blueprintInfo.error} + )} + + + {t`Blueprint Method`} + {nc.method} + + + + + + + {t`Caller`} + {isFirstAddressLoading && ( + + {' '} + + )} + {(hasFirstAddressFailed) && ( + + {' '} + + )} + + {hasCaller && ( + {nc.caller || firstAddress.address} + )} + {isFirstAddressLoading && ( + {t`Loading...`} + )} + {hasFirstAddressFailed && ( + {t`Couldn't determine address, select one`} + )} + + + + + + + + + ) +}; + +const styles = StyleSheet.create({ + contentEditable: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + contentEditableValue: { + flexShrink: 1, + paddingRight: 8, + }, + contentEditableIcon: { + width: 24, + paddingRight: 2, + }, +}); diff --git a/src/components/WalletConnect/NanoContract/NanoContractMethodArgs.js b/src/components/WalletConnect/NanoContract/NanoContractMethodArgs.js new file mode 100644 index 000000000..eb5635b58 --- /dev/null +++ b/src/components/WalletConnect/NanoContract/NanoContractMethodArgs.js @@ -0,0 +1,217 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useMemo } from 'react'; +import { + StyleSheet, + View, + Text, +} from 'react-native'; +import { useSelector } from 'react-redux'; +import { t } from 'ttag'; +import { get } from 'lodash'; +import { Network } from '@hathor/wallet-lib'; +import { COLORS } from '../../../styles/themes'; +import { commonStyles } from '../theme'; +import { DEFAULT_TOKEN, NANOCONTRACT_BLUEPRINTINFO_STATUS as STATUS } from '../../../constants'; +import { FeedbackContent } from '../../FeedbackContent'; +import Spinner from '../../Spinner'; +import { getTimestampFormat, parseScriptData, renderValue } from '../../../utils'; + +/** + * Get method info from registered blueprint data. + * + * @param {{ + * data: Object; + * }} blueprint The blueprint info object + * @param {string} method The method name to get info from blueprint public methods + * + * @returns {Object} + */ +function getMethodInfoFromBlueprint(blueprint, method) { + return get(blueprint.data, `public_methods.${method}`, null); +} + +/** + * Get the fallback entries for the method arguments. + * + * @param {string[]} args A list of argument value + * + * @returns {[argName: string, value: string][]} + * + * @example + * getFallbackArgEntries([...argValues]) + * >>> [['Position 0', 'abc'], ['Position 1', '00'], ['Position 2', 123]] + */ +function getFallbackArgEntries(args) { + return args.map((arg, idx) => [t`Position ${idx}`, arg]); +} + +/** + * It renders a list of method arguments for when the Nano Contract executes. + * + * @param {Object} props + * @param {string} props.blueprintId ID of blueprint. + * @param {string} props.method Method's name. + * @param {string[]} props.ncArgs A list of method's argument. + */ +export const NanoContractMethodArgs = ({ blueprintId, method, ncArgs }) => { + if (!ncArgs || ncArgs.length <= 0) { + return null; + } + + const network = useSelector((state) => new Network(state.networkSettings.network)); + const tokens = useSelector((state) => state.tokens); + + const blueprintInfo = useSelector((state) => state.nanoContract.blueprint[blueprintId]); + // It results a in a list of entries like: + // >>>[ + // >>> ['oracle_script', 'abc', 'TxOutputScript'], + // >>> ['token_uid', '00', 'TokenUid'], + // >>> ['date_last_bet', 123, 'Timestamp'] + // >>>] + // or a fallback like: + // >>> [['Position 0', 'abc'], ['Position 1', '00'], ['Position 2', 123]] + const argEntries = useMemo(() => { + if (blueprintInfo == null || blueprintInfo.status === STATUS.LOADING) { + return []; + } + + const methodInfo = getMethodInfoFromBlueprint(blueprintInfo, method); + if (methodInfo) { + return ncArgs.map((arg, idx) => [methodInfo.args[idx].name, arg, methodInfo.args[idx].type]); + } + + // Still render a fallback + return getFallbackArgEntries(ncArgs); + }, [method, ncArgs, blueprintInfo]); + + // Empty while downloading the bleuprint details + const isEmpty = argEntries.length === 0; + const notEmpty = !isEmpty; + + return ( + + + {t`Arguments`} + + {isEmpty /* This is a redundancy to the general loading */ + && ( + } + offmargin + /> + )} + {notEmpty + && ( + + + {argEntries.map(([argName, argValue, argType]) => ( + + + {argName} + + + + + + + + ))} + + + )} + + ) +}; + +/** + * Component responsible to render the appropriate format for the value + * taking in consideration the type. + * + * Remarks + * The values received here when derived from 'byte' type like + * 'TxOutputScript', 'TokenUid' and 'VertexId' are already in their + * hexadecimal format. + * + * Values of type 'Address', which also derives from 'byte' are + * in base58 format. + * + * Values of type 'SignedData[Result]' arrives here in presentation + * format. + * + * @param {Object} props + * @param {string} props.type An argument type + * @param {string} props.value An argument value + * @param {Object} props.network A network object + * @param {Object} props.tokens A map of registered tokens + */ +const ArgValue = ({ type, value, network, tokens }) => { + if (type === 'Amount') { + return renderValue(value); + } + + if (type === 'Timestamp') { + return getTimestampFormat(value); + } + + if (type === 'TxOutputScript') { + const parsedScript = parseScriptData(value, network); + if (parsedScript && parsedScript.getType() === 'data') { + return parsedScript.data; + } + + if (parsedScript) { + return parsedScript.address.base58; + } + } + + if (type === 'TokenUid') { + if (value === DEFAULT_TOKEN.uid) { + return DEFAULT_TOKEN.symbol; + } + + if (value in tokens) { + return tokens[value].symbol; + } + } + + return value; +}; + +const styles = StyleSheet.create({ + argPosition: { + flexShrink: 10, + width: '30%', + paddingRight: 8, + }, + argPositionText: [ + commonStyles.text, + commonStyles.bold + ], + argValue: { + maxWidth: '70%', + backgroundColor: 'hsla(0, 0%, 96%, 1)', + paddingVertical: 2, + paddingHorizontal: 8, + borderRadius: 4, + }, + argValueText: { + fontSize: 12, + lineHeight: 16, + color: COLORS.black, + }, +}); diff --git a/src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js b/src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js new file mode 100644 index 000000000..94ca30cb0 --- /dev/null +++ b/src/components/WalletConnect/NanoContract/NewNanoContractTransactionModal.js @@ -0,0 +1,82 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useCallback, useEffect } from 'react'; +import { + StyleSheet, + Text, +} from 'react-native'; +import { t } from 'ttag'; +import { useNavigation } from '@react-navigation/native'; +import { useDispatch, useSelector } from 'react-redux'; +import { ModalBase } from '../../ModalBase'; +import { walletConnectReject } from '../../../actions'; +import { WarnDisclaimer } from '../WarnDisclaimer'; + +export const NewNanoContractTransactionModal = ({ + onDismiss, + data, +}) => { + const isRetrying = useSelector(({ walletConnect }) => ( + walletConnect.newNanoContractTransaction.retrying + )); + const navigation = useNavigation(); + const dispatch = useDispatch(); + + const onModalDismiss = useCallback(() => { + dispatch(walletConnectReject()); + onDismiss(); + }, [onDismiss]); + + const navigatesToNewNanoContractScreen = () => { + onDismiss(); + navigation.navigate('NewNanoContractTransactionScreen', { ncTxRequest: data }); + }; + + useEffect(() => { + if (isRetrying) { + navigatesToNewNanoContractScreen(); + } + }, [isRetrying]); + + return ( + + {t`New Nano Contract Transaction`} + + + + {t`You have received a new Nano Contract Transaction. Please`} + + {' '}{t`carefully review the details`}{' '} + + {t`before deciding to accept or decline.`} + + + + + + ); +}; + +const styles = StyleSheet.create({ + body: { + paddingBottom: 24, + }, + text: { + fontSize: 14, + lineHeight: 20, + }, + bold: { + fontWeight: 'bold', + } +}); diff --git a/src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js b/src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js new file mode 100644 index 000000000..b6c8eaa61 --- /dev/null +++ b/src/components/WalletConnect/NanoContract/NewNanoContractTransactionRequest.js @@ -0,0 +1,338 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { + useEffect, + useMemo, + useState +} from 'react'; +import { + StyleSheet, + View, + ScrollView, + TouchableWithoutFeedback, + Image +} from 'react-native'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigation } from '@react-navigation/native'; +import { t } from 'ttag'; +import { + nanoContractBlueprintInfoRequest, + newNanoContractRetry, + newNanoContractRetryDismiss, + setNewNanoContractStatusReady, + walletConnectAccept, + walletConnectReject, + unregisteredTokensDownloadRequest +} from '../../../actions'; +import { COLORS } from '../../../styles/themes'; +import NewHathorButton from '../../NewHathorButton'; +import { SelectAddressModal } from '../../NanoContract/SelectAddressModal'; +import { FeedbackContent } from '../../FeedbackContent'; +import { DEFAULT_TOKEN, NANOCONTRACT_BLUEPRINTINFO_STATUS, WALLETCONNECT_NEW_NANOCONTRACT_TX_STATUS } from '../../../constants'; +import Spinner from '../../Spinner'; +import FeedbackModal from '../../FeedbackModal'; +import errorIcon from '../../../assets/images/icErrorBig.png'; +import checkIcon from '../../../assets/images/icCheckBig.png'; +import { DappContainer } from './DappContainer'; +import { NanoContractExecInfo } from './NanoContractExecInfo'; +import { NanoContractActions } from './NanoContractActions'; +import { NanoContractMethodArgs } from './NanoContractMethodArgs'; +import { DeclineModal } from './DeclineModal'; + +/** + * @param {Object} props + * @param {Object} props.ncTxRequest + * @param {Object} props.ncTxRequest.nc + * @param {string} props.ncTxRequest.nc.ncId + * @param {string} props.ncTxRequest.nc.blueprintId + * @param {Object[]} props.ncTxRequest.nc.actions + * @param {string} props.ncTxRequest.nc.method + * @param {string[]} props.ncTxRequest.nc.args + * @param {Object} props.ncTxRequest.dapp + * @param {string} props.ncTxRequest.dapp.icon + * @param {string} props.ncTxRequest.dapp.proposer + * @param {string} props.ncTxRequest.dapp.url + * @param {string} props.ncTxRequest.dapp.description + */ +export const NewNanoContractTransactionRequest = ({ ncTxRequest }) => { + const { data: nc, dapp } = ncTxRequest; + const dispatch = useDispatch(); + const navigation = useNavigation(); + const newTxStatus = useSelector((state) => state.walletConnect.newNanoContractTransaction.status); + const firstAddress = useSelector((state) => state.firstAddress); + // Nullable if the nano contract method is 'initialize' + const registeredNc = useSelector((state) => state.nanoContract.registered[nc.ncId]); + const knownTokens = useSelector((state) => ({ ...state.tokens, ...state.unregisteredTokens })); + const blueprintInfo = useSelector((state) => state.nanoContract.blueprint[nc.blueprintId]); + + const [showSelectAddressModal, setShowSelectAddressModal] = useState(false); + const [showDeclineModal, setShowDeclineModal] = useState(false); + /** + * If nano-contract's method is 'initialize' then the expression + * should be resolved to firstAddress value by default. + * + * In case of failure to load the first address the user will see + * a feedback message instruction it to select an address for the + * transaction. + */ + const [ncAddress, setNcAddress] = useState(registeredNc?.address || firstAddress.address); + const ncToAccept = useMemo(() => ({ + ...nc, + caller: ncAddress, + }), [ncAddress]) + + const toggleSelectAddressModal = () => setShowSelectAddressModal(!showSelectAddressModal); + const handleAddressSelection = (newAddress) => { + setNcAddress(newAddress); + toggleSelectAddressModal(); + }; + + // Accepts the Nano Contract data preseted. + const onAcceptTransaction = () => { + // Update the caller with the address selected by the user. + const acceptedNc = { ...nc, caller: ncAddress }; + // Signal the user has accepted the current request and pass the accepted data. + dispatch(walletConnectAccept(acceptedNc)); + }; + + const onDeclineTransaction = () => { + setShowDeclineModal(true); + }; + const onDeclineConfirmation = () => { + setShowDeclineModal(false); + dispatch(walletConnectReject()); + navigation.goBack(); + }; + const onDismissDeclineModal = () => { + setShowDeclineModal(false); + }; + + // Control which content to show, if the nano contract is not registered + // a feedback content should tell user the nano contract must be registered first + // and only let user decline the transaction to get out the page, otherwise interaction + // content is showed. + const notInitialize = ncToAccept.method !== 'initialize'; + const notRegistered = notInitialize && registeredNc == null; + // It results in true for registered nc and initialize request + const showRequest = !notRegistered; + + // This effect should run at most twice: + // 1. when in the construct phase + // 2. after firstAddress is set on store after a request to load it + // The mentioned load request at (2) can happen for 'initialize' transaction, + // it is requested from a child component, NanoContractExecInfo. + useEffect(() => { + if (ncToAccept.method === 'initialize' && firstAddress.address) { + setNcAddress(firstAddress.address); + } + }, [firstAddress]); + + // This effect runs only once in the construct phase + useEffect(() => { + // Do nothing if nano contract is not registered and don't call initialize method. + if (notRegistered) return undefined; + + // Request blueprint info if not present to feed the components: + // - NanoContractExecInfo, and + // - NanoContractMethodArgs + if (!blueprintInfo) { + dispatch(nanoContractBlueprintInfoRequest(nc.blueprintId)); + } + + // Request token data for each unknown token present in actions + const unknownTokensUid = []; + const actionTokensUid = nc.actions?.map((each) => each.token) || []; + actionTokensUid.forEach((uid) => { + if (uid !== DEFAULT_TOKEN.uid && !(uid in knownTokens)) { + unknownTokensUid.push(uid); + } + }); + dispatch(unregisteredTokensDownloadRequest({ uids: unknownTokensUid })); + + return () => { + dispatch(setNewNanoContractStatusReady()); + dispatch(newNanoContractRetryDismiss()); + }; + }, []); + + const onFeedbackModalDismiss = () => { + navigation.goBack(); + }; + + const onNavigateToDashboard = () => { + navigation.navigate('Dashboard'); + }; + + const onTryAgain = () => { + dispatch(setNewNanoContractStatusReady()); + dispatch(newNanoContractRetry()); + }; + + // Loading while downloading: + // 1. each token details + // 2. the blueprint details + const isTxInfoLoading = () => ( + knownTokens.isLoading + || blueprintInfo == null + || blueprintInfo.status === NANOCONTRACT_BLUEPRINTINFO_STATUS.LOADING + ); + const isTxInfoLoaded = () => ( + !isTxInfoLoading() && newTxStatus !== WALLETCONNECT_NEW_NANOCONTRACT_TX_STATUS.LOADING + ); + const isTxProcessing = () => ( + !isTxInfoLoading() && newTxStatus === WALLETCONNECT_NEW_NANOCONTRACT_TX_STATUS.LOADING + ); + const isTxSuccessful = () => newTxStatus === WALLETCONNECT_NEW_NANOCONTRACT_TX_STATUS.SUCCESSFUL; + const isTxFailed = () => newTxStatus === WALLETCONNECT_NEW_NANOCONTRACT_TX_STATUS.FAILED; + + return ( + <> + {notRegistered && ( + + )} + /> + )} + {showRequest && ( + + + + {isTxInfoLoading() && ( + } + offmargin + offcard + offbackground + /> + )} + {isTxInfoLoaded() && ( + + + + + + + {/* User actions */} + + + + + + )} + {isTxProcessing() && ( + } + offmargin + offcard + offbackground + /> + )} + + + + )} + + + + {isTxSuccessful() && ( + )} + text={t`Transaction successfully sent.`} + onDismiss={onFeedbackModalDismiss} + action={()} + /> + )} + {isTxFailed() && ( + )} + text={t`Error while sending transaction.`} + onDismiss={onFeedbackModalDismiss} + action={()} + /> + )} + + ); +}; + +const styles = StyleSheet.create({ + wide: { + width: '100%' + }, + wrapper: { + flex: 1, + paddingHorizontal: 16, + backgroundColor: COLORS.lowContrastDetail, // Defines an outer area on the main list content + }, + content: { + flex: 1, + rowGap: 24, + width: '100%', + paddingVertical: 16, + }, + balanceReceived: { + color: 'hsla(180, 85%, 34%, 1)', + fontWeight: 'bold', + }, + actionContainer: { + flexDirection: 'column', + gap: 8, + paddingBottom: 48, + }, + declineModalBody: { + paddingBottom: 24, + }, + text: { + fontSize: 16, + lineHeight: 20, + textAlign: 'center', + }, + feedbackModalIcon: { + height: 105, + width: 105 + }, +}); diff --git a/src/components/WalletConnect/SignMessageModal.js b/src/components/WalletConnect/SignMessageModal.js index c78eff940..e1770470e 100644 --- a/src/components/WalletConnect/SignMessageModal.js +++ b/src/components/WalletConnect/SignMessageModal.js @@ -7,61 +7,71 @@ import React from 'react'; import { useDispatch } from 'react-redux'; +import { useNavigation } from '@react-navigation/native'; import { t } from 'ttag'; import { StyleSheet, Text } from 'react-native'; -import ApproveRejectModal from './ApproveRejectModal'; import { COLORS } from '../../styles/themes'; +import { ModalBase } from '../ModalBase'; +import { WarnDisclaimer } from './WarnDisclaimer'; +import { walletConnectReject } from '../../actions'; -const modalStyle = StyleSheet.create({ - signMessageText: { - backgroundColor: COLORS.textColorShadowLighter, - width: '100%', - height: 100, - borderRadius: 15, - padding: 8, - marginBottom: 12, - marginTop: 12, +const styles = StyleSheet.create({ + body: { + paddingBottom: 24, + }, + text: { + fontSize: 14, + lineHeight: 20, + }, + bold: { + fontWeight: 'bold', + }, + selectionContainer: { + borderRadius: 8, + paddingVertical: 8, + paddingHorizontal: 16, + backgroundColor: COLORS.freeze100, }, }); export default ({ - onAcceptAction, - onRejectAction, onDismiss, data, - baseStyles, }) => { - const styles = { ...baseStyles, modalStyle }; + const navigation = useNavigation(); const dispatch = useDispatch(); - const { message, } = data; - const onAccept = () => { + const onReject = () => { onDismiss(); - dispatch(onAcceptAction); + dispatch(walletConnectReject()); }; - const onReject = () => { + const navigateToSignMessageRequestScreen = () => { onDismiss(); - dispatch(onRejectAction); + navigation.navigate('SignMessageRequest', { signMessageRequest: data }); }; return ( - - - { message } - - - { t`By clicking approve, you will sign the requested message using the first address derived from your root key on the m/44'/280'/0'/0/0 derivation path.` } + + {t`New Sign Message Request`} + + + + {t`You have received a new Sign Message Request. Please`} + + {' '}{t`carefully review the details`}{' '} - - )} - onAccept={onAccept} - onReject={onReject} - data={data} - baseStyles={styles} - /> + {t`before deciding to accept or decline.`} + + + + + ); }; diff --git a/src/components/WalletConnect/SignMessageRequest.js b/src/components/WalletConnect/SignMessageRequest.js new file mode 100644 index 000000000..cb140cf5c --- /dev/null +++ b/src/components/WalletConnect/SignMessageRequest.js @@ -0,0 +1,126 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + StyleSheet, + View, + ScrollView, + TouchableWithoutFeedback, + Text, +} from 'react-native'; +import { useDispatch } from 'react-redux'; +import { useNavigation } from '@react-navigation/native'; +import { t } from 'ttag'; +import { + walletConnectAccept, + walletConnectReject +} from '../../actions'; +import { COLORS } from '../../styles/themes'; +import NewHathorButton from '../NewHathorButton'; +import { DappContainer } from './NanoContract/DappContainer'; +import { commonStyles } from './theme'; +import { NanoContractIcon } from '../Icons/NanoContract.icon'; + +export const SignMessageRequestData = ({ data }) => ( + + + + + + + {t`Message to sign`} + {data.message} + + + + {t`Address`} + {data.address.address} + + + + {t`Address Path`} + {data.address.addressPath} + + + +); + +export const SignMessageRequest = ({ signMessageRequest }) => { + const { dapp, data } = signMessageRequest; + const { message, address } = data; + const dispatch = useDispatch(); + const navigation = useNavigation(); + + const onAcceptSignMessageRequest = () => { + const acceptedReq = { address, message }; + + // Signal the user has accepted the current request and pass the accepted data. + dispatch(walletConnectAccept(acceptedReq)); + navigation.goBack(); + }; + + const onDeclineTransaction = () => { + dispatch(walletConnectReject()); + navigation.goBack(); + }; + + return ( + + + + + + + {/* User actions */} + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + wide: { + width: '100%' + }, + wrapper: { + flex: 1, + paddingHorizontal: 16, + backgroundColor: COLORS.lowContrastDetail, // Defines an outer area on the main list content + }, + content: { + flex: 1, + rowGap: 24, + width: '100%', + paddingVertical: 16, + }, + balanceReceived: { + color: 'hsla(180, 85%, 34%, 1)', + fontWeight: 'bold', + }, + actionContainer: { + flexDirection: 'column', + gap: 8, + paddingBottom: 48, + }, + declineModalBody: { + paddingBottom: 24, + }, + value: [commonStyles.text, commonStyles.value], +}); diff --git a/src/components/WalletConnect/WalletConnectModal.js b/src/components/WalletConnect/WalletConnectModal.js index 4019ce1c9..84ef1b59c 100644 --- a/src/components/WalletConnect/WalletConnectModal.js +++ b/src/components/WalletConnect/WalletConnectModal.js @@ -12,6 +12,8 @@ import { hideWalletConnectModal } from '../../actions'; import SignMessageModal from './SignMessageModal'; import ConnectModal from './ConnectModal'; import { COLORS } from '../../styles/themes'; +import { NewNanoContractTransactionModal } from './NanoContract/NewNanoContractTransactionModal'; +import CreateTokenModal from './CreateTokenModal'; export default () => { const dispatch = useDispatch(); @@ -40,7 +42,20 @@ export default () => { + ); + case WalletConnectModalTypes.SEND_NANO_CONTRACT_TX: + return ( + + ); + case WalletConnectModalTypes.CREATE_TOKEN: + return ( + ); default: @@ -100,4 +115,6 @@ const baseStyles = StyleSheet.create({ export const WalletConnectModalTypes = { CONNECT: 'CONNECT', SIGN_MESSAGE: 'SIGN_MESSAGE', + SEND_NANO_CONTRACT_TX: 'SEND_NANO_CONTRACT_TX', + CREATE_TOKEN: 'CREATE_TOKEN', }; diff --git a/src/components/WalletConnect/WarnDisclaimer.js b/src/components/WalletConnect/WarnDisclaimer.js new file mode 100644 index 000000000..396082cbd --- /dev/null +++ b/src/components/WalletConnect/WarnDisclaimer.js @@ -0,0 +1,83 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { t } from 'ttag'; +import { StyleSheet, View, Text, Linking } from 'react-native'; +import { COLORS } from '../../styles/themes'; +import { CircleInfoIcon } from '../Icons/CircleInfo.icon'; +import SimpleButton from '../SimpleButton'; +import { NANO_CONTRACT_INFO_URL } from '../../constants'; + +export const WarnDisclaimer = () => { + const onReadMore = () => { + Linking.openURL(NANO_CONTRACT_INFO_URL) + }; + + return ( + + + + + + + {t`Caution: There are risks associated with signing dapp transaction requests.`} + + + + + + + ); +}; + +const styles = StyleSheet.create({ + warnContainer: { + flexShrink: 1, + flexDirection: 'row', + alignItems: 'center', + marginBottom: 24, + borderRadius: 8, + paddingTop: 12, + /* It should have been 12 but it is adjusted to compensate the negative + * margin on learnMoreWrapper and the difference between the font size + * and the line height, which amounts to 8 points of compensation. + */ + paddingBottom: 20, + paddingHorizontal: 16, + backgroundColor: COLORS.cardWarning100, + }, + warnContent: { + paddingLeft: 8, + }, + warnMessage: { + fontSize: 12, + lineHeight: 16, + }, + learnMoreWrapper: { + display: 'inline-block', + /* We are using negative margin here to correct the text position + * and create an optic effect of alignment. */ + marginBottom: -4, + paddingLeft: 2, + marginRight: 'auto', + }, + learnMoreContainer: { + justifyContent: 'flex-start', + borderBottomWidth: 1, + }, + learnMoreText: { + fontSize: 12, + lineHeight: 16, + fontWeight: 'bold', + color: 'hsla(0, 0%, 25%, 1)', + }, +}); diff --git a/src/components/WalletConnect/theme.js b/src/components/WalletConnect/theme.js new file mode 100644 index 000000000..bab0f09ee --- /dev/null +++ b/src/components/WalletConnect/theme.js @@ -0,0 +1,74 @@ +import { StyleSheet } from 'react-native'; +import { COLORS } from '../../styles/themes'; + +export const commonStyles = StyleSheet.create({ + // Card + card: { + paddingVertical: 16, + paddingHorizontal: 16, + backgroundColor: COLORS.backgroundColor, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + borderBottomLeftRadius: 16, + borderBottomRightRadius: 16, + shadowOffset: { height: 2, width: 0 }, + shadowRadius: 4, + shadowColor: COLORS.textColor, + shadowOpacity: 0.08, + }, + cardSplit: { + flexDirection: 'row', + gap: 16, + }, + cardSplitIcon: { + flexShrink: 1, + alignSelf: 'flex-start', + }, + cardSplitContent: { + maxWidth: '80%', + flexDirection: 'column', + gap: 8, + }, + cardSeparator: { + width: '100%', + height: 1, + backgroundColor: COLORS.borderColor + }, + cardStack: { + flexDirection: 'column', + }, + cardStackItem: { + flexDirection: 'row', + paddingVertical: 16, + }, + listItem: { + paddingVertical: 24, + paddingHorizontal: 16, + }, + feedbackItem: { + paddingHorizontal: 16, + }, + + // General + text: { + fontSize: 14, + lineHeight: 20, + color: COLORS.black, + }, + bold: { + fontWeight: 'bold', + }, + field: { + color: 'hsla(0, 0%, 38%, 1)', + }, + mb4: { + marginBottom: 4, + }, + sectionTitle: { + fontSize: 16, + fontWeight: 'bold', + lineHeight: 20, + color: COLORS.black, + marginBottom: 24, + }, +}); diff --git a/src/components/WarnTextValue.js b/src/components/WarnTextValue.js new file mode 100644 index 000000000..4ddc85f94 --- /dev/null +++ b/src/components/WarnTextValue.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { AlertUI } from '../styles/themes'; +import { TextValue } from './TextValue'; + +/** + * @param {Object} props + * @param {boolean} props.title It sets font weight to bold and a larger font size + * @param {boolean} props.bold It sets font weight to bold + * @param {boolean} props.oneline It sets numberOfLines to 1 + * @param {boolean} props.shrink It sets flexShrink to 1 + * @param {boolean} props.pb4 It sets padding bottom to 4 + */ +export const WarnTextValue = ({ title, bold, oneline, shrink, pb4, children }) => ( + + {children} + +); diff --git a/src/config.js b/src/config.js index 1a4fd5862..4a1f2182f 100644 --- a/src/config.js +++ b/src/config.js @@ -37,7 +37,10 @@ export const _IS_MULTI_TOKEN = true; * uid: '00' * } */ -export const _DEFAULT_TOKEN = hathorLib.constants.HATHOR_TOKEN_CONFIG; +export const _DEFAULT_TOKEN = { + uid: hathorLib.constants.NATIVE_TOKEN_UID, + ...hathorLib.constants.DEFAULT_NATIVE_TOKEN_CONFIG, +}; /** * App's primary color (Hathor purple) diff --git a/src/constants.js b/src/constants.js index 7d9788cee..a7bae3637 100644 --- a/src/constants.js +++ b/src/constants.js @@ -112,6 +112,7 @@ export const pushNotificationKey = { available: 'pushNotification:available', notificationError: 'pushNotification:notificationError', }; + /** * this is the message key for localization of new transaction when show amount is enabled */ @@ -202,7 +203,7 @@ export const PRE_SETTINGS_TESTNET = { export const NODE_SERVER_MAINNET_URL = 'https://mobile.wallet.hathor.network/v1a/'; export const EXPLORER_MAINNET_URL = 'https://explorer.hathor.network/'; -export const EXPLORER_SERVICE_MAINNET_URL = 'https://explorer-service.hathor.network'; +export const EXPLORER_SERVICE_MAINNET_URL = 'https://explorer-service.hathor.network/'; export const TX_MINING_SERVICE_MAINNET_URL = 'https://txmining.mainnet.hathor.network/'; export const PRE_SETTINGS_MAINNET = { @@ -224,14 +225,34 @@ export const networkSettingsKeyMap = { networkSettings: 'networkSettings:networkSettings' }; -export const NETWORKSETTINGS_STATUS = { +export const BASE_STATUS = { READY: 'ready', FAILED: 'failed', LOADING: 'loading', - WAITING: 'waiting', SUCCESSFUL: 'successful', }; +export const NETWORKSETTINGS_STATUS = { + ...BASE_STATUS, + WAITING: 'waiting', +}; + +export const WALLETCONNECT_CREATE_TOKEN_STATUS = { + ...BASE_STATUS, +}; + +export const NANOCONTRACT_REGISTER_STATUS = { + ...BASE_STATUS, +}; + +export const WALLETCONNECT_NEW_NANOCONTRACT_TX_STATUS = { + ...BASE_STATUS, +}; + +export const NANOCONTRACT_BLUEPRINTINFO_STATUS = { + ...BASE_STATUS, +}; + /** * Timeout in miliseconds to call wallet-service. */ @@ -254,8 +275,34 @@ export const NETWORK_PRIVATENET = 'privatenet'; export const MAX_RETRIES = 8; export const INITIAL_RETRY_LATENCY = 300; // ms export const LATENCY_MULTIPLIER = 30; // multiplier per iteration + /** * Timeout for await wallet load in the context of tx details loading. * It awaits 5 minutes. */ export const TX_DETAILS_TIMEOUT = 5 * 60 * 1000; // 5 minutes + +/** + * Nano Contract's transaction history list size. + */ +export const NANO_CONTRACT_TX_HISTORY_SIZE = 20; +/** + * Nano Contract documentation URL. + */ +export const NANO_CONTRACT_INFO_URL = 'https://docs.hathor.network/explanations/features/nano-contracts/'; + +/** + * Nano Contract Action Enum + */ +export const NANO_CONTRACT_ACTION = { + withdrawal: 'withdrawal', + deposit: 'deposit', +}; + +export const NODE_RATE_LIMIT_CONF = { + thin_wallet_token: { + perSecond: 3, + burst: 10, + delay: 3, + } +}; diff --git a/src/models.js b/src/models.js index e5a14340f..dec798466 100644 --- a/src/models.js +++ b/src/models.js @@ -7,14 +7,75 @@ import moment from 'moment'; import { t } from 'ttag'; +import { transactionUtils, constants } from '@hathor/wallet-lib' export class TxHistory { - constructor({ txId, timestamp, tokenUid, balance, isVoided }) { + /** + * @param {{ + * txId: string; + * timestamp: number; + * tokenUid: string; + * balance: number; + * isVoided: boolean; + * version: number; + * ncId?: string; + * ncMethod?: string; + * ncCaller?: Address; + * firstBlock?: string; + * }} + */ + constructor({ + txId, + timestamp, + tokenUid, + balance, + isVoided, + version, + ncId, + ncMethod, + ncCaller, + firstBlock, + }) { + /** + * @type {string} + */ this.txId = txId; + /** + * @type {number} + */ this.timestamp = timestamp; + /** + * @type {string} + */ this.tokenUid = tokenUid; + /** + * @type {number} + */ this.balance = balance; + /** + * @type {boolean} + */ this.isVoided = isVoided; + /** + * @type {number} + */ + this.version = version; + /** + * @type {string?} + */ + this.ncId = ncId; + /** + * @type {string?} + */ + this.ncMethod = ncMethod; + /** + * @type {Address?} + */ + this.ncCaller = ncCaller; + /** + * @type {string?} + */ + this.firstBlock = firstBlock; } getDescription(token) { @@ -46,4 +107,61 @@ export class TxHistory { sameElse: t`DD MMM YYYY [•] HH:mm`, }); } + + getVersionInfo() { + return transactionUtils.getTxType(this) + } + + isNanoContract() { + return this.version === constants.NANO_CONTRACTS_VERSION; + } + + hasFirstBlock() { + // not null, not undefined and not string empty + return this.firstBlock != null && this.firstBlock.trim() !== ''; + } + + /** + * Creates a TxHistory instance from raw transaction history data. + * + * @param {{ + * txId: string; + * balance: number; + * timestamp: number; + * voided: boolean; + * version: number; + * ncId?: string; + * ncMethod?: string; + * ncCaller?: Address; + * firstBlock?: string; + * }} rawTxHistory - The raw transaction history data. + * @param {string} tokenUid - The UID of the token associated with the transaction. + * + * @returns {TxHistory} A TxHistory instance representing the transaction. + */ + static from(rawTxHistory, tokenUid) { + const { + txId, + timestamp, + balance, + version, + ncId, + ncMethod, + ncCaller, + firstBlock, + } = rawTxHistory; + return new TxHistory({ + txId, + timestamp, + balance, + version, + ncId, + ncMethod, + ncCaller, + tokenUid, + // in wallet service this comes as 0/1 and in the full node comes with true/false + isVoided: Boolean(rawTxHistory.voided), + firstBlock, + }); + } } diff --git a/src/reducers/reducer.init.js b/src/reducers/reducer.init.js index d9288d42b..7829383ac 100644 --- a/src/reducers/reducer.init.js +++ b/src/reducers/reducer.init.js @@ -1,4 +1,17 @@ -import { applyMiddleware, createStore } from 'redux'; +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + applyMiddleware, + // createStore is deprecated, the new recommended way to use redux is by using + // redux-toolkit, more on this at + // https://redux.js.org/introduction/why-rtk-is-redux-today + legacy_createStore as createStore, +} from 'redux'; import createSagaMiddleware from 'redux-saga'; import thunk from 'redux-thunk'; import { reducer } from './reducer'; diff --git a/src/reducers/reducer.js b/src/reducers/reducer.js index 98f2d1700..058249383 100644 --- a/src/reducers/reducer.js +++ b/src/reducers/reducer.js @@ -7,7 +7,18 @@ import hathorLib from '@hathor/wallet-lib'; import { get } from 'lodash'; -import { INITIAL_TOKENS, DEFAULT_TOKEN, PUSH_API_STATUS, FEATURE_TOGGLE_DEFAULTS, PRE_SETTINGS_MAINNET, NETWORKSETTINGS_STATUS } from '../constants'; +import { + INITIAL_TOKENS, + DEFAULT_TOKEN, + PUSH_API_STATUS, + FEATURE_TOGGLE_DEFAULTS, + PRE_SETTINGS_MAINNET, + NETWORKSETTINGS_STATUS, + NANOCONTRACT_REGISTER_STATUS, + WALLETCONNECT_NEW_NANOCONTRACT_TX_STATUS, + NANOCONTRACT_BLUEPRINTINFO_STATUS, + WALLETCONNECT_CREATE_TOKEN_STATUS +} from '../constants'; import { types } from '../actions'; import { TOKEN_DOWNLOAD_STATUS } from '../sagas/tokens'; import { WALLET_STATUS } from '../sagas/wallet'; @@ -22,12 +33,6 @@ import { WALLET_STATUS } from '../sagas/wallet'; * locked: int * } * }]) - * tokensHistory {Object} stores the history for each token (Dict[tokenUid: str, { - * status: string, - * oldStatus: string, - * updatedAt: int, - * data: TxHistory[] - * }]) * loadHistoryStatus {Object} progress on loading tx history { * active {boolean} indicates we're loading the tx history * error {boolean} error loading history @@ -75,6 +80,16 @@ import { WALLET_STATUS } from '../sagas/wallet'; * lastSharedIndex {int} The current address index to use */ const initialState = { + /** + * @type {{ + * [tokenUid: string]: { + * status: TOKEN_DOWNLOAD_STATUS; + * oldStatus: string; + * updatedAt: number; + * data: TxHistory[]; + * } + * }} stores the history for each token () + */ tokensHistory: {}, tokensBalance: {}, loadHistoryStatus: { active: true, error: false }, @@ -85,6 +100,26 @@ const initialState = { * @see {@link INITIAL_TOKENS} */ tokens: INITIAL_TOKENS, + /** + * Remarks + * We use the map of tokens to collect token details for tokens + * used in actions but not registered by the user. + * + * @example + * { + * '000003a3b261e142d3dfd84970d3a50a93b5bc3a66a3b6ba973956148a3eb824': { + * name: 'YanCoin', + * symbol: 'YAN', + * uid: '000003a3b261e142d3dfd84970d3a50a93b5bc3a66a3b6ba973956148a3eb824', + * }, + * isLoading: false, + * error: null, + * } + */ + unregisteredTokens: { + isLoading: false, + error: null, + }, /** * selectedToken {{ * uid: string; @@ -219,6 +254,44 @@ const initialState = { modal: { show: false, }, + /** + * newNanoContractTransaction {{ + * showModal: boolean; + * retrying: boolean; + * data: { + * nc: { + * network: string; + * ncId: string; + * blueprintId: string; + * method: string; + * caller: string; + * actions: { + * type: string; + * token: string; + * amount: number; + * address?: string; + * }[]; + * args: string[]; + * }; + * dapp: { + * icon: string; + * proposer: string; + * url: string; + * description: string; + * }; + * }; + * }} + */ + newNanoContractTransaction: { + status: WALLETCONNECT_NEW_NANOCONTRACT_TX_STATUS.READY, + showModal: false, + retrying: false, + data: null, + }, + createToken: { + status: WALLETCONNECT_CREATE_TOKEN_STATUS.READY, + retrying: false, + }, connectionFailed: false, sessions: {}, }, @@ -242,6 +315,190 @@ const initialState = { networkSettings: PRE_SETTINGS_MAINNET, networkSettingsInvalid: {}, networkSettingsStatus: NETWORKSETTINGS_STATUS.READY, + nanoContract: { + registerStatus: NANOCONTRACT_REGISTER_STATUS.READY, + registerFailureMessage: null, + /** + * registered {{ + * [ncId: string]: { + * address: string, + * ncId: string, + * blueprintId: string, + * blueprintName: string, + * } + * }} registered Nano Contracts per wallet address with basic information. + * @example + * { + * '000001342d3c5b858a4d4835baea93fcc683fa615ff5892bd044459621a0340a': { + * address: 'HTeZeYTCv7cZ8u7pBGHkWsPwhZAuoq5j3V', + * ncId: '000001342d3c5b858a4d4835baea93fcc683fa615ff5892bd044459621a0340a', + * blueprintId: '0025dadebe337a79006f181c05e4799ce98639aedfbd26335806790bdea4b1d4', + * blueprintName: 'Swap', + * }, + * } + */ + registered: {}, + /** + * history {{ + * [ncId: string]: { + * txId: string; + * timestamp: number; + * tokens: string[]; + * isVoided: boolean; + * ncId: string; + * ncMethod: string; + * blueprintId: string; + * firstBlock: string; + * caller: string; + * isMine: boolean; + * actions: { + * type: 'withdrawal'|'deposit'; + * uid: string; + * amount: number; + * }[]; + * }[]; + * }} history of Nano Contracts registered per wallet address. + * @example + * { + * '000001342d3c5b858a4d4835baea93fcc683fa615ff5892bd044459621a0340a': [ + * { + * txId: '000000203e87e8575f121de16d0eb347bd1473eedd9f46cc76c1bc8d4e5a5fce', + * timestamp: 1708356261, + * tokens: [ + * '00000117b0502e9eef9ccbe987af65f153aa899d6eba88d50a6c89e78644713d', + * '0000038c49253f86e6792006dd9124e2c50e6487fde3296b7bd637e3e1a497e7' + * ], + * isVoided: false, + * ncId: '000001342d3c5b858a4d4835baea93fcc683fa615ff5892bd044459621a0340a', + * ncMethod: 'swap', + * blueprintId: '0025dadebe337a79006f181c05e4799ce98639aedfbd26335806790bdea4b1d4'; + * caller: 'HTeZeYTCv7cZ8u7pBGHkWsPwhZAuoq5j3V', + * isMine: true, + * actions: [ + * { + * type: 'withdrawal', + * uid: '00', + * amount: 100, + * } + * ] + * }, + * ], + * } + */ + history: {}, + /** + * historyMeta {{ + * [ncId: string]: { + * isLoading: boolean; + * error: string; + * }; + * }} holds the load state for each nano contract, including the after hash + * from which a new history chunk should be fetched, exclusively. + * @example + * { + * '000001342d3c5b858a4d4835baea93fcc683fa615ff5892bd044459621a0340a': { + * isLoading: false, + * }, + * } + */ + historyMeta: {}, + /** + * blueprint { + * [blueprintId: string]: { + * status: string; + * data?: { + * id: string; + * name: string; + * public_methods: { + * [methodName: string]: { + * args: { + * type: string; + * name: string; + * }[]; + * }; + * }; + * }; + * error?: string; + * } + * } + * @example + * { + * '3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595': { + * status: 'success', + * data: { + * id: '3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595', + * name: 'Bet', + * public_methods: { + * bet: { + * args: [ + * { + * name: "address", + * type: "bytes" + * }, + * { + * name: "score", + * type: "str" + * } + * ], + * return_type: "null" + * }, + * initialize: { + * args: [ + * { + * name: "oracle_script", + * type: "bytes" + * }, + * { + * name: "token_uid", + * type: "bytes" + * }, + * { + * name: "date_last_bet", + * type: "int" + * } + * ], + * return_type: "null" + * }, + * set_result: { + * args: [ + * { + * name: "result", + * type: "SignedData[str]" + * } + * ], + * return_type: "null" + * }, + * withdraw: { + * args: [], + * return_type: "null" + * } + * }, + * }, + * }, + * } + */ + blueprint: {}, + }, + /** + * selectAddressModal {{ + * addresses: string[]; + * error?: string; + * }} it holds all wallet addresses or the error status. + */ + selectAddressModal: { + addresses: [], + error: null, + }, + /** + * firstAddress {{ + * address?: string; + * error?: string; + * }} it holds the first wallet address or the error status. + */ + firstAddress: { + address: null, + error: null, + }, }; export const reducer = (state = initialState, action) => { @@ -398,6 +655,66 @@ export const reducer = (state = initialState, action) => { return onNetworkSettingsUpdateFailure(state); case types.NETWORKSETTINGS_UPDATE_INVALID: return onNetworkSettingsUpdateInvalid(state, action); + case types.NANOCONTRACT_HISTORY_LOADING: + return onNanoContractHistoryLoading(state, action); + case types.NANOCONTRACT_HISTORY_FAILURE: + return onNanoContractHistoryFailure(state, action); + case types.NANOCONTRACT_HISTORY_SUCCESS: + return onNanoContractHistorySuccess(state, action); + case types.NANOCONTRACT_HISTORY_CLEAN: + return onNanoContractHistoryClean(state, action); + case types.NANOCONTRACT_UNREGISTER_SUCCESS: + return onNanoContractUnregisterSuccess(state, action); + case types.NANOCONTRACT_ADDRESS_CHANGE_REQUEST: + return onNanoContractAddressChangeRequest(state, action); + case types.NANOCONTRACT_REGISTER_REQUEST: + return onNanoContractRegisterRequest(state); + case types.NANOCONTRACT_REGISTER_FAILURE: + return onNanoContractRegisterFailure(state, action); + case types.NANOCONTRACT_REGISTER_SUCCESS: + return onNanoContractRegisterSuccess(state, action); + case types.NANOCONTRACT_REGISTER_READY: + return onNanoContractRegisterReady(state); + case types.SELECTADDRESS_ADDRESSES_REQUEST: + return onSelectAddressAddressesRequest(state); + case types.SELECTADDRESS_ADDRESSES_FAILURE: + return onSelectAddressAddressesFailure(state, action); + case types.SELECTADDRESS_ADDRESSES_SUCCESS: + return onSelectAddressAddressesSuccess(state, action); + case types.FIRSTADDRESS_REQUEST: + return onFirstAddressRequest(state); + case types.FIRSTADDRESS_FAILURE: + return onFirstAddressFailure(state, action); + case types.FIRSTADDRESS_SUCCESS: + return onFirstAddressSuccess(state, action); + case types.SET_NEW_NANO_CONTRACT_TRANSACTION: + return onSetNewNanoContractTransaction(state, action); + case types.WALLETCONNECT_NEW_NANOCONTRACT_STATUS: + return onSetNewNanoContractTransactionStatus(state, action); + case types.WALLETCONNECT_CREATE_TOKEN_STATUS: + return onSetCreateTokenStatus(state, action); + case types.WALLETCONNECT_CREATE_TOKEN_RETRY: + return onSetCreateTokenRetry(state, action); + case types.WALLETCONNECT_CREATE_TOKEN_RETRY_DISMISS: + return onSetCreateTokenRetryDismiss(state, action); + case types.NANOCONTRACT_BLUEPRINTINFO_REQUEST: + return onNanoContractBlueprintInfoRequest(state, action); + case types.NANOCONTRACT_BLUEPRINTINFO_FAILURE: + return onNanoContractBlueprintInfoFailure(state, action); + case types.NANOCONTRACT_BLUEPRINTINFO_SUCCESS: + return onNanoContractBlueprintInfoSuccess(state, action); + case types.UNREGISTEREDTOKENS_DOWNLOAD_REQUEST: + return onUnregisteredTokensDownloadRequest(state); + case types.UNREGISTEREDTOKENS_DOWNLOAD_SUCCESS: + return onUnregisteredTokensDownloadSuccess(state, action); + case types.UNREGISTEREDTOKENS_DOWNLOAD_FAILURE: + return onUnregisteredTokensDownloadFailure(state, action); + case types.UNREGISTEREDTOKENS_DOWNLOAD_END: + return onUnregisteredTokensDownloadEnd(state); + case types.WALLETCONNECT_NEW_NANOCONTRACT_RETRY: + return onNewNanoContractTransactionRetry(state); + case types.WALLETCONNECT_NEW_NANOCONTRACT_RETRY_DISMISS: + return onNewNanoContractTransactionRetryDismiss(state); default: return state; } @@ -459,9 +776,9 @@ const onNewTx = (state, action) => { } if (txout.decoded && txout.decoded.address - && txout.decoded.address === state.latestInvoice.address - && txout.value === state.latestInvoice.amount - && txout.token === state.latestInvoice.token.uid) { + && txout.decoded.address === state.latestInvoice.address + && txout.value === state.latestInvoice.amount + && txout.token === state.latestInvoice.token.uid) { invoicePayment = tx; break; } @@ -745,8 +1062,10 @@ export const onTokenFetchBalanceFailed = (state, action) => { }; /** + * @param {Object} state - redux state + * @param {Object} action - token's history * @param {String} action.tokenId - The tokenId to mark as success - * @param {Object} action.data - The token history information to store on redux + * @param {TxHistory} action.data - The token history information to store on redux */ export const onTokenFetchHistorySuccess = (state, action) => { const { tokenId, data } = action; @@ -1087,13 +1406,20 @@ export const onExceptionCaptured = (state, { payload }) => { }; /** - * On wallet reload, tokens data will be reloaded as well. + * On wallet reload, tokens and nano contract data will be + * reloaded as well. + * + * Some flows can request a wallet reload such as: + * - Disable wallet service + * - Change network settings + * - Loose network connection */ export const onReloadWalletRequested = (state) => ({ ...state, tokensHistory: initialState.tokensHistory, tokensBalance: initialState.tokensBalance, loadHistoryStatus: initialState.loadHistoryStatus, + nanoContract: initialState.nanoContract, }); const onWalletReloading = (state) => ({ @@ -1222,3 +1548,569 @@ export const onNetworkSettingsUpdateInvalid = (state, { payload }) => ({ networkSettingsInvalid: payload, networkSettingsStatus: NETWORKSETTINGS_STATUS.READY, }); + +export const onNanoContractRegisterRequest = (state) => ({ + ...state, + nanoContract: { + ...state.nanoContract, + registerStatus: NANOCONTRACT_REGISTER_STATUS.LOADING, + registerFailureMessage: null, + }, +}); + +/** + * @param {Object} state Redux store state + * @param {Object} action + * @param {Object} action.payload + * @param {string} action.payload.error Error message on failure + */ +export const onNanoContractRegisterFailure = (state, { payload: { error } }) => ({ + ...state, + nanoContract: { + ...state.nanoContract, + registerStatus: NANOCONTRACT_REGISTER_STATUS.FAILED, + registerFailureMessage: error, + }, +}); + +/** + * @param {Object} state + * @param {{ + * payload: { + * entryKey: string; + * entryValue: { + * address: string; + * ncId: string; + * blueprintName: string; + * }; + * hasFeedback?: boolean; + * } + * }} action + */ +export const onNanoContractRegisterSuccess = (state, { payload }) => ({ + ...state, + nanoContract: { + ...state.nanoContract, + registerStatus: payload.hasFeedback + ? NANOCONTRACT_REGISTER_STATUS.SUCCESSFUL + : NANOCONTRACT_REGISTER_STATUS.READY, + registered: { + ...state.nanoContract.registered, + [payload.entryKey]: payload.entryValue, + }, + history: { + ...state.nanoContract.history, + [payload.entryKey]: [], + }, + historyMeta: { + ...state.nanoContract.historyMeta, + [payload.entryKey]: { + isLoading: false, + error: null, + after: null, + }, + }, + }, +}); + +export const onNanoContractRegisterReady = (state) => ({ + ...state, + nanoContract: { + ...state.nanoContract, + registerStatus: NANOCONTRACT_REGISTER_STATUS.READY, + registerFailureMessage: null, + }, +}); + +/** + * @param {Object} state + * @param {{ + * payload: { + * ncId: string, + * } + * }} action + */ +export const onNanoContractUnregisterSuccess = (state, { payload }) => { + const { ncId } = payload; + + const newRegisteredContracts = { ...state.nanoContract.registered }; + delete newRegisteredContracts[ncId]; + + const newContractsHistory = { ...state.nanoContract.history }; + delete newContractsHistory[ncId]; + + const newContractsHistoryMeta = { ...state.nanoContract.historyMeta }; + delete newContractsHistoryMeta[ncId]; + + return ({ + ...state, + nanoContract: { + ...state.nanoContract, + registered: newRegisteredContracts, + history: newContractsHistory, + historyMeta: newContractsHistoryMeta, + }, + }); +}; + +/** + * @param {Object} state + * @param {{ + * payload: { + * ncId: string; + * } + * }} action + */ +export const onNanoContractHistoryLoading = (state, { payload }) => ({ + ...state, + nanoContract: { + ...state.nanoContract, + historyMeta: { + ...state.nanoContract.historyMeta, + [payload.ncId]: { + ...(state.nanoContract.historyMeta[payload.ncId]), + isLoading: true, + error: null, + }, + }, + }, +}); + +/** + * @param {Object} state + * @param {{ + * payload: { + * ncId: string; + * error: string; + * } + * }} action + */ +export const onNanoContractHistoryFailure = (state, { payload }) => ({ + ...state, + nanoContract: { + ...state.nanoContract, + historyMeta: { + ...state.nanoContract.historyMeta, + [payload.ncId]: { + ...(state.nanoContract.historyMeta[payload.ncId]), + isLoading: false, + error: payload.error, + }, + }, + }, +}); + +/** + * @param {Object} state + * @param {{ + * payload: { + * ncId: string; + * } + * }} action + */ +export const onNanoContractHistoryClean = (state, { payload }) => ({ + ...state, + nanoContract: { + ...state.nanoContract, + history: { + ...state.nanoContract.history, + [payload.ncId]: [], + }, + historyMeta: { + ...state.nanoContract.historyMeta, + [payload.ncId]: { + ...(state.nanoContract.historyMeta[payload.ncId]), + isLoading: false, + error: null, + }, + }, + }, +}); + +/** + * @param {Object} state + * @param {{ + * payload: { + * ncId: string; + * history?: Object[]; + * beforeHistory?: Object[]; + * afterHistory?: Object[]; + * } + * }} action + */ +export const onNanoContractHistorySuccess = (state, { payload }) => ({ + ...state, + nanoContract: { + ...state.nanoContract, + history: { + ...state.nanoContract.history, + [payload.ncId]: [ + ...(payload.beforeHistory || []), + ...(payload.history || state.nanoContract.history[payload.ncId] || []), + ...(payload.afterHistory || []), + ], + }, + historyMeta: { + ...state.nanoContract.historyMeta, + [payload.ncId]: { + ...(state.nanoContract.historyMeta[payload.ncId]), + isLoading: false, + error: null, + }, + }, + }, +}); + +/** + * @param {Object} state + * @param {{ + * payload: { + * ncId: string; + * newAddress: string; + * } + * }} action + */ +export const onNanoContractAddressChangeRequest = (state, { payload }) => { + const newRegisteredNc = { + ...state.nanoContract.registered[payload.ncId], + address: payload.newAddress, + }; + return { + ...state, + nanoContract: { + ...state.nanoContract, + registered: { + ...state.nanoContract.registered, + [payload.ncId]: newRegisteredNc, + }, + }, + }; +}; + +/** + * @param {Object} state + */ +export const onSelectAddressAddressesRequest = (state) => ({ + ...state, + selectAddressModal: initialState.selectAddressModal, +}); + +/** + * @param {Object} state + * @param {{ + * payload: { + * error: string; + * } + * }} action + */ +export const onSelectAddressAddressesFailure = (state, { payload }) => ({ + ...state, + selectAddressModal: { + addresses: [], + error: payload.error, + }, +}); + +/** + * @param {Object} state + * @param {{ + * payload: { + * addresses: string[]; + * } + * }} action + */ +export const onSelectAddressAddressesSuccess = (state, { payload }) => ({ + ...state, + selectAddressModal: { + addresses: payload.addresses, + error: null, + }, +}); + +/** + * @param {Object} state + */ +export const onFirstAddressRequest = (state) => ({ + ...state, + firstAddress: initialState.firstAddress, +}); + +/** + * @param {Object} state + * @param {{ + * payload: { + * error: string; + * } + * }} action + */ +export const onFirstAddressFailure = (state, { payload }) => ({ + ...state, + firstAddress: { + address: null, + error: payload.error, + }, +}); + +/** + * @param {Object} state + * @param {{ + * payload: { + * address: string; + * } + * }} action + */ +export const onFirstAddressSuccess = (state, { payload }) => ({ + ...state, + firstAddress: { + address: payload.address, + error: null, + }, +}); + +/** + * @param {Object} state + * @param {{ + * payload: { + * showModal: boolean; + * data: { + * nc: { + * network: string; + * ncId: string; + * blueprintId: string; + * method: string; + * caller: string; + * actions: { + * type: string; + * token: string; + * amount: number; + * address?: string; + * }[]; + * args: string[]; + * }; + * dapp: { + * icon: string; + * proposer: string; + * url: string; + * description: string; + * }; + * }; + * }; + * }} action + */ +export const onSetNewNanoContractTransaction = (state, { payload }) => ({ + ...state, + walletConnect: { + ...state.walletConnect, + newNanoContractTransaction: { + ...payload, + status: WALLETCONNECT_NEW_NANOCONTRACT_TX_STATUS.READY, + }, + }, +}); + +export const onNewNanoContractTransactionRetry = (state) => ({ + ...state, + walletConnect: { + ...state.walletConnect, + newNanoContractTransaction: { + ...state.walletConnect.newNanoContractTransaction, + retrying: true, + }, + }, +}); + +export const onNewNanoContractTransactionRetryDismiss = (state) => ({ + ...state, + walletConnect: { + ...state.walletConnect, + newNanoContractTransaction: { + ...state.walletConnect.newNanoContractTransaction, + retrying: false, + }, + }, +}); + +export const onSetNewNanoContractTransactionStatus = (state, { payload }) => ({ + ...state, + walletConnect: { + ...state.walletConnect, + newNanoContractTransaction: { + ...state.walletConnect.newNanoContractTransaction, + status: payload, + }, + }, +}); + +export const onSetCreateTokenRetry = (state) => ({ + ...state, + walletConnect: { + ...state.walletConnect, + createToken: { + ...state.walletConnect.createToken, + retrying: true, + }, + }, +}); + +export const onSetCreateTokenRetryDismiss = (state) => ({ + ...state, + walletConnect: { + ...state.walletConnect, + createToken: { + ...state.walletConnect.createToken, + retrying: false, + }, + }, +}); + +export const onSetCreateTokenStatus = (state, { payload }) => ({ + ...state, + walletConnect: { + ...state.walletConnect, + createToken: { + ...state.walletConnect.createToken, + status: payload, + retrying: false, + }, + }, +}); + +/** + * @param {Object} state + * @param {{ + * payload: { + * id: string; + * }; + * }} action + */ +export const onNanoContractBlueprintInfoRequest = (state, { payload }) => ({ + ...state, + nanoContract: { + ...state.nanoContract, + blueprint: { + ...state.nanoContract.blueprint, + [payload.id]: { + status: NANOCONTRACT_BLUEPRINTINFO_STATUS.LOADING, + data: null, + error: null, + }, + }, + }, +}); + +/** + * @param {Object} state + * @param {{ + * payload: { + * id: string; + * error: string; + * }; + * }} action + */ +export const onNanoContractBlueprintInfoFailure = (state, { payload }) => ({ + ...state, + nanoContract: { + ...state.nanoContract, + blueprint: { + ...state.nanoContract.blueprint, + [payload.id]: { + status: NANOCONTRACT_BLUEPRINTINFO_STATUS.FAILED, + data: null, + error: payload.error, + }, + }, + }, +}); + +/** + * @param {Object} state + * @param {{ + * payload: { + * id: string; + * data: { + * id: string; + * name: string; + * public_methods: { + * [methodName: string]: { + * args: { + * type: string; + * name: string; + * }[]; + * }; + * }; + * }; + * }; + * }} action + */ +export const onNanoContractBlueprintInfoSuccess = (state, { payload }) => ({ + ...state, + nanoContract: { + ...state.nanoContract, + blueprint: { + ...state.nanoContract.blueprint, + [payload.id]: { + status: NANOCONTRACT_BLUEPRINTINFO_STATUS.SUCCESSFUL, + data: payload.data, + error: null, + }, + }, + }, +}); + +/** + * Remarks + * This reducer aims to clean error feedback message before processing the request. + */ +export const onUnregisteredTokensDownloadRequest = (state) => ({ + ...state, + unregisteredTokens: { + ...state.unregisteredTokens, + isLoading: true, + error: null, + }, +}); + +/** + * Update tokens state as the request was successful. + * + * @param {Object} state + * @param {Object} action + * @param {Object} action.payload + * @param {string} action.payload.error The error message as feedback to user + */ +export const onUnregisteredTokensDownloadSuccess = (state, { payload }) => ({ + ...state, + unregisteredTokens: { + ...state.unregisteredTokens, + ...payload.tokens, + }, +}); + +/** + * Set error message as a user feedback. + * + * @param {Object} state + * @param {Object} action + * @param {Object} action.payload + * @param {string} action.payload.error The error message as feedback to user + */ +export const onUnregisteredTokensDownloadFailure = (state, { payload }) => ({ + ...state, + unregisteredTokens: { + ...state.unregisteredTokens, + error: payload.error || null, + }, +}); + +/** + * Change state of isLoading to false while keeping tokens state. + * + * @param {Object} state + */ +export const onUnregisteredTokensDownloadEnd = (state) => ({ + ...state, + unregisteredTokens: { + ...state.unregisteredTokens, + isLoading: false, + }, +}); diff --git a/src/sagas/featureToggle.js b/src/sagas/featureToggle.js index 9300bf984..67fefd829 100644 --- a/src/sagas/featureToggle.js +++ b/src/sagas/featureToggle.js @@ -79,7 +79,6 @@ export function* fetchTogglesRoutine() { } export function* handleToggleUpdate() { - console.log('Handling feature toggle update'); const unleashClient = yield select((state) => state.unleashClient); const networkSettings = yield select((state) => state.networkSettings); diff --git a/src/sagas/helpers.js b/src/sagas/helpers.js index 6a77228e4..63f7710be 100644 --- a/src/sagas/helpers.js +++ b/src/sagas/helpers.js @@ -34,6 +34,10 @@ import { LATENCY_MULTIPLIER, } from '../constants'; import { STORE } from '../store'; +import { logger } from '../logger'; +import { consumeAsyncIterator } from '../utils'; + +const log = logger('helper'); export function* waitForFeatureToggleInitialization() { const featureTogglesInitialized = yield select((state) => state.featureTogglesInitialized); @@ -166,7 +170,7 @@ export function isUnlockScreen(action) { * }>} */ export async function getRegisteredTokens(wallet, excludeHTR = false) { - const htrUid = hathorLib.constants.HATHOR_TOKEN_CONFIG.uid; + const htrUid = hathorLib.constants.NATIVE_TOKEN_UID; const tokens = {}; // redux-saga generator magic does not work well with the "for await..of" syntax @@ -191,6 +195,16 @@ export async function getRegisteredTokens(wallet, excludeHTR = false) { return tokens; } +/** + * Retrieve registered Nano Contracts persisted on App's storage. + * @param {Object} wallet Wallet instance + * @return {Promise} + * @async + */ +export async function getRegisteredNanoContracts(wallet) { + return consumeAsyncIterator(wallet.storage.getRegisteredNanoContracts()); +} + /** * Flat registered tokens to uid. * @param {{ tokens: Object }} Map of registered tokens by uid @@ -219,7 +233,8 @@ export async function getFullnodeNetwork() { }); }); return response.network; - } catch { + } catch (e) { + log.error('Error while getting fullnode network version.', e); throw new Error('Error getting fullnode version data.'); } } @@ -329,3 +344,32 @@ export function* progressiveRetryRequest(request, maxRetries = MAX_RETRIES) { // throw last error after retries exhausted throw lastError; } + +export function* retryHandler(retryAction, dismissAction) { + const { retry } = yield race({ + retry: take(retryAction), + dismiss: take(dismissAction), + }); + + return retry != null; +} + +/** + * A wrapper effect to catch unexpected error and provide a binding + * for the desired handling behavior. + * + * @param {Object} effect The targeted effect. + * @param {(error) => void} onError The error handling effect, + * which receives the error object as first argument. + * + * @returns An anonymous effect. + */ +export function safeEffect(effect, onError) { + return function* _safeEffect(payload) { + try { + yield call(effect, payload); + } catch (error) { + yield call(onError, error); + } + } +} diff --git a/src/sagas/index.js b/src/sagas/index.js index fbcebea0f..ed340924b 100644 --- a/src/sagas/index.js +++ b/src/sagas/index.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import { all, fork } from 'redux-saga/effects'; +import { all, call, put, spawn } from 'redux-saga/effects'; import { saga as walletSagas } from './wallet'; import { saga as tokensSagas } from './tokens'; import { saga as pushNotificationSaga } from './pushNotification'; @@ -14,22 +14,55 @@ import { saga as featureToggleSagas } from './featureToggle'; import { saga as permissionsSagas } from './permissions'; import { saga as walletConnectSagas } from './walletConnect'; import { saga as networkSettingsSagas } from './networkSettings'; +import { saga as nanoContractSagas } from './nanoContract'; +import { onExceptionCaptured } from '../actions'; +import { logger } from '../logger'; -const sagas = [ - walletSagas, - tokensSagas, - pushNotificationSaga, - networkSettingsSagas, - errorHandlerSagas, - featureToggleSagas, - permissionsSagas, - walletConnectSagas, -]; - -function* defaultSaga() { - yield all( - sagas.map((saga) => fork(saga)) - ); +const MAX_RETRIES = 5; + +const log = logger('rootSaga'); + +const sagas = { + walletSagas: { saga: walletSagas, retryCount: 0, critical: true }, + tokensSagas: { saga: tokensSagas, retryCount: 0, critical: true }, + pushNotificationSaga: { saga: pushNotificationSaga, retryCount: 0, critical: true }, + networkSettingsSagas: { saga: networkSettingsSagas, retryCount: 0, critical: true }, + errorHandlerSagas: { saga: errorHandlerSagas, retryCount: 0, critical: true }, + featureToggleSagas: { saga: featureToggleSagas, retryCount: 0, critical: true }, + permissionsSagas: { saga: permissionsSagas, retryCount: 0, critical: true }, + walletConnectSagas: { saga: walletConnectSagas, retryCount: 0, critical: false }, + nanoContractSagas: { saga: nanoContractSagas, retryCount: 0, critical: true }, +}; + +function* rootSaga() { + yield all(Object.keys(sagas).map((name) => spawn(function* supervisor() { + while (true) { + const { saga, retryCount, critical } = sagas[name]; + + try { + if (retryCount > MAX_RETRIES && !critical) { + continue; + } + + yield call(saga); + + break + } catch (e) { + log.error('Error on root saga', e); + log.error(`Saga ${name} crashed, restarting. [${retryCount}/${MAX_RETRIES}]`); + sagas[name].retryCount = retryCount + 1; + + if (retryCount >= MAX_RETRIES) { + log.error(`Max retries reached for saga ${name}`); + yield put(onExceptionCaptured(e, critical)); + break; + } + + yield put(onExceptionCaptured(e, false)); + log.error(e); + } + } + }))); } -export default defaultSaga; +export default rootSaga; diff --git a/src/sagas/nanoContract.js b/src/sagas/nanoContract.js new file mode 100644 index 000000000..cca332e59 --- /dev/null +++ b/src/sagas/nanoContract.js @@ -0,0 +1,475 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + ncApi, + addressUtils, + nanoUtils, +} from '@hathor/wallet-lib'; +import { + takeEvery, + select, + all, + put, + call, + debounce, +} from 'redux-saga/effects'; +import { t } from 'ttag'; +import { NanoRequest404Error } from '@hathor/wallet-lib/lib/errors'; +import { getRegisteredNanoContracts, safeEffect } from './helpers'; +import { isWalletServiceEnabled } from './wallet'; +import { + nanoContractBlueprintInfoFailure, + nanoContractBlueprintInfoSuccess, + nanoContractHistoryClean, + nanoContractHistoryFailure, + nanoContractHistoryLoading, + nanoContractHistorySuccess, + nanoContractRegisterFailure, + nanoContractRegisterSuccess, + nanoContractUnregisterSuccess, + onExceptionCaptured, + types, +} from '../actions'; +import { logger } from '../logger'; +import { NANO_CONTRACT_TX_HISTORY_SIZE } from '../constants'; +import { getNanoContractFeatureToggle } from '../utils'; + +const log = logger('nano-contract-saga'); + +export const failureMessage = { + alreadyRegistered: t`Nano Contract already registered.`, + walletNotReadyError: t`Wallet is not ready yet to register a Nano Contract.`, + addressNotMine: t`The informed address does not belong to the wallet.`, + nanoContractStateNotFound: t`Nano Contract not found.`, + nanoContractFailure: t`Error while trying to register Nano Contract.`, + nanoContractInvalid: t`Invalid transaction to register as Nano Contract.`, + blueprintInfoNotFound: t`Blueprint not found.`, + blueprintInfoFailure: t`Couldn't get Blueprint info.`, + notRegistered: t`Nano Contract not registered.`, + nanoContractHistoryFailure: t`Error while trying to download Nano Contract transactions history.`, +}; + +/** + * Call the async wallet method `isAddressMine` considering the type of wallet. + * + * @param {Object} wallet A wallet instance + * @param {string} address A wallet address to check + * @param {boolean} useWalletService A flag that determines if wallet service is in use + */ +export const isAddressMine = async (wallet, address, useWalletService) => { + // XXX: Wallet Service doesn't implement isAddressMine. + // See issue: https://github.com/HathorNetwork/hathor-wallet-lib/issues/732 + // Default to `false` if using Wallet Service. + if (useWalletService) { + return false; + } + + const isMine = await wallet.isAddressMine(address); + return isMine; +}; + +export function* init() { + const isEnabled = yield select(getNanoContractFeatureToggle); + if (!isEnabled) { + log.debug('Halting nano contract initialization because the feature flag is disabled.'); + return; + } + + const wallet = yield select((state) => state.wallet); + const contracts = yield call(getRegisteredNanoContracts, wallet); + for (const contract of contracts) { + yield put(nanoContractRegisterSuccess({ entryKey: contract.ncId, entryValue: contract })); + } + log.debug('Registered Nano Contracts loaded.'); +} + +/** + * Process Nano Contract registration request. + * @param {{ + * payload: { + * address: string, + * ncId: string, + * } + * }} action with request payload. + */ +export function* registerNanoContract({ payload }) { + const { address, ncId } = payload; + + const wallet = yield select((state) => state.wallet); + if (!wallet.isReady()) { + log.debug('Fail registering Nano Contract because wallet is not ready yet.'); + yield put(nanoContractRegisterFailure(failureMessage.walletNotReadyError)); + // This will show user an error modal with the option to send the error to sentry. + yield put(onExceptionCaptured(new Error(failureMessage.walletNotReadyError), false)); + return; + } + + const isNanoContractRegisteredFn = wallet.storage.isNanoContractRegistered.bind(wallet.storage); + const isRegistered = yield call(isNanoContractRegisteredFn, ncId); + if (isRegistered) { + log.debug('Fail registering Nano Contract because it is already registered.'); + yield put(nanoContractRegisterFailure(failureMessage.alreadyRegistered)); + return; + } + + // XXX: Wallet Service doesn't implement isAddressMine. + // See issue: https://github.com/HathorNetwork/hathor-wallet-lib/issues/732 + // Default to `false` if using Wallet Service. + let isAddrMine = false; + const useWalletService = yield call(isWalletServiceEnabled); + if (!useWalletService) { + isAddrMine = yield call([wallet, wallet.isAddressMine], address); + } + + if (!isAddrMine) { + log.debug('Fail registering Nano Contract because address do not belongs to this wallet.'); + yield put(nanoContractRegisterFailure(failureMessage.addressNotMine)); + return; + } + + let tx; + try { + const response = yield call([wallet, wallet.getFullTxById], ncId); + tx = response.tx; + } catch (error) { + log.debug('Fail registering Nano Contract while getting full transaction by ID.'); + yield put(nanoContractRegisterFailure(failureMessage.nanoContractFailure)); + return; + } + + if (!nanoUtils.isNanoContractCreateTx(tx)) { + log.debug('Fail registering Nano Contract because transaction is not calling initialize.'); + yield put(nanoContractRegisterFailure(failureMessage.nanoContractInvalid)); + return; + } + const { nc_blueprint_id: blueprintId } = tx; + + let blueprintName = null; + try { + const blueprintInfo = yield call([ncApi, ncApi.getBlueprintInformation], blueprintId); + blueprintName = blueprintInfo.name; + // Also set blueprint on store + yield put(nanoContractBlueprintInfoSuccess(blueprintId, blueprintInfo)); + } catch (error) { + if (error instanceof NanoRequest404Error) { + yield put(nanoContractRegisterFailure(failureMessage.blueprintInfoNotFound)); + } else { + log.error('Error while registering Nano Contract.', error); + yield put(nanoContractRegisterFailure(failureMessage.blueprintInfoFailure)); + } + return; + } + + const nc = { + address, + ncId, + blueprintId, + blueprintName, + }; + yield call(wallet.storage.registerNanoContract.bind(wallet.storage), ncId, nc); + + log.debug(`Success registering Nano Contract. nc = ${nc}`); + // emit action NANOCONTRACT_REGISTER_SUCCESS with feedback to user + yield put(nanoContractRegisterSuccess({ entryKey: ncId, entryValue: nc, hasFeedback: true })); +} + +/** + * Effect invoked by safeEffect if an unexpected error occurs. + * + * @param {Object} error The error captured. + */ +function* registerNanoContractOnError(error) { + log.error('Unexpected error while registering Nano Contract.', error); + yield put(nanoContractRegisterFailure(failureMessage.nanoContractFailure)); + yield put(onExceptionCaptured(error, false)); +} + +/** + * @typedef {Object} RawNcTxHistory + * @property {string} hash + * @property {Object[]} inputs + * @property {Object[]} outputs + * @property {string} nc_args + * @property {string} nc_blueprint_id + * @property {string} nc_id + * @property {string} nc_method + * @property {string} nc_pubkey + * @property {number} nonce + * @property {Object[]} parents + * @property {number} signal_bits + * @property {Date} timestamp + * @property {string[]} tokens + * @property {number} version + * @property {number} weight + * @property {boolean} is_voided + * + * @typedef {Object} RawNcTxHistoryResponse + * @property {boolean} success + * @property {RawNcTxHistory} history + */ + +/** + * @typedef {Object} NcTxHistory - Nano Contract's Transactions History + * @property {string} txId - transaction hash + * @property {number} timestamp + * @property {string} tokens - list of token's uid + * @property {{[uid: string]: Object}} balance - balance per token + * @property {boolean} isVoided - flag for transaction void + * @property {string} ncId - ID of Nano Contract + * @property {string} ncMethod - method called on Nano Contract + * @property {string} blueprintId - id of the Blueprint instantiated by Nano Contract + * @property {string} caller - address of the caller + * @property {boolean} isMine - flag indicating the caller address belongs to this wallet + */ + +/** + * Fetch history from Nano Contract wallet's API. + * @req {Object} req + * @param {string} req.ncId Nano Contract ID + * @param {number} req.count Maximum quantity of items to fetch + * @param {string} req.before Transaction hash to start to get newer items + * @param {string} req.after Transaction hash to start to get older items + * @param {Object} req.wallet Wallet instance from redux state + * @param {boolean} req.useWalletService A flag that determines if wallet service is in use + * + * @returns {{ + * history: NcTxHistory; + * }} + * + * @throws {Error} when request code is greater then 399 or when response's success is false + */ +export async function fetchHistory(req) { + const { + wallet, + useWalletService, + ncId, + count, + after, + before, + } = req; + /** + * @type {RawNcTxHistoryResponse} response + */ + const response = await ncApi.getNanoContractHistory( + ncId, + count, + after || null, + before || null, + ); + const { success, history: rawHistory } = response; + + if (!success) { + throw new Error('Failed to fetch nano contract history'); + } + + const network = wallet.getNetworkObject(); + // Translate rawNcTxHistory to NcTxHistory + // Prouce a list ordered from newest to oldest + const transformedTxHistory = rawHistory.map(async (rawTx) => { + const caller = addressUtils.getAddressFromPubkey(rawTx.nc_pubkey, network).base58; + const actions = rawTx.nc_context.actions.map((each) => ({ + type: each.type, // 'deposit' or 'withdrawal' + uid: each.token_uid, + amount: each.amount, + })); + const isMine = await isAddressMine(wallet, caller, useWalletService); + + const tx = { + txId: rawTx.hash, + timestamp: rawTx.timestamp, + tokens: rawTx.tokens, + isVoided: rawTx.is_voided, + ncId: rawTx.nc_id, + ncMethod: rawTx.nc_method, + blueprintId: rawTx.nc_blueprint_id, + firstBlock: rawTx.first_block, + caller, + actions, + isMine, + }; + return tx; + }); + + return { history: await Promise.all(transformedTxHistory) }; +} + +/** + * Process Nano Contract history request. + * @param {{ + * payload: { + * ncId: string; + * before?: string; + * after?: string; + * } + * }} action with request payload. + */ +export function* requestHistoryNanoContract({ payload }) { + const { ncId, before, after } = payload; + log.debug('Start processing request for nano contract transaction history...'); + + const historyMeta = yield select((state) => state.nanoContract.historyMeta); + if (historyMeta[ncId] && historyMeta[ncId].isLoading) { + // Do nothing if nano contract already loading... + log.debug('Halting processing for nano contract transaction history request while it is loading...'); + return; + } + yield put(nanoContractHistoryLoading({ ncId })); + + const wallet = yield select((state) => state.wallet); + if (!wallet.isReady()) { + log.debug('Fail fetching Nano Contract history because wallet is not ready.'); + yield put(nanoContractHistoryFailure({ ncId, error: failureMessage.walletNotReadyError })); + // This will show user an error modal with the option to send the error to sentry. + yield put(onExceptionCaptured(new Error(failureMessage.walletNotReadyError), false)); + return; + } + + const fn = wallet.storage.isNanoContractRegistered.bind(wallet.storage); + const isNcRegistered = yield call(fn, ncId); + if (!isNcRegistered) { + log.debug('Fail fetching Nano Contract history because Nano Contract is not registered yet.'); + yield put(nanoContractHistoryFailure({ ncId, error: failureMessage.notRegistered })); + return; + } + + if (before == null && after == null) { + // it clean the history when starting load from the beginning + log.debug('Cleaning previous history to start over.'); + yield put(nanoContractHistoryClean({ ncId })); + } + + const useWalletService = yield select((state) => state.useWalletService); + try { + const req = { + wallet, + useWalletService, + ncId, + before, + after, + count: NANO_CONTRACT_TX_HISTORY_SIZE, + }; + const { history } = yield call(fetchHistory, req); + + log.debug('Success fetching Nano Contract history.'); + if (before != null) { + log.debug('Adding beforeHistory.'); + yield put(nanoContractHistorySuccess({ ncId, beforeHistory: history })); + } else if (after != null) { + log.debug('Adding afterHistory.'); + yield put(nanoContractHistorySuccess({ ncId, afterHistory: history })); + } else { + log.debug('Initializing history.'); + yield put(nanoContractHistorySuccess({ ncId, history })); + } + } catch (error) { + log.error('Error while fetching Nano Contract history.', error); + // break loading process and give feedback to user + yield put( + nanoContractHistoryFailure({ ncId, error: failureMessage.nanoContractHistoryFailure }) + ); + // give opportunity for users to send the error to our team + yield put(onExceptionCaptured(error, false)); + } +} + +/** + * Process Nano Contract unregister request. + * @param {{ + * payload: { + * ncId: string; + * } + * }} action with request payload. + */ +export function* unregisterNanoContract({ payload }) { + const { ncId } = payload; + + const wallet = yield select((state) => state.wallet); + if (!wallet.isReady()) { + log.error('Fail unregistering Nano Contract because wallet is not ready yet.'); + // This will show user an error modal with the option to send the error to sentry. + yield put(onExceptionCaptured(new Error(failureMessage.walletNotReadyError), true)); + return; + } + + yield call(wallet.storage.unregisterNanoContract.bind(wallet.storage), ncId); + + log.debug(`Success unregistering Nano Contract. ncId = ${ncId}`); + yield put(nanoContractUnregisterSuccess({ ncId })); +} + +/** + * Process update on registered Nano Contract address to persist on store. + * @param {{ + * payload: { + * ncId: string; + * newAddress: string; + * } + * }} + */ +export function* requestNanoContractAddressChange({ payload }) { + const { ncId, newAddress } = payload; + + const wallet = yield select((state) => state.wallet); + if (!wallet.isReady()) { + log.error('Fail updating Nano Contract address because wallet is not ready yet.'); + // This will show user an error modal with the option to send the error to sentry. + yield put(onExceptionCaptured(new Error(failureMessage.walletNotReadyError), true)); + return; + } + + yield call( + [ + wallet.storage, + wallet.storage.updateNanoContractRegisteredAddress + ], + ncId, + newAddress, + ); + log.debug(`Success persisting Nano Contract address update. ncId = ${ncId}`); +} + +/** + * Process request to fetch blueprint info. + * @param {{ + * payload: { + * id: string; + * } + * }} + */ +export function* requestBlueprintInfo({ payload }) { + const { id } = payload; + let blueprintInfo = null; + try { + blueprintInfo = yield call([ncApi, ncApi.getBlueprintInformation], id); + } catch (error) { + if (error instanceof NanoRequest404Error) { + yield put(nanoContractBlueprintInfoFailure(id, failureMessage.blueprintInfoNotFound)); + } else { + log.error('Error while fetching blueprint info.', error); + yield put(nanoContractBlueprintInfoFailure(id, failureMessage.blueprintInfoFailure)); + } + return; + } + + log.debug(`Success fetching blueprint info. id = ${id}`); + yield put(nanoContractBlueprintInfoSuccess(id, blueprintInfo)); +} + +export function* saga() { + yield all([ + debounce(500, [[types.START_WALLET_SUCCESS, types.NANOCONTRACT_INIT]], init), + takeEvery( + types.NANOCONTRACT_REGISTER_REQUEST, + safeEffect(registerNanoContract, registerNanoContractOnError) + ), + takeEvery(types.NANOCONTRACT_HISTORY_REQUEST, requestHistoryNanoContract), + takeEvery(types.NANOCONTRACT_UNREGISTER_REQUEST, unregisterNanoContract), + takeEvery(types.NANOCONTRACT_ADDRESS_CHANGE_REQUEST, requestNanoContractAddressChange), + takeEvery(types.NANOCONTRACT_BLUEPRINTINFO_REQUEST, requestBlueprintInfo), + ]); +} diff --git a/src/sagas/networkSettings.js b/src/sagas/networkSettings.js index f1af235f1..69dcd53dc 100644 --- a/src/sagas/networkSettings.js +++ b/src/sagas/networkSettings.js @@ -1,5 +1,5 @@ import { all, takeEvery, put, call, race, delay, select } from 'redux-saga/effects'; -import { config } from '@hathor/wallet-lib'; +import { config, helpersUtils } from '@hathor/wallet-lib'; import { isEmpty } from 'lodash'; import { t } from 'ttag'; import { @@ -11,7 +11,7 @@ import { types, reloadWalletRequested, onExceptionCaptured, - networkSettingsUpdateReady + networkSettingsUpdateReady, } from '../actions'; import { NETWORK_MAINNET, @@ -22,7 +22,6 @@ import { STAGE_DEV_PRIVNET, STAGE_TESTNET, WALLET_SERVICE_REQUEST_TIMEOUT, - NETWORK_PRIVATENET } from '../constants'; import { getFullnodeNetwork, @@ -30,6 +29,9 @@ import { } from './helpers'; import { STORE } from '../store'; import { isWalletServiceEnabled } from './wallet'; +import { logger } from '../logger'; + +const log = logger('network-settings-saga'); /** * Initialize the network settings saga when the wallet starts successfully. @@ -127,6 +129,7 @@ export function* updateNetworkSettings(action) { yield put(networkSettingsUpdateInvalid(invalidPayload)); if (Object.keys(invalidPayload).length > 0) { + log.debug('Invalid request to update network settings.'); return; } @@ -150,8 +153,8 @@ export function* updateNetworkSettings(action) { // - walletServiceUrl has precedence // - nodeUrl as fallback let potentialNetwork; - let network; if (useWalletService && !isEmpty(walletServiceUrl)) { + log.debug('Configuring wallet-service on custom network settings.'); config.setWalletServiceBaseUrl(walletServiceUrl); config.setWalletServiceBaseWsUrl(walletServiceWsUrl); @@ -166,7 +169,7 @@ export function* updateNetworkSettings(action) { potentialNetwork = response; } } catch (err) { - console.error('Error calling the wallet-service while trying to get network details in updateNetworkSettings effect.', err); + log.error('Error calling the wallet-service while trying to get network details in updateNetworkSettings effect.', err); rollbackConfigUrls(backupUrl); } } @@ -175,7 +178,7 @@ export function* updateNetworkSettings(action) { try { potentialNetwork = yield call(getFullnodeNetwork); } catch (err) { - console.error('Error calling the fullnode while trying to get network details in updateNetworkSettings effect..', err); + log.error('Error calling the fullnode while trying to get network details in updateNetworkSettings effect..', err); rollbackConfigUrls(backupUrl); yield put(networkSettingsUpdateFailure()); return; @@ -184,23 +187,12 @@ export function* updateNetworkSettings(action) { // Fail after try get network from fullnode if (!potentialNetwork) { - console.warn('The network could not be found.'); + log.debug('The network could not be found.'); yield put(networkSettingsUpdateFailure()); return; } - // Validates the potential network and set the network accordingly - if (potentialNetwork === NETWORK_MAINNET) { - network = NETWORK_MAINNET; - } else if (potentialNetwork.startsWith(NETWORK_TESTNET)) { - network = NETWORK_TESTNET; - } else if (potentialNetwork.startsWith(NETWORK_PRIVATENET)) { - network = NETWORK_PRIVATENET; - } else { - console.warn('The network informed is not allowed. Make sure your network is either "mainnet", "testnet" or "privatenet", or starts with "testnet" or "privatenet".'); - yield put(networkSettingsUpdateFailure()); - return; - } + const network = helpersUtils.getNetworkFromFullNodeNetwork(potentialNetwork); let stage; if (network === NETWORK_MAINNET) { @@ -222,6 +214,7 @@ export function* updateNetworkSettings(action) { walletServiceWsUrl, }; + log.debug('Success updading network settings.'); yield put(networkSettingsPersistStore(customNetwork)); } @@ -290,7 +283,7 @@ export function* persistNetworkSettings(action) { } // Stop wallet and clean its storage without clean its access data. - wallet.stop({ cleanStorage: true, cleanAddresses: true }); + wallet.stop({ cleanStorage: true, cleanAddresses: true, cleanTokens: true }); // This action should clean the tokens history on redux. // In addition, the reload also clean the inmemory storage. yield put(reloadWalletRequested()); diff --git a/src/sagas/tokens.js b/src/sagas/tokens.js index c777b74d8..acdc6056d 100644 --- a/src/sagas/tokens.js +++ b/src/sagas/tokens.js @@ -14,12 +14,14 @@ import { take, all, put, + join, } from 'redux-saga/effects'; +import { t } from 'ttag'; import { metadataApi } from '@hathor/wallet-lib'; import { channel } from 'redux-saga'; import { get } from 'lodash'; import { specificTypeAndPayload, dispatchAndWait, getRegisteredTokenUids } from './helpers'; -import { mapTokenHistory } from '../utils'; +import { mapToTxHistory, splitInGroups } from '../utils'; import { types, tokenFetchBalanceRequested, @@ -28,8 +30,25 @@ import { tokenFetchHistoryRequested, tokenFetchHistorySuccess, tokenFetchHistoryFailed, + onExceptionCaptured, + unregisteredTokensDownloadSuccess, + unregisteredTokensDownloadEnd, + unregisteredTokensDownloadFailure, } from '../actions'; +import { logger } from '../logger'; +import { NODE_RATE_LIMIT_CONF } from '../constants'; +const log = logger('tokens-saga'); + +const failureMessage = { + walletNotReadyError: t`Wallet is not ready yet.`, + someTokensNotLoaded: t`Error loading the details of some tokens.`, +}; + +/** + * @readonly + * @enum {string} + */ export const TOKEN_DOWNLOAD_STATUS = { READY: 'ready', FAILED: 'failed', @@ -89,6 +108,8 @@ function* fetchTokenBalance(action) { const tokenBalance = yield select((state) => get(state.tokensBalance, tokenId)); if (!force && tokenBalance && tokenBalance.oldStatus === TOKEN_DOWNLOAD_STATUS.READY) { + log.debug(`Token download status READY.`); + log.debug(`Token balance already downloaded for token ${tokenId}. Skipping download.`); // The data is already loaded, we should dispatch success yield put(tokenFetchBalanceSuccess(tokenId, tokenBalance.data)); return; @@ -107,8 +128,10 @@ function* fetchTokenBalance(action) { locked: token.balance.locked, }; + log.debug(`Success fetching token balance for token ${tokenId}.`); yield put(tokenFetchBalanceSuccess(tokenId, balance)); } catch (e) { + log.error('Error while fetching token balance.', e); yield put(tokenFetchBalanceFailed(tokenId)); } } @@ -122,15 +145,18 @@ function* fetchTokenHistory(action) { if (!force && tokenHistory && tokenHistory.oldStatus === TOKEN_DOWNLOAD_STATUS.READY) { // The data is already loaded, we should dispatch success + log.debug(`Token history already downloaded for token ${tokenId}. Skipping download.`); yield put(tokenFetchHistorySuccess(tokenId, tokenHistory.data)); return; } - const response = yield call(wallet.getTxHistory.bind(wallet), { token_id: tokenId }); - const data = response.map((txHistory) => mapTokenHistory(txHistory, tokenId)); + const response = yield call([wallet, wallet.getTxHistory], { token_id: tokenId }); + const data = response.map(mapToTxHistory(tokenId)); + log.debug(`Success fetching token history for token ${tokenId}.`); yield put(tokenFetchHistorySuccess(tokenId, data)); } catch (e) { + log.error('Error while fetching token history.', e); yield put(tokenFetchHistoryFailed(tokenId)); } } @@ -150,10 +176,12 @@ function* routeTokenChange(action) { switch (action.type) { case 'NEW_TOKEN': + log.debug('[routeTokenChange] fetching token balance on NEW_TOKEN event'); yield put({ type: types.TOKEN_FETCH_BALANCE_REQUESTED, tokenId: action.payload.uid }); break; case 'SET_TOKENS': default: + log.debug('[routeTokenChange] fetching token balance on SET_TOKENS event'); for (const uid of getRegisteredTokenUids({ tokens: action.payload })) { yield put({ type: types.TOKEN_FETCH_BALANCE_REQUESTED, tokenId: uid }); } @@ -221,6 +249,7 @@ export function* fetchTokenMetadata({ tokenId }) { try { const data = yield call(metadataApi.getDagMetadata, tokenId, network); + log.debug('Success fetching token metadata.'); yield put({ type: types.TOKEN_FETCH_METADATA_SUCCESS, tokenId, @@ -228,6 +257,7 @@ export function* fetchTokenMetadata({ tokenId }) { }); return; } catch (e) { + log.error('Error trying to get DAG metadata.', e); yield delay(1000); // Wait 1s before trying again } } @@ -238,8 +268,7 @@ export function* fetchTokenMetadata({ tokenId }) { type: types.TOKEN_FETCH_METADATA_FAILED, tokenId, }); - // eslint-disable-next-line - console.log('Error downloading metadata of token', tokenId); + log.log(`Error downloading metadata of token ${tokenId}`); } } @@ -270,6 +299,80 @@ export function* fetchTokenData(tokenId, force = false) { } } +/** + * Get token details from wallet. + * + * @param {Object} wallet The application wallet. + * @param {string} uid Token UID. + * + * @description + * The token endpoint has 3r/s with 10r of burst and 3s of delay. + */ +export function* getTokenDetails(wallet, uid) { + try { + const { tokenInfo: { symbol, name } } = yield call([wallet, wallet.getTokenDetails], uid); + yield put(unregisteredTokensDownloadSuccess({ tokens: { [uid]: { uid, symbol, name } } })); + } catch (e) { + log.error(`Fail getting token data for token ${uid}.`, e); + yield put(unregisteredTokensDownloadFailure({ error: failureMessage.someTokensNotLoaded })); + } +} + +/** + * Request token details of unregistered tokens to feed new + * nano contract request actions. + * + * @description + * It optimizes for burst because we need the data as soon as possible, + * at the same time we should avoid request denials from the endpoint, + * which justifies a delay from burst to burst. + * + * @param {Object} action + * @param {Object} action.payload + * @param {string[]} action.payload.uids + */ +export function* requestUnregisteredTokensDownload(action) { + const { uids } = action.payload; + + if (uids.length === 0) { + log.debug('No uids to request token details.'); + yield put(unregisteredTokensDownloadEnd()); + return; + } + + const wallet = yield select((state) => state.wallet); + if (!wallet.isReady()) { + log.error('Fail updating loading tokens data because wallet is not ready yet.'); + // This will show user an error modal with the option to send the error to sentry. + yield put(onExceptionCaptured(new Error(failureMessage.walletNotReadyError), true)); + return; + } + + /** + * NOTE: We can improve the follwoing strategy by adopting a more robust + * rate-limit algorithm, like the sliding window or token bucket. + */ + + // These are the default values configured in the nginx conf public nodes. + const perBurst = NODE_RATE_LIMIT_CONF.thin_wallet_token.burst; + const burstDelay = NODE_RATE_LIMIT_CONF.thin_wallet_token.delay; + const uidGroups = splitInGroups(uids, perBurst); + for (const group of uidGroups) { + // Fork is a non-blocking effect, it doesn't cause the caller suspension. + const tasks = yield all(group.map((uid) => fork(getTokenDetails, wallet, uid))); + // Awaits a group to finish before burst the next group + yield join(tasks); + // Skip delay if there is only one group or is the last group + if (uidGroups.length === 1 || group === uidGroups.at(-1)) { + continue; + } + // This is a quick request, we should give a break before next burst + yield delay(burstDelay * 1000); + } + log.log('Success getting tokens data to feed unregisteredTokens.'); + yield put(unregisteredTokensDownloadEnd()); +} + export function* saga() { yield all([ fork(fetchTokenBalanceQueue), @@ -277,5 +380,6 @@ export function* saga() { takeEvery(types.TOKEN_FETCH_HISTORY_REQUESTED, fetchTokenHistory), takeEvery(types.NEW_TOKEN, routeTokenChange), takeEvery(types.SET_TOKENS, routeTokenChange), + takeEvery(types.UNREGISTEREDTOKENS_DOWNLOAD_REQUEST, requestUnregisteredTokensDownload), ]); } diff --git a/src/sagas/wallet.js b/src/sagas/wallet.js index 05c20b0f0..1b4191a18 100644 --- a/src/sagas/wallet.js +++ b/src/sagas/wallet.js @@ -32,6 +32,7 @@ import { import { eventChannel } from 'redux-saga'; import { getUniqueId } from 'react-native-device-info'; import { get, isEmpty } from 'lodash'; +import { t } from 'ttag'; import { DEFAULT_TOKEN, WALLET_SERVICE_FEATURE_TOGGLE, @@ -65,6 +66,10 @@ import { setTokens, onExceptionCaptured, networkSettingsUpdateState, + selectAddressAddressesSuccess, + selectAddressAddressesFailure, + firstAddressFailure, + firstAddressSuccess, } from '../actions'; import { fetchTokenData } from './tokens'; import { @@ -77,7 +82,14 @@ import { getRegisteredTokenUids, progressiveRetryRequest, } from './helpers'; -import { setKeychainPin } from '../utils'; +import { + getAllAddresses, + getFirstAddress, + setKeychainPin +} from '../utils'; +import { logger } from '../logger'; + +const log = logger('wallet'); export const WALLET_STATUS = { NOT_STARTED: 'not_started', @@ -114,7 +126,7 @@ export function* isWalletServiceEnabled() { const delta = now - shouldIgnoreFlagTs; if (delta < EXPIRE_WS_IGNORE_FLAG) { - console.log(`Still ignoring wallet-service, will expire in ${EXPIRE_WS_IGNORE_FLAG - delta}ms`); + log.log(`Still ignoring wallet-service, will expire in ${EXPIRE_WS_IGNORE_FLAG - delta}ms`); return false; } } else { @@ -219,6 +231,10 @@ export function* startWallet(action) { wallet = new HathorWallet(walletConfig); } + // Extra wallet configuration based on customNetwork + config.setExplorerServiceBaseUrl(networkSettings.explorerServiceUrl); + config.setTxMiningUrl(networkSettings.txMiningServiceUrl); + yield put(setWallet(wallet)); // Setup listeners before starting the wallet so we don't lose messages @@ -287,7 +303,7 @@ export function* startWallet(action) { try { yield call(loadTokens); } catch (e) { - console.error('Tokens load failed: ', e); + log.error('Tokens load failed: ', e); yield put(onExceptionCaptured(e, false)); yield put(startWalletFailed()); return; @@ -327,7 +343,7 @@ export function* startWallet(action) { */ export function* loadTokens() { const customTokenUid = DEFAULT_TOKEN.uid; - const htrUid = hathorLibConstants.HATHOR_TOKEN_CONFIG.uid; + const htrUid = hathorLibConstants.NATIVE_TOKEN_UID; // fetchTokenData will throw an error if the download failed, we should just // let it crash as throwing an error is the default behavior for loadTokens @@ -352,12 +368,14 @@ export function* loadTokens() { // spawn a new "thread" to handle it. // // `spawn` is similar to `fork`, but it creates a `detached` fork - yield spawn(fetchTokensMetadata, registeredUids); + yield spawn(fetchTokensMetadata, registeredUids.filter((token) => token !== htrUid)); // Since we already know here what tokens are registered, we can dispatch actions // to asynchronously load the balances of each one. The `put` effect will just dispatch // and continue, loading the tokens asynchronously for (const token of registeredUids) { + // Skip the native token, once it has its balance loaded already + if (token === htrUid) continue; yield put(tokenFetchBalanceRequested(token)); } @@ -369,7 +387,6 @@ export function* loadTokens() { * So we fetch the tokens metadata and store on redux */ export function* fetchTokensMetadata(tokens) { - // No tokens to load if (!tokens.length) { return; } @@ -395,8 +412,7 @@ export function* fetchTokensMetadata(tokens) { const tokenMetadatas = {}; for (const response of responses) { if (response.type === types.TOKEN_FETCH_METADATA_FAILED) { - // eslint-disable-next-line - console.log('Error downloading metadata of token', response.tokenId); + log.error(`Error downloading metadata of token ${response.tokenId}.`); } else if (response.type === types.TOKEN_FETCH_METADATA_SUCCESS) { // When the request returns null, it means that we have no metadata for this token if (response.data) { @@ -409,7 +425,7 @@ export function* fetchTokensMetadata(tokens) { } export function* onWalletServiceDisabled() { - console.debug('We are currently in the wallet-service and the feature-flag is disabled, reloading.'); + log.debug('We are currently in the wallet-service and the feature-flag is disabled, reloading.'); yield put(reloadWalletRequested()); } @@ -553,7 +569,7 @@ export function* handleTx(action) { for (const tokenUid of tokensToDownload) { yield put(tokenFetchBalanceRequested(tokenUid, true)); - if (tokenUid === hathorLibConstants.HATHOR_TOKEN_CONFIG.uid + if (tokenUid === hathorLibConstants.NATIVE_TOKEN_UID || tokenUid === DEFAULT_TOKEN.uid) { yield put(tokenFetchHistoryRequested(tokenUid, true)); } else { @@ -646,7 +662,7 @@ export function* bestBlockUpdate({ payload }) { } if (currentHeight !== payload) { - yield put(tokenFetchBalanceRequested(hathorLibConstants.HATHOR_TOKEN_CONFIG.uid)); + yield put(tokenFetchBalanceRequested(hathorLibConstants.NATIVE_TOKEN_UID)); } } @@ -676,7 +692,7 @@ export function* onWalletReloadData() { const registeredTokens = yield call(loadTokens); const customTokenUid = DEFAULT_TOKEN.uid; - const htrUid = hathorLibConstants.HATHOR_TOKEN_CONFIG.uid; + const htrUid = hathorLibConstants.NATIVE_TOKEN_UID; // We might have lost transactions during the reload, so we must invalidate the // token histories: @@ -704,7 +720,7 @@ export function* onWalletReloadData() { // Finally, set the wallet to READY by dispatching startWalletSuccess yield put(startWalletSuccess()); } catch (e) { - console.log('Wallet reload data failed: ', e); + log.error('Wallet reload data failed: ', e); yield put(onExceptionCaptured(e, false)); yield put(startWalletFailed()); } @@ -756,6 +772,58 @@ export function* refreshSharedAddress() { yield put(sharedAddressUpdate(address, index)); } +export function* fetchAllWalletAddresses() { + const wallet = yield select((state) => state.wallet); + if (!wallet.isReady()) { + const errorMsg = 'Fail fetching all wallet addresses because wallet is not ready yet.'; + log.error(errorMsg); + const feedbackErrorMsg = t`Wallet is not ready to load addresses.`; + // This will show the message in the feedback content at SelectAddressModal + yield put(selectAddressAddressesFailure({ error: feedbackErrorMsg })); + // This will show user an error modal with the option to send the error to sentry. + yield put(onExceptionCaptured(new Error(errorMsg), false)); + return; + } + + try { + const addresses = yield call(getAllAddresses, wallet); + log.log('All wallet addresses loaded with success.'); + yield put(selectAddressAddressesSuccess({ addresses })); + } catch (error) { + log.error('Error while fetching all wallet addresses.', error); + // This will show the message in the feedback content at SelectAddressModal + yield put(selectAddressAddressesFailure({ + error: t`There was an error while loading wallet addresses. Try again.` + })); + } +} + +export function* fetchFirstWalletAddress() { + const wallet = yield select((state) => state.wallet); + if (!wallet.isReady()) { + const errorMsg = 'Fail fetching first wallet address because wallet is not ready yet.'; + log.error(errorMsg); + const feedbackErrorMsg = t`Wallet is not ready to load the first address.`; + // This will show the message in the feedback content + yield put(firstAddressFailure({ error: feedbackErrorMsg })); + // This will show user an error modal with the option to send the error to sentry. + yield put(onExceptionCaptured(new Error(errorMsg), false)); + return; + } + + try { + const address = yield call(getFirstAddress, wallet); + log.log('First wallet address loaded with success.'); + yield put(firstAddressSuccess({ address })); + } catch (error) { + log.error('Error while fetching first wallet address.', error); + // This will show the message in the feedback content + yield put(firstAddressFailure({ + error: t`There was an error while loading first wallet address. Try again.` + })); + } +} + export function* saga() { yield all([ takeLatest('START_WALLET_REQUESTED', errorHandler(startWallet, startWalletFailed())), @@ -768,5 +836,7 @@ export function* saga() { takeEvery('WALLET_BEST_BLOCK_UPDATE', bestBlockUpdate), takeEvery('WALLET_PARTIAL_UPDATE', loadPartialUpdate), takeEvery(types.WALLET_REFRESH_SHARED_ADDRESS, refreshSharedAddress), + takeEvery(types.SELECTADDRESS_ADDRESSES_REQUEST, fetchAllWalletAddresses), + takeEvery(types.FIRSTADDRESS_REQUEST, fetchFirstWalletAddress), ]); } diff --git a/src/sagas/walletConnect.js b/src/sagas/walletConnect.js index ea897affa..ebeadba6c 100644 --- a/src/sagas/walletConnect.js +++ b/src/sagas/walletConnect.js @@ -58,10 +58,19 @@ import { takeEvery, select, race, + spawn, } from 'redux-saga/effects'; import { eventChannel } from 'redux-saga'; import { get, values } from 'lodash'; - +import { + TriggerTypes, + TriggerResponseTypes, + RpcResponseTypes, + SendNanoContractTxFailure, + handleRpcRequest, + CreateTokenError, +} from '@hathor/hathor-rpc-handler'; +import { isWalletServiceEnabled, WALLET_STATUS } from './wallet'; import { WalletConnectModalTypes } from '../components/WalletConnect/WalletConnectModal'; import { WALLET_CONNECT_PROJECT_ID, @@ -74,14 +83,35 @@ import { setWalletConnectSessions, onExceptionCaptured, setWCConnectionFailed, + showSignMessageWithAddressModal, + showNanoContractSendTxModal, + showCreateTokenModal, + setNewNanoContractStatusLoading, + setNewNanoContractStatusReady, + setNewNanoContractStatusFailure, + setNewNanoContractStatusSuccess, + setCreateTokenStatusLoading, + setCreateTokenStatusReady, + setCreateTokenStatusSuccessful, + setCreateTokenStatusFailed, } from '../actions'; -import { checkForFeatureFlag, getNetworkSettings, showPinScreenForResult } from './helpers'; +import { checkForFeatureFlag, getNetworkSettings, retryHandler, showPinScreenForResult } from './helpers'; +import { logger } from '../logger'; + +const log = logger('walletConnect'); const AVAILABLE_METHODS = { - HATHOR_SIGN_MESSAGE: 'hathor_signMessage', + HATHOR_SIGN_MESSAGE: 'htr_signWithAddress', + HATHOR_SEND_NANO_TX: 'htr_sendNanoContractTx', }; const AVAILABLE_EVENTS = []; +// We're mocking it here because we don't want to add the walletconnect +// libraries in our production build. If you really want to add it, just run the +// src/walletconnect.sh script +const Core = class {}; +const Web3Wallet = class {}; + /** * Those are the only ones we are currently using, extracted from * https://docs.walletconnect.com/2.0/specs/clients/sign/error-codes @@ -94,22 +124,33 @@ const ERROR_CODES = { INVALID_PAYLOAD: 5003, }; -// We're mocking it here because we don't want to add the walletconnect -// libraries in our production build. If you really want to add it, just run the -// src/walletconnect.sh script -const Core = class {}; -const Web3Wallet = class {}; - -function* isWalletConnectEnabled() { +function isWalletConnectEnabled() { + return false; + /* const walletConnectEnabled = yield call(checkForFeatureFlag, WALLET_CONNECT_FEATURE_TOGGLE); return walletConnectEnabled; + */ } function* init() { + const walletStartState = yield select((state) => state.walletStartState); + + if (walletStartState !== WALLET_STATUS.READY) { + log.debug('Wallet not ready yet, waiting for START_WALLET_SUCCESS.'); + yield take(types.START_WALLET_SUCCESS); + log.debug('Starting wallet-connect.'); + } + try { + const walletServiceEnabled = yield call(isWalletServiceEnabled); const walletConnectEnabled = yield call(isWalletConnectEnabled); + if (walletServiceEnabled) { + log.debug('Wallet Service enabled, skipping wallet-connect init.'); + return; + } + if (!walletConnectEnabled) { return; } @@ -144,7 +185,7 @@ function* init() { yield cancel(); } catch (error) { - console.error('Error loading wallet connect', error); + log.error('Error loading wallet connect', error); yield put(onExceptionCaptured(error)); } } @@ -230,11 +271,14 @@ export function* onSessionRequest(action) { const { payload } = action; const { params } = payload; + const wallet = yield select((state) => state.wallet); + const { web3wallet } = yield select((state) => state.walletConnect.client); const activeSessions = yield call(() => web3wallet.getActiveSessions()); const requestSession = activeSessions[payload.topic]; + if (!requestSession) { - console.error('Could not identify the request session, ignoring request..'); + log.error('Could not identify the request session, ignoring request.'); return; } @@ -243,18 +287,81 @@ export function* onSessionRequest(action) { proposer: get(requestSession.peer, 'metadata.name', ''), url: get(requestSession.peer, 'metadata.url', ''), description: get(requestSession.peer, 'metadata.description', ''), + chain: get(requestSession.namespaces, 'hathor.chains[0]', ''), }; - switch (params.request.method) { - case AVAILABLE_METHODS.HATHOR_SIGN_MESSAGE: - yield call(onSignMessageRequest, { - ...data, - requestId: payload.id, - topic: payload.topic, - message: get(params, 'request.params.message'), - }); - break; - default: + try { + let dispatch; + yield put((_dispatch) => { + dispatch = _dispatch; + }); + + const response = yield call( + handleRpcRequest, + params.request, + wallet, + data, + promptHandler(dispatch), + ); + + switch (response.type) { + case RpcResponseTypes.SendNanoContractTxResponse: + yield put(setNewNanoContractStatusSuccess()); + break; + case RpcResponseTypes.CreateTokenResponse: + yield put(setCreateTokenStatusSuccessful()); + break; + default: + break; + } + + yield call(() => web3wallet.respondSessionRequest({ + topic: payload.topic, + response: { + id: payload.id, + jsonrpc: '2.0', + result: response, + } + })); + } catch (e) { + let shouldAnswer = true; + switch (e.constructor) { + case SendNanoContractTxFailure: { + yield put(setNewNanoContractStatusFailure()); + + const retry = yield call( + retryHandler, + types.WALLETCONNECT_CREATE_TOKEN_RETRY, + types.WALLETCONNECT_CREATE_TOKEN_RETRY_DISMISS, + ); + + if (retry) { + shouldAnswer = false; + // Retry the action, exactly as it came: + yield spawn(onSessionRequest, action); + } + } break; + case CreateTokenError: { + yield put(setCreateTokenStatusFailed()); + + // User might try again, wait for it. + const retry = yield call( + retryHandler, + types.WALLETCONNECT_CREATE_TOKEN_RETRY, + types.WALLETCONNECT_CREATE_TOKEN_RETRY_DISMISS, + ); + + if (retry) { + shouldAnswer = false; + // Retry the action, exactly as it came: + yield spawn(onSessionRequest, action); + } + } break; + default: + break; + } + + if (shouldAnswer) { yield call(() => web3wallet.respondSessionRequest({ topic: payload.topic, response: { @@ -266,106 +373,255 @@ export function* onSessionRequest(action) { }, }, })); - break; + } } } +/** + * Handles various types of prompt requests by dispatching appropriate actions + * and resolving with the corresponding responses. + * + * @param {function} dispatch - The dispatch function to send actions to the store. + * @returns {function} - A function that receives Trigger requests from the rpc + * library and dispatches actions + * + * The returned function performs the following: + * + * - Depending on the `request.type`, it will: + * - `TriggerTypes.SignMessageWithAddressConfirmationPrompt`: + * - Dispatches `showSignMessageWithAddressModal` with acceptance/rejection handlers. + * - Resolves with `TriggerResponseTypes.SignMessageWithAddressConfirmationResponse`. + * - `TriggerTypes.SendNanoContractTxConfirmationPrompt`: + * - Dispatches `showNanoContractSendTxModal` with acceptance/rejection handlers. + * - Resolves with `TriggerResponseTypes.SendNanoContractTxConfirmationResponse`. + * - `TriggerTypes.SendNanoContractTxLoadingTrigger`: + * - Dispatches `setNewNanoContractStatusLoading`. + * - Resolves immediately. + * - `TriggerTypes.LoadingFinishedTrigger`: + * - Dispatches `setNewNanoContractStatusReady`. + * - Resolves immediately. + * - `TriggerTypes.PinConfirmationPrompt`: + * - Awaits `showPinScreenForResult` to get the PIN code. + * - Resolves with `TriggerResponseTypes.PinRequestResponse`. + * - For any other `request.type`, this method will reject with an error. + * + * @param {Object} request - The request object containing type and data. + * @param {Object} requestMetadata - Additional metadata for the request. + * + * @returns {Promise} - A Promise that resolves with the appropriate + * response based on the request type. + * + * @example + * const handler = promptHandler(dispatch); + */ +const promptHandler = (dispatch) => (request, requestMetadata) => + // eslint-disable-next-line + new Promise(async (resolve, reject) => { + switch (request.type) { + case TriggerTypes.CreateTokenConfirmationPrompt: { + const createTokenResponseTemplate = (accepted) => (data) => resolve({ + type: TriggerResponseTypes.CreateTokenConfirmationResponse, + data: { + accepted, + token: data?.payload, + } + }); + dispatch(showCreateTokenModal( + createTokenResponseTemplate(true), + createTokenResponseTemplate(false), + request.data, + requestMetadata, + )) + } break; + case TriggerTypes.SignMessageWithAddressConfirmationPrompt: { + const signMessageResponseTemplate = (accepted) => () => resolve({ + type: TriggerResponseTypes.SignMessageWithAddressConfirmationResponse, + data: accepted, + }); + dispatch(showSignMessageWithAddressModal( + signMessageResponseTemplate(true), + signMessageResponseTemplate(false), + request.data, + requestMetadata, + )) + } break; + case TriggerTypes.SendNanoContractTxConfirmationPrompt: { + const sendNanoContractTxResponseTemplate = (accepted) => (data) => resolve({ + type: TriggerResponseTypes.SendNanoContractTxConfirmationResponse, + data: { + accepted, + nc: data?.payload, + } + }); + + dispatch(showNanoContractSendTxModal( + sendNanoContractTxResponseTemplate(true), + sendNanoContractTxResponseTemplate(false), + request.data, + requestMetadata, + )); + } break; + case TriggerTypes.SendNanoContractTxLoadingTrigger: + dispatch(setNewNanoContractStatusLoading()); + resolve(); + break; + case TriggerTypes.CreateTokenLoadingTrigger: + dispatch(setCreateTokenStatusLoading()); + resolve(); + break; + case TriggerTypes.CreateTokenLoadingFinishedTrigger: + dispatch(setCreateTokenStatusReady()); + resolve(); + break; + case TriggerTypes.SendNanoContractTxLoadingFinishedTrigger: + dispatch(setNewNanoContractStatusReady()); + resolve(); + break; + case TriggerTypes.PinConfirmationPrompt: { + const pinCode = await showPinScreenForResult(dispatch); + + resolve({ + type: TriggerResponseTypes.PinRequestResponse, + data: { + accepted: true, + pinCode, + } + }); + } break; + default: reject(new Error('Invalid request')); + } + }); + /** * This saga will be called (dispatched from the event listener) when a sign * message RPC is published from a dApp * - * @param {String} data.requestId Unique identifier of the request - * @param {String} data.topic Unique identifier of the connected session - * @param {String} data.message Message the dApp requested a signature for + * @param {String} payload.data.requestId Unique identifier of the request + * @param {String} payload.data.topic Unique identifier of the connected session + * @param {String} payload.data.message Message the dApp requested a signature for + * @param {String} payload.dapp.icon The icon sent by the dApp + * @param {String} payload.dapp.proposer The proposer name sent by the dapp + * @param {String} payload.dapp.url The url sent by the dApp + * @param {String} payload.dapp.description The description sent by the dApp + * @param {String} payload.accept A callback function to indicate that the + * request has been accepted. + * @param {String} payload.deny A callback function to indicate that the request + * has been denied. */ -export function* onSignMessageRequest(data) { - const { web3wallet } = yield select((state) => state.walletConnect.client); - - const onAcceptAction = { type: 'WALLET_CONNECT_ACCEPT' }; - const onRejectAction = { type: 'WALLET_CONNECT_REJECT' }; +export function* onSignMessageRequest({ payload }) { + const { accept, deny: denyCb, data, dapp } = payload; const wallet = yield select((state) => state.wallet); if (!wallet.isReady()) { - console.error('Got a session request but wallet is not ready, ignoring..'); + log.error('Got a session request but wallet is not ready, ignoring.'); return; } yield put(setWalletConnectModal({ show: true, type: WalletConnectModalTypes.SIGN_MESSAGE, - data, - onAcceptAction, - onRejectAction, + data: { + data, + dapp, + }, })); - const { reject } = yield race({ - accept: take(onAcceptAction.type), - reject: take(onRejectAction.type), + const { deny } = yield race({ + accept: take(types.WALLET_CONNECT_ACCEPT), + deny: take(types.WALLET_CONNECT_REJECT), }); - try { - if (reject) { - yield call(() => web3wallet.respondSessionRequest({ - topic: data.topic, - response: { - id: data.requestId, - jsonrpc: '2.0', - error: { - code: ERROR_CODES.USER_REJECTED, - message: 'Rejected by the user', - }, - }, - })); - return; - } + if (deny) { + denyCb(); - if (!data.message) { - yield call(() => web3wallet.respondSessionRequest({ - topic: data.topic, - response: { - id: data.requestId, - jsonrpc: '2.0', - error: { - code: ERROR_CODES.INVALID_PAYLOAD, - message: 'Missing message to sign', - }, - }, - })); + return; + } - return; - } + accept(); +} - const { message } = data; +/** + * This saga will be called (dispatched from the event listener) when a + * sendNanoContractTx message RPC is published from a dApp + * + * @param {String} payload.data.requestId Unique identifier of the request + * @param {String} payload.data.topic Unique identifier of the connected session + * @param {String} payload.data.message Message the dApp requested a signature for + * @param {String} payload.dapp.icon The icon sent by the dApp + * @param {String} payload.dapp.proposer The proposer name sent by the dapp + * @param {String} payload.dapp.url The url sent by the dApp + * @param {String} payload.dapp.description The description sent by the dApp + * @param {String} payload.accept A callback function to indicate that the + * request has been accepted. + * @param {String} payload.deny A callback function to indicate that the request + * has been denied. + */ +export function* onSendNanoContractTxRequest({ payload }) { + const { accept: acceptCb, deny: denyCb, nc, dapp } = payload; - let dispatch; - yield put((_dispatch) => { - dispatch = _dispatch; - }); + const wallet = yield select((state) => state.wallet); - const pinCode = yield call(() => showPinScreenForResult(dispatch)); - const signedMessage = yield call(() => wallet.signMessageWithAddress( - message, - 0, // First address - pinCode, - )); - - const response = { - id: data.requestId, - jsonrpc: '2.0', - result: signedMessage, - }; + if (!wallet.isReady()) { + log.error('Got a session request but wallet is not ready, ignoring.'); + return; + } - yield call(() => web3wallet.respondSessionRequest({ - topic: data.topic, - response, - })); - } catch (error) { - console.log('Captured error on signMessage: ', error); - yield put(onExceptionCaptured(error)); + yield put(setWalletConnectModal({ + show: true, + type: WalletConnectModalTypes.SEND_NANO_CONTRACT_TX, + data: { + dapp, + data: nc, + }, + })); + + const { deny, accept } = yield race({ + accept: take(types.WALLET_CONNECT_ACCEPT), + deny: take(types.WALLET_CONNECT_REJECT), + }); + + if (deny) { + denyCb(); + + return; } + + acceptCb(accept); } +export function* onCreateTokenRequest({ payload }) { + const { accept: acceptCb, deny: denyCb, data, dapp } = payload; + + const wallet = yield select((state) => state.wallet); + + if (!wallet.isReady()) { + log.error('Got a session request but wallet is not ready, ignoring.'); + return; + } + + yield put(setWalletConnectModal({ + show: true, + type: WalletConnectModalTypes.CREATE_TOKEN, + data: { + dapp, + data, + }, + })); + + const { deny, accept } = yield race({ + accept: take(types.WALLET_CONNECT_ACCEPT), + deny: take(types.WALLET_CONNECT_REJECT), + }); + + if (deny) { + denyCb(); + + return; + } + + acceptCb(accept); +} /** * Listens for the wallet reset action, dispatched from the wallet sagas so we * can clear all current sessions. @@ -443,7 +699,7 @@ export function* onSessionProposal(action) { yield call(refreshActiveSessions); } catch (error) { - console.error('Error on sessionProposal: ', error); + log.error('Error on sessionProposal: ', error); yield put(onExceptionCaptured(error)); } } @@ -519,7 +775,10 @@ export function* onSessionDelete(action) { export function* saga() { yield all([ fork(featureToggleUpdateListener), - takeLatest(types.START_WALLET_SUCCESS, init), + fork(init), + takeLatest(types.SHOW_NANO_CONTRACT_SEND_TX_MODAL, onSendNanoContractTxRequest), + takeLatest(types.SHOW_SIGN_MESSAGE_REQUEST_MODAL, onSignMessageRequest), + takeLatest(types.SHOW_CREATE_TOKEN_REQUEST_MODAL, onCreateTokenRequest), takeLeading('WC_SESSION_REQUEST', onSessionRequest), takeEvery('WC_SESSION_PROPOSAL', onSessionProposal), takeEvery('WC_SESSION_DELETE', onSessionDelete), diff --git a/src/screens/CreateTokenAmount.js b/src/screens/CreateTokenAmount.js index 3f1575767..49dc9172c 100644 --- a/src/screens/CreateTokenAmount.js +++ b/src/screens/CreateTokenAmount.js @@ -27,7 +27,7 @@ import { COLORS } from '../styles/themes'; const mapStateToProps = (state) => ({ balance: get( state.tokensBalance, - `[${hathorLib.constants.HATHOR_TOKEN_CONFIG.uid}].data`, + `[${hathorLib.constants.NATIVE_TOKEN_UID}].data`, { available: 0, locked: 0, diff --git a/src/screens/Dashboard.js b/src/screens/Dashboard.js index fb1ad736e..0e7f1ada3 100644 --- a/src/screens/Dashboard.js +++ b/src/screens/Dashboard.js @@ -5,93 +5,208 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; -import { View } from 'react-native'; -import { connect } from 'react-redux'; +import React, { useState } from 'react'; +import { StyleSheet, View, Text } from 'react-native'; +import { useDispatch, useSelector } from 'react-redux'; import { t } from 'ttag'; import { get } from 'lodash'; +import { useNavigation } from '@react-navigation/native'; import AskForPushNotification from '../components/AskForPushNotification'; import HathorHeader from '../components/HathorHeader'; import TokenSelect from '../components/TokenSelect'; import SimpleButton from '../components/SimpleButton'; import OfflineBar from '../components/OfflineBar'; +import { TwoOptionsToggle } from '../components/TwoOptionsToggle'; import { tokenFetchBalanceRequested, updateSelectedToken } from '../actions'; import AskForPushNotificationRefresh from '../components/AskForPushNotificationRefresh'; +import { COLORS } from '../styles/themes'; +import { TOKEN_DOWNLOAD_STATUS } from '../sagas/tokens'; +import { NanoContractsList } from '../components/NanoContract/NanoContractsList'; +import { getNanoContractFeatureToggle } from '../utils'; +import ShowPushNotificationTxDetails from '../components/ShowPushNotificationTxDetails'; /** - * tokens {Array} array with all added tokens on this wallet - * tokensBalance {Object} dict with balance for each token - * selectedToken {Object} token currently selected by the user - * tokenMetadata {Object} metadata of tokens + * State filter to retrieve token-related data from root state. + * + * @typedef {Object} TokenData + * @property {string} selectedToken - Current token selected. + * @property {string[]} tokens - Array containing all the tokens registered on the wallet. + * @property {{ [uid: string]: Object }} tokensBalance - Map of balance per token. + * @property {{ [uid: string]: Object }} tokensMetadata - Map of token's metadata per token. + * + * @returns {TokenData} Token-related data obtained from the root state. */ -const mapStateToProps = (state) => ({ +const getTokensState = (state) => ({ + selectedToken: state.selectedToken, tokens: state.tokens, tokensBalance: state.tokensBalance, - tokensHistory: state.tokensHistory, - selectedToken: state.selectedToken, - tokenMetadata: state.tokenMetadata, - tokenLoadingState: state.tokenLoadingState, + tokensMetadata: state.tokenMetadata, }); -const mapDispatchToProps = (dispatch) => ({ - updateSelectedToken: (token) => dispatch(updateSelectedToken(token)), - getBalance: (token) => dispatch(tokenFetchBalanceRequested(token)), -}); +// Check if the token balance is already loaded +/** + * @param {{ [uid: string]: Object }} tokensBalance a map of token's balance per token uid + * @param {{ uid: string }} token the token data + * @returns {string} the status of the current tokens balance loading process. + */ +const getTokensBalanceStatus = (tokensBalance, token) => get(tokensBalance, `${token.uid}.status`, TOKEN_DOWNLOAD_STATUS.LOADING); + +/** + * @param {string} status the current status from tokens balance loading process. + * @returns {boolean} `true` if loading, `false` otherwise. + */ +const isTokensBalanceLoading = (status) => status === TOKEN_DOWNLOAD_STATUS.LOADING; + +/** + * @param {string} status the current status from tokens balance loading process. + * @returns {boolean} `true` if failed, `false` otherwise. + */ +const isTokensBalanceFailed = (status) => status === TOKEN_DOWNLOAD_STATUS.FAILED; + +/** + * Enum for the list component that can be selected to render on Dashboard. + * @readonly + * @enum {string} + */ +const listOption = { + tokens: 'tokens', + nanoContracts: 'nanoContracts', +}; + +/** + * @param {listOption} currList the list component selected to be rendered. + * @returns {boolean} `true` if tokens list is selected, `false` otherwise. + */ +const isTokensSelected = (currList) => currList === listOption.tokens; + +/** + * @param {listOption} currList the list component selected to be rendered. + * @returns {boolean} `true` if nanoContracts list is selected, `false` otherwise. + */ +const isNanoContractsSelected = (currList) => currList === listOption.nanoContracts; + +export const Dashboard = () => { + const { + tokens, + tokensBalance, + selectedToken, + tokensMetadata, + } = useSelector(getTokensState); + const isNanoContractEnabled = useSelector(getNanoContractFeatureToggle); -class Dashboard extends React.Component { - static navigatorStyle = { tabBarVisible: false } + const [currList, selectList] = useState(listOption.tokens); + const navigation = useNavigation(); + const dispatch = useDispatch(); - onItemPress = (item) => { - // Check if the token balance is already loaded - const tokenBalanceStatus = get(this.props.tokensBalance, `${item.uid}.status`, 'loading'); + const onTokenPress = (token) => { + const status = getTokensBalanceStatus(tokensBalance, token); - if (tokenBalanceStatus === 'loading') { + if (isTokensBalanceLoading(status)) { return; } - if (tokenBalanceStatus === 'failed') { + if (isTokensBalanceFailed(status)) { // If the token balance status is failed, we should try again - this.props.getBalance(item.uid); + dispatch(tokenFetchBalanceRequested(token.uid)) return; } - this.props.updateSelectedToken(item); - this.props.navigation.navigate('MainScreen'); + dispatch(updateSelectedToken(token)); + navigation.navigate('MainScreen'); } - render() { - const ManualInfoButton = () => ( - this.props.navigation.navigate('RegisterToken')} - /> - ); - - const Header = () => ( - } - /> - ); - - return ( - - - - } - renderArrow - onItemPress={this.onItemPress} - selectedToken={this.props.selectedToken} - tokens={this.props.tokens} - tokensBalance={this.props.tokensBalance} - tokenMetadata={this.props.tokenMetadata} - /> - - - ); - } + return ( + + + + + { // Only show the toggle button when Nano Contract is enabled to the wallet + isNanoContractEnabled + && ( + + selectList(listOption.tokens) }, + second: { value: t`Nano Contracts`, onTap: () => selectList(listOption.nanoContracts) } + }} + defaultOption='first' + /> + + ) + } + { // Default behavior is to show tokens list + isTokensSelected(currList) + && ( + } + renderArrow + onItemPress={onTokenPress} + selectedToken={selectedToken} + tokens={tokens} + tokensBalance={tokensBalance} + tokenMetadata={tokensMetadata} + /> + ) + } + { // Only show if Nano Contract is enabled in the wallet + isNanoContractEnabled + && isNanoContractsSelected(currList) + && + } + + + ); } -export default connect(mapStateToProps, mapDispatchToProps)(Dashboard); +const Wrapper = ({ children }) => ( + + {children} + +); + +const DashBoardHeader = ({ children }) => ( + + {children} + +); + +const RegisterToken = () => { + const navigation = useNavigation(); + return ( + navigation.navigate('RegisterToken')} + /> + ); +}; + +const TokensHeader = () => ( + + + {t`Tokens`} + + + + + +); + +const styles = StyleSheet.create({ + wrapper: { + flex: 1, + }, + headerWrapper: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + backgroundColor: COLORS.lowContrastDetail, + }, + headerTitle: { + fontSize: 24, + lineHeight: 24, + fontWeight: 'bold', + }, +}); + +export default Dashboard; diff --git a/src/screens/InitWallet.js b/src/screens/InitWallet.js index 6e60202ba..d921b84c9 100644 --- a/src/screens/InitWallet.js +++ b/src/screens/InitWallet.js @@ -326,6 +326,14 @@ class LoadWordsScreen extends React.Component { autoFocus onSubmitEditing={this.loadClicked} blurOnSubmit + inputMode='text' + secureTextEntry + autoCorrect={false} + autoComplete='off' + // ios only + spellCheck={false} + // android only + importantForAutofill='no' /> {this.state.words.length} diff --git a/src/screens/MainScreen.js b/src/screens/MainScreen.js index ac3f7f6a1..be897f115 100644 --- a/src/screens/MainScreen.js +++ b/src/screens/MainScreen.js @@ -8,7 +8,6 @@ import React from 'react'; import { ActivityIndicator, - FlatList, StyleSheet, Text, View, @@ -31,12 +30,13 @@ import { HathorList } from '../components/HathorList'; import { Strong, str2jsx, renderValue, isTokenNFT } from '../utils'; import chevronUp from '../assets/icons/chevron-up.png'; import chevronDown from '../assets/icons/chevron-down.png'; -import infoIcon from '../assets/icons/info-circle.png'; import { IS_MULTI_TOKEN } from '../constants'; import { fetchMoreHistory, updateTokenHistory } from '../actions'; import Spinner from '../components/Spinner'; import { TOKEN_DOWNLOAD_STATUS } from '../sagas/tokens'; import { COLORS } from '../styles/themes'; +import { HathorFlatList } from '../components/HathorFlatList'; +import { ActionDot } from '../components/Icons/ActionDot.icon'; /** * txList {Array} array with transactions of the selected token @@ -110,7 +110,7 @@ class MainScreen extends React.Component { } tokenInfo = () => { - if (this.props.selectedToken.uid !== hathorConstants.HATHOR_TOKEN_CONFIG.uid) { + if (this.props.selectedToken.uid !== hathorConstants.NATIVE_TOKEN_UID) { this.props.navigation.navigate('TokenDetail'); } } @@ -199,12 +199,11 @@ class MainScreen extends React.Component { }; const renderRightElement = () => { - if (this.props.selectedToken.uid !== hathorConstants.HATHOR_TOKEN_CONFIG.uid) { + if (this.props.selectedToken.uid !== hathorConstants.NATIVE_TOKEN_UID) { return ( - + + + ); } @@ -213,7 +212,7 @@ class MainScreen extends React.Component { return ( {this.state.modal} @@ -228,9 +227,7 @@ class MainScreen extends React.Component { token={this.props.selectedToken} isNFT={this.isNFT()} /> - - {renderTxHistory()} - + {renderTxHistory()} ); @@ -287,16 +284,14 @@ class TxHistoryView extends React.Component { render() { return ( - - item.txId} - onEndReached={this.loadMoreHistory} - onEndReachedThreshold={0.2} - ListFooterComponent={this.renderFooter} - /> - + item.txId} + onEndReached={this.loadMoreHistory} + onEndReachedThreshold={0.2} + ListFooterComponent={this.renderFooter} + /> ); } } @@ -305,30 +300,11 @@ class TxListItem extends React.Component { state = { timestamp: null }; style = StyleSheet.create({ - container: { - marginLeft: 16, - marginRight: 16, - marginTop: 0, - borderColor: COLORS.borderColor, - borderBottomWidth: 1, - shadowOffset: { height: 2, width: 0 }, - shadowRadius: 4, - shadowColor: COLORS.textColor, - shadowOpacity: 0.08, - }, view: { flexDirection: 'row', - alignItems: 'center', - backgroundColor: COLORS.backgroundColor, - height: 80, - }, - firstItemBorder: { - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - }, - lastItemBorder: { - borderBottomLeftRadius: 16, - borderBottomRightRadius: 16, + alignItems: 'flex-start', + minHeight: 80, + paddingVertical: 24, }, middleView: { flex: 1, @@ -351,11 +327,14 @@ class TxListItem extends React.Component { lineHeight: 20, fontWeight: 'bold', }, - timestamp: { + secondaryText: { fontSize: 12, lineHeight: 20, color: COLORS.textColorShadow, }, + bold: { + fontWeight: 'bold', + }, }); styleVoided = ({ ...this.style, @@ -365,7 +344,7 @@ class TxListItem extends React.Component { color: COLORS.textColorShadowLight, }, timestamp: { - ...this.style.timestamp, + ...this.style.secondaryText, color: COLORS.textColorShadowLight, }, balance: { @@ -454,37 +433,33 @@ class TxListItem extends React.Component { } render() { + /** + * @type {{ + * item: TxHistory; + * }} TxListItem properties + */ const { item } = this.props; const style = this.getStyle(item); const image = this.getImage(item); - const viewStyle = [style.view]; - const touchStyle = []; - if (this.props.isFirst) { - viewStyle.push(style.firstItemBorder); - touchStyle.push(style.firstItemBorder); - } - if (this.props.isLast) { - viewStyle.push(style.lastItemBorder); - touchStyle.push(style.lastItemBorder); - } - const balanceStr = renderValue(item.balance, this.props.isNFT); const description = item.getDescription(this.props.token); const { timestamp } = this.state; return ( - - this.onItemPress(item)}> - - {image} - - {description} - {timestamp} - - {balanceStr} + this.onItemPress(item)} + underlayColor={COLORS.primaryOpacity30} + > + + {image} + + {description} + {timestamp} + {item.getVersionInfo()} - - + {balanceStr} + + ); } } diff --git a/src/screens/NanoContract/NanoContractDetailsScreen.js b/src/screens/NanoContract/NanoContractDetailsScreen.js new file mode 100644 index 000000000..a9b7e0ea2 --- /dev/null +++ b/src/screens/NanoContract/NanoContractDetailsScreen.js @@ -0,0 +1,60 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { + StyleSheet, + View, +} from 'react-native'; +import { useSelector } from 'react-redux'; +import { t } from 'ttag'; +import HathorHeader from '../../components/HathorHeader'; +import { NanoContractDetails } from '../../components/NanoContract/NanoContractDetails'; +import OfflineBar from '../../components/OfflineBar'; +import { COLORS } from '../../styles/themes'; + +/** + * Presents an information summary of the Nano Contract, a list of transactions + * and provides some actions to users, for instance: + * - Open Nano Contract details at the Explorer + * - Edit the registered address for the Nano Contract + * - Unregister the Nano Contract + */ +export function NanoContractDetailsScreen({ navigation, route }) { + /* Without this default the app breaks after the current Nano Contract unregistration. + * By having a default value the app can render the screen normally after unregistration + * and let it step aside while coming back to Dashboard screen. This transition happens + * quickly, therefore the user will not have time to see the default state. + */ + const defaultNc = { ncId: '', address: '' }; + const { ncId } = route.params; + const nc = useSelector((state) => state.nanoContract.registered[ncId]) || defaultNc; + return ( + + navigation.goBack()} + /> + + + + ); +} + +const Wrapper = ({ children }) => ( + + {children} + +); + +const styles = StyleSheet.create({ + wrapper: { + flex: 1, + alignItems: 'center', + backgroundColor: COLORS.lowContrastDetail, // Defines an outer area on the main list content + }, +}); diff --git a/src/screens/NanoContract/NanoContractRegisterScreen.js b/src/screens/NanoContract/NanoContractRegisterScreen.js new file mode 100644 index 000000000..d5bed315c --- /dev/null +++ b/src/screens/NanoContract/NanoContractRegisterScreen.js @@ -0,0 +1,334 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { + useEffect, + useState, + useCallback +} from 'react'; +import { + StyleSheet, + View, + Text, + Image, +} from 'react-native'; +import { + useDispatch, + useSelector +} from 'react-redux'; +import { t } from 'ttag'; +import HathorHeader from '../../components/HathorHeader'; +import { CircleInfoIcon } from '../../components/Icons/CircleInfo.icon'; +import NewHathorButton from '../../components/NewHathorButton'; +import OfflineBar from '../../components/OfflineBar'; +import SimpleInput from '../../components/SimpleInput'; +import { TextLabel } from '../../components/TextLabel'; +import { TextValue } from '../../components/TextValue'; +import { COLORS } from '../../styles/themes'; +import { + firstAddressRequest, + nanoContractRegisterReady, + nanoContractRegisterRequest +} from '../../actions'; +import { + feedbackSucceedText, + hasFailed, + hasSucceeded, + isLoading +} from './helper'; +import Spinner from '../../components/Spinner'; +import FeedbackModal from '../../components/FeedbackModal'; +import errorIcon from '../../assets/images/icErrorBig.png'; +import checkIcon from '../../assets/images/icCheckBig.png'; +import { FeedbackContent } from '../../components/FeedbackContent'; +import { hasError } from '../../utils'; + +/** + * Validates the formModel, returning the invalidModel. + * If there is no error in the formModel, the invalidModel is returned empty. + */ +function validate(formModel) { + const invalidModel = {}; + + if (!formModel.ncId) { + invalidModel.ncId = t`Nano Contract ID is required.`; + } + + return invalidModel; +} + +export function NanoContractRegisterScreen({ navigation, route }) { + const ncIdFromQrCode = route.params?.ncId; + + const dispatch = useDispatch(); + const { address, error } = useSelector((state) => state.firstAddress); + const registerState = useSelector((state) => ({ + registerStatus: state.nanoContract.registerStatus, + registerFailureMessage: state.nanoContract.registerFailureMessage, + })); + + const [isClean, setClean] = useState(true); + const [formModel, setFormModel] = useState({ + ncId: null, + }); + const [invalidModel, setInvalidModel] = useState({ + ncId: null, + }); + + /** + * It handles input change to perform validations. + * @param { ((name: 'ncId') => (value: string) => {}) => {} } + */ + const handleInputChange = useCallback( + (name) => (value) => { + if (isClean) { + setClean(false); + } + + // update invalid model + const invalidModelCopy = { ...invalidModel }; + delete invalidModelCopy[name]; + setInvalidModel(invalidModelCopy); + + // update form model + const form = { + ...formModel, + [name]: value, + }; + setFormModel(form); + + // validate form model and update invalid model + setInvalidModel(validate(form)); + }, + [isClean, invalidModel, formModel] + ); + + const handleSubmit = useCallback( + () => { + const newInvalidModel = validate(formModel); + if (hasError(newInvalidModel)) { + setInvalidModel(newInvalidModel); + return; + } + + const { ncId } = formModel; + dispatch(nanoContractRegisterRequest({ address, ncId })); + }, + [formModel, address] + ); + + const handleFeedbackModalDismiss = () => { + dispatch(nanoContractRegisterReady()); + }; + + const navigatesToNanoContractTransactions = () => { + dispatch(nanoContractRegisterReady()); + navigation.replace('NanoContractDetailsScreen', { ncId: formModel.ncId }); + }; + + useEffect(() => { + if (ncIdFromQrCode) { + // Set ncId in the input when given + handleInputChange('ncId')(ncIdFromQrCode); + } + + if (!address) { + dispatch(firstAddressRequest()); + } + }, []); + + const hasFirstAddressFailed = () => error; + const isFirstAddressLoading = () => !error && !address; + const hasFirstAddressLoaded = () => !error && address; + + return ( + + + + {hasSucceeded(registerState.registerStatus) + && ( + )} + text={feedbackSucceedText} + onDismiss={handleFeedbackModalDismiss} + action={()} + /> + )} + + {hasFailed(registerState.registerStatus) + && ( + )} + text={registerState.registerFailureMessage} + onDismiss={handleFeedbackModalDismiss} + action={()} + /> + )} + + {hasFirstAddressFailed() + && ( + )} + title={t`Load First Addresses Error`} + message={error} + /> + )} + + {isFirstAddressLoading() + && ( + + )} + + {hasFirstAddressLoaded() + && ( + + + + + {t`Wallet Address`} + {address} + + + + + + + + + {t`If you want to change the wallet address, you will be able to do`} + + {' '}{t`after the contract is registered.`} + + + + + {isLoading(registerState.registerStatus) + && ( + + + + )} + + + + + )} + + + ); +} + +const FieldContainer = ({ last, children }) => ( + + {children} + +); + +const NavigationHeader = ({ navigation }) => ( + navigation.goBack()} + /> +); + +const Wrapper = ({ children }) => ( + + {children} + +); + +const ContentWrapper = ({ children }) => ( + + {children} + +); + +const styles = StyleSheet.create({ + wrapper: { + flex: 1, + justifyContent: 'flex-start', + alignSelf: 'stretch', + backgroundColor: COLORS.lowContrastDetail, // Defines an outer area on the main list content + }, + contentWrapper: { + flex: 1, + alignSelf: 'stretch', + paddingTop: 16, + paddingBottom: 48, + paddingHorizontal: 16, + }, + infoContainer: { + flexShrink: 1, + flexDirection: 'row', + alignItems: 'center', + marginBottom: 24, + borderRadius: 8, + paddingVertical: 8, + paddingHorizontal: 16, + backgroundColor: 'hsla(203, 100%, 93%, 1)', + }, + infoContent: { + flex: 1, + paddingLeft: 8, + }, + selectionContainer: { + marginBottom: 16, + borderRadius: 8, + paddingVertical: 8, + paddingHorizontal: 16, + backgroundColor: COLORS.freeze100, + }, + loadingContainer: { + alignItems: 'center', + }, + input: { + marginBottom: 24, + }, + buttonContainer: { + alignSelf: 'stretch', + marginTop: 'auto', + }, + feedbackModalIcon: { + height: 105, + width: 105 + }, + feedbackContentIcon: { + height: 36, + width: 36, + }, + text: { + fontSize: 14, + lineHeight: 20, + }, + bold: { + fontWeight: 'bold', + }, + pd0: { + paddingBottom: 0, + }, + pd8: { + paddingBottom: 8, + }, +}); diff --git a/src/screens/NanoContract/NanoContractTransactionScreen.js b/src/screens/NanoContract/NanoContractTransactionScreen.js new file mode 100644 index 000000000..db6db6cfa --- /dev/null +++ b/src/screens/NanoContract/NanoContractTransactionScreen.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { + StyleSheet, + View, +} from 'react-native'; +import { t } from 'ttag'; +import HathorHeader from '../../components/HathorHeader'; +import { NanoContractTransactionHeader } from '../../components/NanoContract/NanoContractTransactionHeader'; +import { NanoContractTransactionActionList } from '../../components/NanoContract/NanoContractTransactionActionList'; +import OfflineBar from '../../components/OfflineBar'; +import { COLORS } from '../../styles/themes'; + +/** + * It presents information about the transaction. + */ +export function NanoContractTransactionScreen({ navigation, route }) { + const { tx } = route.params; + return ( + + + + + + + + + ); +} + +const NavigationHeader = ({ navigation }) => ( + navigation.goBack()} + /> +); + +const Wrapper = ({ children }) => ( + + {children} + +); + +const ContentWrapper = ({ children }) => ( + + {children} + +); + +const styles = StyleSheet.create({ + wrapper: { + flex: 1, + alignItems: 'center', + backgroundColor: COLORS.lowContrastDetail, // Defines an outer area on the main list content + }, +}); diff --git a/src/screens/NanoContract/helper.js b/src/screens/NanoContract/helper.js new file mode 100644 index 000000000..5a292f4a2 --- /dev/null +++ b/src/screens/NanoContract/helper.js @@ -0,0 +1,47 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { t } from 'ttag'; +import { NANOCONTRACT_REGISTER_STATUS } from '../../constants'; + +export const feedbackSucceedText = t`Contract successfully registered.`; + +/** + * Check if the nano contract register status is successful. + * @param {string} status - status from redux store + * @returns {boolean} - true if the status is successful, false otherwise + */ +export const hasSucceeded = (status) => ( + status === NANOCONTRACT_REGISTER_STATUS.SUCCESSFUL +); + +/** + * Check if the nano contract register status is failed. + * @param {string} status - status from redux store + * @returns {boolean} - true if the status is failed, false otherwise + */ +export const hasFailed = (status) => ( + status === NANOCONTRACT_REGISTER_STATUS.FAILED +); + +/** + * Check if the nano contract register status is loading. + * @param {string} status - status from redux store + * @returns {boolean} - true if the status is loading, false otherwise + */ +export const isLoading = (status) => ( + status === NANOCONTRACT_REGISTER_STATUS.LOADING +); + +/** + * Check if the nano contract register status is not ready. + * @param {string} status - status from redux store + * @returns {boolean} - true if the status is not ready, false otherwise + */ +export const notReady = (status) => ( + status !== NANOCONTRACT_REGISTER_STATUS.READY +); diff --git a/src/screens/NanoContractRegisterQrCodeScreen.js b/src/screens/NanoContractRegisterQrCodeScreen.js new file mode 100644 index 000000000..004b8fcfe --- /dev/null +++ b/src/screens/NanoContractRegisterQrCodeScreen.js @@ -0,0 +1,71 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { View } from 'react-native'; +import { t } from 'ttag'; + +import HathorHeader from '../components/HathorHeader'; +import QRCodeReader from '../components/QRCodeReader'; +import SimpleButton from '../components/SimpleButton'; +import { COLORS } from '../styles/themes'; + +export const NanoContractRegisterQrCodeScreen = ({ navigation }) => { + const onSuccess = (e) => { + navigation.navigate('NanoContractRegisterScreen', { ncId: e.data }); + } + + const renderHeaderRightElement = () => ( + navigation.navigate('NanoContractRegisterScreen')} + /> + ); + + return ( + + + navigation.pop()} + rightElement={renderHeaderRightElement()} + /> + + + + + + ); +} + +export default NanoContractRegisterQrCodeScreen; diff --git a/src/screens/NetworkSettings/helper.js b/src/screens/NetworkSettings/helper.js index 187ad08be..8d7be65c0 100644 --- a/src/screens/NetworkSettings/helper.js +++ b/src/screens/NetworkSettings/helper.js @@ -1,3 +1,10 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + import { t } from 'ttag'; import { NETWORKSETTINGS_STATUS } from '../../constants'; diff --git a/src/screens/PinScreen.js b/src/screens/PinScreen.js index 65a4eb214..96bca2061 100644 --- a/src/screens/PinScreen.js +++ b/src/screens/PinScreen.js @@ -268,6 +268,7 @@ class PinScreen extends React.Component { title = t`Reset wallet`; onPress = () => this.goToReset(); } + return ( { const tokenUnregister = this.props.selectedToken.uid; // Preventing unregistering HTR token, even if the user gets on this screen because of an error - if (tokenUnregister === hathorLib.constants.HATHOR_TOKEN_CONFIG.uid) { + if (tokenUnregister === hathorLib.constants.NATIVE_TOKEN_UID) { return; } diff --git a/src/screens/WalletConnect/CreateTokenScreen.js b/src/screens/WalletConnect/CreateTokenScreen.js new file mode 100644 index 000000000..cb1b4344a --- /dev/null +++ b/src/screens/WalletConnect/CreateTokenScreen.js @@ -0,0 +1,45 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { + StyleSheet, + View, +} from 'react-native'; +import { t } from 'ttag'; +import HathorHeader from '../../components/HathorHeader'; +import OfflineBar from '../../components/OfflineBar'; +import { CreateTokenRequest } from '../../components/WalletConnect/CreateTokenRequest'; +import { COLORS } from '../../styles/themes'; + +export function CreateTokenRequestScreen({ route }) { + const { createTokenRequest } = route.params; + + return ( + + + + + + ); +} + +const Wrapper = ({ children }) => ( + + {children} + +); + +const styles = StyleSheet.create({ + wrapper: { + flex: 1, + alignItems: 'center', + backgroundColor: COLORS.lowContrastDetail, // Defines an outer area on the main list content + }, +}); diff --git a/src/screens/WalletConnect/NewNanoContractTransactionScreen.js b/src/screens/WalletConnect/NewNanoContractTransactionScreen.js new file mode 100644 index 000000000..666996548 --- /dev/null +++ b/src/screens/WalletConnect/NewNanoContractTransactionScreen.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { + StyleSheet, + View, +} from 'react-native'; +import { t } from 'ttag'; +import HathorHeader from '../../components/HathorHeader'; +import OfflineBar from '../../components/OfflineBar'; +import { NewNanoContractTransactionRequest } from '../../components/WalletConnect/NanoContract/NewNanoContractTransactionRequest'; +import { COLORS } from '../../styles/themes'; + +export function NewNanoContractTransactionScreen({ route }) { + const { ncTxRequest } = route.params; + return ( + + + + + + ); +} + +const Wrapper = ({ children }) => ( + + {children} + +); + +const styles = StyleSheet.create({ + wrapper: { + flex: 1, + alignItems: 'center', + backgroundColor: COLORS.lowContrastDetail, // Defines an outer area on the main list content + }, +}); diff --git a/src/screens/WalletConnect/SignMessageRequestScreen.js b/src/screens/WalletConnect/SignMessageRequestScreen.js new file mode 100644 index 000000000..1336a3b4c --- /dev/null +++ b/src/screens/WalletConnect/SignMessageRequestScreen.js @@ -0,0 +1,45 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { + StyleSheet, + View, +} from 'react-native'; +import { t } from 'ttag'; +import HathorHeader from '../../components/HathorHeader'; +import OfflineBar from '../../components/OfflineBar'; +import { SignMessageRequest } from '../../components/WalletConnect/SignMessageRequest'; +import { COLORS } from '../../styles/themes'; + +export function SignMessageRequestScreen({ route }) { + const { signMessageRequest } = route.params; + + return ( + + + + + + ); +} + +const Wrapper = ({ children }) => ( + + {children} + +); + +const styles = StyleSheet.create({ + wrapper: { + flex: 1, + alignItems: 'center', + backgroundColor: COLORS.lowContrastDetail, + }, +}); diff --git a/src/store.js b/src/store.js index 3cda91abf..e2e9e04bf 100644 --- a/src/store.js +++ b/src/store.js @@ -13,10 +13,12 @@ import { NETWORK_MAINNET } from './constants'; export const ACCESS_DATA_KEY = 'asyncstorage:access'; export const REGISTERED_TOKENS_KEY = 'asyncstorage:registeredTokens'; export const STORE_VERSION_KEY = 'asyncstorage:version'; +export const REGISTERED_NANO_CONTRACTS_KEY = 'asyncstorage:registeredNanoContracts'; export const walletKeys = [ ACCESS_DATA_KEY, REGISTERED_TOKENS_KEY, + REGISTERED_NANO_CONTRACTS_KEY, ]; /* eslint-disable class-methods-use-this */ @@ -39,6 +41,7 @@ class HybridStore extends MemoryStore { * @returns {Promise} */ async saveAccessData(data) { + await super.saveAccessData(data); STORE.setItem(ACCESS_DATA_KEY, data); } @@ -73,6 +76,7 @@ class HybridStore extends MemoryStore { * @returns {Promise} */ async registerToken(token) { + await super.registerToken(token); const registeredTokens = STORE.getItem(REGISTERED_TOKENS_KEY) || {}; registeredTokens[token.uid] = token; STORE.setItem(REGISTERED_TOKENS_KEY, registeredTokens); @@ -86,6 +90,7 @@ class HybridStore extends MemoryStore { * @returns {Promise} */ async unregisterToken(tokenUid) { + await super.unregisterToken(tokenUid); const registeredTokens = STORE.getItem(REGISTERED_TOKENS_KEY) || {}; if (tokenUid in registeredTokens) { delete registeredTokens[tokenUid]; @@ -103,6 +108,105 @@ class HybridStore extends MemoryStore { const registeredTokens = STORE.getItem(REGISTERED_TOKENS_KEY) || {}; return tokenUid in registeredTokens; } + + /** + * Return true if a nano contract is registered, false otherwise. + * + * @param {string} ncId Nano contract ID. + * @returns {Promise} Nano contract data instance. + * @async + */ + async isNanoContractRegistered(ncId) { + const contracts = STORE.getItem(REGISTERED_NANO_CONTRACTS_KEY) || {}; + return ncId in contracts; + } + + /** + * Get a nano contract data on storage from the ncKey. + * + * @param {string} ncId Nano Contract ID. + * @returns {Promise} Nano contract data instance. + * @async + */ + async getNanoContract(ncId) { + const contracts = STORE.getItem(REGISTERED_NANO_CONTRACTS_KEY) || {}; + return contracts[ncId] || null; + } + + /** + * Register a nano contract. + * + * @param {string} ncId Nano Contract ID. + * @param {INcData} ncValue Nano contract basic information. + * @returns {Promise} + * @async + */ + async registerNanoContract(ncId, ncValue) { + await super.registerNanoContract(ncId, ncValue); + const contracts = STORE.getItem(REGISTERED_NANO_CONTRACTS_KEY) || {}; + contracts[ncId] = ncValue; + STORE.setItem(REGISTERED_NANO_CONTRACTS_KEY, contracts); + } + + /** + * Unregister a nano contract. + * + * @param {string} ncId Nano Contract ID. + * @returns {Promise} + * @async + */ + async unregisterNanoContract(ncId) { + await super.unregisterNanoContract(ncId); + const contracts = STORE.getItem(REGISTERED_NANO_CONTRACTS_KEY) || {}; + delete contracts[ncId]; + STORE.setItem(REGISTERED_NANO_CONTRACTS_KEY, contracts); + } + + /** + * Iterate on registered nano contract. + * + * @async + * @returns {AsyncGenerator} + */ + async* registeredNanoContractsIter() { + const contracts = STORE.getItem(REGISTERED_NANO_CONTRACTS_KEY) || {}; + for (const contract of Object.values(contracts)) { + yield { ...contract }; + } + } + + /** + * Clean the storage. + * @param {boolean} cleanHistory if we should clean the transaction history. + * @param {boolean} cleanAddresses if we should clean the addresses. + * @param {boolean} cleanTokens if we should clean the registered tokens. + * @async + * @returns {Promise} + */ + async cleanStorage(cleanHistory = false, cleanAddresses = false, cleanTokens = false) { + await super.cleanStorage(cleanHistory, cleanAddresses, cleanTokens); + if (cleanTokens) { + // Remove from the cache + STORE.removeItem(REGISTERED_TOKENS_KEY); + STORE.removeItem(REGISTERED_NANO_CONTRACTS_KEY); + } + } + + /** + * Update nano contract registered address. + * @param {string} ncId Nano Contract ID. + * @param {string} address Nano Contract registered address. + * @async + * @returns {Promise} + */ + async updateNanoContractRegisteredAddress(ncId, address) { + await super.updateNanoContractRegisteredAddress(ncId, address); + const contract = await this.getNanoContract(ncId); + if (contract) { + const newContract = { ...contract, address }; + await this.registerNanoContract(ncId, newContract); + } + } } /* eslint-enable class-methods-use-this */ diff --git a/src/styles/themes.js b/src/styles/themes.js index f0ba087d9..970658496 100644 --- a/src/styles/themes.js +++ b/src/styles/themes.js @@ -6,69 +6,99 @@ */ import { DefaultTheme } from '@react-navigation/native'; - import { _PRIMARY_COLOR as PRIMARY_COLOR } from '../config'; import { HslColor } from '../HslColor'; /** * Light theme color scheme - * @type {{ - * midContrastDetail: string, - * errorBgColor: string, - * backgroundColor: string, - * borderColor: string, - * lowContrastDetail: string, - * textColorShadow: string, - * positiveBalanceColor: string, - * textColor: string, - * errorTextColor: string, - * tabBarBackground: string, - * primaryOpacity30: string, - * textColorShadowLight: string, - * primaryOpacity10: string, - * errorTextShadow: string, - * primary: string}} - * @property {string} backgroundColor The main background color - * @property {string} lowContrastDetail Low contrast with background: separator lines, containers... - * @property {string} midContrastDetail Medium contrast with background: placeholders, ... - * @property {string} borderColor Defines borders - * @property {string} textColor Maximum contrast with the background color, for reading - * @property {string} textColorShadow Washed down version of the text - * @property {string} textColorShadowLight More washed down version of the text - * @property {string} tabBarBackground Specific for tab bar selectors - * @property {string} positiveBalanceColor Represents a positive feedback for the user - * @property {string} errorBgColor For containers with error feedbacks - * @property {string} errorTextColor For texts with error messages - * @property {string} errorTextShadow Washed down version of error texts - * @property {string} primary Primary color, as set on the application config file - * @property {string} primaryOpacity10 Primary color washed down to 10% opacity - * @property {string} primaryOpacity30 Primary color washed down to 30% opacity */ export const COLORS = { + white: '#fff', + black: '#000', + /** + * @type {string} The main background color + */ backgroundColor: '#fff', + /** + * @type {string} Low contrast with background, like separator lines + */ lowContrastDetail: '#f7f7f7', + /** + * @type {string} Medium contrast with background, like placeholders + */ midContrastDetail: '#9e9e9e', darkContrastDetail: '#808080', + /** + * @type {string} Defines borders + */ borderColor: '#eee', borderColorMid: '#dcdcdc', borderColorDark: '#cecece', + /** + * @type {string} + * Maximum contrast with the background color, for better reading + */ textColor: '#000', + /** + * @type {string} Washed down version of the text + */ textColorShadow: 'rgba(0, 0, 0, 0.5)', textColorShadowOpacity005: 'rgba(0, 0, 0, 0.05)', textColorShadowLighter: 'rgba(0, 0, 0, 0.1)', + /** + * @type {string} More washed down version of the text + */ textColorShadowLight: 'rgba(0, 0, 0, 0.3)', textColorShadowOpacity06: 'rgba(0, 0, 0, 0.6)', textColorShadowOpacity07: 'rgba(0, 0, 0, 0.7)', textColorShadowDark: 'rgba(0, 0, 0, 0.8)', textColorShadowOpacity09: 'rgba(0, 0, 0, 0.9)', + /** + * @type {string} Specific for tab bar selectors + */ tabBarBackground: '#333', + /** + * @type {string} Represents a positive feedback for the user + */ positiveBalanceColor: '#0DA0A0', + /** + * @type {string} For containers with error feedbacks + */ errorBgColor: '#DE3535', + /** + * @type {string} For texts with error messages + */ errorTextColor: '#F00', + /** + * @type {string} Washed down version of error texts + * */ errorTextShadow: `rgba(255, 0, 0, 0.7)`, + /** + * @type {string} Primary color, as set on the application config file + * */ primary: PRIMARY_COLOR, + /** + * @type {string} Primary color washed down to 10% opacity + */ primaryOpacity10: `${PRIMARY_COLOR}1A`, + /** + * @type {string} Primary color washed down to 30% opacity + */ primaryOpacity30: `${PRIMARY_COLOR}4D`, + feedbackSuccess100: 'hsla(161, 30%, 85%, 1)', + feedbackSuccess400: 'hsla(159, 75%, 17%, 1)', + feedbackWarning100: 'hsla(21, 100%, 90%, 1)', + feedbackWarning300: 'hsla(21, 54%, 49%, 1)', + feedbackError200: 'hsla(7, 69%, 95%, 1)', + feedbackError600: 'hsla(7, 100%, 30%, 1)', + freeze100: 'hsla(0, 0%, 90%, 1)', + freeze300: 'hsla(0, 0%, 45%, 1)', + cardWarning100: 'hsla(46, 100%, 95%, 1)', + cardWarning200: 'hsla(46, 100%, 58%, 1)', + /** + * @type {string} Black with 38% of light and full opaque + */ + textLabel: 'hsla(0, 0%, 38%, 1)', }; /** diff --git a/src/utils.js b/src/utils.js index 86436201c..ef137eaf4 100644 --- a/src/utils.js +++ b/src/utils.js @@ -8,15 +8,19 @@ import hathorLib from '@hathor/wallet-lib'; import * as Keychain from 'react-native-keychain'; import React from 'react'; +import { isEmpty } from 'lodash'; import { t } from 'ttag'; import { Linking, Platform, Text } from 'react-native'; import { getStatusBarHeight } from 'react-native-status-bar-height'; -import { isEmpty } from 'lodash'; +import moment from 'moment'; import baseStyle from './styles/init'; -import { KEYCHAIN_USER, NETWORK_MAINNET } from './constants'; +import { KEYCHAIN_USER, NETWORK_MAINNET, NANO_CONTRACT_FEATURE_TOGGLE } from './constants'; import { STORE } from './store'; import { TxHistory } from './models'; import { COLORS, STYLE } from './styles/themes'; +import { logger } from './logger'; + +const log = logger('utils'); export const Strong = (props) => {props.children}; @@ -32,6 +36,19 @@ export const getShortHash = (hash, length = 4) => ( `${hash.substring(0, length)}...${hash.substring(64 - length, 64)}` ); +/** + * It short any string content without length bound. + * @param {string} content Content to be sliced in two parts + * @param {string} length Size of the substrigs in both sides of `...` + * + * @example + * getShortContent('00c30fc8a1b9a326a766ab0351faf3635297d316fd039a0eda01734d9de40185', 3) + * // output: '00c...185' + */ +export const getShortContent = (content, length = 4) => ( + `${content.substring(0, length)}...${content.substring(content.length - length, content.length)}` +); + /** * Get amount text value and transform in its integer value * @@ -330,22 +347,16 @@ export const changePin = async (wallet, oldPin, newPin) => { }; /** - * Map history element to expected TxHistory model object + * Curry function that maps a raw history element to an expected TxHistory model object. * - * element {Object} Tx history element with {txId, timestamp, balance, voided?} - * token {string} Token uid + * @param {string} tokenUid - Token uid + * + * @returns {(rawTxHistory: Object) => TxHistory} A function that maps a raw + * transaction history element to a TxHistory object */ -export const mapTokenHistory = (element, token) => { - const data = { - txId: element.txId, - timestamp: element.timestamp, - balance: element.balance, - // in wallet service this comes as 0/1 and in the full node comes with true/false - isVoided: Boolean(element.voided), - tokenUid: token - }; - return new TxHistory(data); -}; +export const mapToTxHistory = (tokenUid) => (rawTxHistory) => ( + TxHistory.from(rawTxHistory, tokenUid) +); /** * Select the push notification settings from redux state @@ -396,3 +407,135 @@ export const isPushNotificationAvailableForUser = (state) => ( // Wallet Service API to register the device's token. && !isEmpty(state.networkSettings.walletServiceUrl) ); + +/** + * Get Nano Contract feature toggle state from redux. + * + * @param {Object} state Redux store state + * + * @returns {boolean} the Nano Contract feature toggle state. + */ +export const getNanoContractFeatureToggle = (state) => ( + state.featureToggles[NANO_CONTRACT_FEATURE_TOGGLE] +); + +/** + * Get timestamp in specific format. + * + * @param {number} timestamp + * + * @returns {string} formatted timestamp + */ +export const getTimestampFormat = (timestamp) => moment.unix(timestamp).format(t`DD MMM YYYY [•] HH:mm`) + +/** + * Extract all the items of an async iterator/generator. + * + * @returns {Promise} A promise of an array of unkown object. + * @async + */ +export const consumeAsyncIterator = async (asyncIterator) => { + const list = []; + for (;;) { + /* eslint-disable no-await-in-loop */ + const objYielded = await asyncIterator.next(); + const { value, done } = objYielded; + + if (done) { + break; + } + + list.push(value); + } + return [...list]; +}; + +/** + * Return all addresses of the wallet with info of each of them. + * + * @param {Object} wallet + * + * @returns {Promise<{ + * address: string; + * index: number; + * transactions: number; + * }[]>} a list of addres info. + * + * @throws {Error} either wallet not ready or other http request error if using wallet service. + * @async + */ +export const getAllAddresses = async (wallet) => { + const iterator = await wallet.getAllAddresses(); + return consumeAsyncIterator(iterator); +} + +/** + * Return the first wallet's address. + * + * @param {Object} wallet + * + * @returns {Promise} + * @throws {Error} either wallet not ready or other http request error if using wallet service. + * @async + */ +export const getFirstAddress = async (wallet) => wallet.getAddressAtIndex(0); + +/** + * Verifies if the invalidModel of the form has an error message. + */ +export function hasError(invalidModel) { + return Object + .values({ ...invalidModel }) + .reduce((_hasError, currValue) => _hasError || !isEmpty(currValue), false); +} + +/** + * Parses a script data to return an instance of script type. + * + * @example + * parseScriptData('P2PKH or P2SH script', networkObj); + * >>> { address, timelock } + * + * @example + * parseScriptData('Data script', networkObj); + * >>> { data } + * + * @param {string} scriptData A script in its hexadecimal format + * @param {Object} network A network object + * + * @return {P2PKH | P2SH | ScriptData | null} Parsed script object + */ +export const parseScriptData = (scriptData, network) => { + try { + const script = hathorLib.bufferUtils.hexToBuffer(scriptData); + return hathorLib.scriptsUtils.parseScript(script, network); + } catch (error) { + log.error('Error parsing script data.', error); + // Avoid throwing exception when we can't parse the script no matter the reason + return null; + } +} + +/** + * Split a list in a list of groups defined by group size. + * + * @param {[]} list An array object. + * @param {number} groupSize The size of a group, which determines the final number of groups. + * + * @returns {[][]} Returns an array of grouped itens in array. + * + * @example + * splitInGroups(['a','b'], 1); + * // outputs: [['a'], ['b']] + */ +export function splitInGroups(list, groupSize) { + if (groupSize === 0) { + return list; + } + + const groups = []; + for (let i = 0; i < list.length; i += groupSize) { + groups.push(list.slice(i, i + groupSize)); + } + return groups; +} diff --git a/walletconnect.sh b/walletconnect.sh index 1b5a960fb..6eabbe9b9 100644 --- a/walletconnect.sh +++ b/walletconnect.sh @@ -2,4 +2,17 @@ git apply enable-walletconnect.patch +npm install \ + @walletconnect/core@2.15.1\ + @walletconnect/web3wallet@1.14.1 \ + @ethersproject/shims@5.7.0 \ + @json-rpc-tools/utils@1.7.6 \ + @react-native-community/netinfo@11.3.1 \ + @walletconnect/react-native-compat@2.12.2 \ + ethers@6.13.2 \ + events@3.3.0 \ + fast-text-encoding@1.0.6 \ + react-native-get-random-values@1.11.0 \ + text-encoding@0.7.0 + npm install && cd ios && pod install