From b0203d048879cc9377a9a4ba3b1dcb588d34511d Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Wed, 21 Apr 2021 15:27:15 -0400 Subject: [PATCH] Il react (#139) * Initial React project commit * React: Config setup (#77) * Docker config * Add CircleCI config * Test PR preview * Test PR preview, take 2 * React: Require Node v14 (#79) * Require Node v14 * Remove .npmrc file * Update CircleCI config * Update Dockerfile Node version to 14 * Add .npmrc + disable CircleCI tests * React: send payment (#80) * React: send payment * Handle loading state * React: Add asset (#81) * Add asset * Clean up comments * React: trust asset (#82) * Show all balances * Trust asset * React: deposit asset (#83) * Deposit trusted asset * Deposit untrusted asset * Reject no home domain * [DO NOT MERGE] Refactor React PR preview (#85) * Test * Withdraw asset (#86) * PR preview log * React: Claim asset + claimable balances (#87) * Show claimable balance * Claim asset * PR preview log * Basic logs with example (#88) * PR preview log * React: SEP-31 Send (#89) * SEP-31 Send in progress * Fix search query params * SEP-31 get fields to render * In progress * SEP-31 send works * Update postTransaction * Remove test file * Landing page with new UI (#90) * New UI sign in (#91) * Sign in works * Sign in modal styled * New UI account info (#92) * Account info and actions styled * Basic styles added to Balances * Temp log file added * New UI add asset (#93) * Update log file * New UI logs (#94) * New UI log items styled * Logs scrolling done * Styled account details * Make log action async + add logs to deposit and withdrawal * ToastBanner + add fade to log item * Tweaks * Replace check icon * Cleanup * Move toast banner to top * Update log file * New UI claimable balance + more logs (#95) * Update log file * New UI send payment (#96) * New UI send payment * Add asset tweaks * Update log file * Use title instead of url (#97) * New UI deposit (#98) * Update log file * New UI withdrawal (#99) * Rename withdrawAsset * Rename sep31Send * New UI withdrawal * Update log file * Text update on landing + remove issuer from add asset (#100) * Text update on landing + remove issuer from add asset * Updated SEP-31 check info types * Update log file * Architecture overview doc (#101) * New UI balances and assets (#102) * In progress * Asset action confirmation modal * Refactor asset data for consistency * Create BalanceRow component * Asset type normalized * Active and disabled balance styles * Active asset toast message * Fix assets after fund account * Cleanup * Update log file * Claimable balance with confirmation modal (#105) * In progress * Asset action confirmation modal * Refactor asset data for consistency * Create BalanceRow component * Asset type normalized * Active and disabled balance styles * Active asset toast message * Fix assets after fund account * Cleanup * Claimable balance with confirmation modal * Update log file * SEP-31 Send UX updates (#106) * In progress * Asset action confirmation modal * Refactor asset data for consistency * Create BalanceRow component * Asset type normalized * Active and disabled balance styles * Active asset toast message * Fix assets after fund account * Cleanup * Claimable balance with confirmation modal * In progress * Add polling logs * Modal tweaks * Use native type instead of hard code * Update log file * Fix bug when active asset not set after SEP-24 popup closed (#107) * Updated log file * Download logs markdown file (#108) * Add TransactionStatus enum (#109) * Updated log file * Improve error messages (#115) * User error message * Use helper getErrorMessage for error messages * Cleanup * Code review fixes 1 (#116) * Update fetch account + remove unused settings * Rename activeAsset.asset to activeAsset.action * Update SEP-12 origin URL * Update types in account actions * SEP-24 deposit stop polling when state is pending_external * Put deposit/withdraw end statuses in variable * Rename ActiveAssetAction id to assetString * Sign out confirmation (#117) * Sign out confirmation * Cleanup * Updated log file * Assets update + home domain override (#114) * In progress * Improve add asset flow * Cleanup * Disable trust asset action for unfunded accounts * Cleanup * Update add asset validation * Refactor update/remove URL search params * Refactor to use assets array instead of object * Cleanup * Add allAssets to store + asset overrides action in progress * Show asset overrides * Add/remove home domain override works, needs UI polishes * Add/remove home domain styled * Claimable balances: update on account refresh + remove home domain * Updated log file * Update text + add tooltip to some components (#124) * Update text + add tooltip to some components * Remove commented out text * SEP tweaks (home domain, TOML, check info) (#125) * Update text + add tooltip to some components * SEP home domain + toml cleanup * SEP-24 deposit/withdraw type check * SEP-31 send payment + check info updates * Use public instead of secret key in SEP-12 fields * Use readChallengeTx() to validate SEP-10 tx * Pass home domain to SEP-10 auth request * Code review fixes * Updated log file * Sign out prompt to save URL + code styling (#127) * Copy URL on sign out + add URL to logs download * Add TextCode component * Replace TextCode with code + render markdown in Logs * Code styling for dark mode * Update code dark mode color * More tweaks to code in dark mode * Configuration modal + claimableBalanceSupported search param (#126) * In progress * Toggle component added * Save claimableBalanceSupported search param and use it in SEP-24 deposit * Updated log file * Log updates (#128) * Log polishes * Added claimable balance supported to current session params * Logs updated * Show configuration only when signed in * Improve UX with loaders + some fixes (#129) * Show loader when getting asset overrides * Fix SEP-31 send bug * Updated log file * on re-render, set value of dropdown to "", or initial (#133) * on re-render, set value of dropdown to "", or initial * make select a controlled component to dictate it's value * Updated log file with dropdown changes * add standard SDF metrics handling and landing page tracking (#134) * init Sentry and capture error strings when logged (#135) * init Sentry and capture error strings when logged * capture exception in duck * log sendPaymentAction error * update change log * Adding announcement to landing page (#136) * adding announcement to landing page * PR changes * edits for review * update log * properly pass our keys to Docker and name them in a way react-scripts can handle them (#137) * we need to properly pass our keys to Docker and name them in a way react-scripts can handle them * update Docker and Make files per Jacek * update docker image with necessary packages * add `yes` flag to gnupg1 install * add gnupg * update Docker deps Co-authored-by: Piyal Basu Co-authored-by: Shannon Romano Co-authored-by: Iveta Co-authored-by: Iveta Co-authored-by: Shannon Romano --- .circleci/config.yml | 8 +- .dockerignore | 3 + .editorconfig | 15 - .eslintrc.js | 15 + .gitattributes | 2 - .gitignore | 43 +- .npmrc | 2 +- .prettierignore | 2 - .prettierrc | 9 - .vscode/launch.json | 26 - Dockerfile | 33 + Makefile | 16 + readme.md => README.md | 17 +- architecture.md | 81 + capacitor.config.json | 8 - nginx.conf | 23 + package.json | 138 +- prettier.config.js | 1 + public/favicon.ico | Bin 0 -> 7406 bytes public/images/doc-redux-state.png | Bin 0 -> 21903 bytes public/index.html | 18 + public/logo192.png | Bin 0 -> 3843 bytes public/logo512.png | Bin 0 -> 11240 bytes public/manifest.json | 25 + public/robots.txt | 3 + src/App.scss | 441 + src/App.tsx | 89 + src/assets/icons/arrow-left.svg | 7 + src/assets/icons/arrow-right.svg | 7 + src/assets/icons/bubble.svg | 7 + src/assets/icons/edit.svg | 7 + src/assets/icons/error.svg | 7 + src/components.d.ts | 125 - src/components/AccountInfo.tsx | 172 + src/components/AddAsset.tsx | 201 + src/components/Assets.tsx | 378 + src/components/Balance.tsx | 180 + src/components/BalanceRow.tsx | 141 + src/components/Banner/index.tsx | 8 + src/components/Banner/styles.scss | 10 + src/components/ClaimableBalance.tsx | 64 + src/components/ConfigurationModal.tsx | 43 + src/components/ConfirmAssetAction.tsx | 42 + src/components/ConnectAccount.tsx | 63 + src/components/CopyWithText.tsx | 35 + src/components/CopyWithTooltip/index.tsx | 54 + src/components/CopyWithTooltip/styles.scss | 39 + src/components/Footer.tsx | 54 + src/components/Header.tsx | 32 + src/components/Heading/index.tsx | 27 + src/components/Heading/styles.scss | 58 + src/components/HomeDomainOverrideButtons.tsx | 122 + src/components/HomeDomainOverrideModal.tsx | 116 + src/components/IconButton/index.tsx | 25 + src/components/IconButton/styles.scss | 14 + .../InfoButtonWithTooltip/index.tsx | 59 + .../InfoButtonWithTooltip/styles.scss | 43 + src/components/Input/index.tsx | 41 + src/components/Input/styles.scss | 83 + src/components/LogItem/index.tsx | 107 + src/components/LogItem/styles.scss | 117 + src/components/Logs.tsx | 134 + src/components/Modal/index.tsx | 48 + src/components/Modal/styles.scss | 102 + src/components/PageContent.tsx | 5 + src/components/PrivateRoute.tsx | 25 + src/components/SendPayment.tsx | 163 + src/components/Sep31Send.tsx | 125 + src/components/SettingsHandler.tsx | 123 + src/components/SignOutModal.tsx | 78 + src/components/TextButton/index.tsx | 34 + src/components/TextButton/styles.scss | 58 + src/components/TextLink/index.tsx | 29 + src/components/TextLink/styles.scss | 24 + src/components/ToastBanner/index.tsx | 50 + src/components/ToastBanner/styles.scss | 24 + src/components/Toggle/index.tsx | 30 + src/components/Toggle/styles.scss | 45 + src/components/UntrustedBalance.tsx | 217 + src/components/WarningBanner.tsx | 18 + src/components/jsonviewer/jsonviewer.scss | 15 - src/components/jsonviewer/jsonviewer.tsx | 43 - src/components/jsonviewer/readme.md | 36 - src/components/loader/loader.scss | 16 - src/components/loader/loader.tsx | 48 - src/components/loader/readme.md | 28 - src/components/logview/assets/arrow-left.svg | 4 - src/components/logview/assets/arrow-right.svg | 4 - src/components/logview/assets/error.svg | 11 - src/components/logview/assets/message.svg | 5 - src/components/logview/logview.scss | 78 - src/components/logview/logview.tsx | 97 - src/components/logview/readme.md | 67 - src/components/prompt/prompt.scss | 106 - src/components/prompt/prompt.tsx | 88 - src/components/prompt/promptInput.tsx | 20 - src/components/prompt/readme.md | 28 - .../wallet/events/componentWillLoad.ts | 40 - src/components/wallet/events/render.tsx | 21 - src/components/wallet/methods/addAsset.ts | 62 - src/components/wallet/methods/claimAsset.ts | 75 - src/components/wallet/methods/copyAddress.ts | 7 - src/components/wallet/methods/copySecret.ts | 12 - .../wallet/methods/createAccount.ts | 28 - src/components/wallet/methods/depositAsset.ts | 253 - .../wallet/methods/getAssetIssuer.ts | 67 - src/components/wallet/methods/makePayment.ts | 107 - src/components/wallet/methods/popup.tsx | 33 - src/components/wallet/methods/setPrompt.ts | 24 - src/components/wallet/methods/signOut.ts | 16 - .../wallet/methods/switchNetworks.ts | 30 - src/components/wallet/methods/trustAsset.ts | 60 - .../wallet/methods/updateAccount.ts | 118 - .../wallet/methods/withdrawAsset.ts | 232 - src/components/wallet/readme.md | 34 - .../wallet/views/balanceDisplay.spec.tsx | 81 - .../wallet/views/balanceDisplay.tsx | 74 - .../wallet/views/claimableDisplay.tsx | 52 - .../wallet/views/collapsibleContainer.tsx | 27 - .../wallet/views/loggedInContent.tsx | 71 - .../wallet/views/loggedOutContent.tsx | 41 - src/components/wallet/views/readme.md | 31 - .../wallet/views/transactionSummary.tsx | 34 - src/components/wallet/views/walletButton.tsx | 11 - src/components/wallet/wallet.scss | 122 - src/components/wallet/wallet.ts | 115 - src/config/store.ts | 71 + src/constants/metricNames.ts | 3 + src/constants/settings.ts | 1 + src/ducks/account.ts | 239 + src/ducks/activeAsset.ts | 33 + src/ducks/allAssets.ts | 107 + src/ducks/assetOverrides.ts | 81 + src/ducks/claimAsset.ts | 119 + src/ducks/claimableBalances.ts | 110 + src/ducks/logs.ts | 39 + src/ducks/sendPayment.ts | 72 + src/ducks/sep24DepositAsset.ts | 195 + src/ducks/sep24WithdrawAsset.ts | 174 + src/ducks/sep31Send.ts | 330 + src/ducks/settings.ts | 27 + src/ducks/trustAsset.ts | 80 + src/ducks/untrustedAssets.ts | 146 + src/global/style.scss | 17 - src/helpers/Struct.ts | 47 + src/helpers/capitalizeString.ts | 4 + src/helpers/checkAssetExists.ts | 33 + src/helpers/getAssetData.ts | 40 + src/helpers/getAssetFromHomeDomain.ts | 102 + src/helpers/getAssetOverridesData.ts | 63 + src/helpers/getAssetSettingsFromToml.ts | 58 + src/helpers/getCurrenciesFromDomain.ts | 38 + src/helpers/getCurrentSessionParams.ts | 18 + src/helpers/getErrorMessage.ts | 2 + src/helpers/getErrorString.ts | 166 + src/helpers/getIssuerFromDomain.ts | 23 + src/helpers/getNetworkConfig.ts | 28 + src/helpers/getOverrideHomeDomain.ts | 29 + src/helpers/getUntrustedAssetData.ts | 95 + src/helpers/getValidatedUntrustedAsset.ts | 78 + src/helpers/isEmptyObject.ts | 1 + src/helpers/log.ts | 72 + src/helpers/metrics.ts | 64 + src/helpers/normalizeAssetProps.ts | 55 + src/helpers/sanitizeHtml.ts | 5 + src/helpers/searchKeyPairStringToArray.ts | 26 + src/helpers/searchParam.ts | 206 + src/helpers/shortenStellarKey.ts | 2 + src/helpers/updateAssetsInStore.ts | 17 + src/hooks/useRedux.ts | 30 + src/index.html | 69 - src/index.ts | 1 - src/index.tsx | 13 + src/methods/checkTomlForFields.ts | 58 + src/methods/claimClaimableBalance.ts | 85 + src/methods/createMemoFromType.ts | 31 + src/methods/getHomeDomainFromAssetIssuer.ts | 32 + src/methods/getToml.ts | 23 + src/methods/sep10Auth/index.ts | 9 + src/methods/sep10Auth/send.ts | 38 + src/methods/sep10Auth/sign.ts | 29 + src/methods/sep10Auth/start.ts | 46 + src/methods/sep24/checkInfo.ts | 31 + src/methods/sep24/createPopup.ts | 12 + src/methods/sep24/index.ts | 15 + src/methods/sep24/interactiveDepositFlow.ts | 61 + src/methods/sep24/interactiveWithdrawFlow.ts | 56 + src/methods/sep24/pollDepositUntilComplete.ts | 123 + .../sep24/pollWithdrawUntilComplete.ts | 174 + src/methods/sep31Send/checkInfo.ts | 115 + src/methods/sep31Send/getSep12Fields.ts | 109 + src/methods/sep31Send/index.ts | 17 + .../sep31Send/pollTransactionUntilComplete.ts | 100 + .../sep31Send/pollTransactionUntilReady.ts | 45 + src/methods/sep31Send/postTransaction.ts | 74 + src/methods/sep31Send/putSep12Fields.ts | 114 + src/methods/sep31Send/sendPayment.ts | 121 + src/methods/submitPaymentTransaction.ts | 126 + src/methods/trustAsset.ts | 77 + src/pages/Account.tsx | 53 + src/pages/Landing.tsx | 90 + src/pages/NotFound.tsx | 40 + src/react-app-env.d.ts | 1 + src/reportWebVitals.ts | 15 + src/services/error.ts | 5 - src/setupTests.ts | 5 + src/types/@modules.d.ts | 9 + src/types/types.d.ts | 322 + stencil.config.ts | 53 - tempLogFile.md | 29 + tsconfig.json | 28 +- yarn.lock | 10430 ++++++++++++++-- 212 files changed, 19668 insertions(+), 3772 deletions(-) create mode 100644 .dockerignore delete mode 100644 .editorconfig create mode 100644 .eslintrc.js delete mode 100644 .gitattributes delete mode 100644 .prettierignore delete mode 100644 .prettierrc delete mode 100644 .vscode/launch.json create mode 100644 Dockerfile create mode 100644 Makefile rename readme.md => README.md (56%) create mode 100644 architecture.md delete mode 100644 capacitor.config.json create mode 100644 nginx.conf create mode 100644 prettier.config.js create mode 100644 public/favicon.ico create mode 100644 public/images/doc-redux-state.png create mode 100644 public/index.html create mode 100644 public/logo192.png create mode 100644 public/logo512.png create mode 100644 public/manifest.json create mode 100644 public/robots.txt create mode 100644 src/App.scss create mode 100644 src/App.tsx create mode 100644 src/assets/icons/arrow-left.svg create mode 100644 src/assets/icons/arrow-right.svg create mode 100644 src/assets/icons/bubble.svg create mode 100644 src/assets/icons/edit.svg create mode 100644 src/assets/icons/error.svg delete mode 100644 src/components.d.ts create mode 100644 src/components/AccountInfo.tsx create mode 100644 src/components/AddAsset.tsx create mode 100644 src/components/Assets.tsx create mode 100644 src/components/Balance.tsx create mode 100644 src/components/BalanceRow.tsx create mode 100644 src/components/Banner/index.tsx create mode 100644 src/components/Banner/styles.scss create mode 100644 src/components/ClaimableBalance.tsx create mode 100644 src/components/ConfigurationModal.tsx create mode 100644 src/components/ConfirmAssetAction.tsx create mode 100644 src/components/ConnectAccount.tsx create mode 100644 src/components/CopyWithText.tsx create mode 100644 src/components/CopyWithTooltip/index.tsx create mode 100644 src/components/CopyWithTooltip/styles.scss create mode 100644 src/components/Footer.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/components/Heading/index.tsx create mode 100644 src/components/Heading/styles.scss create mode 100644 src/components/HomeDomainOverrideButtons.tsx create mode 100644 src/components/HomeDomainOverrideModal.tsx create mode 100644 src/components/IconButton/index.tsx create mode 100644 src/components/IconButton/styles.scss create mode 100644 src/components/InfoButtonWithTooltip/index.tsx create mode 100644 src/components/InfoButtonWithTooltip/styles.scss create mode 100644 src/components/Input/index.tsx create mode 100644 src/components/Input/styles.scss create mode 100644 src/components/LogItem/index.tsx create mode 100644 src/components/LogItem/styles.scss create mode 100644 src/components/Logs.tsx create mode 100644 src/components/Modal/index.tsx create mode 100644 src/components/Modal/styles.scss create mode 100644 src/components/PageContent.tsx create mode 100644 src/components/PrivateRoute.tsx create mode 100644 src/components/SendPayment.tsx create mode 100644 src/components/Sep31Send.tsx create mode 100644 src/components/SettingsHandler.tsx create mode 100644 src/components/SignOutModal.tsx create mode 100644 src/components/TextButton/index.tsx create mode 100644 src/components/TextButton/styles.scss create mode 100644 src/components/TextLink/index.tsx create mode 100644 src/components/TextLink/styles.scss create mode 100644 src/components/ToastBanner/index.tsx create mode 100644 src/components/ToastBanner/styles.scss create mode 100644 src/components/Toggle/index.tsx create mode 100644 src/components/Toggle/styles.scss create mode 100644 src/components/UntrustedBalance.tsx create mode 100644 src/components/WarningBanner.tsx delete mode 100644 src/components/jsonviewer/jsonviewer.scss delete mode 100644 src/components/jsonviewer/jsonviewer.tsx delete mode 100644 src/components/jsonviewer/readme.md delete mode 100644 src/components/loader/loader.scss delete mode 100644 src/components/loader/loader.tsx delete mode 100644 src/components/loader/readme.md delete mode 100644 src/components/logview/assets/arrow-left.svg delete mode 100644 src/components/logview/assets/arrow-right.svg delete mode 100644 src/components/logview/assets/error.svg delete mode 100644 src/components/logview/assets/message.svg delete mode 100644 src/components/logview/logview.scss delete mode 100644 src/components/logview/logview.tsx delete mode 100644 src/components/logview/readme.md delete mode 100644 src/components/prompt/prompt.scss delete mode 100644 src/components/prompt/prompt.tsx delete mode 100644 src/components/prompt/promptInput.tsx delete mode 100644 src/components/prompt/readme.md delete mode 100644 src/components/wallet/events/componentWillLoad.ts delete mode 100644 src/components/wallet/events/render.tsx delete mode 100644 src/components/wallet/methods/addAsset.ts delete mode 100644 src/components/wallet/methods/claimAsset.ts delete mode 100644 src/components/wallet/methods/copyAddress.ts delete mode 100644 src/components/wallet/methods/copySecret.ts delete mode 100644 src/components/wallet/methods/createAccount.ts delete mode 100644 src/components/wallet/methods/depositAsset.ts delete mode 100644 src/components/wallet/methods/getAssetIssuer.ts delete mode 100644 src/components/wallet/methods/makePayment.ts delete mode 100644 src/components/wallet/methods/popup.tsx delete mode 100644 src/components/wallet/methods/setPrompt.ts delete mode 100644 src/components/wallet/methods/signOut.ts delete mode 100644 src/components/wallet/methods/switchNetworks.ts delete mode 100644 src/components/wallet/methods/trustAsset.ts delete mode 100644 src/components/wallet/methods/updateAccount.ts delete mode 100644 src/components/wallet/methods/withdrawAsset.ts delete mode 100644 src/components/wallet/readme.md delete mode 100644 src/components/wallet/views/balanceDisplay.spec.tsx delete mode 100644 src/components/wallet/views/balanceDisplay.tsx delete mode 100644 src/components/wallet/views/claimableDisplay.tsx delete mode 100644 src/components/wallet/views/collapsibleContainer.tsx delete mode 100644 src/components/wallet/views/loggedInContent.tsx delete mode 100644 src/components/wallet/views/loggedOutContent.tsx delete mode 100644 src/components/wallet/views/readme.md delete mode 100644 src/components/wallet/views/transactionSummary.tsx delete mode 100644 src/components/wallet/views/walletButton.tsx delete mode 100644 src/components/wallet/wallet.scss delete mode 100644 src/components/wallet/wallet.ts create mode 100644 src/config/store.ts create mode 100644 src/constants/metricNames.ts create mode 100644 src/constants/settings.ts create mode 100644 src/ducks/account.ts create mode 100644 src/ducks/activeAsset.ts create mode 100644 src/ducks/allAssets.ts create mode 100644 src/ducks/assetOverrides.ts create mode 100644 src/ducks/claimAsset.ts create mode 100644 src/ducks/claimableBalances.ts create mode 100644 src/ducks/logs.ts create mode 100644 src/ducks/sendPayment.ts create mode 100644 src/ducks/sep24DepositAsset.ts create mode 100644 src/ducks/sep24WithdrawAsset.ts create mode 100644 src/ducks/sep31Send.ts create mode 100644 src/ducks/settings.ts create mode 100644 src/ducks/trustAsset.ts create mode 100644 src/ducks/untrustedAssets.ts delete mode 100644 src/global/style.scss create mode 100644 src/helpers/Struct.ts create mode 100644 src/helpers/capitalizeString.ts create mode 100644 src/helpers/checkAssetExists.ts create mode 100644 src/helpers/getAssetData.ts create mode 100644 src/helpers/getAssetFromHomeDomain.ts create mode 100644 src/helpers/getAssetOverridesData.ts create mode 100644 src/helpers/getAssetSettingsFromToml.ts create mode 100644 src/helpers/getCurrenciesFromDomain.ts create mode 100644 src/helpers/getCurrentSessionParams.ts create mode 100644 src/helpers/getErrorMessage.ts create mode 100644 src/helpers/getErrorString.ts create mode 100644 src/helpers/getIssuerFromDomain.ts create mode 100644 src/helpers/getNetworkConfig.ts create mode 100644 src/helpers/getOverrideHomeDomain.ts create mode 100644 src/helpers/getUntrustedAssetData.ts create mode 100644 src/helpers/getValidatedUntrustedAsset.ts create mode 100644 src/helpers/isEmptyObject.ts create mode 100644 src/helpers/log.ts create mode 100644 src/helpers/metrics.ts create mode 100644 src/helpers/normalizeAssetProps.ts create mode 100644 src/helpers/sanitizeHtml.ts create mode 100644 src/helpers/searchKeyPairStringToArray.ts create mode 100644 src/helpers/searchParam.ts create mode 100644 src/helpers/shortenStellarKey.ts create mode 100644 src/helpers/updateAssetsInStore.ts create mode 100644 src/hooks/useRedux.ts delete mode 100644 src/index.html delete mode 100644 src/index.ts create mode 100644 src/index.tsx create mode 100644 src/methods/checkTomlForFields.ts create mode 100644 src/methods/claimClaimableBalance.ts create mode 100644 src/methods/createMemoFromType.ts create mode 100644 src/methods/getHomeDomainFromAssetIssuer.ts create mode 100644 src/methods/getToml.ts create mode 100644 src/methods/sep10Auth/index.ts create mode 100644 src/methods/sep10Auth/send.ts create mode 100644 src/methods/sep10Auth/sign.ts create mode 100644 src/methods/sep10Auth/start.ts create mode 100644 src/methods/sep24/checkInfo.ts create mode 100644 src/methods/sep24/createPopup.ts create mode 100644 src/methods/sep24/index.ts create mode 100644 src/methods/sep24/interactiveDepositFlow.ts create mode 100644 src/methods/sep24/interactiveWithdrawFlow.ts create mode 100644 src/methods/sep24/pollDepositUntilComplete.ts create mode 100644 src/methods/sep24/pollWithdrawUntilComplete.ts create mode 100644 src/methods/sep31Send/checkInfo.ts create mode 100644 src/methods/sep31Send/getSep12Fields.ts create mode 100644 src/methods/sep31Send/index.ts create mode 100644 src/methods/sep31Send/pollTransactionUntilComplete.ts create mode 100644 src/methods/sep31Send/pollTransactionUntilReady.ts create mode 100644 src/methods/sep31Send/postTransaction.ts create mode 100644 src/methods/sep31Send/putSep12Fields.ts create mode 100644 src/methods/sep31Send/sendPayment.ts create mode 100644 src/methods/submitPaymentTransaction.ts create mode 100644 src/methods/trustAsset.ts create mode 100644 src/pages/Account.tsx create mode 100644 src/pages/Landing.tsx create mode 100644 src/pages/NotFound.tsx create mode 100644 src/react-app-env.d.ts create mode 100644 src/reportWebVitals.ts delete mode 100644 src/services/error.ts create mode 100644 src/setupTests.ts create mode 100644 src/types/@modules.d.ts create mode 100644 src/types/types.d.ts delete mode 100644 stencil.config.ts create mode 100644 tempLogFile.md diff --git a/.circleci/config.yml b/.circleci/config.yml index af6ce700..f1df1fc0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,14 +6,14 @@ orbs: jobs: test: docker: - - image: 'circleci/node:14' + - image: "circleci/node:14" steps: - checkout - node/install-packages: pkg-manager: yarn - - run: - command: yarn run test - name: Run YARN tests + # - run: + # command: yarn run test + # name: Run YARN tests workflows: test_demo_wallet: diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..9e051592 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +dist +.tmp diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index f1cc3ad3..00000000 --- a/.editorconfig +++ /dev/null @@ -1,15 +0,0 @@ -# http://editorconfig.org - -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true - -[*.md] -insert_final_newline = false -trim_trailing_whitespace = false diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..2a050c77 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + extends: ["@stellar/eslint-config"], + rules: { + "no-console": 0, + "import/no-unresolved": "off", + "react/jsx-filename-extension": [1, { extensions: [".tsx", ".jsx"] }], + "react/prop-types": 0, + // note you must disable the base rule as it can report incorrect errors + "no-shadow": "off", + "@typescript-eslint/no-shadow": ["error"], + // note you must disable the base rule as it can report incorrect errors + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error"], + }, +}; diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index dfe07704..00000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/.gitignore b/.gitignore index 7ff92e2b..d7b8f1bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,25 @@ -dist/ -www/ -loader/ -!src/components/loader +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -*~ -*.sw[mnpcod] -*.log -*.lock -*.tmp -*.tmp.* -log.txt -*.sublime-project -*.sublime-workspace +# dependencies +/node_modules +/.pnp +.pnp.js -.stencil/ -.idea/ -.sass-cache/ -.versions/ -node_modules/ -$RECYCLE.BIN/ +# testing +/coverage +# production +/build + +# misc .DS_Store -Thumbs.db -UserInterfaceState.xcuserstate -.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* -.now +.eslintcache diff --git a/.npmrc b/.npmrc index 43c97e71..b6f27f13 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -package-lock=false +engine-strict=true diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index f704e02f..00000000 --- a/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -src/components.d.ts -readme.md \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index fc5c0886..00000000 --- a/.prettierrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "trailingComma": "es5", - "tabWidth": 2, - "useTabs": false, - "semi": false, - "singleQuote": true, - "bracketSpacing": true, - "arrowParens": "always" -} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 8185c436..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "E2E Test Current File", - "cwd": "${workspaceFolder}", - "program": "${workspaceFolder}/node_modules/.bin/stencil", - "args": ["test", "--e2e", "${relativeFile}"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true - }, - { - "type": "node", - "request": "launch", - "name": "Spec Test Current File", - "cwd": "${workspaceFolder}", - "program": "${workspaceFolder}/node_modules/.bin/stencil", - "args": ["test", "--spec", "${relativeFile}"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true - } - ] -} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..82f573b9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +FROM ubuntu:20.04 as build + +MAINTAINER SDF Ops Team + +RUN mkdir -p /app +RUN apt-get update && apt-get install -y gnupg1 + +WORKDIR /app + +ARG REACT_APP_AMPLITUDE_KEY + +ENV REACT_APP_AMPLITUDE_KEY $REACT_APP_AMPLITUDE_KEY + +ARG REACT_APP_SENTRY_KEY + +ENV REACT_APP_SENTRY_KEY $REACT_APP_SENTRY_KEY + +RUN apt-get update && apt-get install -y gnupg curl git make apt-transport-https && \ + curl -sSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \ + echo "deb https://deb.nodesource.com/node_14.x focal main" | tee /etc/apt/sources.list.d/nodesource.list && \ + curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ + echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ + apt-get update && apt-get install -y nodejs yarn && apt-get clean + + +COPY . /app/ +RUN yarn install +RUN yarn build + +FROM nginx:1.17 + +COPY --from=build /app/build/ /usr/share/nginx/html/ +COPY --from=build /app/nginx.conf /etc/nginx/conf.d/default.conf diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..d334feb1 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +# Check if we need to prepend docker commands with sudo +SUDO := $(shell docker version >/dev/null 2>&1 || echo "sudo") + +# If LABEL is not provided set default value +LABEL ?= $(shell git rev-parse --short HEAD)$(and $(shell git status -s),-dirty-$(shell id -u -n)) +# If TAG is not provided set default value +TAG ?= stellar/stellar-demo-wallet:$(LABEL) +# https://github.com/opencontainers/image-spec/blob/master/annotations.md +BUILD_DATE := $(shell date --utc --rfc-3339=seconds) + +docker-build: + $(SUDO) docker build --pull --label org.opencontainers.image.created="$(BUILD_DATE)" \ + --build-arg REACT_APP_AMPLITUDE_KEY=$(AMPLITUDE_KEY) --build-arg REACT_APP_SENTRY_KEY=$(SENTRY_KEY) -t $(TAG) . + +docker-push: + $(SUDO) docker push $(TAG) diff --git a/readme.md b/README.md similarity index 56% rename from readme.md rename to README.md index 2f4e948f..3456a13c 100644 --- a/readme.md +++ b/README.md @@ -1,8 +1,17 @@ # Stellar Demo Wallet -This Stellar Demo Wallet app will soon be the defacto application to use when testing anchor services interactively. If you would like to automate testing of your anchor service, check out the SDF's [anchor validation suite](https://github.com/stellar/transfer-server-validator) viewable at [anchor-validator.stellar.org](https://anchor-validator.stellar.org/). - -This project was originally created for the [Build a Stellar Wallet](https://developers.stellar.org/docs/building-apps/) tutorial series. ([That repo has since moved over here](https://github.com/stellar/docs-wallet)). If you want to use part or all of the project to kickstart your own wallet, feel free to clone or copy any pieces which may be helpful. +This Stellar Demo Wallet app will soon be the defacto application to use when +testing anchor services interactively. If you would live to automate testing of +your anchor service, check out the SDF's +[anchor validation suite](https://github.com/stellar/transfer-server-validator) +viewable at [anchor-validator.stellar.org](anchor-validator.stellar.org). + +This project was originally created for the +[Build a Stellar Wallet](https://developers.stellar.org/docs/building-apps/) +tutorial series. +([That repo has since moved over here](https://github.com/stellar/docs-wallet)). +If you want to use parts or all of the project to kickstart your own wallet, +feel free to clone or copy any pieces which may be helpful. ## Getting Started @@ -37,7 +46,7 @@ yarn build - [ ] Implement SEP-31 support - [ ] Implement SEP-6 support -### Helpful links: +### Helpful links - [https://www.stellar.org/developers](https://www.stellar.org/developers) - [https://stellar.github.io/js-stellar-sdk/](https://stellar.github.io/js-stellar-sdk/) diff --git a/architecture.md b/architecture.md new file mode 100644 index 00000000..93048224 --- /dev/null +++ b/architecture.md @@ -0,0 +1,81 @@ +# Architecture + +This document describes the high-level architecture and file structure of the +Demo Wallet. + +## Tech stack + +- [TypeScript](https://www.typescriptlang.org/) +- [React](https://reactjs.org/) for UI +- [React Router](https://reactrouter.com/web/guides/quick-start) for routing +- [Sass](https://sass-lang.com/) for CSS styling +- [Stellar Design System](https://github.com/stellar/stellar-design-system) for + re-usable components and styles +- [Redux Toolkit](https://redux-toolkit.js.org/) for global state management +- [Yarn](https://yarnpkg.com/) for package management + +## File structure + +### `index.tsx` + +Root file of the project + +### `App.tsx` + +Top level file/entry point of the app + +### `App.scss` + +Global file of styles + +### `/assets` + +Images and SVGs (icons) + +### `/components` + +Building blocks of the UI. Larger components go into their own directory. + +If a component requires its own styles (and it doesn't belong in the global +`App.scss` file), it will have its own directory also. + +Re-usable components and styles will come from the Stellar Design System. + +### `/config` + +App configuration. Redux store root lives there (`store.ts`). + +### `/constants` + +For various constants used in the app + +### `/ducks` + +Every file in the `/ducks` directory is a reducer in the Redux state (must be +added to the root `config/store.ts`). Inside every reducer are dispatch actions +to update the state (we follow Redux Toolkit conventions). + +![Red banner: You are using PUBLIC network in DEVELOPMENT](public/images/doc-redux-state.png) +_Redux state illustration_ + +### `/helpers` + +Smaller, more generic helper functions + +### `/hooks` + +Custom hooks + +### `/methods` + +Methods are more specific than helper functions. SEPs go into their own +directory (for example, `sep10Auth/`, `sep31Send/`). + +### `/pages` + +Higher level components that match the route (for example, `Landing.tsx`, +`Account.tsx`) + +### `/types` + +TypeScript types diff --git a/capacitor.config.json b/capacitor.config.json deleted file mode 100644 index 4e285bf3..00000000 --- a/capacitor.config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "appId": "com.wallet.stellar", - "appName": "Stellar Wallet", - "bundledWebRuntime": false, - "npmClient": "npm", - "webDir": "www", - "cordova": {} -} diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 00000000..2c513fb3 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name localhost; + + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_types application/javascript application/rss+xml application/vnd.ms-fontobject application/x-font application/x-font-opentype application/x-font-otf application/x-font-truetype application/x-font-ttf application/x-javascript application/xhtml+xml application/xml font/opentype font/otf font/ttf image/svg+xml image/x-icon text/css text/javascript text/plain text/xml; + + location / { + root /usr/share/nginx/html; + try_files $uri /index.html index.htm; + gzip_static on; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/package.json b/package.json index 3bfd55af..06676d82 100644 --- a/package.json +++ b/package.json @@ -1,61 +1,103 @@ { "name": "stellar-demo-wallet", - "version": "0.0.47", + "version": "0.1.0", "description": "Stellar Demo Wallet", "repository": "https://github.com/stellar/stellar-demo-wallet", "license": "Apache-2.0", - "main": "dist/index.cjs.js", - "module": "./dist/index.js", - "es2015": "./dist/esm/index.mjs", - "es2017": "./dist/esm/index.mjs", - "types": "dist/types/index.d.ts", - "collection": "dist/collection/collection-manifest.json", - "collection:main": "dist/collection/index.js", - "unpkg": "dist/stellar-demo-wallet/stellar-demo-wallet.js", - "files": [ - "dist/", - "loader/" - ], - "scripts": { - "prepare": "yarn build", - "build": "stencil build --docs", - "start": "stencil build --dev --watch --serve", - "generate": "stencil generate", - "deploy": "npm publish ; vercel --prod --confirm --name stellar-demo-wallet", - "test": "stencil test --spec", - "test.watch": "stencil test --spec --watch", - "test.e2e": "stencil test --e2e" - }, - "devDependencies": { - "@stencil/core": "^2.3.0", - "@stencil/postcss": "^2.0.0", - "@stencil/sass": "^1.3.2", - "@types/autoprefixer": "^9.7.2", - "@types/jest": "26.0.15", - "@types/node": "^14.14.20", - "@types/puppeteer": "5.4.0", - "autoprefixer": "^9.8.6", - "copy-to-clipboard": "^3.3.1", - "husky": "^4.3.0", - "jest": "26.6.3", - "jest-cli": "26.6.3", - "js-combinatorics": "^0.6.1", - "lodash": "^4.17.20", - "lodash-es": "^4.17.15", - "prettier": "^2.1.2", - "pretty-quick": "^2.0.2", - "puppeteer": "5.4.1", - "rollup-plugin-node-polyfills": "^0.2.1", - "stellar-sdk": "^6.2.0", - "ts-jest": "^26.4.4", - "typescript": "^4.1.2" + "engines": { + "node": "14.x" }, "husky": { "hooks": { - "pre-commit": "pretty-quick --staged" + "pre-commit": "concurrently 'pretty-quick --staged' 'lint-staged' 'tsc --noEmit'", + "post-merge": "yarn install-if-package-changed" } }, + "lint-staged": { + "src/**/*.ts?(x)": [ + "eslint --fix --max-warnings 0" + ] + }, "dependencies": { - "toml": "^3.0.0" + "@reduxjs/toolkit": "^1.5.0", + "@sentry/browser": "^6.2.5", + "@sentry/tracing": "^6.2.5", + "@stellar/design-system": "^0.1.0-alpha.3", + "@stellar/prettier-config": "^1.0.1", + "@stellar/wallet-sdk": "^0.3.0-rc.4", + "@testing-library/jest-dom": "^5.11.9", + "@testing-library/react": "^11.2.3", + "@testing-library/user-event": "^12.6.0", + "@types/jest": "^26.0.20", + "@types/node": "^14.14.20", + "@types/react": "^17.0.0", + "@types/react-copy-to-clipboard": "^5.0.0", + "@types/react-dom": "^17.0.0", + "@types/react-redux": "^7.1.15", + "@types/react-router-dom": "^5.1.7", + "bignumber.js": "^9.0.1", + "crypto": "^1.0.1", + "dompurify": "^2.2.7", + "html-react-parser": "^1.2.4", + "lodash": "^4.17.19", + "marked": "^2.0.1", + "node-sass": "^4.14.1", + "react": "^17.0.1", + "react-copy-to-clipboard": "^5.0.3", + "react-dom": "^17.0.1", + "react-json-view": "^1.21.1", + "react-redux": "^7.2.2", + "react-router-dom": "^5.2.0", + "react-scripts": "4.0.1", + "redux": "^4.0.5", + "stellar-sdk": "^8.0.0", + "styled-components": "^5.2.1", + "toml": "^3.0.0", + "typescript": "~4.1.3", + "web-vitals": "^0.2.4" + }, + "scripts": { + "install-if-package-changed": "git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD | grep --quiet yarn.lock && yarn install || exit 0", + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "prod:build": "docker image build --build-arg REACT_APP_AMPLITUDE_KEY=$AMPLITUDE_KEY --build-arg REACT_APP_SENTRY_KEY=$SENTRY_KEY -t stellar-demo-wallet:localbuild .", + "prod:serve": "docker run -p 8000:80 stellar-demo-wallet:localbuild", + "production": "yarn prod:build && yarn prod:serve" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@stellar/eslint-config": "^1.0.5", + "@stellar/tsconfig": "^1.0.2", + "@types/lodash": "^4.14.167", + "@types/marked": "^2.0.0", + "@types/redux": "^3.6.0", + "@types/styled-components": "^5.1.7", + "concurrently": "^5.3.0", + "eslint": "^7.17.0", + "eslint-config-prettier": "^7.1.0", + "eslint-config-react": "^1.1.7", + "husky": "^4.3.7", + "lint-staged": "^10.5.3", + "prettier": "^2.2.1", + "pretty-quick": "^3.1.0" } } diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 00000000..4b355cb3 --- /dev/null +++ b/prettier.config.js @@ -0,0 +1 @@ +module.exports = require("@stellar/prettier-config"); diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4f17c94a834735ea04cf72bff9589bf3f0a8bbcd GIT binary patch literal 7406 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 literal 0 HcmV?d00001 diff --git a/public/images/doc-redux-state.png b/public/images/doc-redux-state.png new file mode 100644 index 0000000000000000000000000000000000000000..bf6a330e151238ea35602d58db49b57ef463fea9 GIT binary patch literal 21903 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 T6*s + + + + + + + + + + + Demo Wallet - Stellar + + + +
+ + diff --git a/public/logo192.png b/public/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..476ff8d02ec31c87b274f5e06efa3d8df9d56b9e GIT binary patch literal 3843 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 bHzj~oZVvw)4Y`!F6o8(#k=APs`-uMm+|?z~ literal 0 HcmV?d00001 diff --git a/public/logo512.png b/public/logo512.png new file mode 100644 index 0000000000000000000000000000000000000000..41427a2f0ee6dae7d1a19bde8b4699badc7022e9 GIT binary patch literal 11240 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 XUl6;W%L%0-RRF4rnhKS2AA|o7lzflw literal 0 HcmV?d00001 diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 00000000..3c422014 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "Account Viewer", + "name": "Stellar Account Viewer", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/src/App.scss b/src/App.scss new file mode 100644 index 00000000..81ca3684 --- /dev/null +++ b/src/App.scss @@ -0,0 +1,441 @@ +:root { + --size-min-window: 800px; + --size-max-window: 1296px; +} + +body { + overflow-y: hidden; +} + +p { + word-break: break-word; +} + +code { + // TODO: color not in SDS + color: #490be3; + font-family: var(--font-family-monospace); + border-radius: 0.1875rem; + border: 0.5px solid var(--color-background-off); + background-color: var(--color-background-secondary); + padding: 0 0.25rem; + font-size: 0.875rem; + line-height: 1.5rem; + font-weight: var(--font-weight-medium); + display: inline; + line-break: anywhere; +} + +@mixin header-footer-inset { + height: 3rem; + display: flex; + align-items: center; +} + +@mixin header-footer-vertical-padding { + padding-top: 1rem; +} + +.Wrapper { + min-width: var(--size-min-window); + display: flex; + flex-grow: 1; + position: relative; +} + +.SplitContainer { + width: 50%; + position: relative; +} + +.ContentWrapper { + display: flex; + flex-direction: column; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + overflow: auto; +} + +.Announcement { + background-color: var(--color-note); + color: var(--color-text-contrast); + text-align: center; + width: 100%; + margin-bottom: 2rem; + padding: 1rem 0; //vertical padding + // change anchor text to reflect same color as the rest of the announcment + .TextLink { + color: inherit; + } +} + +.Main { + background-color: var(--color-background-main); +} + +.Inset { + position: relative; + margin: 0 auto; + padding-left: 1rem; + padding-right: 1rem; + width: 100%; + max-width: var(--size-max-window); +} + +.Header { + @include header-footer-vertical-padding; + padding-bottom: 1rem; + + .Inset { + @include header-footer-inset; + justify-content: space-between; + } +} + +.Footer { + @include header-footer-vertical-padding; + + .Inset { + @include header-footer-inset; + justify-content: space-between; + + a:not(:last-child) { + margin-right: 1.5rem; + } + } +} + +.IntroText { + margin-bottom: 3rem; +} + +.LandingButtons { + button { + text-align: left; + } +} + +.Inline { + display: flex; + align-items: center; + position: relative; + + & > *:not(:last-child) { + margin-right: 1rem; + } +} + +.LoadingBlock { + margin-bottom: 1rem; +} + +.InfoButtonWrapper { + position: relative; + display: flex; + align-items: center; +} + +.Section { + margin-top: 3rem; +} + +// Generic +.error { + color: var(--color-error); +} + +.success { + color: var(--color-success); +} + +.vertical-spacing { + & > * { + margin-bottom: 1rem; + } +} + +.horizontal-spacing { + & > *:not(:first-child), + & > *:not(:last-child) { + margin-right: 1.5rem; + } +} + +// Account +.Account { + display: flex; + flex-direction: column; + margin-bottom: 1rem; + + .AccountInfo { + display: table; + max-width: 276px; + + &:first-child { + margin-bottom: 1rem; + margin-right: 2rem; + } + + .AccountInfoRow { + display: table-row; + height: 2rem; + } + + .AccountInfoCell { + display: table-cell; + vertical-align: middle; + + button { + padding-top: 0.1rem; + padding-bottom: 0.1rem; + text-align: left; + } + + &.AccountLabel { + text-transform: uppercase; + } + + &.CopyButton { + width: 3.75rem; + } + + &:not(:last-child) { + padding-right: 1rem; + } + } + } + + @media (min-width: 1020px) { + flex-direction: row; + + .AccountInfo { + &:first-child { + margin-bottom: 0; + } + } + } +} + +.AccountDetails { + .AccountDetailsContent { + padding: 1rem; + font-weight: var(--font-weight-light); + font-family: var(--font-family-monospace); + font-size: 0.875rem; + line-height: 1.375rem; + word-break: break-word; + } +} + +.Balances { + border-top: 1px solid var(--color-border-main); + + .BalanceRow { + border-bottom: 1px solid var(--color-border-main); + padding-top: 1rem; + padding-bottom: 1rem; + + &.disabled { + background-color: var(--color-background-off); + cursor: not-allowed; + + .BalanceCell.BalanceInfo { + opacity: 0.6; + } + + a { + pointer-events: none; + } + } + + &.active { + background-color: #dfd8ff; + } + } + + .BalanceCell { + position: relative; + + &:not(:last-child) { + margin-bottom: 1rem; + } + + & > :not(:last-child) { + margin-bottom: 0.5rem; + } + + &.BalanceInfo { + .BalanceAmount { + font-size: 1.1em; + font-weight: var(--font-weight-medium); + + &.error { + font-weight: var(--font-weight-normal); + } + } + + .BalanceOptions { + margin-top: 0.3rem; + display: flex; + align-items: center; + position: relative; + + & > *:not(:last-child) { + margin-right: 0.5rem; + } + } + } + + &.BalanceActions { + .BalanceCellSelect { + display: flex; + align-items: center; + + & > :first-child { + margin-right: 0.5rem; + } + } + + .CustomCell { + display: flex; + align-items: center; + } + } + } + + @media (min-width: 1260px) { + .BalanceRow { + padding-top: 0.7rem; + padding-bottom: 0.7rem; + display: flex; + align-items: center; + justify-content: space-between; + } + + .BalanceCell { + &:not(:last-child) { + margin-bottom: 0; + } + + & > :not(:last-child) { + margin-bottom: 0; + } + + &.BalanceActions { + display: flex; + + .BalanceCellSelect { + width: 14rem; + flex-grow: 0; + flex-shrink: 0; + } + + .CustomCell { + &:not(:last-child) { + margin-right: 1rem; + } + + justify-content: flex-end; + } + } + } + } +} + +.ClaimableBalances { + margin-top: 3rem; +} + +.BalancesButtons { + margin-top: 1.5rem; +} + +// Logs +.Logs { + background-color: var(--color-background-off); + overflow: auto; + display: flex; + flex-direction: column; + + .LogsWrapper { + position: relative; + flex-grow: 1; + } + + .ContentWrapper { + flex-direction: column-reverse; + } + + .LogsFooter { + background-color: var(--color-background-main); + border: 1px solid var(--color-border-main); + + .Inset { + @include header-footer-inset; + + a:not(:last-child) { + margin-right: 1.5rem; + } + } + } + + .LogsContent { + margin-top: 1rem; + margin-bottom: 1rem; + } + + .EmptyLogsContent { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 1rem; + color: var(--color-note); + } + + .LogItem { + margin-bottom: 1rem; + } +} + +.SessionParamsWrapper { + display: flex; + margin-top: -0.5rem; +} + +// Configuration +.ConfigurationItem { + display: flex; + align-items: center; + justify-content: space-between; +} + +// Old styles + +.Content { + flex-grow: 1; + flex-shrink: 0; +} + +.Block { + & > * { + margin-bottom: 1rem; + } +} + +.SendForm { + max-width: 600px; + margin-bottom: 1rem; +} + +.SendFormButtons { + display: flex; + align-items: center; + + & > * { + margin-right: 1rem; + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 00000000..60e292b2 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,89 @@ +import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; +import { Provider } from "react-redux"; +import * as Sentry from "@sentry/browser"; +import { Integrations } from "@sentry/tracing"; + +import { store } from "config/store"; +import { Header } from "components/Header"; +import { Footer } from "components/Footer"; +import { Logs } from "components/Logs"; +import { PageContent } from "components/PageContent"; +import { PrivateRoute } from "components/PrivateRoute"; +import { SettingsHandler } from "components/SettingsHandler"; +import { WarningBanner } from "components/WarningBanner"; +import { TextLink, TextLinkVariant } from "@stellar/design-system"; + +import { Account } from "pages/Account"; +import { Landing } from "pages/Landing"; +import { NotFound } from "pages/NotFound"; +import "./App.scss"; + +if (process.env.REACT_APP_SENTRY_KEY) { + Sentry.init({ + dsn: process.env.REACT_APP_SENTRY_KEY, + release: `demo-wallet@${process.env.npm_package_version}`, + integrations: [new Integrations.BrowserTracing()], + tracesSampleRate: 1.0, + }); +} + +export const App = () => ( + + + + + +
+
+
+
+ +
+
+

+ Welcome to the new and improved Stellar demo wallet! Please + log bugs and feature requests at:   + + https://github.com/stellar/stellar-demo-wallet/issues + +

+
+
+ +
+

+ This demo wallet lets financial application developers test + their integrations and learn how Stellar ecosystem protocols + (SEPs) work. +

+
+ + + + + + + + + + + + + + + +
+
+
+ + +
+
+
+
+); diff --git a/src/assets/icons/arrow-left.svg b/src/assets/icons/arrow-left.svg new file mode 100644 index 00000000..d839f8de --- /dev/null +++ b/src/assets/icons/arrow-left.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/icons/arrow-right.svg b/src/assets/icons/arrow-right.svg new file mode 100644 index 00000000..879fdc34 --- /dev/null +++ b/src/assets/icons/arrow-right.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/icons/bubble.svg b/src/assets/icons/bubble.svg new file mode 100644 index 00000000..51e81062 --- /dev/null +++ b/src/assets/icons/bubble.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/icons/edit.svg b/src/assets/icons/edit.svg new file mode 100644 index 00000000..182fd13c --- /dev/null +++ b/src/assets/icons/edit.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/icons/error.svg b/src/assets/icons/error.svg new file mode 100644 index 00000000..879952ed --- /dev/null +++ b/src/assets/icons/error.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/components.d.ts b/src/components.d.ts deleted file mode 100644 index 7cb527e9..00000000 --- a/src/components.d.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* eslint-disable */ -/* tslint:disable */ -/** - * This is an autogenerated file created by the Stencil compiler. - * It contains typing information for all components that exist in this project. - */ -import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; -import { Prompter } from "./components/prompt/prompt"; -import { Server } from "stellar-sdk"; -import { ILogger } from "./components/logview/logview"; -export namespace Components { - interface CollapsibleContainer { - "hideText": string; - "showText": string; - } - interface JsonViewer { - "data": any; - } - interface LogView { - "error": (title: string, body?: string) => Promise; - "instruction": (title: string, body?: string) => Promise; - "request": (url: string, body?: string | object) => Promise; - "response": (url: string, body?: string | object) => Promise; - } - interface StellarLoader { - "interval": any; - } - interface StellarPrompt { - "prompter": Prompter; - } - interface StellarWallet { - "logger": ILogger; - "network_passphrase": string; - "server": Server; - } -} -declare global { - interface HTMLCollapsibleContainerElement extends Components.CollapsibleContainer, HTMLStencilElement { - } - var HTMLCollapsibleContainerElement: { - prototype: HTMLCollapsibleContainerElement; - new (): HTMLCollapsibleContainerElement; - }; - interface HTMLJsonViewerElement extends Components.JsonViewer, HTMLStencilElement { - } - var HTMLJsonViewerElement: { - prototype: HTMLJsonViewerElement; - new (): HTMLJsonViewerElement; - }; - interface HTMLLogViewElement extends Components.LogView, HTMLStencilElement { - } - var HTMLLogViewElement: { - prototype: HTMLLogViewElement; - new (): HTMLLogViewElement; - }; - interface HTMLStellarLoaderElement extends Components.StellarLoader, HTMLStencilElement { - } - var HTMLStellarLoaderElement: { - prototype: HTMLStellarLoaderElement; - new (): HTMLStellarLoaderElement; - }; - interface HTMLStellarPromptElement extends Components.StellarPrompt, HTMLStencilElement { - } - var HTMLStellarPromptElement: { - prototype: HTMLStellarPromptElement; - new (): HTMLStellarPromptElement; - }; - interface HTMLStellarWalletElement extends Components.StellarWallet, HTMLStencilElement { - } - var HTMLStellarWalletElement: { - prototype: HTMLStellarWalletElement; - new (): HTMLStellarWalletElement; - }; - interface HTMLElementTagNameMap { - "collapsible-container": HTMLCollapsibleContainerElement; - "json-viewer": HTMLJsonViewerElement; - "log-view": HTMLLogViewElement; - "stellar-loader": HTMLStellarLoaderElement; - "stellar-prompt": HTMLStellarPromptElement; - "stellar-wallet": HTMLStellarWalletElement; - } -} -declare namespace LocalJSX { - interface CollapsibleContainer { - "hideText"?: string; - "showText"?: string; - } - interface JsonViewer { - "data"?: any; - } - interface LogView { - } - interface StellarLoader { - "interval"?: any; - } - interface StellarPrompt { - "prompter"?: Prompter; - } - interface StellarWallet { - "logger"?: ILogger; - "network_passphrase"?: string; - "server"?: Server; - } - interface IntrinsicElements { - "collapsible-container": CollapsibleContainer; - "json-viewer": JsonViewer; - "log-view": LogView; - "stellar-loader": StellarLoader; - "stellar-prompt": StellarPrompt; - "stellar-wallet": StellarWallet; - } -} -export { LocalJSX as JSX }; -declare module "@stencil/core" { - export namespace JSX { - interface IntrinsicElements { - "collapsible-container": LocalJSX.CollapsibleContainer & JSXBase.HTMLAttributes; - "json-viewer": LocalJSX.JsonViewer & JSXBase.HTMLAttributes; - "log-view": LocalJSX.LogView & JSXBase.HTMLAttributes; - "stellar-loader": LocalJSX.StellarLoader & JSXBase.HTMLAttributes; - "stellar-prompt": LocalJSX.StellarPrompt & JSXBase.HTMLAttributes; - "stellar-wallet": LocalJSX.StellarWallet & JSXBase.HTMLAttributes; - } - } -} diff --git a/src/components/AccountInfo.tsx b/src/components/AccountInfo.tsx new file mode 100644 index 00000000..544fefc5 --- /dev/null +++ b/src/components/AccountInfo.tsx @@ -0,0 +1,172 @@ +import { useCallback, useState } from "react"; +import { useDispatch } from "react-redux"; +import { Heading2, Loader } from "@stellar/design-system"; +import { TextButton } from "components/TextButton"; +import { TextLink } from "components/TextLink"; +import ReactJson from "react-json-view"; + +import { CopyWithText } from "components/CopyWithText"; +import { ToastBanner } from "components/ToastBanner"; + +import { fetchAccountAction, fundTestnetAccount } from "ducks/account"; +import { fetchClaimableBalancesAction } from "ducks/claimableBalances"; + +import { shortenStellarKey } from "helpers/shortenStellarKey"; +import { useRedux } from "hooks/useRedux"; +import { ActionStatus } from "types/types.d"; + +export const AccountInfo = () => { + const { account } = useRedux("account"); + const [isAccountDetailsVisible, setIsAccountDetailsVisible] = useState(false); + + const dispatch = useDispatch(); + + const handleRefreshAccount = useCallback(() => { + if (account.data?.id) { + dispatch( + fetchAccountAction({ + publicKey: account.data.id, + secretKey: account.secretKey, + }), + ); + dispatch(fetchClaimableBalancesAction({ publicKey: account.data.id })); + } + }, [account.data?.id, account.secretKey, dispatch]); + + const handleCreateAccount = () => { + if (account.data?.id) { + dispatch(fundTestnetAccount(account.data.id)); + } + }; + + if (!account.data?.id) { + return null; + } + + return ( +
+
+ {/* Account keys */} +
+
+
Public
+
+ {shortenStellarKey(account.data.id)} +
+
+ +
+
+
+
Secret
+
+ {shortenStellarKey(account.secretKey)} +
+
+ +
+
+
+ + {/* Account actions */} +
+
+
+ {account.isUnfunded && ( +
+ + Clicking create will fund your test account with XLM. If + you’re testing SEP-24 you may want to leave this account + unfunded.{" "} + + Learn more + + + } + > + Create account + +
+ )} + + {!account.isUnfunded && ( + + setIsAccountDetailsVisible(!isAccountDetailsVisible) + } + >{`${ + isAccountDetailsVisible ? "Hide" : "Show" + } account details`} + )} +
+
+ +
+
+
+ + Refresh account + +
+
+
+
+
+ + {/* Account details */} + {isAccountDetailsVisible && ( +
+ Account details +
+ +
+
+ )} + + +
+ Updating account + +
+
+
+ ); +}; diff --git a/src/components/AddAsset.tsx b/src/components/AddAsset.tsx new file mode 100644 index 00000000..56cbec06 --- /dev/null +++ b/src/components/AddAsset.tsx @@ -0,0 +1,201 @@ +import { useEffect, useState } from "react"; +import { useHistory } from "react-router-dom"; +import { + Button, + Heading2, + InfoBlock, + InfoBlockVariant, + Loader, +} from "@stellar/design-system"; +import { Input } from "components/Input"; +import { getErrorMessage } from "helpers/getErrorMessage"; +import { getNetworkConfig } from "helpers/getNetworkConfig"; +import { getValidatedUntrustedAsset } from "helpers/getValidatedUntrustedAsset"; +import { searchParam } from "helpers/searchParam"; +import { log } from "helpers/log"; +import { useRedux } from "hooks/useRedux"; +import { ActionStatus, SearchParams } from "types/types.d"; +import { TextLink } from "./TextLink"; + +export const AddAsset = ({ onClose }: { onClose: () => void }) => { + const { account, settings, untrustedAssets } = useRedux( + "account", + "settings", + "untrustedAssets", + ); + + const [isValidating, setIsValidating] = useState(false); + // Form data + const [assetCode, setAssetCode] = useState(""); + const [homeDomain, setHomeDomain] = useState(""); + const [issuerPublicKey, setIssuerPublicKey] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + + const history = useHistory(); + + const resetState = () => { + setAssetCode(""); + setHomeDomain(""); + setIssuerPublicKey(""); + setErrorMessage(""); + setIsValidating(false); + }; + + useEffect(() => () => resetState(), []); + + useEffect(() => { + if (untrustedAssets.status === ActionStatus.SUCCESS) { + onClose(); + } + + if (untrustedAssets.errorString) { + setErrorMessage(untrustedAssets.errorString); + } + }, [untrustedAssets.status, untrustedAssets.errorString, onClose]); + + const handleSetUntrustedAsset = async () => { + setErrorMessage(""); + + if (!(homeDomain || issuerPublicKey)) { + const errorMsg = + "Home domain OR issuer public key is required with asset code"; + + log.error({ title: errorMsg }); + setErrorMessage(errorMsg); + return; + } + + setIsValidating(true); + + try { + const asset = await getValidatedUntrustedAsset({ + assetCode, + homeDomain, + issuerPublicKey, + accountBalances: account.data?.balances, + networkUrl: getNetworkConfig(settings.pubnet).url, + }); + + let search = searchParam.update( + SearchParams.UNTRUSTED_ASSETS, + `${asset.assetCode}:${asset.assetIssuer}`, + ); + + if (asset.homeDomain) { + search = searchParam.updateKeyPair({ + searchParam: SearchParams.ASSET_OVERRIDES, + itemId: `${asset.assetCode}:${asset.assetIssuer}`, + keyPairs: { homeDomain }, + urlSearchParams: new URLSearchParams(search), + }); + } + + history.push(search); + setIsValidating(false); + } catch (e) { + const errorMsg = getErrorMessage(e); + + log.error({ title: errorMsg }); + setErrorMessage(errorMsg); + setIsValidating(false); + } + }; + + const isPending = + isValidating || untrustedAssets.status === ActionStatus.PENDING; + + return ( + <> + {/* TODO: move to Modal component */} + Add asset + +
+

Required: asset code AND (home domain OR issuer)

+ + { + setErrorMessage(""); + setAssetCode(e.target.value); + }} + value={assetCode} + placeholder="ex: USDC, EURT, NGNT" + tooltipText={ + <> + Assets are identified by 1) their code and 2) either a home domain + or the public key of the issuing account.{" "} + + Learn more + + + } + /> + + { + setErrorMessage(""); + setHomeDomain(e.target.value); + }} + value={homeDomain} + placeholder="ex: example.com" + tooltipText={ + <> + Domain where the well-known TOML file can be found for this asset.{" "} + + Learn more + + + } + /> + + { + setErrorMessage(""); + setIssuerPublicKey(e.target.value); + }} + value={issuerPublicKey} + placeholder="ex: GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B" + tooltipText={ + <> + Public key for the Asset Issuer.{" "} + + Learn more + + + } + /> + + {errorMessage && ( + +

{errorMessage}

+
+ )} +
+ +
+ {isPending && } + + +
+ + ); +}; diff --git a/src/components/Assets.tsx b/src/components/Assets.tsx new file mode 100644 index 00000000..84300de0 --- /dev/null +++ b/src/components/Assets.tsx @@ -0,0 +1,378 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router-dom"; +import { Button, Heading2, Loader } from "@stellar/design-system"; + +import { AddAsset } from "components/AddAsset"; +import { Balance } from "components/Balance"; +import { ClaimableBalance } from "components/ClaimableBalance"; +import { ConfirmAssetAction } from "components/ConfirmAssetAction"; +import { Modal } from "components/Modal"; +import { ToastBanner } from "components/ToastBanner"; +import { UntrustedBalance } from "components/UntrustedBalance"; + +import { fetchAccountAction, resetAccountStatusAction } from "ducks/account"; +import { + setActiveAssetAction, + setActiveAssetStatusAction, + resetActiveAssetAction, +} from "ducks/activeAsset"; +import { + getAllAssetsAction, + resetAllAssetsStatusAction, +} from "ducks/allAssets"; +import { + addAssetOverridesAction, + resetAssetOverridesStatusAction, +} from "ducks/assetOverrides"; +import { resetClaimAssetAction } from "ducks/claimAsset"; +import { fetchClaimableBalancesAction } from "ducks/claimableBalances"; +import { resetSep24DepositAssetAction } from "ducks/sep24DepositAsset"; +import { resetTrustAssetAction } from "ducks/trustAsset"; +import { + removeUntrustedAssetAction, + resetUntrustedAssetStatusAction, +} from "ducks/untrustedAssets"; +import { resetSep24WithdrawAssetAction } from "ducks/sep24WithdrawAsset"; + +import { searchParam } from "helpers/searchParam"; +import { useRedux } from "hooks/useRedux"; +import { + Asset, + ActionStatus, + AssetActionItem, + SearchParams, + TransactionStatus, +} from "types/types.d"; + +export const Assets = ({ + onSendPayment, +}: { + onSendPayment: (asset?: Asset) => void; +}) => { + const { + account, + activeAsset, + allAssets, + assetOverrides, + claimAsset, + sep24DepositAsset, + sep24WithdrawAsset, + sep31Send, + settings, + trustAsset, + untrustedAssets, + } = useRedux( + "account", + "activeAsset", + "allAssets", + "assetOverrides", + "claimAsset", + "sep24DepositAsset", + "sep24WithdrawAsset", + "sep31Send", + "settings", + "trustAsset", + "untrustedAssets", + ); + + const [activeModal, setActiveModal] = useState(""); + const [toastMessage, setToastMessage] = useState(); + + const dispatch = useDispatch(); + const history = useHistory(); + + enum modalType { + ADD_ASSET = "ADD_ASSET", + CONFIRM_ACTION = "CONFIRM_ACTION", + } + + const handleRemoveUntrustedAsset = useCallback( + (removeAsset?: string) => { + if (removeAsset) { + history.push( + searchParam.remove(SearchParams.UNTRUSTED_ASSETS, removeAsset), + ); + dispatch(removeUntrustedAssetAction(removeAsset)); + } + }, + [history, dispatch], + ); + + const handleRefreshAccount = useCallback(() => { + if (account.data?.id) { + dispatch( + fetchAccountAction({ + publicKey: account.data.id, + secretKey: account.secretKey, + }), + ); + } + }, [account.data?.id, account.secretKey, dispatch]); + + const handleFetchClaimableBalances = useCallback(() => { + if (account.data?.id) { + dispatch(fetchClaimableBalancesAction({ publicKey: account.data.id })); + } + }, [account.data?.id, dispatch]); + + const handleCloseModal = () => { + setActiveModal(""); + dispatch(resetActiveAssetAction()); + }; + + const handleAssetAction = ({ + assetString, + balance, + callback, + title, + description, + options, + }: AssetActionItem) => { + setActiveModal(modalType.CONFIRM_ACTION); + dispatch( + setActiveAssetAction({ + assetString, + title, + description, + callback: () => { + setActiveModal(""); + callback(balance); + }, + options, + }), + ); + }; + + const setActiveAssetStatusAndToastMessage = useCallback( + ({ + status, + message, + }: { + status: ActionStatus | undefined; + message: string | React.ReactNode; + }) => { + if (!status) { + return; + } + + if (status === ActionStatus.SUCCESS || status === ActionStatus.ERROR) { + dispatch(resetActiveAssetAction()); + } + + if ( + status === ActionStatus.PENDING || + status === ActionStatus.NEEDS_INPUT + ) { + dispatch(setActiveAssetStatusAction(ActionStatus.PENDING)); + setToastMessage(message); + } + }, + [dispatch], + ); + + useEffect(() => { + if (!activeAsset.action) { + setToastMessage(undefined); + } + }, [activeAsset.action]); + + useEffect(() => { + if (account.status === ActionStatus.SUCCESS) { + dispatch(resetAccountStatusAction()); + dispatch(getAllAssetsAction()); + } + }, [account.status, dispatch]); + + useEffect(() => { + if (allAssets.status === ActionStatus.SUCCESS) { + dispatch(resetAllAssetsStatusAction()); + } + }, [allAssets.status, dispatch]); + + useEffect(() => { + dispatch(addAssetOverridesAction(settings.assetOverrides)); + }, [settings.assetOverrides, dispatch]); + + useEffect(() => { + if (assetOverrides.status === ActionStatus.SUCCESS) { + dispatch(resetAssetOverridesStatusAction()); + dispatch(getAllAssetsAction()); + } + }, [assetOverrides.status, dispatch]); + + // Trust asset + useEffect(() => { + if (trustAsset.status === ActionStatus.SUCCESS) { + history.push( + searchParam.remove( + SearchParams.UNTRUSTED_ASSETS, + trustAsset.assetString, + ), + ); + dispatch(removeUntrustedAssetAction(trustAsset.assetString)); + dispatch(resetTrustAssetAction()); + handleRefreshAccount(); + } + + setActiveAssetStatusAndToastMessage({ + status: trustAsset.status, + message: "Trust asset in progress", + }); + }, [ + trustAsset.status, + trustAsset.assetString, + handleRefreshAccount, + setActiveAssetStatusAndToastMessage, + dispatch, + history, + ]); + + // Deposit asset + useEffect(() => { + if (sep24DepositAsset.status === ActionStatus.SUCCESS) { + dispatch(resetSep24DepositAssetAction()); + + if (sep24DepositAsset.data.trustedAssetAdded) { + handleRemoveUntrustedAsset(sep24DepositAsset.data.trustedAssetAdded); + } + + if ( + sep24DepositAsset.data.currentStatus === TransactionStatus.COMPLETED + ) { + handleRefreshAccount(); + handleFetchClaimableBalances(); + } + } + + setActiveAssetStatusAndToastMessage({ + status: sep24DepositAsset.status, + message: "SEP-24 deposit in progress", + }); + }, [ + sep24DepositAsset.status, + sep24DepositAsset.data.currentStatus, + sep24DepositAsset.data.trustedAssetAdded, + handleRefreshAccount, + handleFetchClaimableBalances, + handleRemoveUntrustedAsset, + setActiveAssetStatusAndToastMessage, + dispatch, + history, + ]); + + // Withdraw asset + useEffect(() => { + if (sep24WithdrawAsset.status === ActionStatus.SUCCESS) { + dispatch(resetSep24WithdrawAssetAction()); + + if ( + sep24WithdrawAsset.data.currentStatus === TransactionStatus.COMPLETED + ) { + handleRefreshAccount(); + } + } + + setActiveAssetStatusAndToastMessage({ + status: sep24WithdrawAsset.status, + message: "SEP-24 withdrawal in progress", + }); + }, [ + sep24WithdrawAsset.status, + sep24WithdrawAsset.data.currentStatus, + handleRefreshAccount, + setActiveAssetStatusAndToastMessage, + dispatch, + history, + ]); + + // Claim asset + useEffect(() => { + if (claimAsset.status === ActionStatus.SUCCESS) { + handleRemoveUntrustedAsset(claimAsset.data.trustedAssetAdded); + dispatch(resetClaimAssetAction()); + handleRefreshAccount(); + handleFetchClaimableBalances(); + } + + setActiveAssetStatusAndToastMessage({ + status: claimAsset.status, + message: "Claim asset in progress", + }); + }, [ + claimAsset.status, + claimAsset.data.trustedAssetAdded, + account.data?.id, + handleRefreshAccount, + handleFetchClaimableBalances, + handleRemoveUntrustedAsset, + setActiveAssetStatusAndToastMessage, + dispatch, + ]); + + // SEP-31 Send + useEffect(() => { + setActiveAssetStatusAndToastMessage({ + status: sep31Send.status, + message: "SEP-31 send in progress", + }); + }, [sep31Send.status, setActiveAssetStatusAndToastMessage]); + + // Remove untrusted asset + useEffect(() => { + if ( + untrustedAssets.status === ActionStatus.SUCCESS || + untrustedAssets.status === ActionStatus.ERROR + ) { + dispatch(getAllAssetsAction()); + dispatch(resetUntrustedAssetStatusAction()); + dispatch(resetActiveAssetAction()); + } + }, [untrustedAssets.status, dispatch]); + + return ( + <> + {/* Balances */} +
+
+ Balances +
+
+ + +
+ +
+ +
+
+ + {/* Claimable balances */} + + + + {/* Action confirmation */} + {activeModal === modalType.CONFIRM_ACTION && ( + + )} + + {/* Add asset */} + {activeModal === modalType.ADD_ASSET && ( + + )} + + + +
+
{toastMessage}
+ +
+
+ + ); +}; diff --git a/src/components/Balance.tsx b/src/components/Balance.tsx new file mode 100644 index 00000000..49ceefb7 --- /dev/null +++ b/src/components/Balance.tsx @@ -0,0 +1,180 @@ +import { useDispatch } from "react-redux"; +import { TextLink } from "components/TextLink"; +import { BalanceRow } from "components/BalanceRow"; +import { depositAssetAction } from "ducks/sep24DepositAsset"; +import { fetchSendFieldsAction } from "ducks/sep31Send"; +import { withdrawAssetAction } from "ducks/sep24WithdrawAsset"; +import { useRedux } from "hooks/useRedux"; +import { + Asset, + AssetActionItem, + AssetActionId, + AssetType, + AssetCategory, +} from "types/types.d"; + +interface SortedBalancesResult { + native: Asset[]; + other: Asset[]; +} + +export const Balance = ({ + onAssetAction, + onSend, +}: { + onAssetAction: ({ + balance, + callback, + title, + description, + options, + }: AssetActionItem) => void; + onSend: (asset?: Asset) => void; +}) => { + const { activeAsset, allAssets } = useRedux("activeAsset", "allAssets"); + const allBalances = allAssets.data.filter( + (a) => a.category === AssetCategory.TRUSTED, + ); + + const dispatch = useDispatch(); + + const groupBalances = () => { + if (!allBalances) { + return null; + } + + const result: SortedBalancesResult = { + native: [], + other: [], + }; + + allBalances.map((balance) => { + if (balance.assetType === AssetType.NATIVE) { + result.native = [...result.native, balance]; + } else { + result.other = [...result.other, balance]; + } + + return result; + }); + + return result; + }; + + const handleSep24Deposit = (asset: Asset) => { + dispatch(depositAssetAction(asset)); + }; + + const handleSep24Withdraw = (asset: Asset) => { + dispatch(withdrawAssetAction(asset)); + }; + + const handleSep31Send = (asset: Asset) => { + dispatch(fetchSendFieldsAction(asset)); + }; + + const handleAction = ({ + actionId, + balance, + }: { + actionId: string; + balance: Asset; + }) => { + if (!actionId) { + return; + } + + let props: AssetActionItem | undefined; + const defaultProps = { + assetString: balance.assetString, + balance, + }; + + switch (actionId) { + case AssetActionId.SEND_PAYMENT: + props = { + ...defaultProps, + title: `Send ${balance.assetCode}`, + description: ( +

+ {`Send ${balance.assetCode} on-chain to another account.`}{" "} + + Learn more + +

+ ), + callback: onSend, + }; + break; + case AssetActionId.SEP24_DEPOSIT: + props = { + ...defaultProps, + title: `SEP-24 deposit ${balance.assetCode} (with Trusted Asset)`, + description: `Start SEP-24 deposit of trusted asset ${balance.assetCode}?`, + callback: () => handleSep24Deposit(balance), + }; + break; + case AssetActionId.SEP24_WITHDRAW: + props = { + ...defaultProps, + title: `SEP-24 withdrawal ${balance.assetCode}`, + description: `Start SEP-24 withdrawal of ${balance.assetCode}?`, + callback: () => handleSep24Withdraw(balance), + }; + break; + case AssetActionId.SEP31_SEND: + props = { + ...defaultProps, + title: `SEP-31 send ${balance.assetCode}`, + description: `Start SEP-31 send to ${balance.assetCode}?`, + callback: () => handleSep31Send(balance), + }; + break; + default: + // do nothing + } + + if (!props) { + return; + } + + onAssetAction(props); + }; + + const sortedBalances = groupBalances(); + + if (!sortedBalances) { + return null; + } + + return ( + <> + {/* Native (XLM) balance */} + {sortedBalances.native.map((balance) => ( + + handleAction({ actionId, balance: asset }) + } + /> + ))} + + {/* Other balances */} + {sortedBalances.other.map((balance) => ( + + handleAction({ actionId, balance: asset }) + } + /> + ))} + + ); +}; diff --git a/src/components/BalanceRow.tsx b/src/components/BalanceRow.tsx new file mode 100644 index 00000000..7fc47b8f --- /dev/null +++ b/src/components/BalanceRow.tsx @@ -0,0 +1,141 @@ +import React, { ReactNode, useEffect, useState } from "react"; +import { Select } from "@stellar/design-system"; +import { TextLink } from "components/TextLink"; +import { HomeDomainOverrideButtons } from "components/HomeDomainOverrideButtons"; +import { shortenStellarKey } from "helpers/shortenStellarKey"; +import { + Asset, + ActiveAssetAction, + AssetActionId, + AssetType, + ClaimableAsset, +} from "types/types.d"; +import { InfoButtonWithTooltip } from "./InfoButtonWithTooltip"; + +interface BalanceRowProps { + activeAction: ActiveAssetAction | undefined; + asset: Asset | ClaimableAsset; + onAction?: (actionId: string, asset: Asset) => void; + children?: ReactNode; +} + +export const BalanceRow = ({ + activeAction, + asset, + onAction, + children, +}: BalanceRowProps) => { + const { + assetString, + assetCode, + assetIssuer, + total, + supportedActions, + isUntrusted, + notExist, + homeDomain, + } = asset; + const isActive = activeAction?.assetString === assetString; + const disabled = Boolean(activeAction); + const [selectValue, setSelectValue] = useState(""); + + useEffect(() => { + // reset value to default after modal close + if (!isActive) { + setSelectValue(""); + } + }, [isActive]); + + const handleSelectChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setSelectValue(value); + if (onAction) { + onAction(value, asset); + } + }; + + return ( +
+
+ {notExist ? ( +
{`${assetCode}:${shortenStellarKey( + assetIssuer, + )} does not exist`}
+ ) : ( + <> +
{`${ + total || "0" + } ${assetCode}`}
+
+ {homeDomain && ( + + {homeDomain} + + )} + {!asset.isClaimableBalance && + asset.assetType !== AssetType.NATIVE && ( + + )} +
+ + )} +
+
+ {children &&
{children}
} + + {onAction && ( +
+ + + + <> + { + "What you can do with an asset (deposit, withdraw, or send) depends on what transactions the anchor supports." + }{" "} + + Learn more + + + +
+ )} +
+
+ ); +}; diff --git a/src/components/Banner/index.tsx b/src/components/Banner/index.tsx new file mode 100644 index 00000000..c152877a --- /dev/null +++ b/src/components/Banner/index.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import "./styles.scss"; + +export const Banner = ({ children }: { children: React.ReactNode }) => ( +
+
{children}
+
+); diff --git a/src/components/Banner/styles.scss b/src/components/Banner/styles.scss new file mode 100644 index 00000000..fab8306f --- /dev/null +++ b/src/components/Banner/styles.scss @@ -0,0 +1,10 @@ +// TODO: add to SDS +.Banner { + min-width: var(--size-min-window); + background-color: var(--color-error); + color: var(--color-text-contrast); + text-align: center; + padding-top: 1rem; + padding-bottom: 1rem; + z-index: calc(var(--z-index-modal) + 2); +} diff --git a/src/components/ClaimableBalance.tsx b/src/components/ClaimableBalance.tsx new file mode 100644 index 00000000..ad20ae26 --- /dev/null +++ b/src/components/ClaimableBalance.tsx @@ -0,0 +1,64 @@ +import { Heading2, TextButton } from "@stellar/design-system"; +import { useDispatch } from "react-redux"; +import { BalanceRow } from "components/BalanceRow"; +import { claimAssetAction } from "ducks/claimAsset"; +import { useRedux } from "hooks/useRedux"; +import { AssetActionItem, ClaimableAsset } from "types/types.d"; + +export const ClaimableBalance = ({ + onAssetAction, +}: { + onAssetAction: ({ + balance, + callback, + title, + description, + options, + }: AssetActionItem) => void; +}) => { + const { activeAsset, claimableBalances } = useRedux( + "activeAsset", + "claimableBalances", + ); + const balances = claimableBalances.data.records; + + const dispatch = useDispatch(); + + const handleClaim = (balance: ClaimableAsset) => { + onAssetAction({ + assetString: balance.assetString, + balance, + title: `Claim balance ${balance.assetCode}`, + description: `Claimable balance description ${balance.total} ${balance.assetCode}`, + callback: () => dispatch(claimAssetAction(balance)), + }); + }; + + if (!balances || !balances.length) { + return null; + } + + return ( +
+
+ Claimable Balances +
+
+ {balances.map((balance) => ( + + handleClaim(balance)} + disabled={Boolean(activeAsset.action)} + > + Claim + + + ))} +
+
+ ); +}; diff --git a/src/components/ConfigurationModal.tsx b/src/components/ConfigurationModal.tsx new file mode 100644 index 00000000..bada4e7d --- /dev/null +++ b/src/components/ConfigurationModal.tsx @@ -0,0 +1,43 @@ +import { useHistory } from "react-router-dom"; +import { Heading2, Button } from "@stellar/design-system"; +import { Toggle } from "components/Toggle"; +import { searchParam } from "helpers/searchParam"; +import { useRedux } from "hooks/useRedux"; +import { SearchParams } from "types/types.d"; + +export const ConfigurationModal = ({ onClose }: { onClose: () => void }) => { + const { settings } = useRedux("settings"); + const history = useHistory(); + + const handleClaimableBalanceSupported = () => { + history.push( + searchParam.update( + SearchParams.CLAIMABLE_BALANCE_SUPPORTED, + (!settings.claimableBalanceSupported).toString(), + ), + ); + }; + + return ( + <> + Configuration + +
+
+ + +
+
+ +
+ +
+ + ); +}; diff --git a/src/components/ConfirmAssetAction.tsx b/src/components/ConfirmAssetAction.tsx new file mode 100644 index 00000000..d147b918 --- /dev/null +++ b/src/components/ConfirmAssetAction.tsx @@ -0,0 +1,42 @@ +import { Button, ButtonVariant, Heading2 } from "@stellar/design-system"; +import { useRedux } from "hooks/useRedux"; + +export const ConfirmAssetAction = ({ onClose }: { onClose: () => void }) => { + const { activeAsset } = useRedux("activeAsset"); + + if (!activeAsset?.action) { + return null; + } + + const { title, description, callback, options } = activeAsset.action; + + return ( + <> + {/* TODO: move to Modal component */} + {title} + +
+ {description && + (typeof description === "string" ? ( +

{description}

+ ) : ( + description + ))} + {options &&

{options}

} +
+ +
+ + +
+ + ); +}; diff --git a/src/components/ConnectAccount.tsx b/src/components/ConnectAccount.tsx new file mode 100644 index 00000000..72b953aa --- /dev/null +++ b/src/components/ConnectAccount.tsx @@ -0,0 +1,63 @@ +import { useState } from "react"; +import { + Button, + Checkbox, + Heading2, + Input, + Loader, +} from "@stellar/design-system"; +import { useHistory } from "react-router-dom"; +import { searchParam } from "helpers/searchParam"; +import { useRedux } from "hooks/useRedux"; +import { ActionStatus, SearchParams } from "types/types.d"; + +export const ConnectAccount = () => { + const { account, settings } = useRedux("account", "settings"); + const [secretKey, setSecretKey] = useState(""); + const history = useHistory(); + + const handleSetSecretKey = () => { + history.push(searchParam.update(SearchParams.SECRET_KEY, secretKey)); + }; + + const handleSwitchNetwork = () => { + history.push( + searchParam.update(SearchParams.PUBNET, (!settings.pubnet).toString()), + ); + }; + + return ( + <> + {/* TODO: move to Modal component */} + Connect with a secret key + +
+ setSecretKey(e.target.value)} + value={secretKey} + placeholder="Starts with S, example: SCHK…ZLJK" + /> + + +
+ +
+ {account.status === ActionStatus.PENDING && } + + +
+ + ); +}; diff --git a/src/components/CopyWithText.tsx b/src/components/CopyWithText.tsx new file mode 100644 index 00000000..d4c19825 --- /dev/null +++ b/src/components/CopyWithText.tsx @@ -0,0 +1,35 @@ +import React, { useState } from "react"; +import CopyToClipboard from "react-copy-to-clipboard"; +import { TextButton } from "@stellar/design-system"; + +export const CopyWithText = ({ + textToCopy, + children, +}: { + textToCopy: string; + children?: React.ReactNode; +}) => { + const [inProgress, setInProgress] = useState(false); + + const handleCopy = () => { + if (inProgress) { + return; + } + + setInProgress(true); + + const t = setTimeout(() => { + setInProgress(false); + clearTimeout(t); + }, 1000); + }; + + return ( + <> + {children} + + {inProgress ? "Copied" : "Copy"} + + + ); +}; diff --git a/src/components/CopyWithTooltip/index.tsx b/src/components/CopyWithTooltip/index.tsx new file mode 100644 index 00000000..a16f97d0 --- /dev/null +++ b/src/components/CopyWithTooltip/index.tsx @@ -0,0 +1,54 @@ +// TODO: move to Stellar Design System components +import React, { useState } from "react"; +import CopyToClipboard from "react-copy-to-clipboard"; + +import "./styles.scss"; + +export enum TooltipPosition { + bottom = "bottom", + right = "right", +} + +interface CopyWithTooltipProps { + copyText: string; + tooltipLabel?: string; + tooltipPosition?: TooltipPosition; + children: React.ReactNode; +} + +export const CopyWithTooltip = ({ + copyText, + tooltipLabel = "Copied", + tooltipPosition = TooltipPosition.bottom, + children, +}: CopyWithTooltipProps) => { + const [isTooltipVisible, setIsTooltipVisible] = useState(false); + + const handleCopyDone = () => { + if (isTooltipVisible) { + return; + } + + setIsTooltipVisible(true); + + const t = setTimeout(() => { + setIsTooltipVisible(false); + clearTimeout(t); + }, 1000); + }; + + return ( + +
+ {children} +
+ {tooltipLabel} +
+
+
+ ); +}; diff --git a/src/components/CopyWithTooltip/styles.scss b/src/components/CopyWithTooltip/styles.scss new file mode 100644 index 00000000..461a288c --- /dev/null +++ b/src/components/CopyWithTooltip/styles.scss @@ -0,0 +1,39 @@ +.CopyWithTooltip { + cursor: pointer; + position: relative; + + .Tooltip { + // Default Tooltip styles (move to global styles) + position: absolute; + top: 2.7rem; + right: -1rem; + max-width: 270px; + border-radius: 0.25rem; + background-color: var(--color-accent); + padding: 1rem 1.5rem; + color: var(--color-background-secondary); + font-size: 0.875rem; + line-height: 1.5rem; + z-index: var(--z-index-tooltip); + cursor: default; + + // Copy Tooltip styles + visibility: hidden; + padding: 0.5rem 1rem; + background-color: var(--color-background-contrast); + + &[data-position="bottom"] { + right: 50%; + transform: translateX(50%); + } + + &[data-position="right"] { + top: 50%; + transform: translate(100%, -50%); + } + + &[data-visible="true"] { + visibility: visible; + } + } +} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 00000000..968970f9 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,54 @@ +import { useState } from "react"; +import { TextLink, TextLinkVariant, TextButton } from "@stellar/design-system"; +import { Modal } from "components/Modal"; +import { ConfigurationModal } from "components/ConfigurationModal"; +import { useRedux } from "hooks/useRedux"; + +export const Footer = () => { + const [configModalVisible, setConfigModalVisible] = useState(false); + + const { account } = useRedux("account"); + + const handleConfigModalClose = () => { + setConfigModalVisible(false); + }; + + return ( + <> +
+
+
+ + Terms of Service + + + Privacy Policy + +
+ + {account.isAuthenticated && ( +
+ setConfigModalVisible(true)}> + Configuration + +
+ )} +
+
+ + + + + + ); +}; diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 00000000..9c45da54 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,32 @@ +import { useState } from "react"; +import { ProjectLogo, TextButton } from "@stellar/design-system"; +import { Modal } from "components/Modal"; +import { SignOutModal } from "components/SignOutModal"; +import { useRedux } from "hooks/useRedux"; + +export const Header = () => { + const [modalVisible, setModalVisible] = useState(false); + + const { account } = useRedux("account"); + + const handleCloseModal = () => { + setModalVisible(false); + }; + + return ( +
+
+ + {account.isAuthenticated && ( + setModalVisible(true)}> + Sign out + + )} +
+ + + + +
+ ); +}; diff --git a/src/components/Heading/index.tsx b/src/components/Heading/index.tsx new file mode 100644 index 00000000..42beee0f --- /dev/null +++ b/src/components/Heading/index.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { InfoButtonWithTooltip } from "components/InfoButtonWithTooltip"; +import "./styles.scss"; + +interface HeadingProps extends React.HTMLAttributes { + children: string; + tooltipText?: string | React.ReactNode; +} + +const getHeadingComponent = (Component: string): React.FC => ({ + children, + tooltipText, + ...props +}) => ( + + {children} + {tooltipText && ( + {tooltipText} + )} + +); + +export const Heading1 = getHeadingComponent("h1"); +export const Heading2 = getHeadingComponent("h2"); +export const Heading3 = getHeadingComponent("h3"); +export const Heading4 = getHeadingComponent("h4"); +export const Heading5 = getHeadingComponent("h5"); diff --git a/src/components/Heading/styles.scss b/src/components/Heading/styles.scss new file mode 100644 index 00000000..84a07df7 --- /dev/null +++ b/src/components/Heading/styles.scss @@ -0,0 +1,58 @@ +@mixin base-heading { + font-weight: var(--font-weight-normal); + color: var(--color-text); + margin: 0 0 0.5em; + padding: 0; + position: relative; + + // Tooltip + .InfoButton { + display: inline-block; + vertical-align: middle; + margin-left: 0.5rem; + } + + .InfoButtonTooltip { + right: 50%; + transform: translateX(50%); + margin-right: -1rem; + text-align: left; + } +} + +// Base font size 16px +// 32 / 40 (font size / line height) +h1 { + @include base-heading; + font-size: 2rem; + line-height: 2.5rem; +} + +// 24 / 32 +h2 { + @include base-heading; + font-size: 1.5rem; + line-height: 2rem; +} + +// 20 / 30 +h3 { + @include base-heading; + font-size: 1.25rem; + line-height: 1.875rem; +} + +// 16 / 24 +h4 { + @include base-heading; + font-size: 1rem; + line-height: 1.5rem; + font-weight: var(--font-weight-medium); +} + +// 16 / 24 +h5 { + @include base-heading; + font-size: 1rem; + line-height: 1.5rem; +} diff --git a/src/components/HomeDomainOverrideButtons.tsx b/src/components/HomeDomainOverrideButtons.tsx new file mode 100644 index 00000000..c1f06e7b --- /dev/null +++ b/src/components/HomeDomainOverrideButtons.tsx @@ -0,0 +1,122 @@ +import { useState } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router-dom"; +import { TextLink } from "components/TextLink"; +import { ConfirmAssetAction } from "components/ConfirmAssetAction"; +import { HomeDomainOverrideModal } from "components/HomeDomainOverrideModal"; +import { Modal } from "components/Modal"; +import { IconButton } from "components/IconButton"; +import { + setActiveAssetAction, + resetActiveAssetAction, +} from "ducks/activeAsset"; +import { log } from "helpers/log"; +import { searchParam } from "helpers/searchParam"; +import { ActionStatus, Asset, SearchParams } from "types/types.d"; + +import { ReactComponent as IconEdit } from "assets/icons/edit.svg"; +import { ReactComponent as IconRemove } from "assets/icons/error.svg"; +import { useRedux } from "hooks/useRedux"; +import { Loader } from "@stellar/design-system"; + +export const HomeDomainOverrideButtons = ({ asset }: { asset: Asset }) => { + const [activeModal, setActiveModal] = useState(""); + + const { assetOverrides } = useRedux("assetOverrides"); + + const dispatch = useDispatch(); + const history = useHistory(); + + enum ModalType { + REMOVE_ASSET_OVERRIDE = "REMOVE_ASSET_OVERRIDE", + ASSET_OVERRIDE = "ASSET_OVERRIDE", + } + + const showModal = (modalType: ModalType) => { + setActiveModal(modalType); + + let activeAsset; + + switch (modalType) { + case ModalType.ASSET_OVERRIDE: + // Modal text is set in HomeDomainOverrideModal component + activeAsset = { + assetString: asset.assetString, + title: "", + callback: () => {}, + }; + break; + case ModalType.REMOVE_ASSET_OVERRIDE: + activeAsset = { + assetString: asset.assetString, + title: `Remove ${asset.assetCode} home domain override`, + description: `Asset ${asset.assetCode}’s home domain ${asset.homeDomain} override will be removed. Original home domain will be used, if it exists.`, + callback: handleRemove, + }; + break; + default: + // do nothing + } + + dispatch(setActiveAssetAction(activeAsset)); + }; + + const handleRemove = () => { + history.push( + searchParam.removeKeyPair({ + searchParam: SearchParams.ASSET_OVERRIDES, + itemId: asset.assetString, + }), + ); + log.instruction({ + title: `Asset’s ${asset.assetCode} home domain override \`${asset.homeDomain}\` removed`, + }); + handleCloseModal(); + }; + + const handleCloseModal = () => { + setActiveModal(""); + dispatch(resetActiveAssetAction()); + }; + + if (assetOverrides.status === ActionStatus.PENDING) { + return ; + } + + return ( + <> + {asset.homeDomain ? ( + } + altText="Edit home domain" + onClick={() => showModal(ModalType.ASSET_OVERRIDE)} + /> + ) : ( + showModal(ModalType.ASSET_OVERRIDE)}> + Add home domain + + )} + + {asset.isOverride && ( + } + altText="Remove home domain override" + onClick={() => showModal(ModalType.REMOVE_ASSET_OVERRIDE)} + color="var(--color-error)" + /> + )} + + + {/* Action confirmation */} + {activeModal === ModalType.REMOVE_ASSET_OVERRIDE && ( + + )} + + {/* Override home domain */} + {activeModal === ModalType.ASSET_OVERRIDE && ( + + )} + + + ); +}; diff --git a/src/components/HomeDomainOverrideModal.tsx b/src/components/HomeDomainOverrideModal.tsx new file mode 100644 index 00000000..0dffc217 --- /dev/null +++ b/src/components/HomeDomainOverrideModal.tsx @@ -0,0 +1,116 @@ +import { useState } from "react"; +import { useHistory } from "react-router-dom"; +import { + Button, + ButtonVariant, + Heading2, + Input, + InfoBlock, + InfoBlockVariant, + Loader, +} from "@stellar/design-system"; +import { getAssetFromHomeDomain } from "helpers/getAssetFromHomeDomain"; +import { getErrorMessage } from "helpers/getErrorMessage"; +import { getNetworkConfig } from "helpers/getNetworkConfig"; +import { log } from "helpers/log"; +import { searchParam } from "helpers/searchParam"; +import { useRedux } from "hooks/useRedux"; +import { Asset, SearchParams } from "types/types.d"; + +export const HomeDomainOverrideModal = ({ + asset, + onClose, +}: { + asset: Asset; + onClose: () => void; +}) => { + const { settings } = useRedux("settings"); + const history = useHistory(); + + const [homeDomain, setHomeDomain] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + const [isPending, setIsPending] = useState(false); + + const handleOverride = async () => { + setErrorMessage(""); + setIsPending(true); + + const { assetCode, assetIssuer } = asset; + const networkUrl = getNetworkConfig(settings.pubnet).url; + + try { + const validAsset = await getAssetFromHomeDomain({ + assetCode, + homeDomain, + issuerPublicKey: assetIssuer, + networkUrl, + }); + + if (validAsset.homeDomain) { + history.push( + searchParam.updateKeyPair({ + searchParam: SearchParams.ASSET_OVERRIDES, + itemId: `${asset.assetCode}:${asset.assetIssuer}`, + keyPairs: { homeDomain }, + }), + ); + + onClose(); + } else { + throw new Error( + `Override home domain is the same as ${asset.assetCode} asset home domain`, + ); + } + } catch (e) { + const errorMsg = getErrorMessage(e); + setErrorMessage(errorMsg); + log.error({ title: errorMsg }); + setIsPending(false); + } + }; + + return ( + <> + Override home domain + +
+

{`Asset ${asset.assetCode} currently has ${ + asset.homeDomain || "no" + } home domain.`}

+ + { + setErrorMessage(""); + setHomeDomain(e.target.value); + }} + value={homeDomain} + placeholder="ex: example.com" + /> + + {errorMessage && ( + +

{errorMessage}

+
+ )} +
+ +
+ {isPending && } + + + + +
+ + ); +}; diff --git a/src/components/IconButton/index.tsx b/src/components/IconButton/index.tsx new file mode 100644 index 00000000..1781ea28 --- /dev/null +++ b/src/components/IconButton/index.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import "./styles.scss"; + +type IconButtonProps = { + icon: React.ReactNode; + onClick: () => void; + altText: string; + color?: string; +}; + +export const IconButton = ({ + icon, + onClick, + altText, + color, +}: IconButtonProps) => ( +
+ {icon} +
+); diff --git a/src/components/IconButton/styles.scss b/src/components/IconButton/styles.scss new file mode 100644 index 00000000..daaa6bf0 --- /dev/null +++ b/src/components/IconButton/styles.scss @@ -0,0 +1,14 @@ +.IconButton { + cursor: pointer; + width: 1.25rem; + height: 1.25rem; + margin-left: 0.3rem; + flex-shrink: 0; + fill: #666; + + svg { + width: 100%; + height: 100%; + fill: inherit; + } +} diff --git a/src/components/InfoButtonWithTooltip/index.tsx b/src/components/InfoButtonWithTooltip/index.tsx new file mode 100644 index 00000000..e71bc499 --- /dev/null +++ b/src/components/InfoButtonWithTooltip/index.tsx @@ -0,0 +1,59 @@ +import React, { useState, useEffect, useRef } from "react"; +import { IconInfo } from "@stellar/design-system"; +import "./styles.scss"; + +export const InfoButtonWithTooltip = ({ + children, +}: { + children: string | React.ReactNode; +}) => { + const toggleEl = useRef(null); + const infoEl = useRef(null); + const [isInfoVisible, setIsInfoVisible] = useState(false); + + useEffect(() => { + if (isInfoVisible) { + document.addEventListener("mousedown", handleClickOutside); + } else { + document.removeEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isInfoVisible]); + + const handleClickOutside = (event: MouseEvent) => { + // Do nothing if clicking tooltip itself or link inside the tooltip + if ( + event.target === infoEl?.current || + infoEl?.current?.contains(event.target as Node) + ) { + return; + } + + if (!toggleEl?.current?.contains(event.target as Node)) { + setIsInfoVisible(false); + } + }; + + return ( + <> +
setIsInfoVisible((currentState) => !currentState)} + > + +
+ +
+ {children} +
+ + ); +}; diff --git a/src/components/InfoButtonWithTooltip/styles.scss b/src/components/InfoButtonWithTooltip/styles.scss new file mode 100644 index 00000000..f24ab42b --- /dev/null +++ b/src/components/InfoButtonWithTooltip/styles.scss @@ -0,0 +1,43 @@ +.InfoButton { + cursor: pointer; + width: 1.25rem; + height: 1.25rem; + margin-left: 0.3rem; + flex-shrink: 0; + position: relative; + + svg { + width: 100%; + height: 100%; + // TODO: add info icon color to SDS + fill: #ccc; + position: absolute; + top: 0; + left: 0; + } +} + +.InfoButtonTooltip { + position: absolute; + top: 2.2rem; + right: -0.5rem; + width: 270px; + border-radius: 0.25rem; + background-color: var(--color-accent); + padding: 1rem 1.5rem; + color: var(--color-text-contrast); + font-size: 0.875rem; + line-height: 1.5rem; + z-index: var(--z-index-tooltip); + cursor: default; + visibility: hidden; + + &[data-hidden="false"] { + visibility: visible; + } + + a { + color: inherit; + white-space: nowrap; + } +} diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx new file mode 100644 index 00000000..c90dfdb4 --- /dev/null +++ b/src/components/Input/index.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { InfoButtonWithTooltip } from "components/InfoButtonWithTooltip"; +import "./styles.scss"; + +interface InputProps extends React.InputHTMLAttributes { + id: string; + label?: string; + rightElement?: string; + note?: React.ReactNode; + error?: string; + tooltipText?: string | React.ReactNode; +} + +export const Input = ({ + id, + label, + rightElement, + note, + error, + tooltipText, + ...props +}: InputProps) => ( +
+ {label && ( +
+ + {tooltipText && ( + {tooltipText} + )} +
+ )} +
+
+ +
+ {rightElement &&
{rightElement}
} +
+ {error &&
{error}
} + {note &&
{note}
} +
+); diff --git a/src/components/Input/styles.scss b/src/components/Input/styles.scss new file mode 100644 index 00000000..217925b1 --- /dev/null +++ b/src/components/Input/styles.scss @@ -0,0 +1,83 @@ +@mixin input-note-shared { + margin-top: 1rem; +} + +.Input { + width: 100%; + + .InputLabelWrapper { + position: relative; + display: flex; + align-items: center; + margin-bottom: 0.5rem; + + label { + margin-bottom: 0; + } + + .InfoButton { + margin-left: 0.5rem; + } + + .InfoButtonTooltip { + right: auto; + left: 0; + } + } + + .InputContainer { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + + .InputWrapper { + border: 1px solid var(--color-border-input); + border-radius: 0.125rem; + background-color: var(--color-background-input); + flex: 1; + height: 100%; + + &[data-disabled="true"] { + opacity: 0.5; + } + + input { + padding: 0.55rem 0.75rem; + font-size: 1rem; + line-height: 1.5rem; + color: var(--color-text-input); + border-radius: 0.125rem; + border: none; + background-color: transparent; + width: 100%; + min-width: 0; + + &::placeholder { + color: var(--color-text-input-placeholder); + } + } + } + + .InputRightElement { + margin-left: 1rem; + font-size: 1rem; + line-height: 1.5rem; + color: var(--color-text); + } + } + + .InputError { + @include input-note-shared; + font-size: 1rem; + line-height: 1.5rem; + color: var(--color-error); + } + + .InputNote { + @include input-note-shared; + font-size: 0.9rem; + line-height: 1.4rem; + color: var(--color-note); + } +} diff --git a/src/components/LogItem/index.tsx b/src/components/LogItem/index.tsx new file mode 100644 index 00000000..3b56ff97 --- /dev/null +++ b/src/components/LogItem/index.tsx @@ -0,0 +1,107 @@ +import { useEffect, useState } from "react"; +import ReactJson from "react-json-view"; +import marked from "marked"; +import { ReactComponent as IconArrowLeft } from "assets/icons/arrow-left.svg"; +import { ReactComponent as IconArrowRight } from "assets/icons/arrow-right.svg"; +import { ReactComponent as IconBubble } from "assets/icons/bubble.svg"; +import { ReactComponent as IconError } from "assets/icons/error.svg"; +import { sanitizeHtml } from "helpers/sanitizeHtml"; +import { LogType } from "types/types.d"; +import "./styles.scss"; + +marked.setOptions({ + gfm: false, +}); + +const LogItemIcon = { + instruction: , + error: , + request: , + response: , +}; + +interface LogItemProps { + title: string; + variant: LogType; + body?: string | object; +} + +const theme = { + light: { + base00: "#fff", + base01: "#fff", + base02: "#fff", + base03: "#000", + base04: "#000", + base05: "#000", + base06: "#000", + base07: "#000", + base08: "#000", + base09: "#000", + base0A: "#000", + base0B: "#000", + base0C: "#000", + base0D: "#000", + base0E: "#000", + base0F: "#000", + }, + dark: { + base00: "#292d3e", + base01: "#292d3e", + base02: "#292d3e", + base03: "#fbfaf7", + base04: "#fbfaf7", + base05: "#fbfaf7", + base06: "#fbfaf7", + base07: "#fbfaf7", + base08: "#fbfaf7", + base09: "#fbfaf7", + base0A: "#fbfaf7", + base0B: "#fbfaf7", + base0C: "#fbfaf7", + base0D: "#fbfaf7", + base0E: "#fbfaf7", + base0F: "#fbfaf7", + }, +}; + +export const LogItem = ({ title, variant, body }: LogItemProps) => { + const [isFadeReady, setIsFadeReady] = useState(false); + + useEffect(() => { + const t = setTimeout(() => { + setIsFadeReady(true); + clearTimeout(t); + }, 150); + }, []); + + const bodyParsed = body ? JSON.parse(`${body}`) : body; + + return ( +
+
+
{LogItemIcon[variant]}
+
{sanitizeHtml(marked(title))}
+
+ {bodyParsed && ( +
+ {typeof bodyParsed === "object" ? ( + + ) : ( + bodyParsed + )} +
+ )} +
+ ); +}; diff --git a/src/components/LogItem/styles.scss b/src/components/LogItem/styles.scss new file mode 100644 index 00000000..29a4435f --- /dev/null +++ b/src/components/LogItem/styles.scss @@ -0,0 +1,117 @@ +.LogItem { + max-width: 100%; + border-radius: 0.25rem; + overflow: hidden; + opacity: 0; + transition: all 300ms; + + &.open { + opacity: 1; + } + + .LogItemHeader { + display: flex; + align-items: flex-start; + padding: 1rem; + } + + .LogItemIcon { + width: 1rem; + height: 1.25rem; + margin-right: 0.5rem; + flex-shrink: 0; + flex-grow: 0; + + svg { + width: 100%; + height: 100%; + } + } + + .LogItemTitle { + font-size: 1rem; + line-height: 1.25rem; + word-break: break-word; + } + + .LogItemBody { + padding: 1rem; + font-weight: var(--font-weight-light); + font-family: var(--font-family-monospace); + font-size: 0.875rem; + line-height: 1.375rem; + word-break: break-word; + } + + // Light box + &.instruction, + &.error { + width: fit-content; + background-color: var(--color-background-main); + border: 1px solid var(--color-border-input); + box-shadow: 0 0.25rem 0.5rem -0.25rem rgba(0, 0, 0, 0.08); + color: var(--color-text); + + .LogItemBody { + border-top: 1px solid rgba(0, 0, 0, 0.1); + color: var(--color-text); + } + } + + // Dark box + &.request, + &.response { + width: 100%; + background-color: var(--color-background-contrast); + color: var(--color-text-contrast); + + .LogItemTitle { + font-weight: var(--font-weight-medium); + } + + .LogItemBody { + border-top: 1px solid rgba(255, 255, 255, 0.1); + // TODO: add color to SDS + color: #fbfaf7; + } + + code { + // TODO: add color to SDS + color: #ffdd96; + background-color: rgba(255, 221, 150, 0.08); + border-color: rgba(255, 221, 150, 0.16); + } + } + + // Instruction + &.instruction { + .LogItemIcon svg { + // TODO: add color to SDS + fill: #20bf6b; + } + } + + // Error + &.error { + .LogItemIcon svg { + // TODO: add color to SDS + fill: #eb3b5a; + } + } + + // Request + &.request { + .LogItemIcon svg { + // TODO: add color to SDS + fill: #ffdd96; + } + } + + // Response + &.response { + .LogItemIcon svg { + // TODO: add color to SDS + fill: #abcc7d; + } + } +} diff --git a/src/components/Logs.tsx b/src/components/Logs.tsx new file mode 100644 index 00000000..33e60b0f --- /dev/null +++ b/src/components/Logs.tsx @@ -0,0 +1,134 @@ +import { useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { TextButton } from "@stellar/design-system"; + +import { LogItem } from "components/LogItem"; +import { LOG_MESSAGE_EVENT } from "constants/settings"; +import { clearLogsAction, addLogAction } from "ducks/logs"; +import { useRedux } from "hooks/useRedux"; +import { LogItemProps } from "types/types.d"; + +export const Logs = () => { + const { account, logs } = useRedux("account", "logs"); + const dispatch = useDispatch(); + + useEffect(() => { + const onLogEventMessage = (e: any) => { + const { timestamp, type, title, body } = e.detail; + + dispatch( + addLogAction({ + timestamp, + type, + title, + body: JSON.stringify(body), + }), + ); + }; + + document.addEventListener(LOG_MESSAGE_EVENT, onLogEventMessage); + + return document.removeEventListener( + LOG_MESSAGE_EVENT, + onLogEventMessage, + true, + ); + }, [dispatch]); + + const logsToMarkdown = (logItems: LogItemProps[]) => { + const heading = `# Stellar Demo Wallet logs\n\n`; + const date = `${new Date()}\n\n`; + const url = `[URL](${window.location.toString()})\n\n`; + const divider = `---\n\n`; + const contentHeader = `${heading}${date}${url}${divider}`; + + return logItems.reduce((result, log, index) => { + const isLastItem = index === logItems.length - 1; + let content = `**${log.type}:** ${log.title}\n`; + let body = log.body ? JSON.parse(`${log.body}`) : null; + + if (body) { + body = typeof body === "string" ? body : JSON.stringify(body, null, 2); + content += `\n\`\`\`javascript\n${body}\n\`\`\`\n`; + } + + if (!isLastItem) { + content += "\n"; + } + + return `${result}${content}`; + }, contentHeader); + }; + + const handleDownload = () => { + if (!logs.items.length) { + return; + } + + const filename = `stellar-dw-logs-${Date.now()}.md`; + const content = logsToMarkdown(logs.items); + const element = document.createElement("a"); + + element.setAttribute( + "href", + `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`, + ); + element.setAttribute("download", filename); + element.style.display = "none"; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + }; + + if (!account.isAuthenticated) { + return ( +
+
+
+ Operation logs will appear here once a transaction begins +
+
+
+ ); + } + + return ( +
+
+
+
+
+ {logs.items.length ? ( + logs.items.map((log: LogItemProps) => ( + + )) + ) : ( +

No logs to show

+ )} +
+
+
+
+ +
+
+ + Download logs + + + dispatch(clearLogsAction())} + disabled={!logs.items.length} + > + Clear logs + +
+
+
+ ); +}; diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx new file mode 100644 index 00000000..8e90ccba --- /dev/null +++ b/src/components/Modal/index.tsx @@ -0,0 +1,48 @@ +import React, { useEffect } from "react"; +import ReactDOM from "react-dom"; +import { IconClose } from "@stellar/design-system"; + +import "./styles.scss"; + +const MODAL_OPEN_CLASS_NAME = "modal-open"; + +interface ModalProps { + visible: boolean; + onClose: () => void; + children: React.ReactNode; + disableWindowScrollWhenOpened?: boolean; +} + +export const Modal = ({ + visible, + onClose, + disableWindowScrollWhenOpened = false, + children, +}: ModalProps) => { + const parent = document.getElementById("app-wrapper"); + + useEffect(() => { + if (disableWindowScrollWhenOpened && visible) { + document.body.classList.add(MODAL_OPEN_CLASS_NAME); + } else { + document.body.classList.remove(MODAL_OPEN_CLASS_NAME); + } + }, [disableWindowScrollWhenOpened, visible]); + + if (!parent || !visible) { + return null; + } + + return ReactDOM.createPortal( +
+
+
{children}
+ +
+
+
, + parent, + ); +}; diff --git a/src/components/Modal/styles.scss b/src/components/Modal/styles.scss new file mode 100644 index 00000000..b5352d7f --- /dev/null +++ b/src/components/Modal/styles.scss @@ -0,0 +1,102 @@ +.ModalWrapper { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 50%; + z-index: var(--z-index-modal); + min-width: calc(var(--size-min-window) / 2); + min-height: 50vh; + overflow: hidden; + + .Modal { + position: absolute; + width: 80%; + max-width: 600px; + background-color: var(--color-background-main); + border-radius: 0; + z-index: calc(var(--z-index-modal) + 1); + padding: 4.5rem 1.5rem 2rem; + overflow: hidden; + box-shadow: 0 1.5rem 3rem -1.5rem rgba(0, 0, 0, 0.16); + margin-top: 3rem; + top: 35%; + left: 50%; + transform: translate(-50%, -35%); + border-radius: 0.5rem; + + .ModalContent { + overflow-y: auto; + max-height: 70vh; + + .ModalHeading { + margin-bottom: 1.5rem; + text-align: center; + } + + .ModalMessage { + margin-top: 1.5rem; + word-break: break-word; + } + + .ModalButtonsFooter { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + width: 100%; + margin-top: 1.5rem; + + & > *:not(:first-child) { + margin-left: 0.5rem; + } + } + + .ModalBody { + & > *:not(:last-child) { + margin-bottom: 1rem; + } + + .InfoBlock:not(:last-child) { + margin-bottom: 1.5rem; + } + } + } + + .ModalCloseButton { + width: 3rem; + height: 3rem; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + padding: 0; + margin: 0; + position: absolute; + top: 1rem; + right: 0.75rem; + cursor: pointer; + + &:hover { + opacity: 0.8; + } + + svg { + width: 1rem; + height: 1rem; + fill: var(--color-link); + } + } + } + + .ModalBackground { + position: absolute; + background-color: #000; + opacity: 0.24; + top: 0; + bottom: 0; + left: 0; + right: 0; + } +} diff --git a/src/components/PageContent.tsx b/src/components/PageContent.tsx new file mode 100644 index 00000000..3b26de87 --- /dev/null +++ b/src/components/PageContent.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const PageContent = ({ children }: { children: React.ReactNode }) => ( +
{children}
+); diff --git a/src/components/PrivateRoute.tsx b/src/components/PrivateRoute.tsx new file mode 100644 index 00000000..70fbb398 --- /dev/null +++ b/src/components/PrivateRoute.tsx @@ -0,0 +1,25 @@ +import { Route, Redirect, RouteProps, useLocation } from "react-router-dom"; +import { useRedux } from "hooks/useRedux"; + +export const PrivateRoute = ({ children, ...rest }: RouteProps) => { + const { account } = useRedux("account"); + const location = useLocation(); + + return ( + + account.isAuthenticated ? ( + children + ) : ( + + ) + } + /> + ); +}; diff --git a/src/components/SendPayment.tsx b/src/components/SendPayment.tsx new file mode 100644 index 00000000..a916dfeb --- /dev/null +++ b/src/components/SendPayment.tsx @@ -0,0 +1,163 @@ +import { useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { + Button, + Heading2, + InfoBlock, + Input, + Loader, +} from "@stellar/design-system"; +import { TextLink } from "components/TextLink"; +import { DataProvider } from "@stellar/wallet-sdk"; +import { StrKey } from "stellar-sdk"; + +import { fetchAccountAction } from "ducks/account"; +import { resetActiveAssetAction } from "ducks/activeAsset"; +import { sendPaymentAction, resetSendPaymentAction } from "ducks/sendPayment"; +import { getNetworkConfig } from "helpers/getNetworkConfig"; +import { useRedux } from "hooks/useRedux"; +import { ActionStatus, Asset, AssetType } from "types/types.d"; + +export const SendPayment = ({ + asset, + onClose, +}: { + asset?: Asset; + onClose: () => void; +}) => { + const { account, sendPayment, settings } = useRedux( + "account", + "sendPayment", + "settings", + ); + const { data, secretKey } = account; + const dispatch = useDispatch(); + + // Form data + const [destination, setDestination] = useState(""); + const [amount, setAmount] = useState(""); + const [assetCode, setAssetCode] = useState(asset?.assetCode); + const [assetIssuer, setAssetIssuer] = useState(asset?.assetIssuer || ""); + const [isDestinationFunded, setIsDestinationFunded] = useState(true); + + const resetFormState = () => { + setDestination(""); + setAmount(""); + setAssetCode(""); + setAssetIssuer(""); + setIsDestinationFunded(true); + }; + + useEffect(() => { + if (sendPayment.status === ActionStatus.SUCCESS && data?.id) { + dispatch( + fetchAccountAction({ + publicKey: data.id, + secretKey, + }), + ); + dispatch(resetSendPaymentAction()); + dispatch(resetActiveAssetAction()); + resetFormState(); + onClose(); + } + }, [sendPayment.status, secretKey, data?.id, dispatch, onClose]); + + const checkAndSetIsDestinationFunded = async () => { + if (!destination || !StrKey.isValidEd25519PublicKey(destination)) { + return; + } + + const dataProvider = new DataProvider({ + serverUrl: getNetworkConfig(settings.pubnet).url, + accountOrKey: destination, + networkPassphrase: getNetworkConfig(settings.pubnet).network, + }); + + setIsDestinationFunded(await dataProvider.isAccountFunded()); + }; + + const handleSubmit = () => { + if (data?.id) { + const params = { + destination, + isDestinationFunded, + amount, + assetCode, + assetIssuer, + publicKey: data.id, + }; + + dispatch(sendPaymentAction(params)); + } + }; + + return ( + <> + Send payment + +
+ setDestination(e.target.value)} + onBlur={() => { + checkAndSetIsDestinationFunded(); + }} + /> + setAmount(e.target.value)} + /> + setAssetCode(e.target.value)} + /> + {asset?.assetType !== AssetType.NATIVE && ( + setAssetIssuer(e.target.value)} + /> + )} + + {!isDestinationFunded && ( + + The destination account doesn’t exist. A create account operation + will be used to create this account.{" "} + + Learn more about account creation + + + )} +
+ + {sendPayment.errorString && ( +
+

{sendPayment.errorString}

+
+ )} + +
+ {sendPayment.status === ActionStatus.PENDING && } + + +
+ + ); +}; diff --git a/src/components/Sep31Send.tsx b/src/components/Sep31Send.tsx new file mode 100644 index 00000000..0c99562d --- /dev/null +++ b/src/components/Sep31Send.tsx @@ -0,0 +1,125 @@ +import React, { useState, useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { Button, Input } from "@stellar/design-system"; +import { Heading2, Heading3 } from "components/Heading"; +import { TextLink } from "components/TextLink"; +import { Modal } from "components/Modal"; +import { fetchAccountAction } from "ducks/account"; +import { resetActiveAssetAction } from "ducks/activeAsset"; +import { + resetSep31SendAction, + submitSep31SendTransactionAction, +} from "ducks/sep31Send"; +import { capitalizeString } from "helpers/capitalizeString"; +import { useRedux } from "hooks/useRedux"; +import { ActionStatus } from "types/types.d"; + +export const Sep31Send = () => { + const { account, sep31Send } = useRedux("account", "sep31Send"); + const [formData, setFormData] = useState({}); + + const dispatch = useDispatch(); + + useEffect(() => { + if (sep31Send.status === ActionStatus.SUCCESS) { + if (account.data?.id) { + dispatch( + fetchAccountAction({ + publicKey: account.data.id, + secretKey: account.secretKey, + }), + ); + dispatch(resetSep31SendAction()); + } + } + }, [sep31Send.status, account.data?.id, account.secretKey, dispatch]); + + const handleChange = (event: React.ChangeEvent) => { + const { id, value } = event.target; + const [section, field] = id.split("#"); + + const updatedState = { + ...formData, + [section]: { + ...(formData[section] || {}), + [field]: value, + }, + }; + + setFormData(updatedState); + }; + + const handleSubmit = ( + event: React.MouseEvent, + ) => { + event.preventDefault(); + dispatch(submitSep31SendTransactionAction({ ...formData })); + }; + + const handleClose = () => { + dispatch(resetSep31SendAction()); + dispatch(resetActiveAssetAction()); + }; + + if (sep31Send.status === ActionStatus.NEEDS_INPUT) { + const { data } = sep31Send; + const { transaction, sender, receiver } = data.fields; + + const allFields = { + amount: { + amount: { + description: "amount to send", + }, + }, + ...(sender ? { sender } : {}), + ...(receiver ? { receiver } : {}), + ...(transaction ? { transaction } : {}), + }; + + return ( + + + These are the fields the receiving anchor requires. The sending + client obtains them from the /customer endpoint.{" "} + + Learn more + + + } + > + Sender and receiver info + + +
+ {Object.entries(allFields).map(([sectionTitle, sectionItems]) => ( +
+ {capitalizeString(sectionTitle)} + {Object.entries(sectionItems || {}).map(([id, input]) => ( + // TODO: if input.choices, render Select + + ))} +
+ ))} +
+ +
+ +
+
+ ); + } + + return null; +}; diff --git a/src/components/SettingsHandler.tsx b/src/components/SettingsHandler.tsx new file mode 100644 index 00000000..60b5c970 --- /dev/null +++ b/src/components/SettingsHandler.tsx @@ -0,0 +1,123 @@ +import React, { useEffect } from "react"; +import { useLocation, useHistory } from "react-router-dom"; +import { useDispatch } from "react-redux"; +import { Keypair } from "stellar-sdk"; +import { fetchAccountAction } from "ducks/account"; +import { fetchClaimableBalancesAction } from "ducks/claimableBalances"; +import { updateSettingsAction } from "ducks/settings"; +import { getErrorMessage } from "helpers/getErrorMessage"; +import { log } from "helpers/log"; +import { useRedux } from "hooks/useRedux"; +import { ActionStatus, SearchParams } from "types/types.d"; + +export const SettingsHandler = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { account } = useRedux("account"); + + const dispatch = useDispatch(); + const history = useHistory(); + const location = useLocation(); + + const queryParams = new URLSearchParams(location.search); + const pubnetParam = queryParams.get(SearchParams.PUBNET); + const secretKeyParam = queryParams.get(SearchParams.SECRET_KEY); + const untrustedAssetsParam = queryParams.get(SearchParams.UNTRUSTED_ASSETS); + const assetOverridesParam = queryParams.get(SearchParams.ASSET_OVERRIDES); + const claimableBalanceSupportedParam = queryParams.get( + SearchParams.CLAIMABLE_BALANCE_SUPPORTED, + ); + + // Set network param (pubnet=true) + useEffect(() => { + dispatch( + updateSettingsAction({ + [SearchParams.PUBNET]: pubnetParam === "true", + }), + ); + }, [pubnetParam, dispatch]); + + // Set secret key param (secretKey=[SECRET_KEY]) and fetch account info + // This will handle both: secret key submitted on Demo Wallet and directly + // from the URL + useEffect(() => { + dispatch( + updateSettingsAction({ + [SearchParams.SECRET_KEY]: secretKeyParam || "", + }), + ); + + // TODO: validate secret key + if (secretKeyParam) { + try { + const keypair = Keypair.fromSecret(secretKeyParam); + dispatch( + fetchAccountAction({ + publicKey: keypair.publicKey(), + secretKey: keypair.secret(), + }), + ); + + dispatch( + fetchClaimableBalancesAction({ publicKey: keypair.publicKey() }), + ); + } catch (error) { + log.error({ + title: "Fetch account error", + body: getErrorMessage(error), + }); + } + } + }, [secretKeyParam, dispatch]); + + // Untrusted assets + useEffect(() => { + const cleanedAssets = untrustedAssetsParam + ?.split(",") + .reduce( + (unique: string[], item: string) => + unique.includes(item) ? unique : [...unique, item], + [], + ) + .join(","); + + dispatch( + updateSettingsAction({ + [SearchParams.UNTRUSTED_ASSETS]: cleanedAssets || "", + }), + ); + }, [untrustedAssetsParam, dispatch]); + + // Asset overrides + useEffect(() => { + dispatch( + updateSettingsAction({ + [SearchParams.ASSET_OVERRIDES]: assetOverridesParam || "", + }), + ); + }, [assetOverridesParam, dispatch]); + + // Claimabable balance supported + useEffect(() => { + dispatch( + updateSettingsAction({ + [SearchParams.CLAIMABLE_BALANCE_SUPPORTED]: + claimableBalanceSupportedParam === "true", + }), + ); + }, [claimableBalanceSupportedParam, dispatch]); + + // Go to /account page if fetching account was success + useEffect(() => { + if (account.status === ActionStatus.SUCCESS) { + history.push({ + pathname: "/account", + search: history.location.search, + }); + } + }, [account.status, history]); + + return <>{children}; +}; diff --git a/src/components/SignOutModal.tsx b/src/components/SignOutModal.tsx new file mode 100644 index 00000000..b19dbf9d --- /dev/null +++ b/src/components/SignOutModal.tsx @@ -0,0 +1,78 @@ +import { useEffect, useState } from "react"; +import { useHistory } from "react-router-dom"; +import { useDispatch } from "react-redux"; +import { + Button, + ButtonVariant, + TextButton, + IconCopy, + InfoBlock, + InfoBlockVariant, +} from "@stellar/design-system"; +import { CopyWithTooltip, TooltipPosition } from "components/CopyWithTooltip"; +import { resetStoreAction } from "config/store"; +import { getCurrentSessionParams } from "helpers/getCurrentSessionParams"; +import { SearchParams, StringObject } from "types/types.d"; + +export const SignOutModal = ({ onClose }: { onClose: () => void }) => { + const [sessionParams, setSessionParams] = useState([]); + + const dispatch = useDispatch(); + const history = useHistory(); + + useEffect(() => { + setSessionParams(getCurrentSessionParams()); + }, []); + + const handleSignOut = () => { + dispatch(resetStoreAction()); + history.push({ + pathname: "/", + }); + onClose(); + }; + + const getMessageText = () => { + const paramText: StringObject = { + [SearchParams.ASSET_OVERRIDES]: "home domain overrides", + [SearchParams.UNTRUSTED_ASSETS]: "untrusted assets", + [SearchParams.CLAIMABLE_BALANCE_SUPPORTED]: "claimable balance supported", + }; + + return sessionParams.map((s) => paramText[s]).join(", "); + }; + + return ( + <> +
+

+ You can reload the account using your secret key or press back in your + browser to sign back in. +

+ + {sessionParams.length > 0 && ( + +

+ {`You have session data (${getMessageText()}) that will be lost when you sign out. You can copy the URL to save it.`} +

+
+ + }>Copy URL + +
+
+ )} +
+ +
+ + +
+ + ); +}; diff --git a/src/components/TextButton/index.tsx b/src/components/TextButton/index.tsx new file mode 100644 index 00000000..0ec4e6e2 --- /dev/null +++ b/src/components/TextButton/index.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { InfoButtonWithTooltip } from "components/InfoButtonWithTooltip"; +import "./styles.scss"; + +export enum TextButtonVariant { + primary = "primary", + secondary = "secondary", +} + +interface TextButtonProps + extends React.ButtonHTMLAttributes { + icon?: React.ReactNode; + variant?: TextButtonVariant; + tooltipText?: string | React.ReactNode; + children: string; +} + +export const TextButton: React.FC = ({ + icon, + variant = TextButtonVariant.primary, + tooltipText, + children, + ...props +}) => ( +
+ + {tooltipText && ( + {tooltipText} + )} +
+); diff --git a/src/components/TextButton/styles.scss b/src/components/TextButton/styles.scss new file mode 100644 index 00000000..6864787b --- /dev/null +++ b/src/components/TextButton/styles.scss @@ -0,0 +1,58 @@ +.TextButtonWrapper { + display: flex; + align-items: center; + position: relative; +} + +.TextButton { + font-size: 1rem; + font-family: var(--font-family-base); + line-height: 1.75rem; + padding: 0.5rem 0.2rem; + font-weight: var(--font-weight-medium); + color: var(--color-accent); + background-color: transparent; + border: none; + cursor: pointer; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + + &[data-variant="primary"]:hover { + opacity: 0.7; + } + + &[data-variant="secondary"] { + font-weight: var(--font-weight-normal); + color: var(--color-text); + text-decoration: underline; + + &:hover { + text-decoration: none; + } + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } + + .TextButtonIcon { + display: block; + width: 1rem; + height: 1rem; + margin-bottom: 0.3rem; + margin-right: 0.75rem; + position: relative; + + svg { + fill: var(--color-accent); + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + } + } +} diff --git a/src/components/TextLink/index.tsx b/src/components/TextLink/index.tsx new file mode 100644 index 00000000..86199221 --- /dev/null +++ b/src/components/TextLink/index.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import "./styles.scss"; + +export enum TextLinkVariant { + primary = "primary", + secondary = "secondary", +} + +interface TextLinkProps extends React.AnchorHTMLAttributes { + children: string; + variant?: TextLinkVariant; + isExternal?: boolean; +} + +export const TextLink: React.FC = ({ + variant = TextLinkVariant.primary, + children, + isExternal, + ...props +}) => ( + + {children} + +); diff --git a/src/components/TextLink/styles.scss b/src/components/TextLink/styles.scss new file mode 100644 index 00000000..61234004 --- /dev/null +++ b/src/components/TextLink/styles.scss @@ -0,0 +1,24 @@ +.TextLink { + font-weight: var(--font-weight-normal); + color: var(--color-text); + text-decoration: underline; + cursor: pointer; + + &:hover { + text-decoration: none; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } + + &[data-variant="secondary"] { + color: var(--color-link); + text-decoration: none; + + &:hover { + opacity: 0.7; + } + } +} diff --git a/src/components/ToastBanner/index.tsx b/src/components/ToastBanner/index.tsx new file mode 100644 index 00000000..5d20c40d --- /dev/null +++ b/src/components/ToastBanner/index.tsx @@ -0,0 +1,50 @@ +import React, { useLayoutEffect, useState } from "react"; +import ReactDOM from "react-dom"; +import "./styles.scss"; + +interface ToastBannerProps { + parentId: string; + visible: boolean; + children: React.ReactNode; +} + +export const ToastBanner = ({ + parentId, + visible, + children, +}: ToastBannerProps) => { + const parent = document.getElementById(parentId); + const [isVisible, setIsVisible] = useState(visible); + const [isFadeReady, setIsFadeReady] = useState(false); + + useLayoutEffect(() => { + if (visible) { + setIsVisible(true); + + setTimeout(() => { + setIsFadeReady(true); + }, 150); + } else { + // Add a slight delay when closing for better UX + const t = setTimeout(() => { + setIsFadeReady(false); + clearTimeout(t); + + setTimeout(() => { + setIsVisible(false); + }, 400); + }, 600); + } + }, [visible]); + + if (!parent || !isVisible) { + return null; + } + + return ReactDOM.createPortal( +
+
{children}
+
, + parent, + ); +}; diff --git a/src/components/ToastBanner/styles.scss b/src/components/ToastBanner/styles.scss new file mode 100644 index 00000000..9b993772 --- /dev/null +++ b/src/components/ToastBanner/styles.scss @@ -0,0 +1,24 @@ +.ToastBanner { + --loader-color: var(--color-text-contrast); + + background-color: var(--color-background-contrast); + color: var(--color-text-contrast); + position: absolute; + top: 0; + left: 0; + width: 50%; + z-index: calc(var(--z-index-modal) + 3); + opacity: 0; + transition: all 300ms; + + &.open { + opacity: 1; + } + + .Inset { + min-height: 3.5rem; + display: flex; + align-items: center; + justify-content: center; + } +} diff --git a/src/components/Toggle/index.tsx b/src/components/Toggle/index.tsx new file mode 100644 index 00000000..40731a2d --- /dev/null +++ b/src/components/Toggle/index.tsx @@ -0,0 +1,30 @@ +import React, { useEffect, useState } from "react"; +import "./styles.scss"; + +interface ToggleProps extends React.InputHTMLAttributes { + id: string; + checked: boolean; + onChange: () => void; +} + +export const Toggle = ({ id, checked, onChange }: ToggleProps) => { + const [checkedValue, setCheckedValue] = useState(checked); + + useEffect(() => { + setCheckedValue(checked); + }, [checked]); + + return ( +