├── .env ├── .eslintignore ├── .prettierignore ├── .stylelintignore ├── src ├── react-app-env.d.ts ├── account │ ├── util │ │ ├── accountTypes.ts │ │ └── accountConstants.ts │ ├── page │ │ ├── landing │ │ │ ├── _account-landing-page.scss │ │ │ └── AccountLandingPage.tsx │ │ ├── import │ │ │ ├── passphrase │ │ │ │ ├── name │ │ │ │ │ └── _account-import-passphrase-name.scss │ │ │ │ ├── recovery │ │ │ │ │ ├── _account-import-passphrase-recovery.scss │ │ │ │ │ └── util │ │ │ │ │ │ └── accountImportPassphraseRecoveryUtils.ts │ │ │ │ └── prepare │ │ │ │ │ └── _account-import-passphrase-prepare.scss │ │ │ ├── _account-import-landing-page.scss │ │ │ ├── backup │ │ │ │ ├── passphrase │ │ │ │ │ └── _account-import-backup-passphrase.scss │ │ │ │ └── success │ │ │ │ │ └── _account-import-backup-success.scss │ │ │ ├── pera-sync │ │ │ │ └── prepare │ │ │ │ │ └── _account-import-pera-sync-prepare.scss │ │ │ ├── AccountImportLandingPage.tsx │ │ │ └── ledger │ │ │ │ └── modal │ │ │ │ └── device-search │ │ │ │ └── _ledger-device-search-modal.scss │ │ ├── create │ │ │ └── _account-create.scss │ │ └── success │ │ │ └── _account-success-page.scss │ ├── component │ │ ├── account-mnemonic-form │ │ │ ├── util │ │ │ │ └── accountMnemonicFormConstants.ts │ │ │ └── _account-mnemonic-form.scss │ │ ├── account-rename-modal │ │ │ └── _account-rename-modal.scss │ │ ├── account-passphrase-modal │ │ │ └── _account-passphrase-modal.scss │ │ ├── list │ │ │ ├── empty │ │ │ │ ├── _empty-account-list.scss │ │ │ │ └── EmptyAccountList.tsx │ │ │ └── searchable │ │ │ │ └── _searchable-account-list.scss │ │ ├── account-passphrase-box │ │ │ ├── _account-passphrase-box.scss │ │ │ └── AccountPassphraseBox.tsx │ │ └── account-limit-warning-banner │ │ │ └── _account-limit-warning-banner.scss │ └── accountModels.ts ├── core │ ├── ui │ │ ├── typography │ │ │ ├── typographyConstants.ts │ │ │ ├── Milan │ │ │ │ ├── Milan-Bold.otf │ │ │ │ ├── Milan-Medium.otf │ │ │ │ ├── Milan-Regular.otf │ │ │ │ ├── Milan-SemiBold.otf │ │ │ │ └── Milan-ExtraLight.otf │ │ │ ├── DMSans │ │ │ │ └── DMSans-Bold.ttf │ │ │ └── _fonts.scss │ │ ├── image │ │ │ ├── nano-ledger.png │ │ │ ├── pera-qr-logo.png │ │ │ ├── banner-background.png │ │ │ ├── pera-qr-logo--dark.png │ │ │ ├── firefox-not-supported.png │ │ │ ├── create-account-illustration.png │ │ │ ├── create-account-illustration.dark.png │ │ │ ├── recovery-passphrase-illustration.png │ │ │ └── recovery-passphrase-illustration.dark.png │ │ ├── style │ │ │ ├── _align.scss │ │ │ ├── _media-queries.scss │ │ │ ├── util │ │ │ │ └── animate │ │ │ │ │ ├── _slide-in.scss │ │ │ │ │ └── _show-up.scss │ │ │ ├── _common.scss │ │ │ ├── override │ │ │ │ ├── _override.scss │ │ │ │ └── hipo-ui-toolkit │ │ │ │ │ ├── _select.scss │ │ │ │ │ ├── _toggle.scss │ │ │ │ │ ├── _checkbox-input.scss │ │ │ │ │ ├── _input.scss │ │ │ │ │ ├── _form.scss │ │ │ │ │ └── _tab.scss │ │ │ ├── _measure.scss │ │ │ └── color │ │ │ │ └── _shadow.scss │ │ └── icons │ │ │ ├── algo.svg │ │ │ ├── search.svg │ │ │ ├── rekey.svg │ │ │ ├── rekey-dots.svg │ │ │ ├── plus.svg │ │ │ ├── export.svg │ │ │ ├── more.svg │ │ │ ├── undo.svg │ │ │ ├── checkbox.svg │ │ │ ├── chevron-down.svg │ │ │ ├── chevron-right.svg │ │ │ ├── checkmark.svg │ │ │ ├── moon.svg │ │ │ ├── activity.svg │ │ │ ├── algo-asa.svg │ │ │ ├── save.svg │ │ │ ├── send.svg │ │ │ ├── arrow-left.svg │ │ │ ├── arrow-right.svg │ │ │ ├── question-mark.svg │ │ │ ├── receive.svg │ │ │ ├── check-shield.svg │ │ │ ├── asa-verified.svg │ │ │ ├── exclamation-shield.svg │ │ │ ├── close.svg │ │ │ ├── lock.svg │ │ │ ├── backup.svg │ │ │ ├── ledger.svg │ │ │ ├── wallet.svg │ │ │ ├── info.svg │ │ │ ├── key.svg │ │ │ ├── tick-circle.svg │ │ │ ├── dapp.svg │ │ │ ├── account-rekeyed-orphan.svg │ │ │ ├── account-rekeyed-ledger.svg │ │ │ ├── account-rekeyed-standard.svg │ │ │ ├── asa-suspicious.svg │ │ │ ├── lock-2.svg │ │ │ ├── create-password-icon.svg │ │ │ ├── account-watch.svg │ │ │ ├── asa-trusted.svg │ │ │ ├── layer.svg │ │ │ ├── global.svg │ │ │ ├── node.svg │ │ │ ├── twitter.svg │ │ │ ├── add-funds.svg │ │ │ ├── pera-transfer.svg │ │ │ ├── account-ledger.svg │ │ │ └── pera-sync.svg │ ├── app │ │ └── db │ │ │ ├── appDBManager.ts │ │ │ └── index.ts │ ├── util │ │ ├── pera │ │ │ ├── onramp │ │ │ │ └── peraOnrampManager.ts │ │ │ ├── explorer │ │ │ │ └── getPeraExplorerLink.ts │ │ │ └── api │ │ │ │ └── peraApiUtils.ts │ │ ├── time │ │ │ ├── timeTypes.ts │ │ │ ├── timezoneUtils.ts │ │ │ └── timeConstants.ts │ │ ├── storage │ │ │ └── db │ │ │ │ └── DBManagerTypes.d.ts │ │ ├── hook │ │ │ ├── useSetPageTitle.tsx │ │ │ ├── useLocationWithState.tsx │ │ │ ├── useOnUnmount.tsx │ │ │ ├── useDebouncedValue.tsx │ │ │ ├── useTellerListener.ts │ │ │ ├── formito │ │ │ │ ├── formitoStateReducer.ts │ │ │ │ └── useFormito.tsx │ │ │ ├── useDBAccounts.tsx │ │ │ ├── useOnClickOutside.tsx │ │ │ └── useAlgoPricePolling.tsx │ │ ├── type │ │ │ └── typeUtils.d.ts │ │ ├── number │ │ │ └── numberConstants.ts │ │ ├── environment │ │ │ └── environmentConstants.ts │ │ ├── algosdk │ │ │ └── algosdkUtils.ts │ │ ├── url │ │ │ └── urlUtils.ts │ │ ├── image │ │ │ └── imageUtils.ts │ │ ├── string │ │ │ └── stringUtils.ts │ │ ├── device │ │ │ └── deviceUtils.ts │ │ ├── algod │ │ │ └── algodTypes.ts │ │ ├── algoExplorer │ │ │ └── algoExplorerUtils.ts │ │ └── prism │ │ │ └── prismUtils.ts │ ├── network │ │ ├── async-process │ │ │ ├── asyncProcessConstants.ts │ │ │ └── asyncProcess.d.ts │ │ ├── httpStatusCodes.ts │ │ ├── fetcherUtils.ts │ │ ├── teller │ │ │ └── tellerTypes.d.ts │ │ ├── fetcherTypes.ts │ │ ├── globalNetworkModels.d.ts │ │ └── FetcherError.ts │ ├── route │ │ ├── loading │ │ │ ├── _route-loading.scss │ │ │ └── RouteLoading.tsx │ │ └── navigate │ │ │ ├── useNavigateFlow.tsx │ │ │ └── NavigateFlow.tsx │ ├── reportWebVitals.ts │ └── api │ │ └── apiUtils.ts ├── explore │ ├── util │ │ ├── assets │ │ │ ├── gard-cover.jpg │ │ │ ├── pact-cover.jpg │ │ │ ├── wyre-cover.jpg │ │ │ ├── algofi-cover.jpg │ │ │ ├── banxa-cover.jpg │ │ │ ├── humble-cover.jpg │ │ │ ├── algoxnft-cover.jpg │ │ │ ├── coinbase-cover.jpg │ │ │ ├── moonpay-cover.jpg │ │ │ ├── tinyman-cover.jpg │ │ │ ├── randgallery-cover.jpg │ │ │ ├── folksfinance-cover.jpg │ │ │ └── coinbase-logo.svg │ │ ├── exploreTypes.d.ts │ │ ├── exploreUtils.ts │ │ └── exploreConstants.ts │ └── item │ │ └── list │ │ ├── _explore-item-list.scss │ │ └── ExploreItemList.tsx ├── component │ ├── form │ │ └── utils │ │ │ └── form.d.ts │ ├── network-badge │ │ ├── _network-badge.scss │ │ └── NetworkBadge.tsx │ ├── banner │ │ ├── util │ │ │ └── bannerUtils.ts │ │ └── _banner.dark.scss │ ├── skeleton │ │ ├── _skeleton.scss │ │ └── Skeleton.tsx │ ├── simple-toast │ │ ├── SimpleToastItemContext.tsx │ │ └── util │ │ │ ├── simpleToastConstants.ts │ │ │ ├── simpleToastTypes.ts │ │ │ └── simpleToastReducer.ts │ ├── list │ │ ├── selectable-list-item │ │ │ └── _selectable-list-item.scss │ │ └── searchable-list │ │ │ └── _searchable-list.scss │ ├── page │ │ ├── banner │ │ │ ├── _page-banner.scss │ │ │ └── PageBanner.tsx │ │ ├── header │ │ │ ├── util │ │ │ │ ├── useShouldShowPageHeaderGoBackButton.tsx │ │ │ │ ├── pageHeaderUtils.ts │ │ │ │ └── pageHeaderConstants.ts │ │ │ └── _page-header.scss │ │ ├── _page.scss │ │ ├── Page.tsx │ │ └── sidebar │ │ │ └── util │ │ │ ├── sidebarUtils.tsx │ │ │ └── sidebarConstants.tsx │ ├── go-back-button │ │ └── _go-back-button.scss │ ├── algo-currency │ │ ├── _algo-price-change.scss │ │ └── _algo-currency.scss │ ├── loader │ │ ├── simple │ │ │ ├── _simple-loader.scss │ │ │ └── SimpleLoader.tsx │ │ └── pera │ │ │ ├── _pera-loader.scss │ │ │ └── PeraLoader.tsx │ ├── modal │ │ └── util │ │ │ ├── modalConstants.ts │ │ │ └── modalTypes.ts │ ├── image │ │ └── _image.scss │ ├── scroll-to-top │ │ └── ScrollToTop.tsx │ ├── tooltip │ │ └── _tooltip.scss │ ├── clipboard │ │ └── button │ │ │ └── _clipboard-button.scss │ ├── info-box │ │ ├── _info-box.scss │ │ └── InfoBox.tsx │ ├── checkbox │ │ └── Checkbox.tsx │ ├── passphrase-list │ │ ├── PassphraseList.tsx │ │ └── _passphrase-list.scss │ ├── lock-button │ │ └── _lock-button.scss │ ├── async-content │ │ └── AsyncContent.tsx │ ├── pera-qr-code │ │ └── PeraQRCode.tsx │ ├── format-balance │ │ └── usd │ │ │ └── FormatUSDBalance.tsx │ ├── confirmation-modal │ │ └── _confirmation-modal.scss │ └── pera-connect-banner │ │ └── _pera-connect-banner.scss ├── overview │ ├── page │ │ ├── overview │ │ │ ├── list │ │ │ │ └── _account-overview-list.scss │ │ │ └── _account-overview.scss │ │ ├── welcome │ │ │ ├── util │ │ │ │ └── welcomePageUtils.ts │ │ │ └── _welcome-page.scss │ │ ├── show-passphrase │ │ │ └── _account-show-passphrase.scss │ │ └── show-qr │ │ │ └── _account-show-qr.scss │ ├── flow │ │ └── OverviewFlow.tsx │ └── context │ │ └── PortfolioOverviewContext.tsx ├── connect │ └── view │ │ ├── tab │ │ └── add-import-account │ │ │ ├── _connect-page-tab-view-add-import-account.scss │ │ │ └── onboarding-option-list │ │ │ ├── _connect-page-onboarding-option-list.scss │ │ │ ├── import │ │ │ └── ConnectPageTabViewImportOptionList.tsx │ │ │ └── ConnectPageOnboardingOptionList.tsx │ │ └── embedded │ │ └── _connect-page-embedded-view.scss ├── transaction │ ├── sign-box │ │ ├── view │ │ │ ├── default │ │ │ │ ├── _transaction-sign-default-view.scss │ │ │ │ ├── actions │ │ │ │ │ └── _transaction-sign-transaction-actions.scss │ │ │ │ ├── arbitrary-data-list │ │ │ │ │ └── _arbitrary-data-list.scss │ │ │ │ ├── TransactionSignDefaultView.tsx │ │ │ │ └── review-txns │ │ │ │ │ └── asset-list │ │ │ │ │ └── _transaction-sign-assets-list.scss │ │ │ └── detail │ │ │ │ ├── type-labels │ │ │ │ └── _transaction-type-label-list.scss │ │ │ │ └── view │ │ │ │ └── summary │ │ │ │ └── _transaction-sign-detail-summary-view.scss │ │ ├── message │ │ │ ├── _transaction-sign-detail-message.scss │ │ │ └── TransactionSignDetailMessage.tsx │ │ └── _transaction-sign-box.scss │ ├── utils │ │ ├── transactionContants.ts │ │ └── transactionTypes.d.ts │ ├── detail │ │ └── list │ │ │ ├── _transaction-details-list.scss │ │ │ └── list-item │ │ │ ├── _transaction-details-list-item.scss │ │ │ └── content │ │ │ └── _transaction-details-list-item-content.scss │ └── view │ │ ├── embedded │ │ └── _transaction-sign-embedded-view.scss │ │ └── tab │ │ └── _transaction-sign-tab-view.scss ├── send-txn │ ├── components │ │ ├── link │ │ │ ├── account │ │ │ │ └── _send-txn-account-link.scss │ │ │ └── _send-txn-link.scss │ │ ├── form │ │ │ └── _send-txn-form.scss │ │ ├── textarea │ │ │ ├── _send-txn-note-textarea.scss │ │ │ └── SendTxnNoteTextArea.tsx │ │ └── input │ │ │ └── amount │ │ │ └── _send-txn-amount-input.scss │ ├── page │ │ ├── select-txn-asset │ │ │ ├── list-item │ │ │ │ └── _send-txn-select-asset-list-item.scss │ │ │ └── _send-txn-select-asset.scss │ │ ├── select-txn-account │ │ │ └── _send-txn-select-account.scss │ │ ├── send-txn │ │ │ └── _send-txn.scss │ │ └── confirm-txn │ │ │ └── _send-txn-confirm.scss │ ├── util │ │ └── sendTxnConstants.ts │ └── modal │ │ └── _send-txn-info-modal.scss ├── settings │ ├── component │ │ └── clear-wallet-data-modal │ │ │ └── util │ │ │ └── clearWalletDataModalUtils.ts │ ├── util │ │ └── settingsUtils.ts │ ├── transfer-mobile │ │ ├── flow │ │ │ └── TransferMobileFlow.tsx │ │ └── modal │ │ │ ├── _transfer-mobile-info-modal.scss │ │ │ └── transferMobileInfoModalConstants.tsx │ └── backup │ │ └── flow │ │ └── BackupFlow.tsx ├── add-funds │ ├── button │ │ └── _add-funds-button.scss │ └── account-select │ │ └── _add-funds-account-select-modal.scss ├── not-supported │ ├── mobile │ │ └── _mobile-landing-page.scss │ └── firefox-incognito │ │ ├── _firefox-incognito-landing-page.scss │ │ └── FirefoxIncognitoLandingPage.tsx ├── asset │ ├── components │ │ ├── logo │ │ │ ├── _asset-logo.scss │ │ │ └── AssetLogo.tsx │ │ └── list │ │ │ └── item │ │ │ └── _asset-list-item.scss │ └── opt-in │ │ ├── page │ │ ├── select-account │ │ │ └── _asset-optin-select-account.scss │ │ └── _asset-optin-page.scss │ │ ├── components │ │ ├── list │ │ │ └── item │ │ │ │ └── _asset-optin-list-item.scss │ │ └── account-link │ │ │ └── _asset-optin-account-link.scss │ │ ├── util │ │ └── assetOptinUtils.ts │ │ ├── flow │ │ └── AssetOptinFlow.tsx │ │ └── modal │ │ └── info │ │ └── _asset-optin-info-modal.scss ├── layouts │ ├── card-layout │ │ ├── CardLayout.tsx │ │ └── _card-layout.scss │ ├── card-layout-without-route │ │ └── CardLayoutWithoutRoute.tsx │ └── sidebar │ │ └── _sidebar-layout.scss ├── pera-connect-error │ └── util │ │ └── peraConnectErrorScreenConstants.ts └── password │ ├── flow │ └── PasswordFlow.tsx │ └── util │ └── passwordUtils.ts ├── public ├── robots.txt ├── assets │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── mstile-icon-70x70.png │ ├── mstile-icon-144x144.png │ ├── mstile-icon-150x150.png │ ├── mstile-icon-310x310.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── site.webmanifest │ └── manifest.json └── browserconfig.xml ├── .husky └── pre-commit ├── .prettierrc.js ├── .env-cmdrc.json ├── .stylelintrc.json ├── LICENSE ├── .gitignore ├── BUILD.md ├── tsconfig.json └── config-overrides.js /.env: -------------------------------------------------------------------------------- 1 | HTTPS=true 2 | HOST=0.0.0.0 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | react-app-env.d.ts 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | _global-colors.scss -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | _global-colors.scss 2 | **/*.ts 3 | **/*.tsx 4 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/account/util/accountTypes.ts: -------------------------------------------------------------------------------- 1 | export type AccountComponentFlows = "default" | "connect"; 2 | -------------------------------------------------------------------------------- /src/core/ui/typography/typographyConstants.ts: -------------------------------------------------------------------------------- 1 | const ALGO_UNIT = "√"; 2 | 3 | export {ALGO_UNIT}; 4 | -------------------------------------------------------------------------------- /public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/public/assets/favicon.ico -------------------------------------------------------------------------------- /public/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/public/assets/favicon-16x16.png -------------------------------------------------------------------------------- /public/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/public/assets/favicon-32x32.png -------------------------------------------------------------------------------- /src/core/ui/image/nano-ledger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/core/ui/image/nano-ledger.png -------------------------------------------------------------------------------- /public/assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/public/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /public/assets/mstile-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/public/assets/mstile-icon-70x70.png -------------------------------------------------------------------------------- /src/core/ui/image/pera-qr-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/core/ui/image/pera-qr-logo.png -------------------------------------------------------------------------------- /public/assets/mstile-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/public/assets/mstile-icon-144x144.png -------------------------------------------------------------------------------- /public/assets/mstile-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/public/assets/mstile-icon-150x150.png -------------------------------------------------------------------------------- /public/assets/mstile-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/public/assets/mstile-icon-310x310.png -------------------------------------------------------------------------------- /src/explore/util/assets/gard-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/explore/util/assets/gard-cover.jpg -------------------------------------------------------------------------------- /src/explore/util/assets/pact-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/explore/util/assets/pact-cover.jpg -------------------------------------------------------------------------------- /src/explore/util/assets/wyre-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/explore/util/assets/wyre-cover.jpg -------------------------------------------------------------------------------- /public/assets/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/public/assets/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/assets/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/public/assets/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/component/form/utils/form.d.ts: -------------------------------------------------------------------------------- 1 | interface FormValidationInfo { 2 | type: T; 3 | title: string; 4 | message: string[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/core/ui/image/banner-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/core/ui/image/banner-background.png -------------------------------------------------------------------------------- /src/core/ui/image/pera-qr-logo--dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/core/ui/image/pera-qr-logo--dark.png -------------------------------------------------------------------------------- /src/explore/util/assets/algofi-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/explore/util/assets/algofi-cover.jpg -------------------------------------------------------------------------------- /src/explore/util/assets/banxa-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/explore/util/assets/banxa-cover.jpg -------------------------------------------------------------------------------- /src/explore/util/assets/humble-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/explore/util/assets/humble-cover.jpg -------------------------------------------------------------------------------- /src/account/page/landing/_account-landing-page.scss: -------------------------------------------------------------------------------- 1 | .account-landing-page__title { 2 | max-width: 778px; 3 | 4 | margin: 32px auto 0; 5 | } 6 | -------------------------------------------------------------------------------- /src/core/ui/image/firefox-not-supported.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/core/ui/image/firefox-not-supported.png -------------------------------------------------------------------------------- /src/core/ui/typography/Milan/Milan-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/core/ui/typography/Milan/Milan-Bold.otf -------------------------------------------------------------------------------- /src/explore/util/assets/algoxnft-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/explore/util/assets/algoxnft-cover.jpg -------------------------------------------------------------------------------- /src/explore/util/assets/coinbase-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/explore/util/assets/coinbase-cover.jpg -------------------------------------------------------------------------------- /src/explore/util/assets/moonpay-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/explore/util/assets/moonpay-cover.jpg -------------------------------------------------------------------------------- /src/explore/util/assets/tinyman-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/explore/util/assets/tinyman-cover.jpg -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | echo "type-checking..." 6 | ./node_modules/.bin/tsc --noEmit -------------------------------------------------------------------------------- /src/core/ui/typography/DMSans/DMSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/core/ui/typography/DMSans/DMSans-Bold.ttf -------------------------------------------------------------------------------- /src/core/ui/typography/Milan/Milan-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/core/ui/typography/Milan/Milan-Medium.otf -------------------------------------------------------------------------------- /src/explore/util/assets/randgallery-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/explore/util/assets/randgallery-cover.jpg -------------------------------------------------------------------------------- /src/core/ui/typography/Milan/Milan-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/core/ui/typography/Milan/Milan-Regular.otf -------------------------------------------------------------------------------- /src/core/ui/typography/Milan/Milan-SemiBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/core/ui/typography/Milan/Milan-SemiBold.otf -------------------------------------------------------------------------------- /src/explore/util/assets/folksfinance-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/explore/util/assets/folksfinance-cover.jpg -------------------------------------------------------------------------------- /src/core/ui/image/create-account-illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/core/ui/image/create-account-illustration.png -------------------------------------------------------------------------------- /src/core/ui/typography/Milan/Milan-ExtraLight.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/core/ui/typography/Milan/Milan-ExtraLight.otf -------------------------------------------------------------------------------- /src/core/ui/image/create-account-illustration.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/core/ui/image/create-account-illustration.dark.png -------------------------------------------------------------------------------- /src/core/ui/image/recovery-passphrase-illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/core/ui/image/recovery-passphrase-illustration.png -------------------------------------------------------------------------------- /src/overview/page/overview/list/_account-overview-list.scss: -------------------------------------------------------------------------------- 1 | .account-overview-list { 2 | margin: 0 -16px; 3 | padding: 16px 0; 4 | 5 | border-radius: 16px; 6 | } 7 | -------------------------------------------------------------------------------- /src/connect/view/tab/add-import-account/_connect-page-tab-view-add-import-account.scss: -------------------------------------------------------------------------------- 1 | .connect-page-tab-view-add-import-account__option-list { 2 | margin: 80px 0 32px; 3 | } 4 | -------------------------------------------------------------------------------- /src/core/app/db/appDBManager.ts: -------------------------------------------------------------------------------- 1 | export const appDBTables: DBManagerTables = [ 2 | {name: "accounts", autoIncrement: false}, 3 | {name: "sessions", autoIncrement: false} 4 | ]; 5 | -------------------------------------------------------------------------------- /src/core/ui/image/recovery-passphrase-illustration.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perawallet/pera-web-wallet/HEAD/src/core/ui/image/recovery-passphrase-illustration.dark.png -------------------------------------------------------------------------------- /src/account/component/account-mnemonic-form/util/accountMnemonicFormConstants.ts: -------------------------------------------------------------------------------- 1 | const MNEMONIC_KEYS_COMMA_OR_SPACE_REGEX = /[, ]+/; 2 | 3 | export {MNEMONIC_KEYS_COMMA_OR_SPACE_REGEX}; 4 | -------------------------------------------------------------------------------- /src/transaction/sign-box/view/default/_transaction-sign-default-view.scss: -------------------------------------------------------------------------------- 1 | .transaction-sign-default-view__review-transactions-spinner { 2 | height: 398px; 3 | 4 | padding: 28px; 5 | } 6 | -------------------------------------------------------------------------------- /src/connect/view/tab/add-import-account/onboarding-option-list/_connect-page-onboarding-option-list.scss: -------------------------------------------------------------------------------- 1 | .connect-page-onboarding-option-list__title { 2 | max-width: 788px; 3 | 4 | margin: 0 auto; 5 | } 6 | -------------------------------------------------------------------------------- /src/account/page/import/passphrase/name/_account-import-passphrase-name.scss: -------------------------------------------------------------------------------- 1 | .account-import-passphrase-name { 2 | width: 424px; 3 | } 4 | 5 | .account-import-passphrase-name__title { 6 | margin: 0 0 12px; 7 | } 8 | -------------------------------------------------------------------------------- /src/account/page/import/passphrase/recovery/_account-import-passphrase-recovery.scss: -------------------------------------------------------------------------------- 1 | .account-import-passphrase-recovery__title, 2 | .account-import-passphrase-recovery__description { 3 | margin-bottom: 24px; 4 | } 5 | -------------------------------------------------------------------------------- /src/account/util/accountConstants.ts: -------------------------------------------------------------------------------- 1 | export const MNEMONIC_LENGTH = 25; 2 | export const MAX_AVAILABLE_ACCOUNTS = 50; 3 | export const ACCOUNT_ADDRESS_TRUNCATE_LENGTH = 6; 4 | export const ACCOUNT_NAME_TRUNCATE_LENGTH = 22; 5 | -------------------------------------------------------------------------------- /src/core/util/pera/onramp/peraOnrampManager.ts: -------------------------------------------------------------------------------- 1 | import {PeraOnramp} from "@perawallet/onramp"; 2 | 3 | const peraOnrampManager = new PeraOnramp({ 4 | optInEnabled: true 5 | }); 6 | 7 | export default peraOnrampManager; 8 | -------------------------------------------------------------------------------- /src/core/util/time/timeTypes.ts: -------------------------------------------------------------------------------- 1 | export type FormatDateUtilOptions = { 2 | format: string; 3 | timezone?: string | null; 4 | shouldShiftDateToCompensateForTimezone?: boolean; 5 | isProvidedDateInUTC?: boolean; 6 | }; 7 | -------------------------------------------------------------------------------- /src/send-txn/components/link/account/_send-txn-account-link.scss: -------------------------------------------------------------------------------- 1 | .send-txn-account-link { 2 | .warning-icon { 3 | margin-top: 4px; 4 | 5 | path { 6 | fill: var(--helper-red-900); 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/explore/util/exploreTypes.d.ts: -------------------------------------------------------------------------------- 1 | interface ExploreItem { 2 | name: string; 3 | description: string; 4 | cover: string; 5 | logo: string; 6 | cta_text: string; 7 | cta_url: string; 8 | category: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/component/network-badge/_network-badge.scss: -------------------------------------------------------------------------------- 1 | .network-badge { 2 | margin-left: 12px; 3 | padding: 4px 8px; 4 | 5 | color: var(--helper-pink-900); 6 | 7 | background: var(--helper-pink-100); 8 | border-radius: 12px; 9 | } 10 | -------------------------------------------------------------------------------- /src/transaction/utils/transactionContants.ts: -------------------------------------------------------------------------------- 1 | export const MAX_ALLOWED_TRANSACTION_COUNT = 1000; 2 | export const MAX_ALLOWED_TRANSACTION_IN_GROUP = 16; 3 | 4 | export const SIGN_LEDGER_DATA_SIGN_INFO_TOAST_ID = "sign-ledger-data-sign-info-toast-id"; 5 | -------------------------------------------------------------------------------- /src/core/network/async-process/asyncProcessConstants.ts: -------------------------------------------------------------------------------- 1 | const INITIAL_ASYNC_PROCESS_STATE: AsyncProcessState = { 2 | isRequestPending: false, 3 | isRequestFetched: false, 4 | data: null, 5 | error: null 6 | }; 7 | 8 | export {INITIAL_ASYNC_PROCESS_STATE}; 9 | -------------------------------------------------------------------------------- /src/core/util/storage/db/DBManagerTypes.d.ts: -------------------------------------------------------------------------------- 1 | type DBManagerTable = { 2 | name: string; 3 | keypath?: string | string[]; 4 | autoIncrement: boolean; 5 | indexes?: Parameters[]; 6 | }; 7 | type DBManagerTables = DBManagerTable[]; 8 | -------------------------------------------------------------------------------- /src/explore/item/list/_explore-item-list.scss: -------------------------------------------------------------------------------- 1 | .explore-item-list { 2 | display: grid; 3 | grid-template-columns: repeat(4, minmax(264px, 1fr)); 4 | gap: 16px; 5 | grid-auto-rows: 1fr; 6 | 7 | .explore-item-card { 8 | height: 100%; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/settings/component/clear-wallet-data-modal/util/clearWalletDataModalUtils.ts: -------------------------------------------------------------------------------- 1 | function validateClearWalletDataConfirmationText(confirmationText: string) { 2 | return confirmationText === "clear wallet data"; 3 | } 4 | 5 | export {validateClearWalletDataConfirmationText}; 6 | -------------------------------------------------------------------------------- /src/add-funds/button/_add-funds-button.scss: -------------------------------------------------------------------------------- 1 | .add-funds-button { 2 | gap: 8px; 3 | 4 | .plus-icon { 5 | width: 20px; 6 | height: 20px; 7 | } 8 | } 9 | 10 | .add-funds-button__add-funds-icon { 11 | path { 12 | fill: var(--helper-purple-900); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/core/util/hook/useSetPageTitle.tsx: -------------------------------------------------------------------------------- 1 | import {useLayoutEffect} from "react"; 2 | 3 | function useSetPageTitle(title: string) { 4 | useLayoutEffect(() => { 5 | document.title = `${title} | Pera Wallet`; 6 | }, [title]); 7 | } 8 | 9 | export default useSetPageTitle; 10 | -------------------------------------------------------------------------------- /src/transaction/detail/list/_transaction-details-list.scss: -------------------------------------------------------------------------------- 1 | .transaction-details-list { 2 | overflow: auto; 3 | 4 | margin: 16px 24px; 5 | padding: 4px 20px; 6 | 7 | background: var(--layer-2); 8 | border-radius: 12px; 9 | box-shadow: var(--shadow-inline); 10 | } 11 | -------------------------------------------------------------------------------- /src/transaction/detail/list/list-item/_transaction-details-list-item.scss: -------------------------------------------------------------------------------- 1 | .transaction-details-list-item { 2 | .list-item__click-wrapper { 3 | padding: 16px 0; 4 | } 5 | 6 | &:not(:last-of-type) { 7 | border-bottom: 1px solid var(--border-default); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/account/page/create/_account-create.scss: -------------------------------------------------------------------------------- 1 | .account-create { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | 6 | width: 424px; 7 | } 8 | 9 | .account-create__title, 10 | .account-create__address-box { 11 | margin-bottom: 32px; 12 | } 13 | -------------------------------------------------------------------------------- /src/core/util/type/typeUtils.d.ts: -------------------------------------------------------------------------------- 1 | type ValueOf = T[keyof T]; 2 | 3 | type ArrayToUnion = T[number]; 4 | 5 | type Only = { 6 | [P in keyof T]: T[P]; 7 | } & { 8 | [P in keyof U]?: never; 9 | }; 10 | 11 | type Either = Only | Only; 12 | -------------------------------------------------------------------------------- /src/send-txn/page/select-txn-asset/list-item/_send-txn-select-asset-list-item.scss: -------------------------------------------------------------------------------- 1 | .send-txn-select-asset-list-item__grid-cell { 2 | display: grid; 3 | 4 | &:first-child { 5 | align-items: center; 6 | } 7 | 8 | &:last-child { 9 | justify-items: flex-end; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 90, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: false, 7 | jsxSingleQuote: false, 8 | trailingComma: "none", 9 | bracketSpacing: false, 10 | bracketSameLine: true, 11 | arrowParens: "always" 12 | }; 13 | -------------------------------------------------------------------------------- /src/component/banner/util/bannerUtils.ts: -------------------------------------------------------------------------------- 1 | import webStorage, {STORED_KEYS} from "../../../core/util/storage/web/webStorage"; 2 | 3 | function getHiddenBanners(): string[] { 4 | return (webStorage.local.getItem(STORED_KEYS.HIDDEN_BANNERS) || []) as string[]; 5 | } 6 | 7 | export {getHiddenBanners}; 8 | -------------------------------------------------------------------------------- /src/component/skeleton/_skeleton.scss: -------------------------------------------------------------------------------- 1 | .skeleton { 2 | background: var(--skeleton); 3 | 4 | background-size: 200% 100%; 5 | 6 | animation: 1.5s skeleton-shine linear infinite; 7 | } 8 | 9 | @keyframes skeleton-shine { 10 | to { 11 | background-position-x: -200%; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/component/banner/_banner.dark.scss: -------------------------------------------------------------------------------- 1 | .dark-theme { 2 | .banner__close-cta { 3 | &:hover { 4 | background-color: rgba(var(--black-rgb), 0.15); 5 | border-radius: 50%; 6 | 7 | .close-icon path { 8 | fill: var(--banner-text); 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/core/route/loading/_route-loading.scss: -------------------------------------------------------------------------------- 1 | .route-loading { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | 12 | background-color: var(--background-color); 13 | } 14 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffed06 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/account/page/import/_account-import-landing-page.scss: -------------------------------------------------------------------------------- 1 | .account-import-landing-page { 2 | max-width: 788px; 3 | 4 | margin: 0 auto; 5 | } 6 | 7 | .account-import-landing-page__title { 8 | margin-top: 32px; 9 | } 10 | 11 | .account-import-landing-page__subtitle { 12 | margin-top: 16px; 13 | } 14 | -------------------------------------------------------------------------------- /src/core/util/number/numberConstants.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_DECIMAL_SEPARATOR = "."; 2 | 3 | const MEMORY_UNITS = ["bytes", "KB", "MB"] as const; 4 | 5 | // eslint-disable-next-line no-magic-numbers 6 | const MEMORY_CONVERSION_UNIT = 1000; 7 | 8 | export {MEMORY_UNITS, MEMORY_CONVERSION_UNIT, DEFAULT_DECIMAL_SEPARATOR}; 9 | -------------------------------------------------------------------------------- /src/send-txn/page/select-txn-account/_send-txn-select-account.scss: -------------------------------------------------------------------------------- 1 | @import "../../../layouts/card-layout/_card-layout.scss"; 2 | 3 | .send-txn__account { 4 | @extend .card-layout; 5 | 6 | display: flex; 7 | flex-direction: column; 8 | 9 | width: 424px; 10 | height: 590px; 11 | 12 | padding: 0; 13 | } 14 | -------------------------------------------------------------------------------- /src/component/simple-toast/SimpleToastItemContext.tsx: -------------------------------------------------------------------------------- 1 | import {createContext} from "react"; 2 | 3 | const SimpleToastItemContext = createContext<{ 4 | toastId: string; 5 | }>({ 6 | toastId: "" 7 | }); 8 | 9 | SimpleToastItemContext.displayName = "SimpleToastItemContext"; 10 | 11 | export {SimpleToastItemContext}; 12 | -------------------------------------------------------------------------------- /src/component/list/selectable-list-item/_selectable-list-item.scss: -------------------------------------------------------------------------------- 1 | .selectable-list-item__with-checkbox { 2 | .list-item__click-wrapper { 3 | display: grid; 4 | grid-template-columns: 20px 1fr; 5 | grid-gap: 16px; 6 | } 7 | } 8 | 9 | .selectable-list-item__checkbox-input { 10 | pointer-events: none; 11 | } 12 | -------------------------------------------------------------------------------- /src/core/network/httpStatusCodes.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-magic-numbers */ 2 | enum HttpStatusCodes { 3 | OK = 200, 4 | NOT_MODIFIED = 304, 5 | BAD_REQUEST = 400, 6 | UNAUTHORIZED = 401, 7 | FORBIDDEN = 403, 8 | NOT_FOUND = 404 9 | } 10 | /* eslint-enable no-magic-numbers */ 11 | 12 | export default HttpStatusCodes; 13 | -------------------------------------------------------------------------------- /src/component/page/banner/_page-banner.scss: -------------------------------------------------------------------------------- 1 | .page-banner { 2 | display: flex; 3 | align-items: center; 4 | justify-content: flex-end; 5 | gap: 8px; 6 | 7 | height: 68px; 8 | 9 | padding: 22px 24px; 10 | 11 | background-image: url("../../../core/ui/image/banner-background.png"); 12 | background-size: cover; 13 | } 14 | -------------------------------------------------------------------------------- /src/component/go-back-button/_go-back-button.scss: -------------------------------------------------------------------------------- 1 | .go-back-button-container { 2 | display: flex; 3 | align-items: center; 4 | gap: 16px; 5 | 6 | margin-bottom: 32px; 7 | 8 | .arrow-left-icon { 9 | path { 10 | fill: var(--text-main); 11 | } 12 | } 13 | } 14 | 15 | .go-back-button { 16 | display: flex; 17 | } 18 | -------------------------------------------------------------------------------- /src/core/network/fetcherUtils.ts: -------------------------------------------------------------------------------- 1 | function fetchJSONMiddleware(response: globalThis.Response) { 2 | return response.json().catch((error) => { 3 | console.error("Response may not support `json` conversion, so suppress the error"); 4 | console.error(error); 5 | return {}; 6 | }); 7 | } 8 | 9 | export {fetchJSONMiddleware}; 10 | -------------------------------------------------------------------------------- /src/not-supported/mobile/_mobile-landing-page.scss: -------------------------------------------------------------------------------- 1 | .mobile-landing-page__heading-image { 2 | max-width: 326px; 3 | 4 | margin-bottom: 77px; 5 | } 6 | 7 | .mobile-landing-page__content { 8 | padding: 0 40px; 9 | } 10 | 11 | .mobile-landing-page__message { 12 | margin: 32px 0 48px; 13 | 14 | color: var(--text-gray); 15 | } 16 | -------------------------------------------------------------------------------- /src/component/algo-currency/_algo-price-change.scss: -------------------------------------------------------------------------------- 1 | .algo-price-change-percentage { 2 | display: flex; 3 | 4 | padding: 4px 8px; 5 | 6 | color: var(--gray-900); 7 | background-color: rgba(var(--purple-600-rgb), 0.2); 8 | 9 | border-radius: 40px; 10 | 11 | .algo-price-change-percentage__text { 12 | font-weight: 400; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/component/loader/simple/_simple-loader.scss: -------------------------------------------------------------------------------- 1 | .simple-loader { 2 | width: 16px; 3 | height: 16px; 4 | } 5 | 6 | .simple-loader__content { 7 | transform-origin: center; 8 | animation: SimpleLoaderAnimation 0.75s infinite linear; 9 | } 10 | 11 | @keyframes SimpleLoaderAnimation { 12 | 100% { 13 | transform: rotate(360deg); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/component/modal/util/modalConstants.ts: -------------------------------------------------------------------------------- 1 | import {SECOND_IN_MS} from "../../../core/util/time/timeConstants"; 2 | 3 | const MODAL_CLOSE_TIMEOUT = 4 | parseFloat( 5 | getComputedStyle(document.documentElement) 6 | .getPropertyValue("--default-animation") 7 | .trim() || "0.2" 8 | ) * SECOND_IN_MS; 9 | 10 | export {MODAL_CLOSE_TIMEOUT}; 11 | -------------------------------------------------------------------------------- /src/core/ui/style/_align.scss: -------------------------------------------------------------------------------- 1 | .align-center { 2 | &--horizontally { 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | } 7 | 8 | &--vertically { 9 | display: flex; 10 | align-items: center; 11 | } 12 | } 13 | 14 | .has-space-between { 15 | display: flex; 16 | justify-content: space-between; 17 | } 18 | -------------------------------------------------------------------------------- /src/overview/flow/OverviewFlow.tsx: -------------------------------------------------------------------------------- 1 | import {Routes, Route} from "react-router-dom"; 2 | 3 | import AccountOverview from "../page/overview/AccountOverview"; 4 | 5 | function OverviewFlow() { 6 | return ( 7 | 8 | } /> 9 | 10 | ); 11 | } 12 | 13 | export default OverviewFlow; 14 | -------------------------------------------------------------------------------- /src/core/ui/icons/algo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/transaction/sign-box/view/default/actions/_transaction-sign-transaction-actions.scss: -------------------------------------------------------------------------------- 1 | .transaction-sign-transaction-actions { 2 | padding: 28px; 3 | 4 | &--sign-required { 5 | display: grid; 6 | grid-template-columns: 132px auto; 7 | gap: 16px; 8 | } 9 | } 10 | 11 | .transaction-sign-transaction-actions__button { 12 | width: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /src/account/component/account-rename-modal/_account-rename-modal.scss: -------------------------------------------------------------------------------- 1 | .account-rename-modal { 2 | width: 424px; 3 | 4 | .account-name-form__cta { 5 | margin-top: 36px; 6 | } 7 | } 8 | 9 | .account-rename-modal__title { 10 | margin-bottom: 12px; 11 | } 12 | 13 | .account-rename-modal__cancel-cta { 14 | width: 100%; 15 | 16 | margin-top: 12px; 17 | } 18 | -------------------------------------------------------------------------------- /src/core/route/navigate/useNavigateFlow.tsx: -------------------------------------------------------------------------------- 1 | import {NavigateOptions, To, useNavigate} from "react-router-dom"; 2 | 3 | function useNavigateFlow() { 4 | const navigate = useNavigate(); 5 | 6 | return (to: To, options?: NavigateOptions) => 7 | navigate(to, {...options, state: {...options?.state, isNavigated: true}}); 8 | } 9 | 10 | export default useNavigateFlow; 11 | -------------------------------------------------------------------------------- /src/transaction/utils/transactionTypes.d.ts: -------------------------------------------------------------------------------- 1 | interface SignedTxn { 2 | txnId: string; 3 | signedTxn: string; 4 | } 5 | 6 | interface TransactionSignPageState { 7 | hasMessageReceived: boolean; 8 | isSignStarted: boolean; 9 | txns: Transaction[]; 10 | currentSession: AppDBSession | null; 11 | signerAddress: string; 12 | signerAccountName: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/core/ui/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/send-txn/components/form/_send-txn-form.scss: -------------------------------------------------------------------------------- 1 | .send-txn-form { 2 | display: grid; 3 | gap: 16px; 4 | 5 | width: 424px; 6 | height: 534px; 7 | 8 | .form-field { 9 | margin: 0; 10 | } 11 | 12 | > *:nth-child(-n + 2) { 13 | border: none; 14 | box-shadow: var(--shadow-2); 15 | } 16 | } 17 | 18 | .send-txn-form__asset-img { 19 | height: 20px; 20 | } 21 | -------------------------------------------------------------------------------- /src/core/network/teller/tellerTypes.d.ts: -------------------------------------------------------------------------------- 1 | interface TellerOptions { 2 | channel: string; 3 | allowedOrigins?: string | string[]; 4 | } 5 | 6 | interface TellerMessageOptions { 7 | message: T; 8 | targetWindow: Window | MessageEventSource; 9 | origin?: string; 10 | timeout?: number; 11 | } 12 | 13 | interface TellerMessage { 14 | channel: string; 15 | message: T; 16 | } 17 | -------------------------------------------------------------------------------- /src/core/ui/icons/rekey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/component/simple-toast/util/simpleToastConstants.ts: -------------------------------------------------------------------------------- 1 | import {SimpleToastContextState} from "./simpleToastTypes"; 2 | 3 | const DEFAULT_SIMPLE_TOAST_TIMEOUT = 4000; 4 | 5 | const initialSimpleToastState: SimpleToastContextState = { 6 | toast: null, 7 | defaultAutoCloseTimeout: DEFAULT_SIMPLE_TOAST_TIMEOUT 8 | }; 9 | 10 | export {DEFAULT_SIMPLE_TOAST_TIMEOUT, initialSimpleToastState}; 11 | -------------------------------------------------------------------------------- /src/core/util/hook/useLocationWithState.tsx: -------------------------------------------------------------------------------- 1 | import {Location, useLocation} from "react-router"; 2 | 3 | type LocationWithState = Location & {state?: T}; 4 | 5 | function useLocationWithState(): Partial { 6 | const location = useLocation() as LocationWithState; 7 | 8 | return location.state || {}; 9 | } 10 | 11 | export default useLocationWithState; 12 | -------------------------------------------------------------------------------- /src/component/page/banner/PageBanner.tsx: -------------------------------------------------------------------------------- 1 | import "./_page-banner.scss"; 2 | 3 | import AlgoCurrency from "../../algo-currency/AlgoCurrency"; 4 | import AlgoPriceChange from "../../algo-currency/AlgoPriceChange"; 5 | 6 | function PageBanner() { 7 | return ( 8 |
9 | 10 | 11 |
12 | ); 13 | } 14 | 15 | export default PageBanner; 16 | -------------------------------------------------------------------------------- /src/transaction/sign-box/view/detail/type-labels/_transaction-type-label-list.scss: -------------------------------------------------------------------------------- 1 | .transaction-sign-label-list { 2 | display: flex; 3 | flex-wrap: wrap; 4 | align-items: center; 5 | gap: 8px; 6 | 7 | padding: 0 28px; 8 | } 9 | 10 | .transaction-sign-label-list__item { 11 | padding: 6px 24px; 12 | 13 | background-color: var(--layer-2); 14 | border-radius: 16px; 15 | box-shadow: var(--shadow-1); 16 | } 17 | -------------------------------------------------------------------------------- /src/account/component/account-passphrase-modal/_account-passphrase-modal.scss: -------------------------------------------------------------------------------- 1 | .account-passphrase-modal { 2 | width: 480px; 3 | } 4 | 5 | .account-passphrase-modal__title { 6 | margin-bottom: 12px; 7 | } 8 | 9 | .account-passphrase-modal__description { 10 | display: flex; 11 | flex-direction: column; 12 | gap: 8px; 13 | } 14 | 15 | .account-passphrase-modal__cta { 16 | width: 100%; 17 | 18 | margin-top: 12px; 19 | } 20 | -------------------------------------------------------------------------------- /src/asset/components/logo/_asset-logo.scss: -------------------------------------------------------------------------------- 1 | @import "../../../core/ui/typography/_typography-mixins.scss"; 2 | 3 | .asset-logo { 4 | .image__img { 5 | border-radius: 100%; 6 | } 7 | 8 | .image__placeholder { 9 | @include typography--tagline(); 10 | 11 | position: static; 12 | 13 | color: var(--light-button-disabled-text); 14 | 15 | background: var(--layer-3); 16 | border-radius: 100%; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/component/loader/pera/_pera-loader.scss: -------------------------------------------------------------------------------- 1 | .pera-loader--gray { 2 | width: 100px; 3 | height: 100px; 4 | } 5 | 6 | .pera-loader--align-center { 7 | position: absolute; 8 | top: 50%; 9 | left: 50%; 10 | 11 | transform: translate(-50%, -50%); 12 | } 13 | 14 | .pera-loader-wrapper--colorful { 15 | width: 56px; 16 | height: 56px; 17 | 18 | background-color: var(--helper-purple-900); 19 | border-radius: 50%; 20 | } 21 | -------------------------------------------------------------------------------- /src/core/util/environment/environmentConstants.ts: -------------------------------------------------------------------------------- 1 | const isProductionBuild = process.env.REACT_APP_BUILD_ENVIRONMENT === "production"; 2 | const isStagingBuild = process.env.REACT_APP_BUILD_ENVIRONMENT === "staging"; 3 | const isLocalBuild = process.env.REACT_APP_BUILD_ENVIRONMENT === "local"; 4 | 5 | const isOnStagingOrLocal = isLocalBuild || isStagingBuild; 6 | 7 | export {isProductionBuild, isStagingBuild, isLocalBuild, isOnStagingOrLocal}; 8 | -------------------------------------------------------------------------------- /src/core/util/hook/useOnUnmount.tsx: -------------------------------------------------------------------------------- 1 | import {useLayoutEffect} from "react"; 2 | 3 | function useOnUnmount(callback?: VoidFunction) { 4 | // eslint-disable-next-line arrow-body-style 5 | useLayoutEffect(() => { 6 | return () => { 7 | if (callback) { 8 | callback(); 9 | } 10 | }; 11 | // eslint-disable-next-line react-hooks/exhaustive-deps 12 | }, []); 13 | } 14 | 15 | export default useOnUnmount; 16 | -------------------------------------------------------------------------------- /src/component/image/_image.scss: -------------------------------------------------------------------------------- 1 | .image { 2 | position: relative; 3 | 4 | &__placeholder { 5 | position: absolute; 6 | top: 0; 7 | bottom: 0; 8 | left: 0; 9 | right: 0; 10 | 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | 15 | height: 100%; 16 | } 17 | 18 | &__img { 19 | width: 100%; 20 | height: 100%; 21 | 22 | object-fit: contain; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/core/ui/icons/rekey-dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/core/util/time/timezoneUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the browser's system timezone. 3 | * eg. "Canada/Central" 4 | * @return {string} Local timezone 5 | */ 6 | function getLocalTimezone() { 7 | let timeZone = ""; 8 | 9 | try { 10 | timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone; 11 | } catch (error) { 12 | console.error(error); 13 | } 14 | 15 | return timeZone; 16 | } 17 | 18 | export {getLocalTimezone}; 19 | -------------------------------------------------------------------------------- /src/component/scroll-to-top/ScrollToTop.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect} from "react"; 2 | import {useLocation} from "react-router-dom"; 3 | 4 | interface ScrollToTopProps { 5 | children: JSX.Element; 6 | } 7 | 8 | function ScrollToTop({children}: ScrollToTopProps) { 9 | const {pathname} = useLocation(); 10 | 11 | useEffect(() => { 12 | window.scrollTo(0, 0); 13 | }, [pathname]); 14 | 15 | return children; 16 | } 17 | 18 | export default ScrollToTop; 19 | -------------------------------------------------------------------------------- /src/component/network-badge/NetworkBadge.tsx: -------------------------------------------------------------------------------- 1 | import {useAppContext} from "../../core/app/AppContext"; 2 | import "./_network-badge.scss"; 3 | 4 | function NetworkBadge() { 5 | const { 6 | state: {preferredNetwork} 7 | } = useAppContext(); 8 | 9 | if (preferredNetwork !== "testnet") { 10 | return null; 11 | } 12 | 13 | return
{"TESTNET"}
; 14 | } 15 | 16 | export default NetworkBadge; 17 | -------------------------------------------------------------------------------- /src/overview/page/welcome/util/welcomePageUtils.ts: -------------------------------------------------------------------------------- 1 | function generateWelcomePageCopies(hasMasterkey: boolean) { 2 | return hasMasterkey 3 | ? { 4 | title: "Now, add your first account" 5 | } 6 | : { 7 | title: "Welcome to Pera Wallet", 8 | description: 9 | "Pera Wallet is the easiest and safest way to store, buy and swap on the Algorand blockchain." 10 | }; 11 | } 12 | 13 | export {generateWelcomePageCopies}; 14 | -------------------------------------------------------------------------------- /src/send-txn/util/sendTxnConstants.ts: -------------------------------------------------------------------------------- 1 | export const ALGORAND_ADDRESS_LENGTH = 58; 2 | export const ALGORAND_TXN_NOTE_MAX_LENGTH = 1000; 3 | export const ALGORAND_DEFAULT_TXN_WAIT_ROUNDS = 1000; 4 | 5 | export enum SEND_TXN_FORM_VALIDATION_MESSAGES { 6 | INVALID_ADDRESS = "Recipient address is not valid.", 7 | ACCOUNT_BALANCE = "Balance is not enough for this transaction.", 8 | INVALID_TXN_LENGTH = "Txn note should be no longer than 1000 characters." 9 | } 10 | -------------------------------------------------------------------------------- /.env-cmdrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "common":{ 3 | "REACT_APP_ALGOD_CLIENT_TOKEN":"", 4 | "REACT_APP_ALGOD_INDEXER_TOKEN":"" 5 | }, 6 | "development":{ 7 | "HTTPS":true, 8 | "HOST":"dev.web.perawallet.app", 9 | "PORT":3000 10 | }, 11 | "staging":{ 12 | "GENERATE_SOURCEMAP":false, 13 | "REACT_APP_BUILD_ENVIRONMENT":"staging" 14 | }, 15 | "production":{ 16 | "GENERATE_SOURCEMAP":false, 17 | "REACT_APP_BUILD_ENVIRONMENT":"production" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/component/page/header/util/useShouldShowPageHeaderGoBackButton.tsx: -------------------------------------------------------------------------------- 1 | import {useLocation} from "react-router-dom"; 2 | 3 | import {AVAILABLE_PAGE_HEADER_GO_BACK_ROUTES} from "./pageHeaderConstants"; 4 | 5 | function useShouldShowPageHeaderGoBackButton() { 6 | const location = useLocation(); 7 | 8 | return AVAILABLE_PAGE_HEADER_GO_BACK_ROUTES.some( 9 | (route) => route === location.pathname 10 | ); 11 | } 12 | 13 | export default useShouldShowPageHeaderGoBackButton; 14 | -------------------------------------------------------------------------------- /src/asset/opt-in/page/select-account/_asset-optin-select-account.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../layouts/card-layout/_card-layout.scss"; 2 | 3 | .asset-optin-select-account { 4 | @extend .card-layout; 5 | 6 | display: flex; 7 | flex-direction: column; 8 | 9 | width: 424px; 10 | height: 590px; 11 | 12 | padding: 0; 13 | } 14 | 15 | .asset-optin-select-account__go-back-button { 16 | padding: 28px 0 0; 17 | } 18 | 19 | .asset-optin-select-account__loader { 20 | margin: auto; 21 | } 22 | -------------------------------------------------------------------------------- /src/component/tooltip/_tooltip.scss: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | &.__react_component_tooltip { 3 | width: auto; 4 | max-width: 360px; 5 | 6 | padding: 10px 20px; 7 | 8 | background-color: var(--popover-bg); 9 | border: none; 10 | border-radius: 12px; 11 | 12 | &.place-top { 13 | &:before, 14 | &:after { 15 | border-top-color: var(--popover-bg); 16 | } 17 | } 18 | } 19 | } 20 | 21 | .tooltip-content { 22 | color: var(--popover-text); 23 | } 24 | -------------------------------------------------------------------------------- /src/core/ui/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/asset/opt-in/components/list/item/_asset-optin-list-item.scss: -------------------------------------------------------------------------------- 1 | .asset-optin-list-item { 2 | .list-item__click-wrapper { 3 | cursor: auto; 4 | } 5 | } 6 | 7 | .asset-optin-list-item__optin-button { 8 | width: 48px; 9 | 10 | .checkmark-icon { 11 | flex-shrink: 0; 12 | 13 | path { 14 | fill: var(--text-gray-lighter); 15 | } 16 | } 17 | 18 | .plus-icon { 19 | flex-shrink: 0; 20 | 21 | path { 22 | fill: var(--text-gray); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/account/page/import/passphrase/recovery/util/accountImportPassphraseRecoveryUtils.ts: -------------------------------------------------------------------------------- 1 | import {MNEMONIC_LENGTH} from "../../../../../util/accountConstants"; 2 | 3 | function validatePassphraseForm({ 4 | mnemonicKeys, 5 | mnemonicLength = MNEMONIC_LENGTH 6 | }: { 7 | mnemonicKeys: string[]; 8 | mnemonicLength?: number; 9 | }) { 10 | return ( 11 | mnemonicKeys.filter((key) => Boolean(key) && key.trim()).length === mnemonicLength 12 | ); 13 | } 14 | 15 | export {validatePassphraseForm}; 16 | -------------------------------------------------------------------------------- /src/send-txn/components/textarea/_send-txn-note-textarea.scss: -------------------------------------------------------------------------------- 1 | @import "../../../core/ui/typography/_typography-mixins.scss"; 2 | 3 | .send-txn-note__form-field { 4 | --textarea-height: 20px; 5 | --text-color: var(--text-main); 6 | 7 | .textarea { 8 | @include typography--medium-body(); 9 | 10 | padding: 0; 11 | 12 | background-color: var(--input-bg); 13 | 14 | border: none; 15 | 16 | &::placeholder { 17 | color: var(--text-gray-lightest); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/send-txn/page/send-txn/_send-txn.scss: -------------------------------------------------------------------------------- 1 | .send-txn { 2 | height: 600px; 3 | } 4 | 5 | .send-txn__heading { 6 | display: flex; 7 | 8 | margin-bottom: 32px; 9 | 10 | .button:active { 11 | background: none; 12 | } 13 | 14 | .button:focus { 15 | box-shadow: none; 16 | } 17 | 18 | .info-icon { 19 | path, 20 | circle { 21 | fill: var(--ghost-button-text); 22 | } 23 | } 24 | } 25 | 26 | .send-txn__header-icon path { 27 | fill: var(--helper-purple-900); 28 | } 29 | -------------------------------------------------------------------------------- /src/core/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import {ReportHandler} from "web-vitals"; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import("web-vitals").then(({getCLS, getFID, getFCP, getLCP, getTTFB}) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/core/util/algosdk/algosdkUtils.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore wordlist is not exported from algosdk/types 2 | import ALGORAND_MNEMONIC_WORDLIST from "algosdk/dist/esm/mnemonic/wordlists/english"; 3 | 4 | /** 5 | * Checks the given word exists in the mnemonic wordlist 6 | * @param {string} word string 7 | * @returns {boolean} 8 | */ 9 | function isValidMnemonicWord(word: string): boolean { 10 | return ALGORAND_MNEMONIC_WORDLIST.includes(word); 11 | } 12 | 13 | export {ALGORAND_MNEMONIC_WORDLIST, isValidMnemonicWord}; 14 | -------------------------------------------------------------------------------- /src/component/modal/util/modalTypes.ts: -------------------------------------------------------------------------------- 1 | import {ModalProps} from "../Modal"; 2 | 3 | type ModalPosition = 4 | | "center" 5 | | "top" 6 | | "top-left" 7 | | "top-right" 8 | | "right-center" 9 | | "bottom-right" 10 | | "bottom-center" 11 | | "bottom-left" 12 | | "left-center"; 13 | 14 | interface ModalStackItem extends Omit { 15 | id: string; 16 | isOpen?: boolean; 17 | onClose?: ModalProps["onClose"]; 18 | } 19 | 20 | export type {ModalPosition, ModalStackItem}; 21 | -------------------------------------------------------------------------------- /src/core/ui/icons/export.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/account/component/list/empty/_empty-account-list.scss: -------------------------------------------------------------------------------- 1 | .empty-account-list { 2 | margin: auto; 3 | 4 | text-align: center; 5 | 6 | .empty-wallet-icon { 7 | margin-bottom: 32px; 8 | 9 | path { 10 | fill: var(--text-gray-lighter); 11 | } 12 | } 13 | } 14 | 15 | .empty-account-list__cta { 16 | display: flex; 17 | gap: 8px; 18 | 19 | width: 240px; 20 | height: 48px; 21 | 22 | margin: 48px auto 0; 23 | 24 | .plus-icon path { 25 | fill: var(--primary-button-text); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/core/api/apiUtils.ts: -------------------------------------------------------------------------------- 1 | import {assetDBManager} from "../app/db"; 2 | import algod from "../util/algod/algod"; 3 | import {DEFAULT_ALGORAND_NODE_PROVIDER_TYPE} from "../util/algod/algodConstants"; 4 | import {peraApi} from "../util/pera/api/peraApi"; 5 | 6 | function updateAPIsPreferredNetwork(network: NetworkToggle) { 7 | algod.updateClient(network, DEFAULT_ALGORAND_NODE_PROVIDER_TYPE); 8 | peraApi.updateNetwork(network); 9 | assetDBManager.updateTable(network); 10 | } 11 | 12 | export {updateAPIsPreferredNetwork}; 13 | -------------------------------------------------------------------------------- /src/core/util/hook/useDebouncedValue.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from "react"; 2 | 3 | function useDebouncedValue(value: T, delay: number) { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const handler = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(handler); 13 | }; 14 | }, [value, delay]); 15 | 16 | return debouncedValue; 17 | } 18 | 19 | export default useDebouncedValue; 20 | -------------------------------------------------------------------------------- /src/core/util/hook/useTellerListener.ts: -------------------------------------------------------------------------------- 1 | import {useEffect} from "react"; 2 | 3 | import appTellerManager, {PeraTeller} from "../../app/teller/appTellerManager"; 4 | 5 | function useTellerListener( 6 | onReceiveMessage: (event: MessageEvent>) => void 7 | ) { 8 | useEffect(() => { 9 | appTellerManager.setupListener({ 10 | onReceiveMessage 11 | }); 12 | 13 | return () => appTellerManager.close(); 14 | }, [onReceiveMessage]); 15 | } 16 | 17 | export default useTellerListener; 18 | -------------------------------------------------------------------------------- /src/core/ui/icons/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/core/ui/icons/undo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/core/ui/style/_media-queries.scss: -------------------------------------------------------------------------------- 1 | $small-max-width: 1024px; 2 | $small-screen-query: "only screen and (max-width: " + $small-max-width + ")"; 3 | 4 | $large-max-width: 1900px; 5 | $large-screen-query: "only screen and (max-width: " + $large-max-width + ")" + 6 | "and (min-width: " + $small-max-width + ")"; 7 | 8 | @mixin for-small-screens { 9 | @media #{$small-screen-query} { 10 | @content; 11 | } 12 | } 13 | 14 | @mixin for-large-screens { 15 | @media #{$large-screen-query} { 16 | @content; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/core/ui/style/util/animate/_slide-in.scss: -------------------------------------------------------------------------------- 1 | .animation--slide-in { 2 | @for $i from 0 through 15 { 3 | &--delay--#{$i} { 4 | --slide-in-delay: #{calc($i / 6)}s; 5 | } 6 | } 7 | 8 | --slide-in-delay: 0s; 9 | 10 | opacity: 0; 11 | 12 | transform: translateY(-10%); 13 | animation: AnimationSlideIn 0.3s ease-in-out forwards; 14 | animation-delay: var(--slide-in-delay); 15 | } 16 | 17 | @keyframes AnimationSlideIn { 18 | 100% { 19 | opacity: 1; 20 | 21 | transform: translateY(0%); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/component/algo-currency/_algo-currency.scss: -------------------------------------------------------------------------------- 1 | .algo-currency { 2 | display: flex; 3 | align-items: center; 4 | gap: 8px; 5 | 6 | color: var(--gray-900); 7 | } 8 | 9 | .algo-currency__icon-wrapper { 10 | display: grid; 11 | place-content: center; 12 | 13 | width: 24px; 14 | height: 24px; 15 | 16 | background-color: var(--black); 17 | border-radius: 100%; 18 | 19 | .algo-icon { 20 | path { 21 | fill: var(--white); 22 | } 23 | } 24 | } 25 | 26 | .algo-currency__loader { 27 | margin: auto; 28 | } 29 | -------------------------------------------------------------------------------- /src/core/ui/icons/checkbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/core/ui/icons/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/core/ui/icons/chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/core/ui/style/util/animate/_show-up.scss: -------------------------------------------------------------------------------- 1 | .animation--show-up { 2 | @for $i from 0 through 15 { 3 | &--delay--#{$i} { 4 | --show-up-delay: #{calc($i/6)}s; 5 | } 6 | } 7 | 8 | --show-up-delay: 0s; 9 | 10 | opacity: 0; 11 | 12 | transform: scale(0.98) translateY(24px); 13 | 14 | animation: AnimationShowUp 0.28s ease-out forwards; 15 | animation-delay: var(--show-up-delay); 16 | } 17 | 18 | @keyframes AnimationShowUp { 19 | 100% { 20 | opacity: 1; 21 | 22 | transform: scale(1) translateY(0); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /public/assets/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pera Wallet", 3 | "short_name": "Pera Wallet", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#F3F3F7", 17 | "background_color": "#F3F3F7", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/transaction/sign-box/view/detail/view/summary/_transaction-sign-detail-summary-view.scss: -------------------------------------------------------------------------------- 1 | .transaction-sign-detail-summary-view__header { 2 | display: flex; 3 | align-items: center; 4 | gap: 12px; 5 | 6 | margin-bottom: 32px; 7 | padding: 28px 28px 0; 8 | } 9 | 10 | .transaction-sign-detail-summary-view__header__back-button.button { 11 | .arrow-left-icon path { 12 | fill: var(--text-main); 13 | } 14 | 15 | &:hover, 16 | &:focus { 17 | .arrow-left-icon path { 18 | fill: var(--ghost-button-text); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/core/app/db/index.ts: -------------------------------------------------------------------------------- 1 | import {DBManager} from "../../util/storage/db/DBManager"; 2 | import {appDBTables} from "./appDBManager"; 3 | import {AssetDBManager, assetDBTables} from "./assetDBManager"; 4 | 5 | const appDBManager = new DBManager("pera-wallet", appDBTables); 6 | const assetDBManager = new AssetDBManager(assetDBTables); 7 | 8 | const DATABASES_CONTAINING_SENSITIVE_INFO = [ 9 | "pera-wallet-assets" 10 | ]; 11 | 12 | export { 13 | appDBManager, 14 | assetDBManager, 15 | DATABASES_CONTAINING_SENSITIVE_INFO 16 | }; 17 | -------------------------------------------------------------------------------- /src/core/network/fetcherTypes.ts: -------------------------------------------------------------------------------- 1 | type FetcherMiddleware = ( 2 | dataFromLastMiddleware: Argument 3 | ) => Promise; 4 | 5 | interface FetcherConfig { 6 | baseUrl: string; 7 | initOptions?: RequestInit; 8 | responseMiddlewares?: FetcherMiddleware[]; 9 | rejectMiddlewares?: FetcherMiddleware[]; 10 | bodyParser?: (body: any) => any; 11 | } 12 | 13 | type FetcherErrorStatus = number | "ConnectionError" | "Cancelled" | "ApiError"; 14 | 15 | export type {FetcherMiddleware, FetcherConfig, FetcherErrorStatus}; 16 | -------------------------------------------------------------------------------- /src/settings/util/settingsUtils.ts: -------------------------------------------------------------------------------- 1 | import {DATE_FORMAT} from "../../core/util/time/timeConstants"; 2 | import {formatDateWithOptions} from "../../core/util/time/timeUtils"; 3 | 4 | function formatPeraConnectSessionDate(date: Date) { 5 | const dayMonthYear = formatDateWithOptions({ 6 | format: DATE_FORMAT.DEFAULT 7 | })(date); 8 | 9 | const hourMinute = formatDateWithOptions({ 10 | format: DATE_FORMAT.DEFAULT_HOUR_MINUTE 11 | })(date); 12 | 13 | return `${dayMonthYear} at ${hourMinute}`; 14 | } 15 | 16 | export {formatPeraConnectSessionDate}; 17 | -------------------------------------------------------------------------------- /src/account/page/import/backup/passphrase/_account-import-backup-passphrase.scss: -------------------------------------------------------------------------------- 1 | .account-import-backup-passphrase { 2 | .account-import-backup-passphrase__mnemonic-form { 3 | grid-template: repeat(3, 1fr) / repeat(4, 1fr); 4 | 5 | margin-top: 24px; 6 | } 7 | 8 | .account-mnemonic-form__grid-cell { 9 | width: 150px; 10 | } 11 | 12 | .account-mnemonic-form__cta { 13 | margin-top: 16px; 14 | } 15 | } 16 | 17 | .account-import-backup-passphrase__title, 18 | .account-import-backup-passphrase__description { 19 | margin-bottom: 24px; 20 | } 21 | -------------------------------------------------------------------------------- /src/core/ui/icons/checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/send-txn/components/input/amount/_send-txn-amount-input.scss: -------------------------------------------------------------------------------- 1 | .send-txn-amount__max-button { 2 | position: absolute; 3 | 4 | top: 50%; 5 | right: 20px; 6 | 7 | transform: translateY(-50%); 8 | } 9 | 10 | .send-txn-amount__input-container { 11 | display: grid; 12 | 13 | grid-template-columns: max-content 1fr; 14 | grid-gap: 2px; 15 | 16 | grid-auto-flow: column; 17 | } 18 | 19 | .send-txn-amount__usd-value { 20 | margin-left: 2px; 21 | } 22 | 23 | .send-txn-amount__value-placeholder { 24 | display: flex; 25 | align-items: center; 26 | } 27 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@hipo/stylelint-config-base"], 3 | "plugins": [ 4 | "stylelint-order", 5 | "stylelint-scss", 6 | "stylelint-no-unsupported-browser-features" 7 | ], 8 | "rules": { 9 | "value-list-comma-newline-after": null, 10 | "font-family-name-quotes": null, 11 | "scss/at-mixin-argumentless-call-parentheses": null, 12 | "selector-class-pattern": null, 13 | "no-descending-specificity": null, 14 | "color-hex-case": null, 15 | "color-no-hex": null, 16 | "color-function-notation": null 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/component/clipboard/button/_clipboard-button.scss: -------------------------------------------------------------------------------- 1 | .clipboard-button { 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | .clipboard-button__content { 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | gap: 8px; 11 | 12 | .copy-icon { 13 | path { 14 | fill: var(--light-button-text); 15 | } 16 | } 17 | } 18 | 19 | .clipboard-button__content__text { 20 | width: max-content; 21 | 22 | color: var(--light-button-text); 23 | } 24 | 25 | .clipboard-button__copied-message { 26 | margin-left: 24px; 27 | } 28 | -------------------------------------------------------------------------------- /src/core/network/globalNetworkModels.d.ts: -------------------------------------------------------------------------------- 1 | interface ListRequestResponse { 2 | next: null | string; 3 | previous: null | string; 4 | results: Result[]; 5 | } 6 | 7 | // TODO: confirm the existing list request params on BE 8 | type ListRequestParams = Partial<{ 9 | ordering: Ordering; 10 | limit: "all" | number; 11 | offset: number; 12 | search: string; 13 | cursor: string; 14 | }>; 15 | 16 | interface HipoApiErrorShape { 17 | type: string; 18 | detail: Record; 19 | fallback_message: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/explore/util/assets/coinbase-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/layouts/card-layout/CardLayout.tsx: -------------------------------------------------------------------------------- 1 | import "./_card-layout.scss"; 2 | 3 | import {Outlet} from "react-router-dom"; 4 | 5 | interface CardLayoutProps { 6 | hasOverlay?: boolean; 7 | } 8 | 9 | function CardLayout({hasOverlay = false}: CardLayoutProps) { 10 | return hasOverlay ? ( 11 |
12 |
13 | 14 |
15 |
16 | ) : ( 17 |
18 | 19 |
20 | ); 21 | } 22 | 23 | export default CardLayout; 24 | -------------------------------------------------------------------------------- /src/core/ui/icons/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/core/ui/icons/activity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/core/ui/icons/algo-asa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/core/util/url/urlUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Initiates URLSearchParams with the provided params object and stringifies it 3 | * @param {object} params A params object. 4 | * @returns {string} Stringified search string 5 | * @example 6 | * 7 | * stringifySearchParams({a: "123"}) 8 | * // => "a=123" 9 | * 10 | * stringifySearchParams({a: 1, b: "test", c: true}) 11 | * // => "a=1&b=test&c=true" 12 | */ 13 | function stringifySearchParams>(params: Params) { 14 | return new URLSearchParams(params).toString(); 15 | } 16 | 17 | export {stringifySearchParams}; 18 | -------------------------------------------------------------------------------- /src/explore/util/exploreUtils.ts: -------------------------------------------------------------------------------- 1 | import exploreItems from "./exploreItems.json"; 2 | 3 | /** 4 | * Returns the explore items within the given category. 5 | * @param {string} category 6 | * @returns {ExploreItem[]} ExploreItem[] 7 | */ 8 | function getExploreItems(category?: string): ExploreItem[] { 9 | let items = exploreItems; 10 | 11 | if (category?.length) { 12 | // split by comma the categories and search each item 13 | items = items.filter((item) => item.category?.split(",").includes(category)); 14 | } 15 | 16 | return items; 17 | } 18 | 19 | export {getExploreItems}; 20 | -------------------------------------------------------------------------------- /src/layouts/card-layout-without-route/CardLayoutWithoutRoute.tsx: -------------------------------------------------------------------------------- 1 | import "../card-layout/_card-layout.scss"; 2 | 3 | interface CardLayoutProps { 4 | children: React.ReactNode; 5 | hasOverlay?: boolean; 6 | } 7 | 8 | function CardLayoutWithoutRoute({children, hasOverlay = false}: CardLayoutProps) { 9 | return hasOverlay ? ( 10 |
11 |
{children}
12 |
13 | ) : ( 14 |
{children}
15 | ); 16 | } 17 | 18 | export default CardLayoutWithoutRoute; 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Pera Wallet, LDA. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /src/account/page/success/_account-success-page.scss: -------------------------------------------------------------------------------- 1 | .account-success-page__checkmark-animation { 2 | position: relative; 3 | left: -28px; 4 | 5 | width: 120px; 6 | height: 120px; 7 | } 8 | 9 | .account-success-page__header { 10 | display: flex; 11 | align-items: center; 12 | justify-content: space-between; 13 | 14 | margin-bottom: 48px; 15 | } 16 | 17 | .account-success-page__header-cta { 18 | padding: 14px 48px; 19 | } 20 | 21 | .account-success-page__explore-section { 22 | display: flex; 23 | flex-direction: column; 24 | gap: 28px; 25 | 26 | margin-top: 80px; 27 | } 28 | -------------------------------------------------------------------------------- /src/explore/util/exploreConstants.ts: -------------------------------------------------------------------------------- 1 | import {TabItem} from "@hipo/react-ui-toolkit"; 2 | 3 | const EXPLORE_CATEGORIES: TabItem[] = [ 4 | { 5 | id: "recommended", 6 | content: "Recommended" 7 | }, 8 | { 9 | id: "add_funds", 10 | content: "Add Funds" 11 | }, 12 | { 13 | id: "browse_nfts", 14 | content: "Browse NFTs" 15 | }, 16 | { 17 | id: "swap", 18 | content: "Swap" 19 | }, 20 | { 21 | id: "stake", 22 | content: "Stake" 23 | } 24 | // { 25 | // id: "shop", 26 | // content: "Shop" 27 | // } 28 | ]; 29 | 30 | export {EXPLORE_CATEGORIES}; 31 | -------------------------------------------------------------------------------- /src/component/info-box/_info-box.scss: -------------------------------------------------------------------------------- 1 | .info-box { 2 | display: flex; 3 | gap: 12px; 4 | 5 | padding: 16px 20px; 6 | 7 | background: var(--layer-1); 8 | border-radius: 16px; 9 | box-shadow: var(--shadow-2); 10 | } 11 | 12 | .info-box-icon { 13 | width: 28px; 14 | height: 28px; 15 | 16 | padding: 6px; 17 | 18 | background-color: var(--helper-blue-100); 19 | border-radius: 50%; 20 | 21 | .info-icon { 22 | path { 23 | fill: var(--helper-blue-900); 24 | } 25 | } 26 | } 27 | 28 | .info-box-content { 29 | display: flex; 30 | flex-direction: column; 31 | gap: 12px; 32 | } 33 | -------------------------------------------------------------------------------- /src/core/ui/icons/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pera Wallet", 3 | "short_name": "Pera Wallet", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "android-chrome-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "android-chrome-512x512.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | } 20 | ], 21 | "theme_color": "#F3F3F7", 22 | "background_color": "#F3F3F7", 23 | "display": "standalone" 24 | } 25 | -------------------------------------------------------------------------------- /src/asset/opt-in/util/assetOptinUtils.ts: -------------------------------------------------------------------------------- 1 | import algosdk, {Transaction} from "algosdk"; 2 | 3 | import algod from "../../../core/util/algod/algod"; 4 | 5 | async function generateAssetOptinTxn( 6 | accountAddress: string, 7 | assetId: number 8 | ): Promise { 9 | const suggestedParams = await algod.client.getTransactionParams().do(); 10 | 11 | return algosdk.makeAssetTransferTxnWithSuggestedParamsFromObject({ 12 | from: accountAddress, 13 | to: accountAddress, 14 | assetIndex: assetId, 15 | amount: 0, 16 | suggestedParams 17 | }); 18 | } 19 | 20 | export {generateAssetOptinTxn}; 21 | -------------------------------------------------------------------------------- /src/core/ui/style/_common.scss: -------------------------------------------------------------------------------- 1 | @import "../typography/_typography-mixins.scss"; 2 | @import "../style/media-queries"; 3 | 4 | .is-bold { 5 | &, 6 | &.typography { 7 | @include is-bold(); 8 | } 9 | } 10 | 11 | .hide-on-sm { 12 | @include for-small-screens { 13 | /* stylelint-disable declaration-no-important */ 14 | display: none !important; 15 | /* stylelint-enable declaration-no-important */ 16 | } 17 | } 18 | 19 | .bullet { 20 | display: inline-block; 21 | 22 | width: 4px; 23 | height: 4px; 24 | 25 | background-color: var(--text-gray-lightest); 26 | 27 | border-radius: 50%; 28 | } 29 | -------------------------------------------------------------------------------- /src/core/ui/icons/send.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/layouts/card-layout/_card-layout.scss: -------------------------------------------------------------------------------- 1 | .card-layout { 2 | position: relative; 3 | 4 | display: flex; 5 | flex-direction: column; 6 | 7 | width: max-content; 8 | 9 | margin: auto; 10 | 11 | border-radius: 16px; 12 | } 13 | 14 | .card-layout-overlay { 15 | position: fixed; 16 | top: 0; 17 | bottom: 0; 18 | left: 0; 19 | right: 0; 20 | 21 | width: 100vw; 22 | height: 100vh; 23 | 24 | background: var(--overlay); 25 | } 26 | 27 | .card-layout-content { 28 | @extend .card-layout; 29 | 30 | position: absolute; 31 | top: 50%; 32 | left: 50%; 33 | 34 | transform: translate(-50%, -50%); 35 | } 36 | -------------------------------------------------------------------------------- /src/explore/item/list/ExploreItemList.tsx: -------------------------------------------------------------------------------- 1 | import "./_explore-item-list.scss"; 2 | 3 | import {List, ListItem} from "@hipo/react-ui-toolkit"; 4 | 5 | import ExploreItemCard from "../card/ExploreItemCard"; 6 | 7 | interface ExploreItemListProps { 8 | items: ExploreItem[]; 9 | } 10 | 11 | function ExploreItemList({items}: ExploreItemListProps) { 12 | return ( 13 | 14 | {(item) => ( 15 | 16 | 17 | 18 | )} 19 | 20 | ); 21 | } 22 | 23 | export default ExploreItemList; 24 | -------------------------------------------------------------------------------- /src/component/checkbox/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CheckboxInput as HipoCheckboxInput, 3 | CheckboxInputProps as HipoCheckboxInputProps 4 | } from "@hipo/react-ui-toolkit"; 5 | import classNames from "classnames"; 6 | 7 | // This SCSS file is imported to the app under index.tsx 8 | // import "./_checkbox-input.scss"; 9 | 10 | function CheckboxInput(props: HipoCheckboxInputProps) { 11 | const {customClassName, ...otherProps} = props; 12 | const className = classNames(customClassName, `pera-checkbox-input`); 13 | 14 | return ; 15 | } 16 | 17 | export default CheckboxInput; 18 | -------------------------------------------------------------------------------- /src/core/ui/icons/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/core/ui/icons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/core/ui/icons/question-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/core/ui/icons/receive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/overview/page/show-passphrase/_account-show-passphrase.scss: -------------------------------------------------------------------------------- 1 | .account-show-passphrase-modal { 2 | width: 424px; 3 | height: auto; 4 | } 5 | 6 | .account-show-passphrase-modal__header { 7 | font-size: 24px; 8 | font-weight: 600; 9 | line-height: 29px; 10 | } 11 | 12 | .account-show-passphrase-modal__info-box { 13 | margin: 32px 0 16px; 14 | } 15 | 16 | .account-show-passphrase-modal__learn-more-link { 17 | display: flex; 18 | align-items: center; 19 | 20 | .arrow-right-icon { 21 | margin-left: 2px; 22 | } 23 | } 24 | 25 | .account-show-passphrase-modal__close-button { 26 | width: 100%; 27 | 28 | margin-top: 8px; 29 | } 30 | -------------------------------------------------------------------------------- /src/send-txn/components/link/_send-txn-link.scss: -------------------------------------------------------------------------------- 1 | .send-txn-link { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | 6 | color: currentColor; 7 | 8 | &:visited { 9 | color: inherit; 10 | } 11 | 12 | &--disabled { 13 | background: var(--background); 14 | 15 | cursor: not-allowed; 16 | 17 | pointer-events: none; 18 | } 19 | } 20 | 21 | .send-txn-link__name-row { 22 | gap: 8px; 23 | 24 | margin-top: 4px; 25 | } 26 | 27 | .send-txn-link__description-container { 28 | gap: 16px; 29 | 30 | .chevron-right-icon { 31 | path { 32 | fill: var(--text-main); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/component/skeleton/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import "./_skeleton.scss"; 3 | 4 | interface SkeletonProps { 5 | borderRadius: number; 6 | height: number; 7 | width?: number; 8 | customClassName?: string; 9 | } 10 | 11 | function Skeleton({borderRadius, height, width, customClassName}: SkeletonProps) { 12 | const skeletonStyle = { 13 | borderRadius: `${borderRadius}px`, 14 | height: `${height}px`, 15 | width: width ? `${width}px` : `100%` 16 | }; 17 | 18 | return ( 19 |
20 | ); 21 | } 22 | 23 | export default Skeleton; 24 | -------------------------------------------------------------------------------- /src/core/ui/icons/check-shield.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/transaction/view/embedded/_transaction-sign-embedded-view.scss: -------------------------------------------------------------------------------- 1 | .transaction-sign-embedded-view { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | 6 | width: 100%; 7 | height: 100%; 8 | 9 | color: var(--text-main); 10 | background: var(--card-default); 11 | 12 | transform: translate(-50%, -50%); 13 | 14 | .transaction-sign-review-transactions { 15 | height: var(--pera-wallet-modal-transaction-sign-review-box-height); 16 | } 17 | } 18 | 19 | .transaction-sign-embedded-view__spinner-wrapper { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | 24 | width: 100%; 25 | height: 100%; 26 | } 27 | -------------------------------------------------------------------------------- /src/account/component/account-mnemonic-form/_account-mnemonic-form.scss: -------------------------------------------------------------------------------- 1 | .account-mnemonic-form__grid { 2 | display: grid; 3 | grid-template: repeat(6, 1fr) / repeat(5, 1fr); 4 | gap: 32px 16px; 5 | } 6 | 7 | .account-mnemonic-form__grid-cell { 8 | width: 120px; 9 | height: 72px; 10 | 11 | margin: 0; 12 | 13 | &.form-field--has-error { 14 | border: 1px solid var(--input-border-error); 15 | box-shadow: var(--shadow-error); 16 | } 17 | } 18 | 19 | .account-mnemonic-form__grid-input .input { 20 | width: 100%; 21 | } 22 | 23 | .account-mnemonic-form__cta { 24 | width: 100%; 25 | grid-column: 1/-1; 26 | 27 | margin: auto auto 0; 28 | } 29 | -------------------------------------------------------------------------------- /src/connect/view/embedded/_connect-page-embedded-view.scss: -------------------------------------------------------------------------------- 1 | .connect-page-embedded-view__select-account-content { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | 6 | height: 100%; 7 | } 8 | 9 | .connect-page-embedded-view__select-account-content__app-information { 10 | height: calc(100% - 69px); 11 | 12 | overflow: auto; 13 | 14 | padding: 44px 28px 28px; 15 | } 16 | 17 | .connect-page-embedded-view__box { 18 | position: absolute; 19 | top: 50%; 20 | left: 50%; 21 | 22 | width: 100%; 23 | height: 100%; 24 | 25 | background: var(--card-default); 26 | 27 | transform: translate(-50%, -50%); 28 | } 29 | -------------------------------------------------------------------------------- /src/core/ui/style/override/_override.scss: -------------------------------------------------------------------------------- 1 | @import "../../typography/_typography-mixins.scss"; 2 | 3 | * { 4 | box-sizing: border-box; 5 | 6 | margin: 0; 7 | padding: 0; 8 | 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | overscroll-behavior: none; 13 | 14 | &:before { 15 | box-sizing: border-box; 16 | } 17 | 18 | &:after { 19 | box-sizing: border-box; 20 | } 21 | } 22 | 23 | ul, 24 | ol, 25 | li { 26 | list-style-type: none; 27 | } 28 | 29 | body { 30 | @include typography--body(); 31 | 32 | background-color: var(--background); 33 | } 34 | 35 | a { 36 | text-decoration: none; 37 | } 38 | -------------------------------------------------------------------------------- /src/component/simple-toast/util/simpleToastTypes.ts: -------------------------------------------------------------------------------- 1 | export interface SimpleToastData { 2 | message: string; 3 | type?: "info" | "success" | "error"; 4 | id?: string; 5 | timeout?: number; 6 | customClassName?: string; 7 | } 8 | 9 | export type SimpleToastAction = 10 | | { 11 | type: "DISPLAY"; 12 | toastData: Omit & {id: string}; 13 | } 14 | | {type: "HIDE"} 15 | | { 16 | type: "SET_DEFAULT_AUTO_CLOSE_TIMEOUT"; 17 | timeout: number; 18 | }; 19 | 20 | export interface SimpleToastContextState { 21 | toast: (Omit & {id: string}) | null; 22 | defaultAutoCloseTimeout: number; 23 | } 24 | -------------------------------------------------------------------------------- /src/core/ui/icons/asa-verified.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/component/passphrase-list/PassphraseList.tsx: -------------------------------------------------------------------------------- 1 | import "./_passphrase-list.scss"; 2 | 3 | import {List, ListItem} from "@hipo/react-ui-toolkit"; 4 | import classNames from "classnames"; 5 | 6 | function PassphraseList({ 7 | passphrase, 8 | customClassname 9 | }: { 10 | passphrase: string; 11 | customClassname?: string; 12 | }) { 13 | return ( 14 | 18 | {(word) => {word}} 19 | 20 | ); 21 | } 22 | 23 | export default PassphraseList; 24 | -------------------------------------------------------------------------------- /src/account/page/import/backup/success/_account-import-backup-success.scss: -------------------------------------------------------------------------------- 1 | .account-import-backup-success__checkmark-animation { 2 | position: relative; 3 | left: -28px; 4 | 5 | width: 120px; 6 | height: 120px; 7 | } 8 | 9 | .account-import-backup-success__header { 10 | display: flex; 11 | align-items: center; 12 | justify-content: space-between; 13 | } 14 | 15 | .account-import-backup-success__header-cta { 16 | padding: 14px 48px; 17 | } 18 | 19 | .account-import-backup-success__description { 20 | margin: 16px 0 48px; 21 | 22 | color: var(--text-gray-light); 23 | } 24 | 25 | .account-import-backup-success__list { 26 | display: grid; 27 | gap: 28px; 28 | } 29 | -------------------------------------------------------------------------------- /src/overview/page/overview/_account-overview.scss: -------------------------------------------------------------------------------- 1 | .account-overview { 2 | min-width: 948px; 3 | 4 | margin: auto; 5 | } 6 | 7 | .account-overview__onboarding { 8 | margin-top: 40px; 9 | } 10 | 11 | .account-overview__onboarding-option-list { 12 | margin-top: 20px; 13 | } 14 | 15 | .account-overview__accounts-heading { 16 | display: inline-flex; 17 | align-items: center; 18 | } 19 | 20 | .account-overview-list__skeleton { 21 | height: 240px; 22 | 23 | padding: 16px 0; 24 | 25 | background-color: var(--card-default); 26 | border-radius: 16px; 27 | box-shadow: var(--shadow-small); 28 | } 29 | 30 | .account-overview__account-number { 31 | margin-left: 8px; 32 | } 33 | -------------------------------------------------------------------------------- /src/component/page/_page.scss: -------------------------------------------------------------------------------- 1 | .page { 2 | min-height: 100vh; 3 | 4 | &--without-header { 5 | .page-header { 6 | display: none; 7 | } 8 | } 9 | 10 | &--with-pera-connect-banner { 11 | .page__header-wrapper { 12 | background-color: var(--gray-800); 13 | } 14 | 15 | .page-header { 16 | background-color: var(--background); 17 | } 18 | } 19 | } 20 | 21 | .page-content { 22 | max-width: var(--page-content-max-width); 23 | 24 | margin: 0 auto; 25 | padding: 0 32px 40px; 26 | } 27 | 28 | .page__header-wrapper { 29 | position: sticky; 30 | top: 0; 31 | z-index: var(--page-header-z-index); 32 | 33 | background-color: #dcd6ff; 34 | } 35 | -------------------------------------------------------------------------------- /src/account/page/import/pera-sync/prepare/_account-import-pera-sync-prepare.scss: -------------------------------------------------------------------------------- 1 | .account-import-pera { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | width: 424px; 6 | 7 | text-align: center; 8 | } 9 | 10 | .account-import-pera__title { 11 | margin: 60px 0 12px; 12 | 13 | font-size: 24px; 14 | line-height: 29px; 15 | letter-spacing: -0.47px; 16 | } 17 | 18 | .account-import-pera__description { 19 | color: #7e7e8c; 20 | 21 | font-size: 14px; 22 | font-weight: 400; 23 | line-height: 24px; 24 | } 25 | 26 | .account-import-pera__icon { 27 | margin: 64px auto 0; 28 | } 29 | 30 | .account-import-pera__cta { 31 | width: 100%; 32 | 33 | margin-top: 48px; 34 | } 35 | -------------------------------------------------------------------------------- /src/core/ui/icons/exclamation-shield.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/core/ui/style/override/hipo-ui-toolkit/_select.scss: -------------------------------------------------------------------------------- 1 | .select-trigger { 2 | --select-trigger-height: unset; 3 | --select-trigger-bg: unset; 4 | --select-trigger-color: unset; 5 | --select-trigger-border-radius: unset; 6 | 7 | justify-content: unset; 8 | 9 | border: unset; 10 | 11 | &:focus { 12 | border: none; 13 | } 14 | } 15 | 16 | .select-content { 17 | right: 0; 18 | 19 | background-color: var(--background); 20 | border: 1px solid var(--border-default); 21 | border-radius: 12px; 22 | box-shadow: var(--shadow-2); 23 | } 24 | 25 | .select-item { 26 | justify-content: unset; 27 | 28 | &:hover { 29 | color: unset; 30 | background-color: unset; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/component/page/header/util/pageHeaderUtils.ts: -------------------------------------------------------------------------------- 1 | import ROUTES from "../../../../core/route/routes"; 2 | 3 | function getPageHeaderBackButtonText(goBackLink: string) { 4 | let text = ""; 5 | 6 | switch (goBackLink) { 7 | case ROUTES.ACCOUNT.ROUTE: 8 | text = "Add Account"; 9 | break; 10 | 11 | case ROUTES.OVERVIEW.ROUTE: 12 | text = "Accounts"; 13 | break; 14 | 15 | case ROUTES.ACCOUNT.IMPORT.FULL_PATH: 16 | text = "Import Account"; 17 | break; 18 | 19 | case ROUTES.BASE: 20 | text = "Home"; 21 | break; 22 | 23 | default: 24 | break; 25 | } 26 | 27 | return `Back to ${text}`; 28 | } 29 | 30 | export {getPageHeaderBackButtonText}; 31 | -------------------------------------------------------------------------------- /src/core/ui/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/transaction/view/tab/_transaction-sign-tab-view.scss: -------------------------------------------------------------------------------- 1 | .transaction-sign-tab-view { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | gap: 24px; 6 | 7 | width: 480px; 8 | 9 | margin: 0 auto; 10 | } 11 | 12 | .transaction-sign-tab-view__access-page { 13 | background-color: var(--card-default); 14 | border-radius: 16px; 15 | box-shadow: var(--shadow-small); 16 | } 17 | 18 | .transaction-sign-tab-view__spinner-wrapper { 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | 23 | width: 480px; 24 | height: 578px; 25 | 26 | color: var(--text-main); 27 | background: var(--card-default); 28 | 29 | border-radius: 16px; 30 | box-shadow: var(--shadow-small); 31 | } 32 | -------------------------------------------------------------------------------- /src/core/ui/icons/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/core/ui/style/_measure.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable custom-property-empty-line-before */ 2 | @import "./_media-queries.scss"; 3 | 4 | :root { 5 | --page-content-max-width: 1175px; 6 | --page-header-height: auto; 7 | --page-header-z-index: 1; 8 | --page-body-horizontal-padding: auto; 9 | --sidebar-width: 240px; 10 | 11 | --pera-wallet-modal-compact-width: 380px; 12 | --pera-wallet-modal-compact-height: 396px; 13 | --pera-wallet-modal-transaction-sign-review-box-height: 398px; 14 | 15 | --simple-toast-z-index: 5; 16 | --send-txn-select-account-z-index: 2; 17 | --default-animation: 0.25s ease; 18 | } 19 | 20 | @include for-small-screens { 21 | :root { 22 | --page-content-max-width: 100%; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/core/util/image/imageUtils.ts: -------------------------------------------------------------------------------- 1 | import {AccountASA} from "../pera/api/peraApiModels"; 2 | import {generatePrismUrl} from "../prism/prismUtils"; 3 | 4 | function getAssetImgSrc(asset: Asset | AccountASA, width?: number, height?: number) { 5 | const imgSrc = asset.collectible?.primary_image || asset.logo; 6 | 7 | return imgSrc 8 | ? // eslint-disable-next-line no-magic-numbers 9 | generatePrismUrl({width: width || 96, height: height || 96, quality: 70})(imgSrc) 10 | : ""; 11 | } 12 | 13 | function getAssetPlaceholderContent(asset: Asset): string { 14 | // eslint-disable-next-line no-magic-numbers 15 | return asset.name.substring(0, 2).toUpperCase(); 16 | } 17 | 18 | export {getAssetImgSrc, getAssetPlaceholderContent}; 19 | -------------------------------------------------------------------------------- /src/core/util/time/timeConstants.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-magic-numbers */ 2 | 3 | const DATE_FORMAT = { 4 | DEFAULT: "MMMM d, y", 5 | DEFAULT_MONTH_AND_DAY: "MMM d", 6 | MONTH_YEAR_AND_HOUR_MINUTE: "MMM yyyy, 'at' h:mm a", 7 | DEFAULT_HOUR_MINUTE: "h:mm a", 8 | MONTH: "MMM", 9 | API_FORMAT: "yyyy-MM-dd" 10 | }; 11 | const SECOND_IN_MS = 1000; 12 | const MINUTE_IN_S = 60; 13 | const MINUTE_IN_MS = MINUTE_IN_S * SECOND_IN_MS; 14 | const HALF_MINUTE_IN_MS = MINUTE_IN_MS / 2; 15 | const HOUR_IN_MINUTES = 60; 16 | const DAY_IN_HOURS = 24; 17 | const WEEK_IN_DAYS = 7; 18 | 19 | export { 20 | DATE_FORMAT, 21 | SECOND_IN_MS, 22 | HALF_MINUTE_IN_MS, 23 | MINUTE_IN_MS, 24 | HOUR_IN_MINUTES, 25 | DAY_IN_HOURS, 26 | WEEK_IN_DAYS 27 | }; 28 | -------------------------------------------------------------------------------- /src/component/list/searchable-list/_searchable-list.scss: -------------------------------------------------------------------------------- 1 | .searchable-list { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 16px; 5 | 6 | overflow-y: auto; 7 | } 8 | 9 | .searchable-list__search { 10 | .form-field__label { 11 | display: flex; 12 | gap: 16px; 13 | } 14 | 15 | .search-icon { 16 | path { 17 | stroke: var(--text-gray-lighter); 18 | } 19 | } 20 | 21 | .input::placeholder { 22 | color: var(--text-gray-lighter); 23 | opacity: 1; 24 | } 25 | 26 | .input-container__icon { 27 | width: 16px; 28 | height: 16px; 29 | } 30 | 31 | .input-container__left-icon { 32 | margin-right: 16px; 33 | } 34 | } 35 | 36 | .searchable-list__no-result-message { 37 | margin: auto; 38 | } 39 | -------------------------------------------------------------------------------- /src/component/page/header/util/pageHeaderConstants.ts: -------------------------------------------------------------------------------- 1 | import ROUTES from "../../../../core/route/routes"; 2 | 3 | const AVAILABLE_PAGE_HEADER_GO_BACK_ROUTES = [ 4 | ROUTES.ACCOUNT.ROUTE, 5 | ROUTES.ACCOUNT.CREATE.FULL_PATH, 6 | ROUTES.ACCOUNT.IMPORT.FULL_PATH, 7 | ROUTES.ACCOUNT.IMPORT.PASSPHRASE.FULL_PATH, 8 | ROUTES.ACCOUNT.IMPORT.PASSPHRASE.NAME.FULL_PATH, 9 | ROUTES.ACCOUNT.IMPORT.PASSPHRASE.RECOVERY.FULL_PATH, 10 | ROUTES.ACCOUNT.IMPORT.PERA_SYNC.FULL_PATH, 11 | ROUTES.ACCOUNT.IMPORT.PERA_SYNC.QR.FULL_PATH, 12 | ROUTES.ACCOUNT.IMPORT.LEDGER.FULL_PATH, 13 | ROUTES.PASSWORD.CREATE.FULL_PATH, 14 | ROUTES.ASSET_OPTIN.ROUTE, 15 | ROUTES.ASSET_OPTIN.ACCOUNTS.FULL_PATH 16 | ]; 17 | 18 | export {AVAILABLE_PAGE_HEADER_GO_BACK_ROUTES}; 19 | -------------------------------------------------------------------------------- /src/core/ui/icons/backup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/core/ui/icons/ledger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/core/route/navigate/NavigateFlow.tsx: -------------------------------------------------------------------------------- 1 | import {Navigate, NavigateProps, To} from "react-router-dom"; 2 | 3 | import useLocationWithState from "../../util/hook/useLocationWithState"; 4 | import ROUTES from "../routes"; 5 | 6 | type NavigateFlowProps = Omit & { 7 | children: JSX.Element; 8 | to?: To; 9 | }; 10 | 11 | type LocationState = { 12 | isNavigated: boolean; 13 | }; 14 | 15 | function NavigateFlow(props: NavigateFlowProps) { 16 | const {to, children, ...rest} = props; 17 | const {isNavigated} = useLocationWithState(); 18 | 19 | if (!isNavigated) { 20 | return ; 21 | } 22 | 23 | return children; 24 | } 25 | 26 | export default NavigateFlow; 27 | -------------------------------------------------------------------------------- /src/core/ui/style/override/hipo-ui-toolkit/_toggle.scss: -------------------------------------------------------------------------------- 1 | @import "../../../typography/_typography-mixins.scss"; 2 | 3 | .toggle--is-horizontal { 4 | width: 100%; 5 | 6 | border: none; 7 | } 8 | 9 | .toggle-item { 10 | --toggle-active-color: var(--text-gray); 11 | --toggle-active-bg: transparent; 12 | --toggle-inactive-color: var(--text-gray); 13 | --toggle-inactive-bg: transparent; 14 | 15 | border-radius: 10px; 16 | 17 | transition: background-color var(--default-animation); 18 | 19 | &--is-selected { 20 | --toggle-active-color: var(--secondary-button-text); 21 | --toggle-active-bg: var(--secondary-button-bg); 22 | 23 | pointer-events: none; 24 | } 25 | } 26 | 27 | .toggle-item__label { 28 | @include typography--button(); 29 | } 30 | -------------------------------------------------------------------------------- /src/add-funds/account-select/_add-funds-account-select-modal.scss: -------------------------------------------------------------------------------- 1 | .add-funds-account-select-modal-container { 2 | .modal__body { 3 | padding: 0; 4 | } 5 | } 6 | 7 | .add-funds-account-select-modal { 8 | width: 480px; 9 | 10 | .searchable-list__search { 11 | margin: 0 28px; 12 | } 13 | } 14 | 15 | .add-funds-account-select-modal__go-back-button-container { 16 | display: flex; 17 | align-items: center; 18 | 19 | margin-bottom: 32px; 20 | padding: 28px 28px 0 0; 21 | } 22 | 23 | .add-funds-account-select-modal__go-back-button { 24 | &.button--custom { 25 | margin-left: 8px; 26 | padding: 0 16px; 27 | } 28 | } 29 | 30 | .add-funds-account-select-modal__go-back-button-icon { 31 | path { 32 | fill: var(--text-main); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/component/lock-button/_lock-button.scss: -------------------------------------------------------------------------------- 1 | @import "../../core//ui//typography/typography-mixins"; 2 | 3 | .lock-button, 4 | .lock-button__shortcut { 5 | @include typography--button(); 6 | 7 | display: flex; 8 | align-items: center; 9 | } 10 | 11 | .lock-button.button { 12 | --button-bg: var(--layer-1); 13 | 14 | gap: 8px; 15 | 16 | width: 124px; 17 | height: 40px; 18 | 19 | box-shadow: var(--shadow-2); 20 | 21 | .lock-icon { 22 | path { 23 | fill: var(--light-button-text); 24 | } 25 | } 26 | 27 | &.button--is-inactive { 28 | .lock-icon { 29 | path { 30 | fill: var(--light-button-disabled-text); 31 | } 32 | } 33 | } 34 | } 35 | 36 | .lock-button__shortcut { 37 | color: var(--text-gray-lighter); 38 | } 39 | -------------------------------------------------------------------------------- /src/core/ui/icons/wallet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/core/ui/icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/core/ui/icons/key.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/transaction/detail/list/list-item/content/_transaction-details-list-item-content.scss: -------------------------------------------------------------------------------- 1 | .transaction-details-list-item-content__footer { 2 | display: flex; 3 | align-items: center; 4 | gap: 8px; 5 | } 6 | 7 | .transaction-details-list-item-content { 8 | .chevron-right-icon { 9 | opacity: 0; 10 | 11 | transition: opacity var(--default-animation); 12 | 13 | path { 14 | fill: var(--text-gray-lighter); 15 | } 16 | } 17 | 18 | &:hover { 19 | .chevron-right-icon { 20 | opacity: 1; 21 | } 22 | } 23 | } 24 | 25 | .transaction-details-list-item-content__warning { 26 | display: flex; 27 | align-items: center; 28 | gap: 4px; 29 | 30 | .warning-icon { 31 | path { 32 | fill: var(--helper-red-900); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # editors 9 | .*.swp 10 | .*.swo 11 | 12 | # testing 13 | /coverage 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 25 | 26 | # dependencies 27 | /node_modules 28 | /.pnp 29 | .pnp.js 30 | 31 | # testing 32 | /coverage 33 | 34 | # production 35 | /build 36 | 37 | # misc 38 | .DS_Store 39 | .env.local 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | 44 | npm-debug.log* 45 | yarn-debug.log* 46 | yarn-error.log* 47 | -------------------------------------------------------------------------------- /src/core/network/FetcherError.ts: -------------------------------------------------------------------------------- 1 | import {FetcherErrorStatus} from "./fetcherTypes"; 2 | 3 | class FetcherError extends Error { 4 | data: any; 5 | type: FetcherErrorStatus; 6 | statusCode: number; 7 | 8 | constructor( 9 | options: { 10 | type: FetcherErrorStatus; 11 | data: any; 12 | message: string; 13 | statusCode: number; 14 | }, 15 | ...args: any[] 16 | ) { 17 | super(...args); 18 | 19 | if (Error.captureStackTrace) { 20 | Error.captureStackTrace(this, FetcherError); 21 | } 22 | 23 | this.name = "FetcherError"; 24 | this.type = options.type; 25 | this.data = options.data; 26 | this.message = options.message; 27 | this.statusCode = options.statusCode; 28 | } 29 | } 30 | 31 | export default FetcherError; 32 | -------------------------------------------------------------------------------- /src/core/ui/icons/tick-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/core/util/hook/formito/formitoStateReducer.ts: -------------------------------------------------------------------------------- 1 | type FormitoReducerAction = 2 | | { 3 | type: "SET_FORM_VALUE"; 4 | payload: Partial; 5 | } 6 | | {type: "RESET_FORM_STATE"; state: T}; 7 | 8 | function formitoStateReducer(state: T, action: FormitoReducerAction): T { 9 | let newState = state; 10 | 11 | switch (action.type) { 12 | case "SET_FORM_VALUE": { 13 | newState = { 14 | ...state, 15 | ...action.payload 16 | }; 17 | 18 | break; 19 | } 20 | 21 | case "RESET_FORM_STATE": { 22 | newState = action.state; 23 | break; 24 | } 25 | 26 | default: 27 | break; 28 | } 29 | 30 | return newState; 31 | } 32 | 33 | export default formitoStateReducer; 34 | export type {FormitoReducerAction}; 35 | -------------------------------------------------------------------------------- /src/core/network/async-process/asyncProcess.d.ts: -------------------------------------------------------------------------------- 1 | interface AsyncProcessState { 2 | isRequestPending: boolean; 3 | isRequestFetched: boolean; 4 | data: Data | null; 5 | error: Error | null; 6 | requestPayload?: Payload; 7 | } 8 | 9 | interface UseAsyncProcessOptions { 10 | initialState?: AsyncProcessState; 11 | shouldResetDataWhenPending?: boolean; 12 | } 13 | 14 | type AsyncProcessCallBack = ( 15 | promise: Promise, 16 | options?: { 17 | forceResetPreviousAsyncState?: boolean; 18 | responseSerializer?: (response: Response) => Data; 19 | } 20 | ) => Promise; 21 | 22 | type AsyncStateSetter = React.Dispatch< 23 | React.SetStateAction> 24 | >; 25 | -------------------------------------------------------------------------------- /src/core/util/string/stringUtils.ts: -------------------------------------------------------------------------------- 1 | function encodeString(text: string) { 2 | return new TextEncoder().encode(text); 3 | } 4 | 5 | /** 6 | * @param options Options 7 | * @returns A randomly generated string 8 | */ 9 | function generateRandomString(options?: {radix?: number; substringStartIndex?: number}) { 10 | // eslint-disable-next-line no-magic-numbers 11 | const {radix = 36, substringStartIndex = 7} = options || {}; 12 | 13 | return Math.random().toString(radix).substring(substringStartIndex); 14 | } 15 | 16 | function getFirstChars(text: string, charCount?: number) { 17 | const words = text.split(" ").slice(0, charCount); 18 | const firstChars = words.map((word) => word[0]); 19 | 20 | return firstChars; 21 | } 22 | 23 | export {encodeString, generateRandomString, getFirstChars}; 24 | -------------------------------------------------------------------------------- /src/overview/page/show-qr/_account-show-qr.scss: -------------------------------------------------------------------------------- 1 | .account-show-qr-modal { 2 | width: 360px; 3 | 4 | text-align: center; 5 | } 6 | 7 | #react-qrcode-logo { 8 | margin: 32px 0 40px; 9 | 10 | border-radius: 24px; 11 | box-shadow: var(--shadow-3); 12 | } 13 | 14 | .account-show-qr-modal__address { 15 | width: 320px; 16 | 17 | margin: 8px auto 16px; 18 | 19 | word-wrap: break-word; 20 | } 21 | 22 | .account-show-qr-modal__clipboard-button.button { 23 | width: 127px; 24 | 25 | margin: 0 auto 36px; 26 | 27 | .clipboard-button__content__text { 28 | color: var(--secondary-button-text); 29 | } 30 | 31 | .copy-icon { 32 | path { 33 | fill: var(--secondary-button-text); 34 | } 35 | } 36 | } 37 | 38 | .account-show-qr-modal__close-button { 39 | width: 100%; 40 | } 41 | -------------------------------------------------------------------------------- /src/core/ui/icons/dapp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/core/ui/icons/account-rekeyed-orphan.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/core/ui/icons/account-rekeyed-ledger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/core/ui/icons/account-rekeyed-standard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/component/loader/simple/SimpleLoader.tsx: -------------------------------------------------------------------------------- 1 | import "./_simple-loader.scss"; 2 | 3 | import classNames from "classnames"; 4 | 5 | function SimpleLoader({customClassName}: {customClassName?: string}) { 6 | return ( 7 | 13 | 20 | 21 | ); 22 | } 23 | 24 | export default SimpleLoader; 25 | -------------------------------------------------------------------------------- /src/asset/components/logo/AssetLogo.tsx: -------------------------------------------------------------------------------- 1 | import "./_asset-logo.scss"; 2 | 3 | import {memo} from "react"; 4 | import classNames from "classnames"; 5 | 6 | import Image from "../../../component/image/Image"; 7 | interface AssetLogoProps { 8 | src: string; 9 | assetName: string; 10 | size?: number; 11 | customClassName?: string; 12 | } 13 | 14 | // eslint-disable-next-line no-magic-numbers 15 | function AssetLogo({src, assetName, size = 32, customClassName}: AssetLogoProps) { 16 | return ( 17 | {assetName.substring(0, 25 | ); 26 | } 27 | 28 | export default memo(AssetLogo); 29 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # Pera Web Wallet 2 | 3 | ## Development 4 | 5 | This repo was created with [Create React App](https://github.com/facebook/create-react-app). Therefore, the usual react-scripts are available in this project. 6 | 7 | Recommended system versions 8 | 9 | - `node 18.x` 10 | - `npm 8.x` 11 | 12 | ### Install dependencies 13 | 14 | - `npm install` 15 | 16 | ### Start the development environment 17 | 18 | - `npm start` 19 | 20 | ### Husky and lint-staged 21 | 22 | [Husky](https://github.com/typicode/husky) is configured with [lint-staged](https://github.com/okonet/lint-staged) to run ESLint, Stylelint and Prettier on the staged files, and then type-check the application before committing your changes. 23 | 24 | ### Versioning 25 | 26 | We follow [SemVer](https://semver.org/) convention to update the version for each release. 27 | -------------------------------------------------------------------------------- /src/core/ui/icons/asa-suspicious.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/pera-connect-error/util/peraConnectErrorScreenConstants.ts: -------------------------------------------------------------------------------- 1 | export const PERA_CONNECT_ERROR_SCREEN_RESOLVED_BY_ITEMS = [ 2 | { 3 | id: "disable-pop-up-blocker", 4 | title: "Disabling your browser's pop up blocker", 5 | link: "https://support.perawallet.app/en/article/disabling-pop-up-blockers-and-extensions-on-common-browsers-1gq3ppe/?bust=1678959962006", 6 | description: "How to disable pop-ups on common browsers" 7 | }, 8 | 9 | { 10 | id: "disable-browser-extension", 11 | title: "Disabling browser extensions", 12 | link: "https://support.perawallet.app/en/article/disabling-pop-up-blockers-and-extensions-on-common-browsers-1gq3ppe/?bust=1678959962006", 13 | description: "How to disable extensions on common browsers" 14 | } 15 | ]; 16 | 17 | export const PERA_CONNECT_SHOW_ERROR_SCREEN_TIMEOUT = 30000; 18 | -------------------------------------------------------------------------------- /src/transaction/sign-box/message/_transaction-sign-detail-message.scss: -------------------------------------------------------------------------------- 1 | .transaction-sign-detail-message { 2 | display: grid; 3 | align-items: center; 4 | grid-template-columns: 24px auto; 5 | gap: 12px; 6 | 7 | margin-bottom: 20px; 8 | padding: 16px; 9 | 10 | border-radius: 16px; 11 | 12 | word-break: break-word; 13 | 14 | &--warning { 15 | color: var(--helper-red-900); 16 | background-color: var(--helper-red-100); 17 | 18 | .warning-icon { 19 | path { 20 | fill: var(--helper-red-900); 21 | } 22 | } 23 | } 24 | 25 | &--info { 26 | color: var(--helper-purple-900); 27 | background-color: var(--helper-purple-100); 28 | box-shadow: var(--shadow-1); 29 | 30 | .info-icon { 31 | path { 32 | fill: var(--helper-purple-900); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/not-supported/firefox-incognito/_firefox-incognito-landing-page.scss: -------------------------------------------------------------------------------- 1 | .firefox-incognito-landing-page { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | 7 | width: 100vw; 8 | height: 100vh; 9 | 10 | color: var(--white); 11 | background-color: var(--purple-500); 12 | 13 | text-align: center; 14 | 15 | .pera-logo { 16 | position: absolute; 17 | top: 10%; 18 | left: 50%; 19 | 20 | width: 150px; 21 | height: 56px; 22 | 23 | transform: translateX(-50%); 24 | } 25 | 26 | .firefox-incognito-landing-page__header { 27 | margin: 40px 0 16px; 28 | } 29 | 30 | .firefox-incognito-landing-page__figure { 31 | width: 50%; 32 | } 33 | 34 | .firefox-incognito-landing-page__figure-img { 35 | width: 75%; 36 | height: auto; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/core/ui/icons/lock-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/component/async-content/AsyncContent.tsx: -------------------------------------------------------------------------------- 1 | export type AsyncContentStatus = "error" | "pending" | "success"; 2 | export type AsyncContentError = undefined | AsyncProcessState["error"]; 3 | 4 | interface AsyncContentProps { 5 | requestStates: AsyncProcessState[]; 6 | content: (status: AsyncContentStatus, error?: AsyncContentError) => JSX.Element; 7 | } 8 | 9 | function AsyncContent({requestStates, content}: AsyncContentProps) { 10 | const isAllFetched = requestStates.every((request) => request.isRequestFetched); 11 | const requestError = requestStates.find((request) => request.error); 12 | let node = content("pending"); 13 | 14 | if (requestError) { 15 | node = content("error", requestError.error); 16 | } else if (isAllFetched) { 17 | node = content("success"); 18 | } 19 | 20 | return node; 21 | } 22 | 23 | export default AsyncContent; 24 | -------------------------------------------------------------------------------- /src/core/ui/icons/create-password-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/component/pera-qr-code/PeraQRCode.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-magic-numbers */ 2 | import PeraQRLogo from "../../core/ui/image/pera-qr-logo.png"; 3 | 4 | import {QRCode} from "react-qrcode-logo"; 5 | 6 | function PeraQRCode({value}: {value: string}) { 7 | return ( 8 | 31 | ); 32 | } 33 | 34 | export default PeraQRCode; 35 | /* eslint-enable no-magic-numbers */ 36 | -------------------------------------------------------------------------------- /src/core/util/hook/useDBAccounts.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from "react"; 2 | 3 | import {useAppContext} from "../../app/AppContext"; 4 | import {appDBManager} from "../../app/db"; 5 | 6 | function useDBAccounts() { 7 | const { 8 | state: {masterkey} 9 | } = useAppContext(); 10 | 11 | const [dbAccounts, setDBAccounts] = useState(); 12 | 13 | useEffect(() => { 14 | let ignore = false; 15 | 16 | if (!masterkey) return undefined; 17 | 18 | appDBManager 19 | .decryptTableEntries( 20 | "accounts", 21 | masterkey! 22 | )("address") 23 | .then((accounts) => { 24 | if (!ignore) setDBAccounts(accounts); 25 | }); 26 | 27 | return () => { 28 | ignore = true; 29 | }; 30 | }, [masterkey]); 31 | 32 | return dbAccounts; 33 | } 34 | 35 | export default useDBAccounts; 36 | -------------------------------------------------------------------------------- /src/settings/transfer-mobile/flow/TransferMobileFlow.tsx: -------------------------------------------------------------------------------- 1 | import {Routes, Route} from "react-router-dom"; 2 | 3 | import ROUTES from "../../../core/route/routes"; 4 | import CardLayout from "../../../layouts/card-layout/CardLayout"; 5 | import AccountQRCodeSync from "../../../account/page/qr-code-sync/AccountQRCodeSync"; 6 | import TransferMobileSelectAccounts from "../page/select-accounts/TransferMobileSelectAccounts"; 7 | 8 | function TransferMobileFlow() { 9 | return ( 10 | 11 | }> 12 | } /> 13 | 14 | } 17 | /> 18 | 19 | 20 | ); 21 | } 22 | 23 | export default TransferMobileFlow; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | 19 | /* Additional Checks */ 20 | "noUnusedLocals": true /* Report errors on unused locals. */, 21 | "noUnusedParameters": true /* Report errors on unused parameters. */, 22 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */ 23 | }, 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /src/account/accountModels.ts: -------------------------------------------------------------------------------- 1 | export interface AccountBackup { 2 | id: string; 3 | encrypted_backup: null; 4 | date_created: string; 5 | modification_key: string; 6 | } 7 | 8 | export type LegacyMobileSyncAccount = { 9 | name: string; 10 | private_key: string; // stringified uint8Array 11 | }; 12 | 13 | export type ARCStandardAccountType = 14 | | "single" 15 | | "multisig" 16 | | "watch" 17 | | "ledger" 18 | | "contact"; 19 | 20 | export type ARCStandardMobileSyncAccount = { 21 | name: string; 22 | address: string; 23 | private_key: string; // base64 24 | account_type: "single"; // pera supports single accounts for now 25 | metadata?: string; 26 | }; 27 | 28 | export type MobileSyncAccount = ARCStandardMobileSyncAccount | LegacyMobileSyncAccount; 29 | 30 | export type EncryptedPeraMobileAccounts = { 31 | device_id: string; 32 | accounts: string; 33 | }; 34 | -------------------------------------------------------------------------------- /src/asset/opt-in/flow/AssetOptinFlow.tsx: -------------------------------------------------------------------------------- 1 | import {Route, Routes} from "react-router-dom"; 2 | 3 | import ROUTES from "../../../core/route/routes"; 4 | import AssetOptinPage from "../page/AssetOptinPage"; 5 | import AssetOptinSelectAccount from "../page/select-account/AssetOptinSelectAccount"; 6 | import Page from "../../../component/page/Page"; 7 | import {withGoBackLink} from "../../../core/route/context/NavigationContext"; 8 | 9 | function AssetOptinFlow() { 10 | return ( 11 | 12 | }> 13 | } /> 14 | 15 | } 18 | /> 19 | 20 | 21 | ); 22 | } 23 | 24 | export default withGoBackLink(AssetOptinFlow, ROUTES.OVERVIEW.ROUTE); 25 | -------------------------------------------------------------------------------- /src/core/ui/icons/account-watch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/component/passphrase-list/_passphrase-list.scss: -------------------------------------------------------------------------------- 1 | .passphrase-list { 2 | display: grid; 3 | grid-template-rows: repeat(9, 1fr); 4 | grid-template-columns: repeat(3, auto); 5 | grid-gap: 8px 12px; 6 | 7 | grid-auto-flow: column; 8 | 9 | margin: 16px auto 24px; 10 | padding: 24px 8px; 11 | 12 | background-color: var(--layer-2); 13 | border-radius: 12px; 14 | 15 | counter-reset: item; 16 | } 17 | 18 | .passphrase-list-item { 19 | transform: translateX(28px); 20 | list-style-type: none; 21 | 22 | &:before { 23 | display: inline-block; 24 | /* stylelint-disable-next-line unit-allowed-list */ 25 | width: 2ch; 26 | 27 | margin-right: 8px; 28 | 29 | color: var(--text-gray-lighter); 30 | 31 | content: counter(item) ""; 32 | counter-increment: item; 33 | } 34 | 35 | &:nth-child(8n + 1):not(:last-child) { 36 | grid-row: 1; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/core/util/hook/formito/useFormito.tsx: -------------------------------------------------------------------------------- 1 | import {Reducer, useReducer} from "react"; 2 | 3 | import formitoStateReducer, {FormitoReducerAction} from "./formitoStateReducer"; 4 | 5 | function useFormito(initialState: T) { 6 | const [formitoState, dispatchFormitoAction] = useReducer< 7 | Reducer> 8 | >(formitoStateReducer, initialState); 9 | 10 | return { 11 | formitoState, 12 | dispatchFormitoAction 13 | }; 14 | } 15 | 16 | export default useFormito; 17 | 18 | /* USAGE: 19 | const initialLoginFormState = { 20 | email: "", 21 | password: "" 22 | rememberMe: false 23 | }; 24 | 25 | const {formitoState, dispatchFormitoAction} = useFormito(initialLoginFormState); 26 | 27 | dispatchFormitoAction({ 28 | type: "SET_FORM_VALUE", 29 | payload: { 30 | [currentTarget.name]: currentTarget.value 31 | } 32 | }) 33 | */ 34 | -------------------------------------------------------------------------------- /src/core/util/pera/explorer/getPeraExplorerLink.ts: -------------------------------------------------------------------------------- 1 | // TODO add other types here in case of need 2 | type PeraExplorerLinkType = "account-detail" | "asset-detail"; 3 | 4 | function getPeraExplorerLink({ 5 | network, 6 | type, 7 | id 8 | }: { 9 | network: NetworkToggle; 10 | type: PeraExplorerLinkType; 11 | id: string; 12 | }): string { 13 | const origin = 14 | network === "mainnet" 15 | ? "https://explorer.perawallet.app" 16 | : "https://testnet.explorer.perawallet.app"; 17 | let link = ""; 18 | 19 | switch (type) { 20 | case "account-detail": 21 | link = `${origin}/accounts/${encodeURIComponent(id)}`; 22 | break; 23 | 24 | case "asset-detail": 25 | link = `${origin}/assets/${encodeURIComponent(id)}`; 26 | break; 27 | 28 | default: 29 | break; 30 | } 31 | 32 | return link; 33 | } 34 | 35 | export {getPeraExplorerLink}; 36 | -------------------------------------------------------------------------------- /src/overview/context/PortfolioOverviewContext.tsx: -------------------------------------------------------------------------------- 1 | import {createContext, ReactNode, useContext} from "react"; 2 | 3 | import usePortfolioOverview from "../util/hook/usePortfolioOverview"; 4 | 5 | const PortfolioContext = createContext( 6 | undefined as unknown as ReturnType 7 | ); 8 | 9 | PortfolioContext.displayName = "PortfolioContext"; 10 | 11 | export function PortfolioContextProvider({children}: {children: ReactNode}) { 12 | const portfolioOverview = usePortfolioOverview(); 13 | 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | } 20 | 21 | export function usePortfolioContext() { 22 | const context = useContext(PortfolioContext); 23 | 24 | // password required routes except settings 25 | // display spinner until context is fetched 26 | return context!; 27 | } 28 | -------------------------------------------------------------------------------- /src/account/component/list/searchable/_searchable-account-list.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../core/ui/typography/_typography-mixins.scss"; 2 | 3 | .searchable-account-list { 4 | display: flex; 5 | flex-direction: column; 6 | 7 | .searchable-list { 8 | min-height: 210px; 9 | 10 | padding: 28px; 11 | } 12 | 13 | &--colored-background { 14 | .searchable-list { 15 | background-color: var(--layer-2); 16 | border-radius: 12px; 17 | box-shadow: var(--shadow-inline); 18 | } 19 | } 20 | } 21 | 22 | .searchable-account-list-search { 23 | margin: 0 28px 24px; 24 | } 25 | 26 | .searchable-account-list-item { 27 | padding-bottom: 16px; 28 | 29 | &:not(:last-child) { 30 | border-bottom: 1px solid var(--border-default); 31 | } 32 | 33 | &:last-child { 34 | padding-bottom: 0; 35 | } 36 | 37 | .account-balance { 38 | @include typography--bold-body(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/send-txn/modal/_send-txn-info-modal.scss: -------------------------------------------------------------------------------- 1 | @import "../../core/ui/typography/typography-mixins"; 2 | 3 | .send-txn__info-modal { 4 | .info-modal__description-list-item { 5 | @include text-color--gray(); 6 | @include typography--secondary-body(); 7 | 8 | &:first-of-type { 9 | @include typography--medium-body(); 10 | 11 | color: var(--text-main); 12 | 13 | .send-txn__info-tip-icon path { 14 | fill: var(--helper-purple-900); 15 | } 16 | 17 | .info-modal__description-list-item__icon-wrapper { 18 | background: none; 19 | } 20 | } 21 | 22 | &:not(:first-of-type) { 23 | .send-txn__info-tip-icon path { 24 | fill: var(--text-gray-lighter); 25 | } 26 | } 27 | } 28 | } 29 | 30 | .send-txn__info-modal-link { 31 | color: var(--helper-purple-900); 32 | } 33 | 34 | .send-txn__info-warning { 35 | color: var(--helper-red-900); 36 | } 37 | -------------------------------------------------------------------------------- /src/transaction/sign-box/view/default/arbitrary-data-list/_arbitrary-data-list.scss: -------------------------------------------------------------------------------- 1 | .arbitrary-data-list-item { 2 | display: grid; 3 | align-items: center; 4 | 5 | grid-template-columns: 40px 1fr; 6 | grid-gap: 12px; 7 | 8 | .arbitrary-data-list-item__description { 9 | display: flex; 10 | flex-direction: column; 11 | gap: 4px; 12 | 13 | padding: 12px 0; 14 | } 15 | 16 | &:not(:last-of-type) { 17 | .arbitrary-data-list-item__description { 18 | border-bottom: 1px solid var(--border-default); 19 | } 20 | } 21 | } 22 | 23 | .arbitrary-data-list-item__icon-wrapper { 24 | display: grid; 25 | place-content: center; 26 | 27 | width: 40px; 28 | height: 40px; 29 | 30 | border-radius: 100%; 31 | box-shadow: var(--shadow-small); 32 | } 33 | 34 | .arbitrary-data-list__message { 35 | max-height: 180px; 36 | 37 | overflow-y: scroll; 38 | 39 | word-break: break-word; 40 | } 41 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const webpack = require("webpack"); 3 | 4 | module.exports = function override(config) { 5 | const fallback = config.resolve.fallback || {}; 6 | 7 | Object.assign(fallback, { 8 | crypto: require.resolve("crypto-browserify"), 9 | stream: require.resolve("stream-browserify") 10 | }); 11 | config.resolve.fallback = fallback; 12 | // ignore warning about source map of perawallet/connect 13 | 14 | config.ignoreWarnings = [/Failed to parse source map/]; 15 | config.plugins = (config.plugins || []).concat([ 16 | new webpack.ProvidePlugin({ 17 | process: "process/browser", 18 | Buffer: ["buffer", "Buffer"] 19 | }), 20 | new webpack.IgnorePlugin({ 21 | checkResource(resource) { 22 | return /.*\/wordlists\/(?!english).*\.json/.test(resource); 23 | } 24 | }) 25 | ]); 26 | return config; 27 | }; 28 | -------------------------------------------------------------------------------- /src/overview/page/welcome/_welcome-page.scss: -------------------------------------------------------------------------------- 1 | .welcome-page__description { 2 | width: 452px; 3 | 4 | margin-top: 16px; 5 | } 6 | 7 | .welcome-page__onboarding-option-list { 8 | margin: 80px 0 32px; 9 | } 10 | 11 | .welcome-page__nano-ledger { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | 16 | margin-left: auto; 17 | padding: 16px; 18 | 19 | background-color: rgba(var(--gray-200-rgb) 0.5); 20 | border-radius: 16px; 21 | box-shadow: var(--shadow-disabled); 22 | 23 | cursor: not-allowed; 24 | 25 | .ledger-icon { 26 | margin-right: 12px; 27 | 28 | path { 29 | fill: var(--text-gray-light); 30 | } 31 | 32 | circle { 33 | fill: var(--text-gray-light); 34 | } 35 | } 36 | } 37 | 38 | .welcome-page__nano-ledger__badge { 39 | margin-left: 16px; 40 | padding: 6px 12px; 41 | 42 | background-color: var(--white); 43 | border-radius: 16px; 44 | } 45 | -------------------------------------------------------------------------------- /src/transaction/sign-box/message/TransactionSignDetailMessage.tsx: -------------------------------------------------------------------------------- 1 | import {ReactComponent as WarningIcon} from "../../../core/ui/icons/warning.svg"; 2 | import {ReactComponent as InfoIcon} from "../../../core/ui/icons/info.svg"; 3 | 4 | import "./_transaction-sign-detail-message.scss"; 5 | 6 | export interface TransactionSignDetailMessageProps { 7 | message: string; 8 | type: "warning" | "info"; 9 | } 10 | 11 | function TransactionSignDetailMessage({ 12 | message, 13 | type 14 | }: TransactionSignDetailMessageProps) { 15 | return ( 16 |
18 | {type === "warning" ? ( 19 | 20 | ) : ( 21 | 22 | )} 23 | 24 |

{message}

25 |
26 | ); 27 | } 28 | 29 | export default TransactionSignDetailMessage; 30 | -------------------------------------------------------------------------------- /src/settings/backup/flow/BackupFlow.tsx: -------------------------------------------------------------------------------- 1 | import {Routes, Route} from "react-router-dom"; 2 | 3 | import ROUTES from "../../../core/route/routes"; 4 | import CardLayout from "../../../layouts/card-layout/CardLayout"; 5 | import BackupFile from "../page/backup-file/BackupFile"; 6 | import BackupPassphrase from "../page/backup-passphrase/BackupPassphrase"; 7 | import BackupSelectAccounts from "../page/select-accounts/BackupSelectAccounts"; 8 | 9 | function BackupFlow() { 10 | return ( 11 | 12 | }> 13 | } /> 14 | 15 | } 18 | /> 19 | 20 | } /> 21 | 22 | 23 | ); 24 | } 25 | 26 | export default BackupFlow; 27 | -------------------------------------------------------------------------------- /src/transaction/sign-box/_transaction-sign-box.scss: -------------------------------------------------------------------------------- 1 | .transaction-sign-box { 2 | width: 480px; 3 | height: 578px; 4 | 5 | overflow: auto; 6 | 7 | color: var(--text-main); 8 | background: var(--card-default); 9 | 10 | border-radius: 16px; 11 | box-shadow: var(--shadow-small); 12 | 13 | &--arbitrary-data { 14 | height: 738px; 15 | } 16 | 17 | &--compact { 18 | height: var(--pera-wallet-modal-compact-height); 19 | 20 | .transaction-sign-review-transactions { 21 | height: 238px; 22 | 23 | padding: 20px 20px 0; 24 | } 25 | 26 | .transaction-sign-transaction-actions { 27 | padding: 20px; 28 | } 29 | 30 | .transaction-sign-arbitrary-data-view__account-container { 31 | padding: 0; 32 | } 33 | } 34 | } 35 | 36 | .transaction-sign-box__spinner-wrapper { 37 | display: flex; 38 | align-items: center; 39 | justify-content: center; 40 | 41 | width: 100%; 42 | height: 100%; 43 | } 44 | -------------------------------------------------------------------------------- /src/component/format-balance/usd/FormatUSDBalance.tsx: -------------------------------------------------------------------------------- 1 | import {useAppContext} from "../../../core/app/AppContext"; 2 | import {defaultPriceFormatter} from "../../../core/util/number/numberUtils"; 3 | 4 | interface FormatUSDBalanceProps { 5 | value: string | number; 6 | prefix?: string; 7 | customClassName?: string; 8 | } 9 | 10 | function FormatUSDBalance({value, prefix, customClassName}: FormatUSDBalanceProps) { 11 | const { 12 | state: {preferredNetwork} 13 | } = useAppContext(); 14 | const {usdFormatter} = defaultPriceFormatter(); 15 | 16 | return
{renderBalance()}
; 17 | 18 | function renderBalance() { 19 | let balance = "-"; 20 | 21 | if (preferredNetwork === "mainnet") { 22 | const presign = prefix?.concat(" ") || ""; 23 | 24 | balance = `${presign}${usdFormatter(Number(value))}`; 25 | } 26 | 27 | return balance; 28 | } 29 | } 30 | 31 | export default FormatUSDBalance; 32 | -------------------------------------------------------------------------------- /src/core/ui/style/override/hipo-ui-toolkit/_checkbox-input.scss: -------------------------------------------------------------------------------- 1 | .checkbox-input-label__icon { 2 | --checkbox-focus-bg: var(--primary-button-bg); 3 | --checkbox-focus-border-color: var(--gray-400); 4 | --checkbox-border-color: var(--gray-400); 5 | 6 | border-width: 2px; 7 | border-radius: 6px; 8 | 9 | &:hover { 10 | --checkbox-border-color: var(--primary-button-bg); 11 | 12 | transition: none; 13 | } 14 | } 15 | 16 | .checkbox-input-label--is-selected { 17 | .checkbox-input-label__icon { 18 | border: none; 19 | } 20 | } 21 | 22 | .dark-theme { 23 | .checkbox-input:focus ~ .checkbox-input-label__icon { 24 | --checkbox-focus-border-color: var(--white); 25 | } 26 | 27 | .checkbox-input-label__icon { 28 | .check-icon { 29 | display: none; 30 | } 31 | } 32 | 33 | .checkbox-input-label--is-selected { 34 | .checkbox-input-label__icon { 35 | .check-icon { 36 | display: block; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/core/ui/style/override/hipo-ui-toolkit/_input.scss: -------------------------------------------------------------------------------- 1 | @import "../../../typography/_typography-mixins.scss"; 2 | 3 | :root { 4 | --input-bg: var(--layer-1); 5 | --input-border: var(--border-medium); 6 | --input-border-hover: var(--helper-purple-900); 7 | --input-border-error: var(--helper-red-900); 8 | --input-disabled-bg: var(--layer-3); 9 | } 10 | 11 | .input { 12 | @include typography--button(); 13 | 14 | position: relative; 15 | 16 | height: 20px; 17 | 18 | padding: 0; 19 | 20 | color: var(--text-main); 21 | background-color: var(--input-bg); 22 | border: unset; 23 | 24 | .input-container__icon { 25 | height: 24px; 26 | } 27 | 28 | &::placeholder { 29 | color: var(--text-gray-lightest); 30 | } 31 | 32 | &:focus + .input-container__right-icon { 33 | display: block; 34 | 35 | width: auto; 36 | } 37 | 38 | &:autofill { 39 | background: var(--input-bg); 40 | box-shadow: 0 0 0 100px var(--input-bg) inset; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/core/util/device/deviceUtils.ts: -------------------------------------------------------------------------------- 1 | function isNavigatorAvailable() { 2 | return typeof navigator !== "undefined"; 3 | } 4 | 5 | function isSmallMobileDevice() { 6 | return ( 7 | isNavigatorAvailable() && 8 | ((/Mobile/i.test(navigator.userAgent) && /Android/i.test(navigator.userAgent)) || 9 | /iPhone/i.test(navigator.userAgent)) 10 | ); 11 | } 12 | 13 | function isFirefoxPrivate(): Promise { 14 | const isUserAgentFirefox = !!navigator.userAgent.match(/Firefox/i); 15 | 16 | return new Promise((resolve) => { 17 | if (!isUserAgentFirefox) resolve(false); 18 | 19 | const temporaryDBName = "firefox-private-test"; 20 | const db = indexedDB.open(temporaryDBName); 21 | 22 | db.onerror = function () { 23 | resolve(true); 24 | }; 25 | 26 | db.onsuccess = function () { 27 | indexedDB.deleteDatabase(temporaryDBName); 28 | resolve(false); 29 | }; 30 | }); 31 | } 32 | 33 | export {isSmallMobileDevice, isFirefoxPrivate}; 34 | -------------------------------------------------------------------------------- /src/core/util/pera/api/peraApiUtils.ts: -------------------------------------------------------------------------------- 1 | import {getHiddenBanners} from "../../../../component/banner/util/bannerUtils"; 2 | import {FetcherMiddleware} from "../../../network/fetcherTypes"; 3 | import { 4 | AccountASA, 5 | BannerResponse 6 | } from "./peraApiModels"; 7 | 8 | function mapAddressAccountASADataMiddleware( 9 | address: string 10 | ): FetcherMiddleware> { 11 | return (response) => 12 | Promise.resolve({ 13 | ...response, 14 | results: response.results.map((result) => ({...result, address})) 15 | }); 16 | } 17 | 18 | function filterBannerMiddleware(bannerData: BannerResponse) { 19 | const hiddenBanners = getHiddenBanners(); 20 | const filteredBanners = bannerData.results.filter( 21 | (banner) => !hiddenBanners.includes(String(banner.id)) 22 | ); 23 | 24 | return Promise.resolve({results: filteredBanners}); 25 | } 26 | 27 | export { 28 | mapAddressAccountASADataMiddleware, 29 | filterBannerMiddleware 30 | }; 31 | -------------------------------------------------------------------------------- /src/asset/opt-in/modal/info/_asset-optin-info-modal.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../core/ui/typography/typography-mixins"; 2 | 3 | .asset-optin-page__info-modal { 4 | .info-modal__icon-wrapper { 5 | margin-top: 0; 6 | } 7 | 8 | .info-modal__description-list-item:not(:first-of-type) { 9 | @include text-color--gray(); 10 | @include typography--secondary-body(); 11 | } 12 | 13 | .info-modal__description-list-item:first-of-type { 14 | @include typography--medium-body(); 15 | 16 | .info-modal__description-list-item__icon-wrapper { 17 | background-color: transparent; 18 | 19 | .tip-icon { 20 | path { 21 | fill: var(--helper-purple-900); 22 | } 23 | } 24 | } 25 | } 26 | 27 | .asset-icon, 28 | .collectibles-icon, 29 | .warning-icon { 30 | path { 31 | fill: var(--text-gray-lighter); 32 | } 33 | } 34 | 35 | .opt-in-icon { 36 | path { 37 | fill: var(--helper-purple-900); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/account/page/landing/AccountLandingPage.tsx: -------------------------------------------------------------------------------- 1 | import "./_account-landing-page.scss"; 2 | 3 | import {withGoBackLink} from "../../../core/route/context/NavigationContext"; 4 | import ROUTES from "../../../core/route/routes"; 5 | import AccountPageOnboardingOptionList from "../../../overview/page/welcome/account-page-onboarding-option-list/AccountPageOnboardingOptionList"; 6 | 7 | function AccountLandingPage({title, subtitle}: {title: string; subtitle?: string}) { 8 | return ( 9 | <> 10 |

11 | {title} 12 |

13 | 14 | {!!subtitle && ( 15 |

16 | {subtitle} 17 |

18 | )} 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default withGoBackLink(AccountLandingPage, ROUTES.OVERVIEW.ROUTE); 26 | -------------------------------------------------------------------------------- /src/core/util/hook/useOnClickOutside.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect, useRef} from "react"; 2 | 3 | function useOnClickOutside(element: HTMLElement | null, callback: () => void) { 4 | const callbackRef = useRef(callback); 5 | 6 | useEffect(() => { 7 | callbackRef.current = callback; 8 | }, [callback]); 9 | 10 | useEffect(() => { 11 | function eventListener(event: MouseEvent | TouchEvent) { 12 | if ( 13 | !element || 14 | !(event.target instanceof HTMLElement) || 15 | element.contains(event.target) 16 | ) { 17 | return; 18 | } 19 | 20 | callbackRef.current(); 21 | } 22 | 23 | document.addEventListener("mousedown", eventListener); 24 | document.addEventListener("touchstart", eventListener); 25 | 26 | return () => { 27 | document.removeEventListener("mousedown", eventListener); 28 | document.removeEventListener("touchstart", eventListener); 29 | }; 30 | }, [element]); 31 | } 32 | 33 | export default useOnClickOutside; 34 | -------------------------------------------------------------------------------- /src/transaction/sign-box/view/default/TransactionSignDefaultView.tsx: -------------------------------------------------------------------------------- 1 | import "./_transaction-sign-default-view.scss"; 2 | 3 | import TransactionSignReviewTransactions from "./review-txns/TransactionSignReviewTransactions"; 4 | import TransactionSignAppMeta from "./app-meta/TransactionSignAppMeta"; 5 | import TransactionSignTransactionActions from "./actions/TransactionSignTransactionActions"; 6 | 7 | interface TransactionSignDefaultViewProps { 8 | handleSignClick: VoidFunction; 9 | handleSignCancel: VoidFunction; 10 | } 11 | 12 | function TransactionSignDefaultView({ 13 | handleSignClick, 14 | handleSignCancel 15 | }: TransactionSignDefaultViewProps) { 16 | return ( 17 | <> 18 | 19 | 20 | 21 | 22 | 26 | 27 | ); 28 | } 29 | 30 | export default TransactionSignDefaultView; 31 | -------------------------------------------------------------------------------- /src/core/ui/icons/asa-trusted.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/core/ui/icons/layer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/core/ui/icons/global.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/component/simple-toast/util/simpleToastReducer.ts: -------------------------------------------------------------------------------- 1 | import {initialSimpleToastState} from "./simpleToastConstants"; 2 | import {SimpleToastAction} from "./simpleToastTypes"; 3 | 4 | type SimpleToastState = typeof initialSimpleToastState; 5 | 6 | function simpleToastReducer( 7 | state: SimpleToastState, 8 | action: SimpleToastAction 9 | ): SimpleToastState { 10 | let newState = state; 11 | 12 | switch (action.type) { 13 | case "DISPLAY": { 14 | newState = { 15 | ...state, 16 | toast: action.toastData 17 | }; 18 | break; 19 | } 20 | 21 | case "HIDE": { 22 | newState = { 23 | ...state, 24 | toast: null 25 | }; 26 | break; 27 | } 28 | 29 | case "SET_DEFAULT_AUTO_CLOSE_TIMEOUT": { 30 | newState = { 31 | ...state, 32 | defaultAutoCloseTimeout: action.timeout 33 | }; 34 | break; 35 | } 36 | 37 | default: 38 | break; 39 | } 40 | 41 | return newState; 42 | } 43 | 44 | export default simpleToastReducer; 45 | -------------------------------------------------------------------------------- /src/send-txn/page/select-txn-asset/_send-txn-select-asset.scss: -------------------------------------------------------------------------------- 1 | @import "../../../layouts/card-layout/_card-layout.scss"; 2 | 3 | .send-txn__select-asset { 4 | @extend .card-layout; 5 | 6 | display: flex; 7 | flex-direction: column; 8 | 9 | width: 424px; 10 | height: 590px; 11 | 12 | padding: 0; 13 | } 14 | 15 | .send-txn-asset__list { 16 | display: grid; 17 | 18 | overflow-y: auto; 19 | 20 | .searchable-list { 21 | height: 388px; 22 | 23 | padding: 20px; 24 | 25 | background-color: var(--layer-2); 26 | 27 | border-radius: 12px; 28 | 29 | box-shadow: var(--shadow-inline); 30 | } 31 | } 32 | 33 | .send-txn__select-asset__go-back-button { 34 | padding: 28px 0 0; 35 | } 36 | 37 | .send-txn-asset__fund { 38 | display: grid; 39 | justify-items: center; 40 | gap: 32px; 41 | 42 | margin: auto; 43 | } 44 | 45 | .send-txn-asset__fund-cta { 46 | display: flex; 47 | gap: 8px; 48 | 49 | width: 240px; 50 | 51 | .plus-icon path { 52 | fill: var(--primary-button-text); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/core/ui/style/override/hipo-ui-toolkit/_form.scss: -------------------------------------------------------------------------------- 1 | @import "../../../typography/_typography-mixins.scss"; 2 | 3 | .form-field { 4 | position: relative; 5 | 6 | padding: 14px 16px; 7 | 8 | background-color: var(--input-bg); 9 | border: 1px solid var(--input-border); 10 | border-radius: 12px; 11 | 12 | &:focus-within { 13 | border: 1px solid var(--input-border-hover); 14 | box-shadow: var(--shadow-pressed); 15 | } 16 | 17 | .form-field-message--is-error { 18 | color: var(--helper-red-900); 19 | } 20 | } 21 | 22 | .form-field--has-error { 23 | &:focus-within { 24 | border: 1px solid var(--input-border-error); 25 | box-shadow: var(--shadow-error); 26 | } 27 | } 28 | 29 | .form-field__title { 30 | @include typography--caption(); 31 | 32 | margin-bottom: 4px; 33 | 34 | color: var(--text-gray); 35 | } 36 | 37 | .form-field-message--is-error { 38 | @include typography--tiny(); 39 | 40 | position: absolute; 41 | bottom: -24px; 42 | left: 0; 43 | 44 | color: var(--helper-red-900); 45 | } 46 | -------------------------------------------------------------------------------- /src/core/util/algod/algodTypes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MAINNET_NODE_CHAIN_ID, 3 | TESTNET_NODE_CHAIN_ID, 4 | BETANET_NODE_CHAIN_ID, 5 | ALGORAND_NODE_CHAIN_ID 6 | } from "./algodConstants"; 7 | 8 | export type AlgorandNodeProviderType = "algodev"; 9 | 10 | export type AlgodCredentialShape = Record< 11 | AlgorandNodeProviderType, 12 | Readonly<{ 13 | clientToken: string; 14 | clientServer: string; 15 | indexerToken: string; 16 | indexerServer: string; 17 | port: number; 18 | chainId?: number; 19 | }> 20 | >; 21 | 22 | export interface AlgorandNodeProvider { 23 | type: AlgorandNodeProviderType; 24 | isHealthy: boolean; 25 | title: string; 26 | } 27 | 28 | export interface AlgodCredentials { 29 | mainnet: AlgodCredentialShape; 30 | testnet: AlgodCredentialShape; 31 | } 32 | 33 | export type NetworkToggle = "testnet" | "mainnet"; 34 | 35 | export type AlgorandChainIDs = 36 | | typeof ALGORAND_NODE_CHAIN_ID 37 | | typeof MAINNET_NODE_CHAIN_ID 38 | | typeof TESTNET_NODE_CHAIN_ID 39 | | typeof BETANET_NODE_CHAIN_ID; 40 | -------------------------------------------------------------------------------- /src/asset/opt-in/page/_asset-optin-page.scss: -------------------------------------------------------------------------------- 1 | @import "../../../layouts/card-layout/_card-layout.scss"; 2 | 3 | .asset-optin-page { 4 | @extend .card-layout; 5 | 6 | position: static; 7 | 8 | display: flex; 9 | flex-direction: column; 10 | 11 | width: 424px; 12 | height: fit-content; 13 | 14 | padding: 0 0 100px; 15 | 16 | .searchable-list { 17 | height: 546px; 18 | 19 | padding: 20px; 20 | 21 | background-color: var(--layer-2); 22 | border-radius: 12px; 23 | box-shadow: var(--shadow-inline); 24 | } 25 | 26 | .spinner { 27 | margin: auto; 28 | } 29 | } 30 | 31 | .asset-optin-page__header { 32 | display: flex; 33 | align-items: center; 34 | gap: 8px; 35 | 36 | padding: 28px 0 32px; 37 | } 38 | 39 | .asset-optin-page__info-button { 40 | width: max-content; 41 | height: max-content; 42 | 43 | padding: 0; 44 | 45 | border: unset; 46 | 47 | .info-icon { 48 | width: 20px; 49 | height: 20px; 50 | 51 | path, 52 | circle { 53 | fill: var(--text-main); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/send-txn/components/textarea/SendTxnNoteTextArea.tsx: -------------------------------------------------------------------------------- 1 | import "./_send-txn-note-textarea.scss"; 2 | 3 | import {SyntheticEvent} from "react"; 4 | import {FormField, Textarea} from "@hipo/react-ui-toolkit"; 5 | 6 | import {useSendTxnFlowContext} from "../../context/SendTxnFlowContext"; 7 | 8 | function SendTxnNoteTextArea() { 9 | const {formitoState, dispatchFormitoAction} = useSendTxnFlowContext(); 10 | 11 | return ( 12 | 13 |