From 2c5372142947c19ee2f4b31f3af620935744ae54 Mon Sep 17 00:00:00 2001 From: Maxi Date: Thu, 30 May 2024 15:44:55 +0200 Subject: [PATCH 01/46] feat: set country selector top left in buy modal --- src/components/modals/BuyOptionsModal.vue | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/modals/BuyOptionsModal.vue b/src/components/modals/BuyOptionsModal.vue index 6596ee0cd..b332f6964 100644 --- a/src/components/modals/BuyOptionsModal.vue +++ b/src/components/modals/BuyOptionsModal.vue @@ -9,10 +9,8 @@
- - {{ country.name }} - {{ $t('Loading...') }} - open + + open
@@ -309,6 +307,9 @@ header { } .country-selector { + position: absolute; + top: 2rem; + left: 2rem; z-index: 100; display: inline-block; margin-bottom: 3rem; @@ -322,6 +323,11 @@ header { } } } + + ::v-deep .dropdown { + left: 0; + transform: translateX(0); + } } .country-flag, From da0d25b8c9d8b764b0b439b00a9f20788d908ea4 Mon Sep 17 00:00:00 2001 From: Maxi Date: Thu, 30 May 2024 18:24:31 +0200 Subject: [PATCH 02/46] feat: add better text defaults for wrapping texts --- src/scss/themes.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/scss/themes.scss b/src/scss/themes.scss index 98705e926..4e6d15a9b 100644 --- a/src/scss/themes.scss +++ b/src/scss/themes.scss @@ -205,3 +205,11 @@ button.reset:disabled { line-height: unset; } } + +h1, h2, h3, h4, h5, h6 { + text-wrap: pretty; +} + +p, li { + text-wrap: balance; +} From 8d9379a7b16a484a0a8fd7c7240b32a84cee2e94 Mon Sep 17 00:00:00 2001 From: Maxi Date: Fri, 31 May 2024 18:15:12 +0200 Subject: [PATCH 03/46] feat: added RouteName enum This is not a breaking change, and it will help us ensure the string values are consintent across the app --- src/config/config.local.ts | 5 ++ src/lib/Countries.ts | 2 + src/router.ts | 142 ++++++++++++++++++++++++++----------- 3 files changed, 109 insertions(+), 40 deletions(-) diff --git a/src/config/config.local.ts b/src/config/config.local.ts index 9380b2e93..74bd874a8 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -116,6 +116,11 @@ export default { apiKey: 'pk_test_0c3e2ecd-1546-4068-ae01-d49382e1266a', }, + sinpeMovil: { + enabledSell: true, + enabledBuy: true, + }, + goCrypto: { enabled: true, apiEndpoint: 'https://api.staging.gocrypto.com/', diff --git a/src/lib/Countries.ts b/src/lib/Countries.ts index b897b777a..0d13541c4 100644 --- a/src/lib/Countries.ts +++ b/src/lib/Countries.ts @@ -367,3 +367,5 @@ const SIMPLEX_UNSUPPORTED_COUNTRY_CODES: string[] = [ export const SIMPLEX_COUNTRY_CODES = ALL_COUNTRY_CODES .filter((code) => !SIMPLEX_UNSUPPORTED_COUNTRY_CODES.includes(code)); + +export const SINPE_MOVIL_COUNTRY_CODES = [CC['Costa Rica']] as string[]; diff --git a/src/router.ts b/src/router.ts index 56b83326e..bbfc30ff7 100644 --- a/src/router.ts +++ b/src/router.ts @@ -14,8 +14,8 @@ import { AccountType, useAccountStore } from './stores/Account'; import { CryptoCurrency } from './lib/Constants'; // Main views -const Settings = () => import(/* webpackChunkName: "settings" */ './components/layouts/Settings.vue'); -const Network = () => +const SettingsLayout = () => import(/* webpackChunkName: "settings" */ './components/layouts/Settings.vue'); +const NetworkLayout = () => import(/* webpackChunkName: "network" */ './components/layouts/Network.vue'); // Modals @@ -73,6 +73,10 @@ const MoonpaySellInfoModal = () => import(/* webpackChunkName: "moonpay-modal" */ './components/modals/MoonpaySellInfoModal.vue'); const SimplexModal = () => import(/* webpackChunkName: "simplex-modal" */ './components/modals/SimplexModal.vue'); +const SinpeMovilModal = () => + import(/* webpackChunkName: "sinpe-movil-modal" */ './components/modals/SinpeMovilModal.vue'); +const SinpeMovileSellInfoModal = () => + import(/* webpackChunkName: "sinpe-movil-info-modal" */ './components/modals/SinpeMovilSellInfoModal.vue'); // Prestaking Modals const PrestakingModal = () => @@ -86,6 +90,50 @@ export enum Columns { ADDRESS, } +export enum RouteName { + Root = 'root', + RootAccounts = 'root-accounts', + Send = 'send', + SendNim = 'send-nim', + SendBtc = 'send-btc', + SendUsdc = 'send-usdc', + Receive = 'receive', + ReceiveNim = 'receive-nim', + ReceiveBtc = 'receive-btc', + ReceiveUsdc = 'receive-usdc', + Transaction = 'transaction', + Trade = 'trade', + Buy = 'buy', + BuyCrypto = 'buy-crypto', + SellCrypto = 'sell-crypto', + Scan = 'scan', + SendViaUri = 'send-via-uri', + Welcome = 'welcome', + MigrationWelcome = 'migration-welcome', + BtcActivation = 'btc-activation', + BtcTransaction = 'btc-transaction', + SendViaBtcUri = 'send-via-btc-uri', + UsdcActivation = 'usdc-activation', + UsdcTransaction = 'usdc-transaction', + SendViaPolygonUri = 'send-via-polygon-uri', + Swap = 'swap', + MoonpaySellInfo = 'moonpay-sell-info', + Moonpay = 'moonpay', + Simplex = 'simplex', + SinpeMovilSellInfo = 'sinpe-movil-sell-info', + SinpeMovil = 'sinpe-movil', + RootReleaseNotes = 'root-release-notes', + ExportHistory = 'export-history', + Prestaking = 'prestaking', + Settings = 'settings', + SettingsAccounts = 'settings-accounts', + Disclaimer = 'disclaimer', + SettingsReleaseNotes = 'settings-release-notes', + Network = 'network', + NetworkAccounts = 'network-accounts', + NetworkReleaseNotes = 'network-release-notes', +} + const routes: RouteConfig[] = [{ path: '/', components: { @@ -97,7 +145,7 @@ const routes: RouteConfig[] = [{ accountOverview: AccountOverview, addressOverview: AddressOverview, }, - name: 'root', + name: RouteName.Root, alias: '/transactions', meta: { column: Columns.DYNAMIC }, children: [{ @@ -105,35 +153,35 @@ const routes: RouteConfig[] = [{ components: { modal: AccountMenuModal, }, - name: 'root-accounts', + name: RouteName.RootAccounts, meta: { column: Columns.DYNAMIC }, }, { path: '/send', components: { modal: AddressSelectorModal, }, - name: 'send', + name: RouteName.Send, meta: { column: Columns.DYNAMIC }, }, { path: '/send/nim', components: { modal: SendModal, }, - name: 'send-nim', + name: RouteName.SendNim, meta: { column: Columns.DYNAMIC }, }, { path: '/send/btc', components: { modal: BtcSendModal, }, - name: 'send-btc', + name: RouteName.SendBtc, meta: { column: Columns.DYNAMIC }, }, { path: '/send/usdc', components: { modal: UsdcSendModal, }, - name: 'send-usdc', + name: RouteName.SendUsdc, props: { modal: true }, meta: { column: Columns.DYNAMIC }, }, { @@ -141,35 +189,35 @@ const routes: RouteConfig[] = [{ components: { modal: AddressSelectorModal, }, - name: 'receive', + name: RouteName.Receive, meta: { column: Columns.DYNAMIC }, }, { path: '/receive/nim', components: { modal: ReceiveModal, }, - name: 'receive-nim', + name: RouteName.ReceiveNim, meta: { column: Columns.DYNAMIC }, }, { path: '/receive/btc', components: { modal: BtcReceiveModal, }, - name: 'receive-btc', + name: RouteName.ReceiveBtc, meta: { column: Columns.DYNAMIC }, }, { path: '/receive/usdc', components: { modal: UsdcReceiveModal, }, - name: 'receive-usdc', + name: RouteName.ReceiveUsdc, meta: { column: Columns.DYNAMIC }, }, { path: '/transaction/:hash', components: { modal: TransactionModal, }, - name: 'transaction', + name: RouteName.Transaction, props: { modal: true }, meta: { column: Columns.ADDRESS }, }, { @@ -177,42 +225,42 @@ const routes: RouteConfig[] = [{ components: { modal: TradeModal, }, - name: 'trade', + name: RouteName.Trade, meta: { column: Columns.DYNAMIC }, }, { path: '/buy', components: { modal: BuyOptionsModal, }, - name: 'buy', + name: RouteName.Buy, meta: { column: Columns.DYNAMIC }, }, { path: '/buy-crypto', components: { modal: BuyCryptoModal, }, - name: 'buy-crypto', + name: RouteName.BuyCrypto, meta: { column: Columns.DYNAMIC }, }, { path: '/sell-crypto', components: { modal: SellCryptoModal, }, - name: 'sell-crypto', + name: RouteName.SellCrypto, meta: { column: Columns.DYNAMIC }, }, { path: '/scan', components: { modal: ScanQrModal, }, - name: 'scan', + name: RouteName.Scan, meta: { column: Columns.DYNAMIC }, }, { path: '/:requestUri(nimiq:.+)', components: { modal: SendModal, }, - name: 'send-via-uri', + name: RouteName.SendViaUri, props: { // Pass full path including query parameters. modal: (route: Route) => ({ requestUri: route.fullPath.substring(1) }), @@ -223,21 +271,21 @@ const routes: RouteConfig[] = [{ components: { modal: WelcomeModal, }, - name: 'welcome', + name: RouteName.Welcome, meta: { column: Columns.ACCOUNT }, }, { path: '/migration-welcome', components: { modal: MigrationWelcomeModal, }, - name: 'migration-welcome', + name: RouteName.MigrationWelcome, meta: { column: Columns.ACCOUNT }, }, { path: '/btc-activation', components: { modal: BtcActivationModal, }, - name: 'btc-activation', + name: RouteName.BtcActivation, props: { modal: parseActivationRedirect, }, @@ -247,7 +295,7 @@ const routes: RouteConfig[] = [{ components: { modal: BtcTransactionModal, }, - name: 'btc-transaction', + name: RouteName.BtcTransaction, props: { modal: true }, meta: { column: Columns.ADDRESS }, }, { @@ -255,7 +303,7 @@ const routes: RouteConfig[] = [{ components: { modal: BtcSendModal, }, - name: 'send-via-btc-uri', + name: RouteName.SendViaBtcUri, props: { // Pass full path including query parameters. modal: (route: Route) => ({ requestUri: route.fullPath.substring(1) }), @@ -266,7 +314,7 @@ const routes: RouteConfig[] = [{ components: { modal: UsdcActivationModal, }, - name: 'usdc-activation', + name: RouteName.UsdcActivation, props: { modal: parseActivationRedirect, }, @@ -276,7 +324,7 @@ const routes: RouteConfig[] = [{ components: { modal: UsdcTransactionModal, }, - name: 'usdc-transaction', + name: RouteName.UsdcTransaction, props: { modal: true }, meta: { column: Columns.ADDRESS }, }, { @@ -286,7 +334,7 @@ const routes: RouteConfig[] = [{ components: { modal: UsdcSendModal, }, - name: 'send-via-polygon-uri', + name: RouteName.SendViaPolygonUri, props: { // Pass full path including query parameters. modal: (route: Route) => ({ requestUri: route.fullPath.substring(1) }), @@ -297,7 +345,7 @@ const routes: RouteConfig[] = [{ components: { modal: SwapModal, }, - name: 'swap', + name: RouteName.Swap, props: { modal: true }, meta: { column: Columns.ACCOUNT }, }, { @@ -305,14 +353,14 @@ const routes: RouteConfig[] = [{ components: { modal: MoonpaySellInfoModal, }, - name: 'moonpay-sell-info', + name: RouteName.MoonpaySellInfo, meta: { column: Columns.DYNAMIC }, }, { path: '/moonpay', components: { modal: MoonpayModal, }, - name: 'moonpay', + name: RouteName.Moonpay, props: { modal: true }, meta: { column: Columns.DYNAMIC }, }, { @@ -320,21 +368,35 @@ const routes: RouteConfig[] = [{ components: { 'persistent-modal': SimplexModal, }, - name: 'simplex', + name: RouteName.Simplex, + meta: { column: Columns.DYNAMIC }, + }, { + path: '/sinpe-movil-sell-info', + components: { + 'persistent-modal': SinpeMovileSellInfoModal, + }, + name: RouteName.SinpeMovilSellInfo, + meta: { column: Columns.DYNAMIC }, + }, { + path: '/sinpe-movil', + components: { + 'persistent-modal': SinpeMovilModal, + }, + name: RouteName.SinpeMovil, meta: { column: Columns.DYNAMIC }, }, { path: '/release-notes', components: { modal: ReleaseNotesModal, }, - name: 'root-release-notes', + name: RouteName.RootReleaseNotes, meta: { column: Columns.DYNAMIC }, }, { path: '/export-history/:type', components: { modal: HistoryExportModal, }, - name: 'export-history', + name: RouteName.ExportHistory, props: { modal: true }, meta: { column: Columns.ADDRESS }, }, { @@ -342,7 +404,7 @@ const routes: RouteConfig[] = [{ components: { modal: PrestakingModal, }, - name: 'prestaking', + name: RouteName.Prestaking, props: { modal: true }, meta: { column: Columns.DYNAMIC }, }], @@ -361,37 +423,37 @@ const routes: RouteConfig[] = [{ }, { path: '/settings', components: { - settings: Settings, + settings: SettingsLayout, }, - name: 'settings', + name: RouteName.Settings, meta: { column: Columns.ACCOUNT }, children: [{ path: '/accounts', components: { modal: AccountMenuModal, }, - name: 'settings-accounts', + name: RouteName.SettingsAccounts, meta: { column: Columns.DYNAMIC }, }, { path: '/disclaimer', components: { modal: DisclaimerModal, }, - name: 'disclaimer', + name: RouteName.Disclaimer, meta: { column: Columns.ACCOUNT }, }, { path: '/release-notes', components: { modal: ReleaseNotesModal, }, - name: 'settings-release-notes', + name: RouteName.SettingsReleaseNotes, meta: { column: Columns.DYNAMIC }, }], }], }, { path: '/network', components: { - basement: Network, + basement: NetworkLayout, }, name: 'network', meta: { column: Columns.ACCOUNT }, From b8d3daa098c6fff11ed83ed24b74ffbf5a5d78f0 Mon Sep 17 00:00:00 2001 From: Maxi Date: Fri, 31 May 2024 18:15:50 +0200 Subject: [PATCH 04/46] feat: added SinpeMovilSellInfoModal --- src/assets/exchanges/sinpe-movil-full-bg.svg | 1 + src/assets/exchanges/sinpe-movil-full.svg | 1 + .../modals/SinpeMovilSellInfoModal.vue | 168 ++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 src/assets/exchanges/sinpe-movil-full-bg.svg create mode 100644 src/assets/exchanges/sinpe-movil-full.svg create mode 100644 src/components/modals/SinpeMovilSellInfoModal.vue diff --git a/src/assets/exchanges/sinpe-movil-full-bg.svg b/src/assets/exchanges/sinpe-movil-full-bg.svg new file mode 100644 index 000000000..6bb13fe4c --- /dev/null +++ b/src/assets/exchanges/sinpe-movil-full-bg.svg @@ -0,0 +1 @@ + diff --git a/src/assets/exchanges/sinpe-movil-full.svg b/src/assets/exchanges/sinpe-movil-full.svg new file mode 100644 index 000000000..dbcd44238 --- /dev/null +++ b/src/assets/exchanges/sinpe-movil-full.svg @@ -0,0 +1 @@ + diff --git a/src/components/modals/SinpeMovilSellInfoModal.vue b/src/components/modals/SinpeMovilSellInfoModal.vue new file mode 100644 index 000000000..4d608d8ac --- /dev/null +++ b/src/components/modals/SinpeMovilSellInfoModal.vue @@ -0,0 +1,168 @@ + + + + + From c4c8203f731b10fcc95fb337d7a392e219c84e68 Mon Sep 17 00:00:00 2001 From: Maxi Date: Fri, 31 May 2024 18:16:21 +0200 Subject: [PATCH 05/46] feat: Updated logic in sidebar to toggle sell feature --- src/components/layouts/Sidebar.vue | 78 +++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/src/components/layouts/Sidebar.vue b/src/components/layouts/Sidebar.vue index 55acbe558..cf772d7ec 100644 --- a/src/components/layouts/Sidebar.vue +++ b/src/components/layouts/Sidebar.vue @@ -49,7 +49,7 @@ @@ -65,17 +65,16 @@ :container="$parent" theme="inverse" :styles="{ minWidth: '30.5rem' }" - :disabled="canSellCryptoWithMoonpay" + :disabled="sellEnabled" ref="sellTooltip" > - @@ -69,13 +69,13 @@ import { computed, defineComponent, onMounted, onUnmounted, ref, watch } from '@vue/composition-api'; import { PageHeader, PageBody, PageFooter, LabelInput } from '@nimiq/vue-components'; import { useConfig } from '@/composables/useConfig'; -import { RouteName, useRouter } from '@/router'; +import { RouteName } from '@/router'; import { captureException } from '@sentry/vue'; import { useAddressStore } from '@/stores/Address'; import { SwapAsset } from '@nimiq/libswap'; import { useSinpeMovilStore } from '@/stores/SinpeMovil'; +import { AssetTransferDirection, AssetTransferMethod } from '@/composables/asset-transfer/types'; import Modal from './Modal.vue'; -import { FiatCurrency } from '../../lib/Constants'; export enum SinpeMovilFlowState { Idle = 'idle', @@ -261,7 +261,7 @@ export default defineComponent({ + 'Please try again later.'; // TODO Wording + translations return; } - const json = await res.json() as { token: string}; + const json = await res.json() as { token: string }; if (!json || !json.token) { state.value = SinpeMovilFlowState.Error; errorMessage.value = 'There was an error with the phone verification system.' @@ -271,8 +271,11 @@ export default defineComponent({ state.value = SinpeMovilFlowState.PhoneVerified; connect({ phoneNumber: phoneNumber.value, token: json.token! }); context.root.$router.push({ - name: RouteName.SellCrypto, - params: { fiatCurrency: FiatCurrency.CRC }, + name: RouteName.AssetTransfer, + params: { + direction: AssetTransferDirection.CryptoToFiat, + method: AssetTransferMethod.SinpeMovil, + }, query: context.root.$router.currentRoute.query, }); }) @@ -346,7 +349,7 @@ export default defineComponent({ diff --git a/src/composables/asset-transfer/README.md b/src/composables/asset-transfer/README.md new file mode 100644 index 000000000..5b708c147 --- /dev/null +++ b/src/composables/asset-transfer/README.md @@ -0,0 +1,9 @@ +# Asset Transfer + +In order to create a basic set of rules and guidelines for the transfer of assets, I propose to use the following rules: + +- We use the `AssetTransfer.vue` component which will display just the UI for the asset transfer. +- The `AssetTransfer.vue` component will have a prop of type `AssetTransferMethod` that will be used to determine which composable to use. +- The logic will be handled by unique composables specifically for the service being in used: `useSinpeMovil()`, `useEurSwap()`... +- The composable need to return an object of type `SwapTransferParams` that includes the necessary information to perform the transfer. +- It will also be responsible for adding the necessary hooks and methods to perform the transfer. diff --git a/src/composables/asset-transfer/types.ts b/src/composables/asset-transfer/types.ts new file mode 100644 index 000000000..853ef0639 --- /dev/null +++ b/src/composables/asset-transfer/types.ts @@ -0,0 +1,49 @@ +import { FiatCurrency, CryptoCurrency } from '@/lib/Constants'; +import { Ref } from '@vue/composition-api'; +import { VueConstructor } from 'vue'; + +export enum AssetTransferMethod { + SinpeMovil = 'sinpe-movil', +} + +export enum AssetTransferDirection { + CryptoToFiat = 'crypto-to-fiat', + FiatToCrypto = 'fiat-to-crypto', +} + +type VueComponent = VueConstructor; + +export interface AssetTransferOptions { + direction: AssetTransferDirection; +} + +// The object type that will be returned by the composable found in the same folder, +// which will be used in the SwapTransfer.vue component. +export interface AssetTransferParams { + fiatCurrency: FiatCurrency; + cryptoCurrency: CryptoCurrency; + + fiatAmount: Ref; + + // The amount of crypto currency to be transferred. + // It will be computed based on the fiatAmount and the exchange rate. + cryptoAmount: Readonly>>; // Computed + + // The exchange rate between the fiat and crypto currencies. + fiatFeeAmount: Readonly>>; // Computed + + // The maximum amount of fiat currency that can be transferred. + max: Readonly>>; // Computed + + // The name of the component that will be used to display where + // the user is transferring the funds from. + // The component should use data from store to display the options. + componentFrom: VueComponent; + + // The name of the component that will be used to display where + // the user is transferring the funds to. + // The component should use data from store to display the options. + componentTo: VueComponent; + + // TODO Callbacks and hooks +} diff --git a/src/composables/asset-transfer/useSinpeMovilSwap.ts b/src/composables/asset-transfer/useSinpeMovilSwap.ts new file mode 100644 index 000000000..54b81c64b --- /dev/null +++ b/src/composables/asset-transfer/useSinpeMovilSwap.ts @@ -0,0 +1,28 @@ +import { FiatCurrency, CryptoCurrency } from '@/lib/Constants'; +import { computed, ref } from '@vue/composition-api'; +import IdenticonStack from '@/components/IdenticonStack.vue'; +import { AssetTransferOptions, AssetTransferParams } from './types'; + +export function useSinpeMovilSwap({ direction }: AssetTransferOptions): AssetTransferParams { + const fiatCurrency = FiatCurrency.CRC; + const cryptoCurrency = CryptoCurrency.NIM; + + const fiatAmount = ref(10); + const cryptoAmount = computed(() => fiatAmount.value * 100); + + const fiatFeeAmount = computed(() => fiatAmount.value * 0.01); + const max = computed(() => 1000); + + return { + fiatCurrency, + cryptoCurrency, + + fiatAmount, + cryptoAmount, + + fiatFeeAmount, + componentFrom: IdenticonStack, + max, + componentTo: IdenticonStack, + }; +} diff --git a/src/router.ts b/src/router.ts index 39b60cb19..c258a7680 100644 --- a/src/router.ts +++ b/src/router.ts @@ -66,6 +66,8 @@ const BuyCryptoModal = () => import(/* webpackChunkName: "buy-crypto-modal" */ './components/modals/BuyCryptoModal.vue'); const SellCryptoModal = () => import(/* webpackChunkName: "sell-crypto-modal" */ './components/modals/SellCryptoModal.vue'); +const AssetTransferModal = () => + import(/* webpackChunkName: "asset-transfer-modal" */ './components/modals/AssetTransferModal.vue'); const MoonpayModal = () => import(/* webpackChunkName: "moonpay-modal" */ './components/modals/MoonpayModal.vue'); @@ -106,6 +108,7 @@ export enum RouteName { Buy = 'buy', BuyCrypto = 'buy-crypto', SellCrypto = 'sell-crypto', + AssetTransfer = 'asset-transfer', Scan = 'scan', SendViaUri = 'send-via-uri', Welcome = 'welcome', @@ -251,6 +254,17 @@ const routes: RouteConfig[] = [{ fiatCurrency: true, }, meta: { column: Columns.DYNAMIC }, + }, { + path: '/asset-transfer', + components: { + modal: AssetTransferModal, + }, + name: RouteName.AssetTransfer, + props: { + method: true, + direction: true, + }, + meta: { column: Columns.DYNAMIC }, }, { path: '/scan', components: { From 11be158623cb5b4e07a8b4af1d06079c93a6af14 Mon Sep 17 00:00:00 2001 From: Maxi Date: Wed, 12 Jun 2024 12:41:34 +0300 Subject: [PATCH 15/46] chore: lint issues --- src/lib/export/Format.ts | 1 - src/stores/SinpeMovil.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/export/Format.ts b/src/lib/export/Format.ts index 06452e7ed..8bd76245d 100644 --- a/src/lib/export/Format.ts +++ b/src/lib/export/Format.ts @@ -1,4 +1,3 @@ -import { SwapAsset } from '@nimiq/fastspot-api'; import Config from 'config'; import { type FiatCurrency } from '../Constants'; import { useAddressStore } from '../../stores/Address'; diff --git a/src/stores/SinpeMovil.ts b/src/stores/SinpeMovil.ts index e738995d0..57770bb39 100644 --- a/src/stores/SinpeMovil.ts +++ b/src/stores/SinpeMovil.ts @@ -1,6 +1,6 @@ import { UserLimits } from '@nimiq/fastspot-api'; import { createStore } from 'pinia'; -import { useConfig } from '../composables/useConfig'; +import { useConfig } from '../composables/useConfig'; export type SinpeMovilState = { phoneNumber: string | null, From 619eb20f5305994918b3c4156c49dfca70460f4f Mon Sep 17 00:00:00 2001 From: Maxi Date: Thu, 13 Jun 2024 18:36:36 +0300 Subject: [PATCH 16/46] chore: AssetTransfer UI with data from composables --- src/components/AddressSelector.vue | 47 ++++ src/components/DualCurrencyInput.vue | 245 ++++++++++++++++++ src/components/IdenticonStack.vue | 20 +- src/components/SinpeUserInfo.vue | 94 +++++++ src/components/layouts/Sidebar.vue | 3 +- src/components/modals/AssetTransferModal.vue | 197 ++++++++++++-- src/components/modals/SinpeMovilModal.vue | 15 +- src/components/swap/SwapNotification.vue | 5 +- src/composables/asset-transfer/types.ts | 39 +-- .../asset-transfer/useSinpeMovilSwap.ts | 97 +++++-- src/config/config.local.ts | 1 - src/router.ts | 3 +- 12 files changed, 687 insertions(+), 79 deletions(-) create mode 100644 src/components/AddressSelector.vue create mode 100644 src/components/DualCurrencyInput.vue create mode 100644 src/components/SinpeUserInfo.vue diff --git a/src/components/AddressSelector.vue b/src/components/AddressSelector.vue new file mode 100644 index 000000000..3959fccbb --- /dev/null +++ b/src/components/AddressSelector.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/src/components/DualCurrencyInput.vue b/src/components/DualCurrencyInput.vue new file mode 100644 index 000000000..482ac0603 --- /dev/null +++ b/src/components/DualCurrencyInput.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/src/components/IdenticonStack.vue b/src/components/IdenticonStack.vue index 4654798ff..8d88be546 100644 --- a/src/components/IdenticonStack.vue +++ b/src/components/IdenticonStack.vue @@ -1,15 +1,14 @@ + + diff --git a/src/components/layouts/Sidebar.vue b/src/components/layouts/Sidebar.vue index bbbdebe4a..0d2f0edae 100644 --- a/src/components/layouts/Sidebar.vue +++ b/src/components/layouts/Sidebar.vue @@ -186,6 +186,7 @@ import { SwapAsset } from '@nimiq/fastspot-api'; import { GearIcon, Tooltip, InfoCircleIcon } from '@nimiq/vue-components'; import { RouteName } from '@/router'; import { assetToCurrency } from '@/lib/swap/utils/Assets'; +import { SINPE_MOVIL_PAIRS } from '@/composables/asset-transfer/useSinpeMovilSwap'; import AnnouncementBox from '../AnnouncementBox.vue'; import AccountMenu from '../AccountMenu.vue'; import PriceChart, { TimeRange } from '../PriceChart.vue'; @@ -316,7 +317,7 @@ export default defineComponent({ return activeCurrency.value !== CryptoCurrency.NIM; }); const canSellCryptoWithSinpeMovil = computed(() => { // eslint-disable-line arrow-body-style - if (config.sinpeMovil.pairs.some(([from]) => assetToCurrency(from) === activeCurrency.value)) return true; + if (SINPE_MOVIL_PAIRS.some(([from]) => assetToCurrency(from) === activeCurrency.value)) return true; if (!countryCode.value) return false; // @ts-expect-error It is fine return true || SINPE_MOVIL_COUNTRY_CODES.includes(countryCode.value); diff --git a/src/components/modals/AssetTransferModal.vue b/src/components/modals/AssetTransferModal.vue index 371e92a81..693715fea 100644 --- a/src/components/modals/AssetTransferModal.vue +++ b/src/components/modals/AssetTransferModal.vue @@ -1,55 +1,96 @@ @@ -92,23 +138,25 @@ export default defineComponent({ >li { scroll-snap-align: center; flex-shrink: 0; - width: calc(100% - 5rem); + width: 100%; display: flex; flex-direction: column; + padding: 0 5rem; - &:first-child { - padding-left: 5rem; - } + // &:first-child { + // padding-left: 5rem; + // } - &:last-child { - padding-right: 5rem; - } + // &:last-child { + // padding-right: 5rem; + // } .page-body { display: flex; flex-direction: column; align-items: center; - padding: 0 4rem; + padding: 0; + overflow: inherit; } .page-footer { @@ -116,5 +164,100 @@ export default defineComponent({ } } } + + .pills { + display: flex; + gap: 0.75rem; + overflow-x: auto; + padding: 0 5rem 1.5rem 5rem; + margin: 0 -5rem; + // width: calc(100% + 10rem); + + >div { + flex-shrink: 0; + margin: 1.5px; + box-shadow: 0 0 0 1.5px rgb(31 35 72 / 0.15); + border-radius: 999px; + padding: 3px 1.25rem; + color: rgb(31 35 72 / 0.6); + font-size: 14px; + font-weight: 800; + line-height: 1.4; + + &.max-limit { + color: #EAA617; + box-shadow: 0 0 0 1.5px #EAA617; + } + } + } + + .options-section { + justify-content: space-around; + align-items: stretch; + align-self: stretch; + z-index: 10; + margin-top: 3.5rem; + + &>.flex-column { + align-items: center; + } + + .separator-wrapper { + --height: 0.25rem; + + height: var(--height); + margin-left: -2rem; + margin-right: -2rem; + margin-bottom: 5rem; + + position: relative; + flex-grow: 1; + align-self: center; + overflow: hidden; + mask: radial-gradient(circle at center, white, white calc(100% - 3rem), rgba(255, 255, 255, 0)); + + .separator { + height: 100%; + width: 50%; + background: var(--text-14); + border-radius: calc(var(--height) / 2); + + position: absolute; + left: -50%; + animation: separatorSliding 2.2s infinite; + + @keyframes separatorSliding { + 0% { + transform: translateX(0) + } + + 100% { + transform: translateX(300%) + } + } + } + } + } + + .overlay { + overflow: hidden; + + .page-header { + padding-bottom: 2rem; + } + + .page-body { + padding: 0 2rem 2rem; + } + + .address-list { + --padding-sides: 2rem; + max-height: 100%; + + ::v-deep .scroll-mask.bottom { + bottom: -1px; + } + } +} } diff --git a/src/components/modals/SinpeMovilModal.vue b/src/components/modals/SinpeMovilModal.vue index 2dc6c04c3..1b9b11ba4 100644 --- a/src/components/modals/SinpeMovilModal.vue +++ b/src/components/modals/SinpeMovilModal.vue @@ -74,7 +74,9 @@ import { captureException } from '@sentry/vue'; import { useAddressStore } from '@/stores/Address'; import { SwapAsset } from '@nimiq/libswap'; import { useSinpeMovilStore } from '@/stores/SinpeMovil'; -import { AssetTransferDirection, AssetTransferMethod } from '@/composables/asset-transfer/types'; +import { AssetTransferMethod } from '@/composables/asset-transfer/types'; +import { SINPE_MOVIL_PAIRS } from '@/composables/asset-transfer/useSinpeMovilSwap'; +import { CryptoCurrency, FiatCurrency } from '@/lib/Constants'; import Modal from './Modal.vue'; export enum SinpeMovilFlowState { @@ -124,8 +126,8 @@ export default defineComponent({ const slidePrev = () => scrollTo(scrollerIndex.value - 1); const slideNext = () => scrollTo(scrollerIndex.value + 1); - const { pairs, sendSmsGetEndpoint, verifySmsPostEndpoint, enabled } = config.sinpeMovil; - const sinpaMovilDisabled = computed(() => !enabled && pairs.length === 0); + const { sendSmsGetEndpoint, verifySmsPostEndpoint, enabled } = config.sinpeMovil; + const sinpaMovilDisabled = computed(() => !enabled && SINPE_MOVIL_PAIRS.length === 0); const state = ref(SinpeMovilFlowState.Idle); const errorMessage = ref(null); @@ -273,7 +275,8 @@ export default defineComponent({ context.root.$router.push({ name: RouteName.AssetTransfer, params: { - direction: AssetTransferDirection.CryptoToFiat, + pairFrom: CryptoCurrency.NIM, + pairTo: FiatCurrency.CRC, method: AssetTransferMethod.SinpeMovil, }, query: context.root.$router.currentRoute.query, @@ -291,9 +294,9 @@ export default defineComponent({ // Validate that what the user is trying to do is enabled if (props.flow !== 'buy' && props.flow !== 'sell') { context.root.$router.push('/'); - } else if (props.flow === 'buy' && pairs.some(([from]) => from === SwapAsset.CRC)) { + } else if (props.flow === 'buy' && SINPE_MOVIL_PAIRS.some(([from]) => from === SwapAsset.CRC)) { context.root.$router.push('/'); - } else if (props.flow === 'sell' && pairs.some(([, to]) => to === SwapAsset.CRC)) { + } else if (props.flow === 'sell' && SINPE_MOVIL_PAIRS.some(([, to]) => to === SwapAsset.CRC)) { context.root.$router.push('/'); } }); diff --git a/src/components/swap/SwapNotification.vue b/src/components/swap/SwapNotification.vue index 413570c8c..c166e606f 100644 --- a/src/components/swap/SwapNotification.vue +++ b/src/components/swap/SwapNotification.vue @@ -67,6 +67,7 @@ import { SwapHandler, Swap as GenericSwap, SwapAsset, Client, Transaction } from import type { ForwardRequest } from '@opengsn/common/dist/EIP712/ForwardRequest'; import { Event as PolygonEvent, EventType as PolygonEventType } from '@nimiq/libswap/dist/src/UsdcAssetAdapter'; import { captureException } from '@sentry/vue'; +import { SINPE_MOVIL_PAIRS } from '@/composables/asset-transfer/useSinpeMovilSwap'; import MaximizeIcon from '../icons/MaximizeIcon.vue'; import { useSwapsStore, SwapState, ActiveSwap, SwapEurData, SwapErrorAction } from '../../stores/Swaps'; import { useNetworkStore } from '../../stores/Network'; @@ -806,9 +807,7 @@ export default defineComponent({ const fromAsset = activeSwap.value.from.asset as any; const toAsset = activeSwap.value.to.asset as any; - const { sinpeMovil } = useConfig().config; - - if (sinpeMovil.pairs.find(([from, to]) => from === fromAsset && to === toAsset)) { + if (SINPE_MOVIL_PAIRS.find(([from, to]) => from === fromAsset && to === toAsset)) { // The pair from the activeSwap matches the pair that it is enabled in the Sinpe Movil config context.root.$router.push('/swap'); } else if (fromAsset === 'BTC_LN' || toAsset === 'BTC_LN') { diff --git a/src/composables/asset-transfer/types.ts b/src/composables/asset-transfer/types.ts index 853ef0639..b8cc767e2 100644 --- a/src/composables/asset-transfer/types.ts +++ b/src/composables/asset-transfer/types.ts @@ -6,34 +6,32 @@ export enum AssetTransferMethod { SinpeMovil = 'sinpe-movil', } -export enum AssetTransferDirection { - CryptoToFiat = 'crypto-to-fiat', - FiatToCrypto = 'fiat-to-crypto', +export interface AssetTransferOptions { + pair: (FiatCurrency | CryptoCurrency)[]; } type VueComponent = VueConstructor; -export interface AssetTransferOptions { - direction: AssetTransferDirection; -} - // The object type that will be returned by the composable found in the same folder, // which will be used in the SwapTransfer.vue component. export interface AssetTransferParams { - fiatCurrency: FiatCurrency; - cryptoCurrency: CryptoCurrency; + currencyFrom: FiatCurrency | CryptoCurrency; + currencyTo: FiatCurrency | CryptoCurrency; + currencyFiatFallback: FiatCurrency; + currencyCrypto: Readonly>>; - fiatAmount: Ref; + amountFiat: Ref; + amountCrypto: Ref; - // The amount of crypto currency to be transferred. - // It will be computed based on the fiatAmount and the exchange rate. - cryptoAmount: Readonly>>; // Computed + decimalsCrypto: Readonly>>; + decimalsFiat: Readonly>>; - // The exchange rate between the fiat and crypto currencies. - fiatFeeAmount: Readonly>>; // Computed + feeAmount: Ref; + feeCurrency: FiatCurrency | CryptoCurrency; // The maximum amount of fiat currency that can be transferred. - max: Readonly>>; // Computed + limitMaxAmount: Readonly>>; // Computed + limitMaxCurrency: FiatCurrency | CryptoCurrency; // The name of the component that will be used to display where // the user is transferring the funds from. @@ -45,5 +43,14 @@ export interface AssetTransferParams { // The component should use data from store to display the options. componentTo: VueComponent; + // For NIM address selector screen + addressListOpened: Ref; + + insufficientLimit: Readonly>>; + insufficientBalance: Readonly>>; + + modalTitle: string; + // TODO Callbacks and hooks + setMax: () => void; } diff --git a/src/composables/asset-transfer/useSinpeMovilSwap.ts b/src/composables/asset-transfer/useSinpeMovilSwap.ts index 54b81c64b..a238898af 100644 --- a/src/composables/asset-transfer/useSinpeMovilSwap.ts +++ b/src/composables/asset-transfer/useSinpeMovilSwap.ts @@ -1,28 +1,93 @@ import { FiatCurrency, CryptoCurrency } from '@/lib/Constants'; import { computed, ref } from '@vue/composition-api'; -import IdenticonStack from '@/components/IdenticonStack.vue'; +// import { useAddressStore } from '@/stores/Address'; +// import { useCurrentLimitCrypto, useCurrentLimitFiat } from '@/lib/swap/utils/CommonUtils'; +// import { useSwapLimits } from '../useSwapLimits'; +import { calculateDisplayedDecimals } from '@/lib/NumberFormatting'; +import { i18n } from '@/i18n/i18n-setup'; +import { SwapAsset } from '@nimiq/libswap'; import { AssetTransferOptions, AssetTransferParams } from './types'; +import AddressSelector from '../../components/AddressSelector.vue'; +import SinpeUserInfo from '../../components/SinpeUserInfo.vue'; -export function useSinpeMovilSwap({ direction }: AssetTransferOptions): AssetTransferParams { - const fiatCurrency = FiatCurrency.CRC; - const cryptoCurrency = CryptoCurrency.NIM; +export const SINPE_MOVIL_PAIRS = [ + [SwapAsset.NIM, SwapAsset.CRC], +]; - const fiatAmount = ref(10); - const cryptoAmount = computed(() => fiatAmount.value * 100); +function isCryptoCurrency(currency: any): currency is CryptoCurrency { + return Object.values(CryptoCurrency).includes(currency); +} + +function isFiatCurrency(currency: any): currency is FiatCurrency { + return Object.values(FiatCurrency).includes(currency); +} + +const fiatDecimals: Partial> = { + [FiatCurrency.CRC]: 0, +}; + +export function useSinpeMovilSwap({ pair: [currencyFrom, currencyTo] }: AssetTransferOptions): AssetTransferParams { + const fiatCurrency = isFiatCurrency(currencyFrom) ? currencyFrom : currencyTo as FiatCurrency; + const cryptoCurrency = isCryptoCurrency(currencyFrom) ? currencyFrom : currencyTo as CryptoCurrency; + const isSelling = currencyFrom === cryptoCurrency; + + const amountFiat = ref(10); + const amountCrypto = ref(1); + + const decimalsCrypto = computed(() => calculateDisplayedDecimals(amountFiat.value, cryptoCurrency)); + const decimalsFiat = computed(() => fiatDecimals[fiatCurrency] || 0); + + const feeAmount = ref(0.5); + + const limitMaxAmount = ref(1000); - const fiatFeeAmount = computed(() => fiatAmount.value * 0.01); - const max = computed(() => 1000); + const insufficientBalance = computed(() => amountFiat.value > limitMaxAmount.value); + const insufficientLimit = computed(() => amountFiat.value > limitMaxAmount.value); + + const addressListOpened = ref(false); + + // const { activeAddressInfo, activeAddress } = useAddressStore(); + // const { limits } = useSwapLimits({ nimAddress: activeAddress.value! }); + // const currentLimitFiat = useCurrentLimitFiat(limits); + // const currentLimitCrypto = useCurrentLimitCrypto(currentLimitFiat); + + // function setMax() { + // if (!currentLimitCrypto.value) { + // amountFiat.value = activeAddressInfo.value?.balance || 0; + // } else if (currentLimitCrypto.value < (activeAddressInfo.value?.balance || 0)) { + // amountFiat.value = currentLimitCrypto.value; + // } else { + // amountFiat.value = activeAddressInfo.value?.balance || 0; + // } + // } return { - fiatCurrency, - cryptoCurrency, + currencyFrom, + currencyTo, + currencyFiatFallback: fiatCurrency, + currencyCrypto: computed(() => isCryptoCurrency(currencyTo) ? currencyTo : currencyFrom as CryptoCurrency), + + amountFiat, + amountCrypto, + + decimalsCrypto, + decimalsFiat, + + feeAmount, + feeCurrency: fiatCurrency, + + limitMaxAmount, + limitMaxCurrency: fiatCurrency, + + insufficientLimit, + insufficientBalance, + + componentFrom: isSelling ? AddressSelector : SinpeUserInfo, + componentTo: !isSelling ? AddressSelector : SinpeUserInfo, + addressListOpened, - fiatAmount, - cryptoAmount, + modalTitle: i18n.t('Sell Crypto') as string, - fiatFeeAmount, - componentFrom: IdenticonStack, - max, - componentTo: IdenticonStack, + setMax: () => undefined, }; } diff --git a/src/config/config.local.ts b/src/config/config.local.ts index cacb2c885..0c08f0059 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -118,7 +118,6 @@ export default { sinpeMovil: { enabled: true, - pairs: [[SwapAsset.NIM, SwapAsset.CRC]], // For now we only support selling NIM to CRC sendSmsGetEndpoint: 'https://sinpemovil.sandbox.nimiqoasis.com/sms/send/{phone}', verifySmsPostEndpoint: 'https://sinpemovil.sandbox.nimiqoasis.com/sms/verify', }, diff --git a/src/router.ts b/src/router.ts index c258a7680..d3a563305 100644 --- a/src/router.ts +++ b/src/router.ts @@ -262,7 +262,8 @@ const routes: RouteConfig[] = [{ name: RouteName.AssetTransfer, props: { method: true, - direction: true, + pairFrom: true, + pairTo: true, }, meta: { column: Columns.DYNAMIC }, }, { From dbca53ed915048e85e761d737d3527cbc56f6416 Mon Sep 17 00:00:00 2001 From: Maxi Date: Thu, 13 Jun 2024 22:58:49 +0300 Subject: [PATCH 17/46] chore: add amount components to display values --- src/components/SinpeUserInfo.vue | 2 +- src/components/modals/AssetTransferModal.vue | 54 +++++++++++-------- src/composables/asset-transfer/types.ts | 11 +--- .../asset-transfer/useSinpeMovilSwap.ts | 7 ++- 4 files changed, 36 insertions(+), 38 deletions(-) diff --git a/src/components/SinpeUserInfo.vue b/src/components/SinpeUserInfo.vue index d2bcfbfcd..6e5ecfcbe 100644 --- a/src/components/SinpeUserInfo.vue +++ b/src/components/SinpeUserInfo.vue @@ -84,7 +84,7 @@ export default defineComponent({ margin-top: 0.5rem; color: var(--nimiq-blue); opacity: 0.5; - font-size: 1.875rem; + font-size: 2rem; font-weight: 400; line-height: 1.25; font-family: "Fira Mono"; diff --git a/src/components/modals/AssetTransferModal.vue b/src/components/modals/AssetTransferModal.vue index 693715fea..87fa2f7c4 100644 --- a/src/components/modals/AssetTransferModal.vue +++ b/src/components/modals/AssetTransferModal.vue @@ -1,23 +1,23 @@